Gcode Sender over USB

Hello everybody,
I have been using octoprint for a long time and i'm developing a custom app using a raspberry pi 4 and one printer with motherboard MKS Base 1.6 inspired by octoprint, but I'm struggling in one thing.
I want to create a Gcode Sender in python like octoprint's one that can send one gcode file over USB so the printer can print it.
At first I was using one I found on Github (gcodesender.py/gcodesender.py at c0572efca5950bf0cf9a8a612ee465c701c30685 · bborncr/gcodesender.py · GitHub) but I had some problems cause It seems like the script doesn't really wait and it sends more gcode lines than the motherboard can handle.
So I downloaded github files of octoprint and I think the core part of what I want is in "src/octoprint/util/comm.py". I created one test file trying to copy the methods "_do_increment_and_send_with_checksum", "_do_send_with_checksum" and "_do_send_without_checksum" from the class called "MachineCom(object)".

But I'm still getting some problems with the buffer/wait-to-send part. I expect no fails on my print so I don't need any protections/fail warnings. Just a Gcode Sender that can print one file over usb. I was wondering if someone who knows the core part of this program could help me or give me one direction I should go.

This is my first post here, I don't know if I should post this on the github site instead, sorry in advance.

The class I wrote:

import serial
import threading
import time

ser = serial.Serial("/dev/ttyUSB0", baudrate=115200)
time.sleep(1)
print("do home")
ser.write(str.encode("G28\r\n"))
time.sleep(1)

class octo_sender():
    def __init__(self):
        self._line_mutex = threading.RLock()
        self._current_line=0
        self._transmitted_lines = 0
        self._max_write_passes=3

    def _do_increment_and_send_with_checksum(self, cmd):
            with self._line_mutex:
                linenumber = self._current_line
                self._current_line += 1
                self._do_send_with_checksum(cmd, linenumber)
                
    def _do_send_with_checksum(self, command, linenumber):
            command_to_send = b"N" + str(linenumber).encode("ascii") + b" " + command
            checksum = 0
            for c in bytearray(command_to_send):
                checksum ^= c
            command_to_send = command_to_send + b"*" + str(checksum).encode("ascii")
            self._do_send_without_checksum(command_to_send)
            print(str(command_to_send))

    def _do_send_without_checksum(self, cmd, log=True):
        cmd += b"\n"
        written = 0
        passes = 0
        while written < len(cmd):
            to_send = cmd[written:]
            old_written = written

            try:
                result = ser.write(to_send)
                
                if result is None or not isinstance(result, int):
                    # probably some plugin not returning the written bytes, assuming all of them
                    written += len(cmd)
                else:
                    written += result
            except serial.SerialTimeoutException:
                print("Serial timeout while writing to serial port, trying again.")
                try:
                    result = ser.write(to_send)
                    if result is None or not isinstance(result, int):
                        # probably some plugin not returning the written bytes, assuming all of them
                        written += len(cmd)
                    else:
                        written += result
                except Exception as ex:
                    print("Unexpected error while writing to serial port")
                        
                    break
           
            if old_written == written:
                # nothing written this pass
                passes += 1
                if passes > self._max_write_passes:
                    # nothing written in max consecutive passes, we give up
                    message = "Could not write anything to the serial port in {} tries, something appears to be wrong with the printer communication".format(
                        self._max_write_passes
                    )
                    print(message)
                    break
                # if we have failed to write data after an initial retry then the printer/system
                # may be busy, so give things a little time before we try again. Extend this
                # period each time we fail until either we write the data or run out of retry attempts.
                if passes > 1:
                    time.sleep((passes - 1) / 10.0)

        self._transmitted_lines += 1

    ##~~ command handlers

    ## gcode

def removeComment(string):
    if (string.find(';') == -1):
        return string
    else:
        return string[:string.index(';')]



def sender(file):
    myThread=octo_sender()

    f = open(file, "r")
    # Wake up
    # Hit enter a few times to wake the Printrbot
    ser.write(str.encode("\r\n\r\n"))
    # time.sleep(2)   # Wait for Printrbot to initialize
    ser.flushInput()  # Flush startup text in serial input
    print('Sending gcode')
    for line in f:
        l = removeComment(line)
        l = l.strip()  # Strip all EOL characters for streaming
        if (l.isspace() == False and len(l) > 0):
            myThread._do_increment_and_send_with_checksum(str.encode(l))
    f.close()
    time.sleep(1)



if __name__ == "__main__":
    sender("/media/pi/0DD8-0D9F/mode1.gcode")

Btw, does someone know what the "W:?" or "W:(number)" mean in Marlin? I'm getting this messages from the console and I think they mean WAIT(they go lower value everytime) but didn't find info on the internet. For example:

T:207.03 /210.00 B:49.72 /50.00 @:0 B@:40 W:?
T:210.27 /210.00 B:49.97 /50.00 @:0 B@:19 W:5
T:209.96 /210.00 B:50.01 /50.00 @:3 B@:14 W:4
 echo:busy: processing
 T:209.53 /210.00 B:50.00 /50.00 @:16 B@:17 W:3
 T:209.26 /210.00 B:50.01 /50.00 @:23 B@:15 W:2

I'm also not sure what the W stands for - I also guess wait.
But I can tell you what the numbers mean:

It's the remaining time until the temperature has settled.
You can configure that value in the configuration.h of marlin


#define TEMP_RESIDENCY_TIME     10  // (seconds) Time to wait for hotend to "settle" in M109
#define TEMP_WINDOW              1  // (°C) Temperature proximity for the "temperature reached" timer
#define TEMP_HYSTERESIS          3  // (°C) Temperature proximity considered "close enough" to the target

#define TEMP_BED_RESIDENCY_TIME 10  // (seconds) Time to wait for bed to "settle" in M190
#define TEMP_BED_WINDOW          1  // (°C) Temperature proximity for the "temperature reached" timer
#define TEMP_BED_HYSTERESIS      3  // (°C) Temperature proximity considered "close enough" to the target

Aaaah, that makes sense, thank you!

1 Like

Anyway, how does octoprint manage the queue to send the gcode lines to the printer?

Feeding a gcode file to a printer is anything but straightforward. But rest assured I thought it was when I embarked on this adventure myself.

If you just keep pushing lines to the printer, unless there's some other means of flow control employed you will simply overwhelm the firmware. General rule of thumb is you do never send a line unless the firmware has signaled that it is ready to receive, which it does with an ok message. In OctoPrint that will set a flag which then allows the send queue to send the next line to the printer.

That so called ping pong protocol is incredibly ineffective since there IS a certain amount of command buffer slots available, but the majority of firmware forks out there still don't report on this in any way, meaning it's impossible for OctoPrint to do anything but the least common denominator which is ping pong.

As long as you are only targeting one specific printer/firmware with one specific configuration, you can get away with a lot of shortcuts - more than one command inflight for example, you can tailor your parser better if needed and such.

If you are trying to stay compatible with whatever wacky stuff gets thrown on the market however, I recommend getting a punching bag first.

edit You also want to read G-code - RepRap