G-Code Interpreter

I'm becoming quite unimpressed with the quality of the software available for the hardware part of 3D printers. A quick look at SkyNet3D v1.1 makes me feel like it's been hacked together. I am wondering if anyone has started working on a Rust based version of a G-code interpreter. I've seen several posts that Rust can run quite well on Arm processors and I'm looking for a project to possibly start on.

I don't have a ton of Rust experience or 3D printing experience, but I have a solid knowledge of what it would take to put something like this together. I really need to focus my skill on something specific that doesn't exist yet.

8 Likes

I don't believe anyone has published anything for working with gcode, searching "gcode" and "cnc" on crates.io and google don't show any useful results.

I've considered doing something like this a couple times now. Having a #[no_std] library for parsing and interpreting a stream of gcode would be really useful for people wanting to do embedded systems and cnc-like applications.

What kind of stuff would you expect from a hypothetical library for interpreting gcode?

3 Likes

I also did a quick search and everything I find is C/C++ code written in a very non modular method. It seems like each variation of printer (Cartesian, polar, Delta) have just been #ifdef'd in where necessary. The code does work, but it's spaghetti.

My idea is simple, design a library that is well structured and performance oriented. I'd love to support

  • Raspberry Pi / Compute module
  • ARM Cortex series

as targets with hardware simple at first with lots of testing to ensure accuracy.

Stuff I think would do well in a library like this is

  • Legible / fail-safe G-Code parsing
  • Open/Closed loop motion control via traits (possibly) so different types of devices can be well supported/maintainable
  • Configurable acceleration/deceleration curves
  • Auto-leveling bed as default
  • Well defined modules i.e.- MotionControl, LCD, GCode etc. other projects code is mostly everywhere as far as I can tell.

Anyway I'm sure a #[no_std] library is necessary here to support as many platforms as possible.

2 Likes

It sounds like what you're asking for goes much further than a simple gcode interpreter. It's fairly easy to create a "parser" which effectively takes in an iterator of characters and then transforms that into an iterator who's item is Result<GCode, Error>. Gcode itself is a very easy language to parse and I could probably knock up something like that in 100-200 lines of code.

What you are asking for extends much further though, and would probably end up being a fairly in-depth project.

The top-level design is fairly straightforward though. You'd set up the 3D printer library to have some sort of Controller struct which takes in a stream of characters (the gcode instructions), and your printer's implementation of some Printer trait. The controller then reads the instructions and uses your Printer implementation as a driver to execute the instructions.

I imagine the Printer trait would look something like this:

trait Printer {
  fn set_bed_height(&mut self, height: f64) -> Result<(), Error>;
  fn move_extruder(&mut self, x: f64, y: f64) -> Result<(), Error>;
  fn set_extrusion_rate(&mut self, rate: f64) -> Result<(), Error>;
}

Overall what you are talking about is definitely doable. With Rust it's easy to design things in a way that is modular and extensible, and which use proper dependency injection so you don't need to resort to a bunch of #ifdef statements to swap between printer implementations.

I'd be really keen to work on something like this if you want to take it further. Overall it would benefit the Rust embedded community massively, plus it sounds like a genuinely interesting project :slight_smile:

3 Likes

Sounds great!

Any ideas for what we could name this project?

Should we keep the main modules in separate repositories with a main one using the others (Cargo is great for this!)

I'll start coming up with a outline of what's needed and where to divide it up.

Feel free to PM me.

+1 for separate crates where doable. A crate for parsing g-code would be useful for a desktop qaqc tool. Also, If the cnc control code only used a type that deserialized into a g-code enum, then users could swap out for one of the binary formats that never got much traction.

2 Likes

As I see it, we've got a couple fairly distinct modules/crates here.

  • A streaming gcode parser which can would be work regardless of if the gcodes are read from some file on a SD card, or if it were receiving via a serial line (maybe it reads from a buffered reader or whatever?)
  • A platform-agnostic "controller" which can take in a stream of instructions and execute them using some platform-specific Driver trait
  • At least one Driver implementation so we can test this out on actual hardware

Rust looks to be a really nice language for this! Having some generic Driver trait means you'd easily be able to test out and simulate a gcode program without needing to do any difficult hacking or code copying. You just write a Driver implementation which records what it does for playback later. Plus you get modularity and crates.io out of the box.

@japaric may have some interesting ideas for this. He's currently doing a lot to help further the Rust embedded ecosystem and has a heap of experience with microcontrollers.

2 Likes

I thought I'd get the ball rolling on the gcode parsing. I've started up a repo and hooked in CI.

1 Like

Very nice. I will start on a simply math crate for doing the motion
control. And try to come up with some simple test files and a list of
G-Codes we should support. I think the Controller should perform the
interpolation and create a buffer of positions and then the Driver can
perform any coordinate translations for the different types of machines.

1 Like

Man I am not used to writing Rust with no_std! I wrote an entire vector math file and then remembered to add #![no_std] and realized there is no operator overloading! I guess my hint should have been 'use std::...'

What is the suggested method of overcoming this limitation? Just write all the code with no operator overloading?

This looks really cool. thanks for doing this!
Have you considered using one of the parser libraries instead of rolling your own? If they don't suit the purpose, what are they missing?

I think the #![no_std] is the limitation.

Never mind. Core is what I want.

1 Like

Yeah, I considered it. But the problem with a lot of the libraries out there is it takes longer to learn how to use them than to write a simple recursive descent parser. Plus they often give really cryptic error messages during development due to the use of generics or unreadable macros (I'm looking at you nom).

So yeah, for a super simple line-base language like gcode I thought I'd roll my own recursive descent parser. I've written my fair share of interpreters and compilers in the past, so I'm hoping it shouldn't be overly difficult.

1 Like

I agreed that G-code is quite easy to parse. I had hoped to include almost no external libraries just to decrease any external dependencies. Not using the standard library should enable a fairly easy transition to running on embedded platforms. Smoothieboard is around the level of minimum hardware I'd like to support. 8 bit AVRs running at 16 MHz isn't enough, although it may be possible once these are supported by Rust. I'd like to think an STM32F0 running at 48 MHz should be enough to run just g-code parsing and motion control of a 4 axis machine. I do want to design my own board simply because I want to include an option for closed loop motion control. Of course first step is to get g-code parsing and a stepper moving :slight_smile:

I have 2 cnc driver boards with these chips on them -

It's important to note that these are just the driver boards, I will use a -

to run tests with initially. These are low cost, Arm based systems, with at least 2 having Rust support already. I'll also have an ANET A8 3D printer soon and it has an Atmega1284 processor, far to slow in my opinion.

Interesting to note that the only the RaspberryPi has floating point hardware support. I will have to get a Teensy 3.5 & 3.6 to try as well and note the difference.

The G30TH has an STM32F401 processor which includes 32-bit FPU.

The NUCLEO-L432KC is an 80 MHz Cortex-M4 with a 32-bit FPU as well.

Or, for a few more dollars, you can get the NUCLEO-F446RE which runs at 180 MHz (also includes a 32-bit FPU).

The 2 NUCLEO boards include debugging support. They each have a builtin stlink processor in addition to the featured processor.

@iAlwaysLoseThis, how strict do you think the gcode parser should be? I can make the parser quite strict so it'll only accept valid gcode commands. Or I could make it a bit more relaxed so if you were to pass in R30.0 to a command which doesn't accept an R command, it'll just be ignored.

At the moment I'm thinking I'll ignore invalid stuff, but log it at the info level.

1 Like

I should hope that unless your writing g-code by hand there shouldn't be any syntax errors. Mostly just trying to move the tool pass the end stops. My opinion is relaxed and just log errors as we go. Don't forget to include E as an axis token (Extruder), I'm not sure how multi-head extrudors work yet. Also I love your power implementation.

Here is the first lines of a simple box generated with Simplfy3D

G90
M82
M106 S0
M104 S190 T0
M109 S190 T0
G28 ; home all axes
G92 E0
G1 E-1.0000 F1800
G1 Z0.180 F1000
; layer 1, Z = 0.18
T0
; tool H0.200 W0.400
; skirt
G1 X52.003 Y64.991 F4800
G1 E0.0000 F540
G92 E0
G1 X52.224 Y64.900 E0.0071 F1800
G1 X147.776 Y64.900 E2.8674
G1 X147.997 Y64.991 E2.8746
...
G92 E0
G1 X52.303 Y65.300 E0.0024 F1800
G1 X147.697 Y65.300 E2.8579
...
G92 E0
G1 E-1.0000 F1800
; layer 2, Z = 0.38
M106 S255
; inner perimeter
G1 X145.400 Y70.100 F4800
G1 Z0.380 F1000
G1 E0.0000 F1800
G92 E0

I'll send you the whole file when I get a chance. I haven't had much time to work on my part, but I should be able to make some progress this weekend.

1 Like

http://reprap.org/wiki/G-code is a really nice description of g-code in relation to 3d printers, but it seems to cover it all.

1 Like

Haha oh, you're talking about this? Yeah I'm not proud of it, but it's about the only thing I could think of to get a floating point number from the fractional and integer parts and doesn't rely on std or nightly-only stuff. That naive implementation shouldn't be overly expensive though because most numbers used in CNC/3D printing would only go out to a couple decimal places.

I'm having a couple troubles representing a command without any dynamic memory allocation though. Turns out it's not really possible to have a list of arguments of unknown length with no_std (a la Vec), so I'm thinking I'll pull in arrayvec and settle for a reasonably sized buffer.

If you've got any decently sized gcode examples feel free to send them to me via pull request/issue. I've got a couple in the tests/data/ directory which I'm using for integration tests to ensure the parser and lexer can handle non-trivial programs.

1 Like