Update: See my post on Adding buffer monitoring to Marlin.

I recently picked up a Creality Ender 3 v2 to finally check out 3D printing. Initially, I was very impressed with the quality of my prints for a budget printer, however, at some point during tinkering, I noticed zits on my prints, specifically around curves.

I eventually tracked this down to a combination of printing over USB in Octoprint vs printing from SD card, as well as switching from Creality's slicer (Cura 4.2) to Cura 4.7.1.

A bug (#8321) was introduced in Cura 4.7 where it was adding loads of tiny segments in curves, which in turn generates way more gcode for the same curve. This, combined with how Octoprint streams the gcode over the USB serial connection to the printer, results in the printer's buffers to empty, thus causing brief pauses as it waits for more gcode, which manifests in zits on the surface of prints as the pressure in the nozzle still causes extrusion to occur.

Even though printing via SD card is likely to resolve the issue, it removes a lot of the convenience and power of printing with Octoprint, such as the ability to selectively cancel certain regions of your print, saving time and reducing waste. Cura's segment issue is likely to be addressed at some point, which will decrease the likelihood of reduced print quality due to less gcode, however the issue with Octoprint's gcode streaming still exists, and has been reported since 2014.

What causes print artifacts when printing over USB?

There are a couple of factors at play that can affect the streaming of gcode to the printer when using Octoprint, especially on embedded-class devices like Raspberry Pis.

  • CPU load: If the process in Octoprint that's responsible for streaming gcode doesn't get to run due to load on the system, then the printer is likely to be waiting for additional instructions, causing movement stutter and print artifacts. This can occur when just loading the Octoprint interface. There is a reason why stuff like plugin management and timelapse processing is prevented while a print is in progress.
  • USB cable: I was using a longer cable I had lying around which occasionally would have disconnect issues, which was extremely noticable when I experimented with Klipper (more below). Switching to a shorter cable resolved this particular issue.
  • Communication speed: It's possible that the gcode is simply too dense for it to be communicated over the USB/serial connection at a rate that it's needed to be processed by the printer to perform the print as desired.

Mitigations

After learning the issue is likely to do with gcode density around curves, I tried to use the ArcWelder plugin which makes gcode smaller by converting the straight G0/G1 commands into G2/G3 arc commands, which can reduce the resulting gcode by as much as 80%!

However, the stock firmware I was using did not have ARC_SUPPORT enabled, so I gave Klipper a shot. The idea of Klipper is to move the motion calculation off the printer's (usually underpowered) microcontrollers, and on to a more powerful device like a Raspberry Pi (compared to a microcontroller, of course). It supplies its own firmware for the printer, which takes a compressed data stream that in turn tells it how to do what to its various stepper motors. Klipper then exposes its own serial port to Octoprint which receives gcode commands.

Using Klipper noticably improved print quality for me, even when using Cura 4.7.x, however the printer display was just blank and not being able to control the printer with its built-in controls was a significant negative. I also ran into severe extruder chattering which caused filament grinding when I was using its pressure advance feature to handle better corners.

Marlin has a DIRECT_STEPPING option which is similar to how Klipper works, however it recommends 250k-500k baud and it doesn't seem to be used very much just yet.

My BLTouch bed levelling sensor eventually arrived so I followed Smith3D Ender 3 v2 BLTouch guide, and used their fork of Marlin which has some nice improvements.

I downgraded Cura to 4.6.2 for the time being, which resolved most of my print quality issues.

However, the core issue of potential print artifacts due to printing over USB still remain, and I'm not willing to give up the convenience of Octoprint, so I investigated further.

Ideally, the code responsible for streaming to the printer should be run at a much higher priority to ensure it gets scheduled. It appears Octoprint's sending_thread in comm.py is being run as a daemon thread, which I initially got excited cause it sounded like it would run it as a separate process (somehow?) which means we could just snice it to a higher priority, but that's not what it does.

My limited understanding of comm.py seems to indicate that Octoprint sends the next command to the printer once it's received an ok from the printer, which happens when Marlin commits the command into its ring buffer. The size of the ring buffer is defined by BUFSIZE, and it's usually set to 4 for most configurations.

This seems pretty small to me, but if Octoprint isn't filing the buffer reliably, then increasing BUFSIZE should decrease the likelihood but not mitigate it completely.

There is a WIP pull request that enables parsing ADVANCED_OK to fill buffers accordingly, however it was last touched September 2019, so it may never get merged in.

Detecting the issue

We should be able to discover the minimum speed that we can communicate reliably to the printer via a benchmark, and in theory, we should be able to look at a gcode file and determine if there are parts of the gcode that cannot be transmitted at the speed that it's meant to be parsed at.

There is also an ADVANCED_OK configuration option which will report the line number of the command being acked, as well as the remaining command buffer and motion planning buffers.

I'm in the progress of writing an Octoprint plugin which should be able to track these buffers, as well as hopefully calculate a median latency of command to response to further diagnose this issue.

Ideally, there should be a way to detect when the command buffer is empty and report back to the host so users can be aware that there are issues happening. Initial research shows that void GCodeQueue::advance() is where the change should be made.

Next up: Adding buffer monitoring to Marlin.