Python PI GCode Sender Questions

Hello Folks,
Hope everyone is keeping well. Im making some progress with a raspberry Pi python GRBL sender. Right now,

  1. It picks a random G-Code file from a folder
  2. Streams the file accross the serial to the GRBL
  3. Closes everything - It completes one pattern and then stops.

Im looking for a little assistance if possible

  1. Scheduling: Id like to be able to run the machine on certain days and time. It sounds like Crontab is used for this. Is it better to use a python library like datetime and control the stuff in the actual program OR use crontab to schedule to when to run the program. Can I have a while loop that keeps on running the patterns if using crontab does anyone know?

  2. Plotting the G-Code: I’d like to have a little screen that plots the X,Y co-ordinates so students could appreciate that the sand pattern is controlled by the raspberry pi. Does anyone know of a simple python library that opens up a little window and prints out the plot.

  3. Any chance someone more experienced in programming and python could look over the code and give me some feedback - is there any error checking we could add - any other suggestions?

Big thanks for all the help so far - If your ever in Maryland - ill buy you a pint ,
Tim

import serial
import time
import os, random

# Open grbl serial port
s = serial.Serial('/dev/ttyUSB0',115200)
 
# Opens up a random G-Code File
random_file = random.choice(os.listdir("/home/timcallinan/GCODE/"))
file_path = os.path.join("/home/timcallinan/GCODE/", random_file)
print(file_path)
f = open(file_path,'r');

# Wake up grbl
s.write("\r\n\r\n".encode())
time.sleep(2)   # Wait for grbl to initialize
s.flushInput()  # Flush startup text in serial input
 
# Stream g-code to grbl
for line in f:
    l = line.strip() # Strip all EOL characters for streaming
    print('Sending: ' + l,)
    s.write((l + '\n').encode()) # Send g-code block to grbl
    grbl_out = s.readline() # Wait for grbl response with carriage return
    grbl_out_str=str(grbl_out)
    print(' : ' + grbl_out_str)
 
# Close file and serial port
f.close()
s.close()

I’m working on something similar for my sand table but im at about the same point as you are with totally different setup… grbl and esp32 for a polar machine

It will be fun to see how you tackle it. I suspect that the buffering bit may be a bit of a pain to deal with but not sure yet.

May we toast each other’s victories as I’m also in maryland.

1 Like

Definitely! Are you in Annapolis? I am. Id like to see your setup. Ill see if i can privately message you here.
Tim

Running your program forever, with a while loop, is fine. The suggestion for crontab was a little different. I imagined sandypi running forever, with no changes. Crontab would send a command at 8am to say, “play” and at 8 to say, “stop” pr “pause”.

With a while loop, you could just ad something like this:

if hour < 8 or hour > 20:
    print("sleeping")
    time.sleep(60)
    continue

Inside a while loop.

Your code looks fine. I have a couple of suggestions:

  1. Use with to open the serial port and the file. It makes errors easier to handle and closes then automatically.
  2. Use a log. Whenever you get this running. And you want to see why it stopped, you’ll want to look at the text log. You’re doing a decent job of printing frequently. Just be sure you record the output of the app so you can see the log later, after an error.
  3. If you want this to be more generally useful, make the serial port and the gcode folder parameters.
  4. How are you going to run this? You could run it from cron with @restart. You could configure systemd. You could use docker with restart=unless stopped. You’ll have to look into each of these options. There are many choices.
  5. About error handling. I imagine the ways this is going to fail is that there will be a loose connection on the USB, or a command will get lost and you’ll be waiting for a new line. If you lose the serial port, then things like send will raise an exception. I would recommend just not handling that for now. But make sure you have a good log to see where it is falling before it fails. The log can also help you detect a stalled scenario. But remember that output can be cached. Eventually, if that becomes a significant problem, you can make a watchdog that you feed when things are working, and if you haven’t fed the WD in 10 minutes, then it will flush the serial port and start over, or whatever.
1 Like

Hello Jeff & Co,
I hope everyone is doing well. Sorry I havent been able to respond in a long time. The start of my semester is always chaos. Anyway Ive been playing with the sand plotter and been making some slow progress with the Python Pi GCode Sender. Ive enclosed a photo of the machine as well as the code Im using.

I was able to figure out the whole timing - which day/hours I want the machine to run. This is in the check_program_window function. It seems to work quite well. I was also able to figure out the program picking a random G-Code file and the parsing of each line.

Where I could use some advice is the serial buffering of the G-Code sentences - I dont know if ‘buffering’ is the correct term. My program sends the next command once it gets a carriage return back from the controller. The program seems to keep sending the G-Code out. Is there a better way of doing this. Bart Drings CNC controller does kick back some data. Here is an example of it . Maybe I could check this GRBL response before we move to the next line of G-Code? Hopefully my ramblings make some sense. Any advice is appreciated. Thank you
Tim

Sending: $HY
: b’<Home|MPos:-139.850,241.467,0.000|FS:1000,0>\r\n’
Sending: F2000
: b’<Home|MPos:-223.224,241.467,0.000|FS:1000,0>\r\n’
Sending: G01 X260.500 Y250.000
: b’<Home|MPos:4.280,241.467,0.000|FS:202,0|WCO:0.000,0.000,0.000>\r\n’


#START OF CODE
from datetime import datetime
import time
import serial
import os, random

INTERVAL = 5  # seconds between loops
VALID_DAYS = [0,1,2,3,4] # days of week to run program MONDAY TO FRIDAY
VALID_HOURS = (9, 19)  # (start, stop) hours 24 hour time

def GRBL_Sender():
    
    # Open grbl serial port
    s = serial.Serial('/dev/ttyUSB0',115200)
     
    # Opens up a random G-Code File
    random_file = random.choice(os.listdir("/home/timcallinan/GCODE/"))
    file_path = os.path.join("/home/timcallinan/GCODE/", random_file)
    print(file_path)
    f = open(file_path,'r');

    # Wake up grbl
    s.write("\r\n\r\n".encode())
    time.sleep(2)   # Wait for grbl to initialize
    s.flushInput()  # Flush startup text in serial input
     
    # Stream g-code to grbl
    for line in f:
        l = line.strip() # Strip all EOL characters for streaming
        print('Sending: ' + l,)
        s.write((l + '\n').encode()) # Send g-code block to grbl
        grbl_out = s.readline() # Wait for grbl response with carriage return
        grbl_out_str=str(grbl_out)
        print(' : ' + grbl_out_str)
     
    # Close file and serial port
    f.close()
    s.close()



def run_program():
    #stuff to run 9am-7pm Mon-Sat
    print("sand pltter is running")
    GRBL_Sender()
    pass


def check_program_window() -> bool:
        
    dt = datetime.now()
    
    print(dt.weekday())
    if dt.weekday() not in VALID_DAYS:
        return False  # not a valid day
        
    if VALID_HOURS[0] <= dt.hour and dt.hour <= VALID_HOURS[1]:
        return True
    else:
        return False
        

def start() -> None:

    while True:  # keep program looping
        if check_program_window():
            run_program()

        time.sleep(INTERVAL)


#This just starts the program 
if __name__ == "__main__":
    start()
1 Like

That seems fine to me. I would look at other senders, like the cncjs , ugs, or octoprint to see how they manage that buffering. But what you have looks fine to me.

You could add in a huge timeout (longer than any possible move) to make sure it will break free if it ever stalls.

This makes me want to make a micropython/circuitpython board to do this on a little microcontroller.

1 Like

Thank You @timcallinan your code helped me with a little CNC touchpad for a MARLIN based CNC.

Machined a PCB on it with 6 directional buttons (XYZ +/-), and 5 function Buttons (Zero XYZ, Zero Z, Move Slow/Small, Medium, and Fast/Big)

import board
import digitalio
import busio
import time
import serial

try:
	print("Attempting Serial Connection")
	s = serial.Serial('/dev/ttyACM0', 250000)
	print("Serial Connected")
except:
	print("Serial Connection Failed")

try:
	print("Sending CR")
	s.write("\n".encode())
	print ("Flushing Input")
	s.flushInput()
	print ("Reading")
	out = s.readline()
	print(str(out))
	s.flushInput()
	print("Serial initialized")
except:
	print("Failed to Send")

# define buttons x+/-, y+/-, z +/-, zero XYZ, zero z, Fast, Medium, Slow
button_xp = digitalio.DigitalInOut(board.D6)
button_xm = digitalio.DigitalInOut(board.D19)
button_yp = digitalio.DigitalInOut(board.D26)
button_ym = digitalio.DigitalInOut(board.D13)
button_zp = digitalio.DigitalInOut(board.D4)
button_zm = digitalio.DigitalInOut(board.D5)
button_zeroxyz = digitalio.DigitalInOut(board.D11)
button_zeroz = digitalio.DigitalInOut(board.D0)
button_slow = digitalio.DigitalInOut(board.D9)
button_medium = digitalio.DigitalInOut(board.D10)
button_fast = digitalio.DigitalInOut(board.D22)

# set all buttons to input & pull up
button_xp.direction = digitalio.Direction.INPUT
button_xp.pull = digitalio.Pull.UP
button_xm.direction = digitalio.Direction.INPUT
button_xm.pull = digitalio.Pull.UP
button_yp.direction = digitalio.Direction.INPUT
button_yp.pull = digitalio.Pull.UP
button_ym.direction = digitalio.Direction.INPUT
button_ym.pull = digitalio.Pull.UP
button_zp.direction = digitalio.Direction.INPUT
button_zp.pull = digitalio.Pull.UP
button_zm.direction = digitalio.Direction.INPUT
button_zm.pull = digitalio.Pull.UP
button_zeroxyz.direction = digitalio.Direction.INPUT
button_zeroxyz.pull = digitalio.Pull.UP
button_zeroz.direction = digitalio.Direction.INPUT
button_zeroz.pull = digitalio.Pull.UP
button_slow.direction = digitalio.Direction.INPUT
button_slow.pull = digitalio.Pull.UP
button_medium.direction = digitalio.Direction.INPUT
button_medium.pull = digitalio.Pull.UP
button_fast.direction = digitalio.Direction.INPUT
button_fast.pull = digitalio.Pull.UP

# set initial movement increment based on speed
# actual speed is increement / debounce (+ some delay)
slow_increment = 0.1
medium_increment = 1
fast_increment = 10
move_increment = slow_increment

# set debounce time
debounce = 0.5

def move(axis, distance, debounce):
	print("G91\nG0 {0}{1} F300\nG90\n".format(axis,distance))
	try:
		s.write("G91\nG0 {0}{1} F300\nG90\n".format(axis,distance).encode())
		out = s.readline()
		print(str(out))
		s.flushInput()
		print("Command Sent")
	except:
		print("Failed to send Command")
	time.sleep(debounce)

def zeroxyz(debounce):
	print("G92 X0 Y0 Z0\nG0 Z1 F300\n")
	try:
		s.write("G92 X0 Y0 Z0\nG0 Z1 F300\n".encode())
		out = s.readline()
		print(str(out))
		s.flushInput()
		print("Command Sent")
	except:
		print("Failed to send Command")
	time.sleep(debounce)

def zeroz(debounce):
	print("G92 Z0\nG0 Z1 F300\n")
	try:
		s.write("G92 Z0\nG0 Z1 F300\n".encode())
		out = s.readline()
		print(str(out))
		s.flushInput()
		print("Command Sent")
	except:
		print("Failed to send Command")
	time.sleep(debounce)


while True:
	if button_xp.value == 0:
		move("X", move_increment, debounce)
	if button_xm.value == 0:
		move("X", -1 * move_increment, debounce)
	if button_yp.value == 0:
		move("Y", move_increment, debounce)
	if button_ym.value == 0:
		move("Y", -1 * move_increment, debounce)
	if button_zp.value == 0:
		move("Z", move_increment, debounce)
	if button_zm.value == 0:
		move("Z", -1 * move_increment, debounce)
	if button_zeroxyz.value == 0:
		zeroxyz(debounce)
	if button_zeroz.value == 0:
		zeroz(debounce)
	if button_slow.value == 0:
		move_increment = slow_increment
		print(move_increment)
		time.sleep(debounce)
	if button_medium.value == 0:
		move_increment = medium_increment
		print(move_increment)
		time.sleep(debounce)
	if button_fast.value == 0:
		move_increment = fast_increment
		print(move_increment)
		time.sleep(debounce)

1 Like