Devices

G-code for 3D printers

G-code from 3D printers and CNC machines read: print time with acceleration, filament length, weight and cost, the print's size, layers and their heights, temperatures and the slicer's notes, and a layer drawn as SVG.

Version 1.0.0 · 3 functions · 0 crates

#files#devices#calculators#svg

Functions

What use gcode.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use gcode_stats, gcode_layers from gcode (Quillo 0.6).

gcode_stats(path: str, options: any) → anyor an error

What a G-code file will do: { lines, time_s, time (h m s), filament_mm, filament_m, volume_cm3, weight_g, cost, layers, first_layer_height, layer_height, print_bounds, travel_bounds, temperatures, tools, counts, notes (the slicer's), unknown (commands not followed) }. options: { diameter: 1.75 (mm), density: 1.24 (g/cm³, PLA), price_per_kg, acceleration: 1000 (mm/s²; 0 for none), max_speed: 500 (mm/s) }. The time is an estimate: firmware look-ahead and jerk make real prints a little faster or slower.

gcode_layers(path: str, options: any) → listor an error

Layer by layer: [{ layer, z, filament_mm, time_s }], for a progress bar or to find where a print will be at a time.

gcode_layer_svg(path: str, layer: int, size: int, travel: bool) → stror an error

One layer (from 1) drawn from above as SVG, size pixels wide: extrusion in dark lines, travel moves dashed when travel is true.

Get it

$ quillo install gcode               # gcode.qwer into this folder$ quillo install gcode --example     # and its example, gcode.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/gcode.qwer$ curl -O https://quillolang.org/plugins/files/gcode.qwe$ curl -O https://quillolang.org/plugins/files/gcode_sample.gcode

Then, in your program, all of it or only what you call:

main.qwe
use gcode.qweruse gcode_stats, gcode_layers from gcode      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

gcode.qwe
// A print farm's queue: each uploaded G-code file read for its print time, filament, weight and// cost, size and temperatures before it goes to a printer; a layer drawn to check it.////   quillo run gcode.qwe      then open http://localhost:8080/stats and http://localhost:8080/layer/3use "gcode.qwer"let FILE = "gcode_sample.gcode"route GET /stats = gcode_stats(FILE, { diameter: 1.75, density: 1.24, price_per_kg: 22.0, acceleration: 1500 })?route GET /layers = gcode_layers(FILE, {})?route GET /layer/:n(int) = respond(200, gcode_layer_svg(FILE, n, 400, true)?, { "content-type": "image/svg+xml" })
$ quillo run gcode.qwe$ curl 'localhost:8080/stats'$ curl 'localhost:8080/layers'$ curl 'localhost:8080/layer/3'

Source

gcode.qwer
//! G-code from 3D printers and CNC machines read: moves with absolute and relative positioning, arcs, units, extruder modes and resets followed; print time estimated with acceleration, filament length, volume, weight and cost, the print's bounds and travel's, layers and their heights, temperatures, fan and tool changes, the slicer's notes; and a layer drawn as SVG.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Map, Value};use std::f64::consts::PI;#[derive(Default, Clone, Copy)]struct Bounds {    min: [f64; 3],    max: [f64; 3],    any: bool,}impl Bounds {    fn add(&mut self, p: [f64; 3]) {        if !self.any {            self.min = p;            self.max = p;            self.any = true;        } else {            for i in 0..3 {                self.min[i] = self.min[i].min(p[i]);                self.max[i] = self.max[i].max(p[i]);            }        }    }    fn json(&self) -> Value {        if !self.any {            return Value::Null;        }        let r = |x: f64| (x * 1000.0).round() / 1000.0;        json!({ "min": { "x": r(self.min[0]), "y": r(self.min[1]), "z": r(self.min[2]) }, "max": { "x": r(self.max[0]), "y": r(self.max[1]), "z": r(self.max[2]) }, "size": { "x": r(self.max[0] - self.min[0]), "y": r(self.max[1] - self.min[1]), "z": r(self.max[2] - self.min[2]) } })    }}/// A segment the head moves: from, to, extruding or not, and its layer's index.struct Seg {    a: [f64; 3],    b: [f64; 3],    extrude: bool,    layer: usize,}struct Run {    time: f64,    filament: f64,    print: Bounds,    travel: Bounds,    layers: Vec<(f64, f64, f64)>, // z, filament, time    segs: Vec<Seg>,    temps: Map<String, Value>,    tools: Vec<i64>,    counts: Map<String, Value>,    notes: Map<String, Value>,    lines: usize,    unknown: Vec<String>,}fn word(line: &str) -> Vec<(char, f64)> {    let mut out = vec![];    let cs: Vec<char> = line.chars().collect();    let mut i = 0;    while i < cs.len() {        let c = cs[i].to_ascii_uppercase();        if c.is_ascii_alphabetic() {            let mut j = i + 1;            while j < cs.len() && (cs[j].is_ascii_digit() || cs[j] == '.' || cs[j] == '-' || cs[j] == '+' || cs[j] == ' ') {                if cs[j] == ' ' && j + 1 < cs.len() && cs[j + 1].is_ascii_alphabetic() {                    break;                }                j += 1;            }            let num: String = cs[i + 1..j].iter().filter(|c| **c != ' ').collect();            out.push((c, num.parse().unwrap_or(f64::NAN)));            i = j;        } else {            i += 1;        }    }    out}/// Time to move `d` mm at up to `v` mm/s with acceleration `acc` mm/s² (from and to a stop).fn move_time(d: f64, v: f64, acc: f64) -> f64 {    if d <= 0.0 || v <= 0.0 {        return 0.0;    }    if acc <= 0.0 {        return d / v;    }    let d_acc = v * v / acc;    if d >= d_acc { d / v + v / acc } else { 2.0 * (d / acc).sqrt() }}fn run(text: &str, options: &Value, keep_segments: bool) -> Result<Run, String> {    let acc = options["acceleration"].as_f64().unwrap_or(1000.0).max(0.0);    let max_feed = options["max_speed"].as_f64().unwrap_or(500.0).max(1.0);    let mut r = Run { time: 0.0, filament: 0.0, print: Bounds::default(), travel: Bounds::default(), layers: vec![], segs: vec![], temps: Map::new(), tools: vec![], counts: Map::new(), notes: Map::new(), lines: 0, unknown: vec![] };    let mut pos = [0.0f64; 3];    let mut e = 0.0f64;    let mut feed = options["default_feed"].as_f64().unwrap_or(3000.0) / 60.0;    let (mut abs, mut e_abs, mut inch) = (true, true, false);    let mut last_layer_z: Option<f64> = None;    let mut count = |r: &mut Run, k: &str| {        let n = r.counts.get(k).and_then(Value::as_i64).unwrap_or(0);        r.counts.insert(k.into(), json!(n + 1));    };    for raw in text.lines() {        r.lines += 1;        let (code, comment) = match raw.find(';') {            Some(i) => (&raw[..i], Some(raw[i + 1..].trim())),            None => (raw, None),        };        if let Some(c) = comment {            // Slicer notes: ;FLAVOR:Marlin, ;TIME:1234, ;Filament used: 1.2m, ; generated by PrusaSlicer...            if let Some((k, v)) = c.split_once(':').or_else(|| c.split_once(" = ")) {                let k = k.trim();                let known = ["Generated", "generated", "filament used", "estimated printing time"].iter().any(|p| k.starts_with(p));                if !k.is_empty() && k.len() < 40 && r.notes.len() < 60 && !k.eq_ignore_ascii_case("LAYER") && (!k.contains(' ') || known) {                    r.notes.entry(k.to_string()).or_insert(json!(v.trim()));                }            } else if c.starts_with("Generated with") || c.starts_with("generated by") {                r.notes.insert("generator".into(), json!(c));            }        }        let code = code.trim();        if code.is_empty() {            continue;        }        let w = word(code);        let Some(&(letter, num)) = w.first() else { continue };        let get = |c: char| w.iter().skip(1).find(|(l, _)| *l == c).map(|(_, v)| *v);        let scale = if inch { 25.4 } else { 1.0 };        match (letter, num as i64) {            ('G', 0) | ('G', 1) | ('G', 2) | ('G', 3) => {                count(&mut r, if num as i64 <= 1 { "linear" } else { "arc" });                if let Some(f) = get('F') {                    feed = (f * scale / 60.0).min(max_feed);                }                let mut to = pos;                for (i, c) in ['X', 'Y', 'Z'].iter().enumerate() {                    if let Some(v) = get(*c) {                        to[i] = if abs { v * scale } else { pos[i] + v * scale };                    }                }                let de = match get('E') {                    Some(v) => {                        let v = v * scale;                        let d = if e_abs { v - e } else { v };                        e = if e_abs { v } else { e + v };                        d                    }                    None => 0.0,                };                let xy_z = ((to[0] - pos[0]).powi(2) + (to[1] - pos[1]).powi(2) + (to[2] - pos[2]).powi(2)).sqrt();                let mut dist = xy_z;                let mut path: Vec<[f64; 3]> = vec![to];                if num as i64 >= 2 {                    // Arc: the centre from I, J (or the radius R); length and a few points along it.                    let cw = num as i64 == 2;                    let center = match (get('I'), get('J'), get('R')) {                        (i, j, _) if i.is_some() || j.is_some() => Some([pos[0] + i.unwrap_or(0.0) * scale, pos[1] + j.unwrap_or(0.0) * scale]),                        (_, _, Some(rad)) => {                            let rad = rad * scale;                            let (dx, dy) = (to[0] - pos[0], to[1] - pos[1]);                            let q = (dx * dx + dy * dy).sqrt();                            if q == 0.0 || q > 2.0 * rad.abs() + 1e-9 {                                None                            } else {                                let h = (rad * rad - q * q / 4.0).max(0.0).sqrt();                                let (mx, my) = (pos[0] + dx / 2.0, pos[1] + dy / 2.0);                                let s = if (cw && rad > 0.0) || (!cw && rad < 0.0) { -1.0 } else { 1.0 };                                Some([mx - s * h * dy / q, my + s * h * dx / q])                            }                        }                        _ => None,                    };                    if let Some(c) = center {                        let rad = ((pos[0] - c[0]).powi(2) + (pos[1] - c[1]).powi(2)).sqrt();                        let a0 = (pos[1] - c[1]).atan2(pos[0] - c[0]);                        let a1 = (to[1] - c[1]).atan2(to[0] - c[0]);                        let mut sweep = a1 - a0;                        if cw && sweep >= 0.0 {                            sweep -= 2.0 * PI;                        }                        if !cw && sweep <= 0.0 {                            sweep += 2.0 * PI;                        }                        dist = (sweep.abs() * rad).hypot(to[2] - pos[2]);                        let n = ((sweep.abs() * rad) / 1.0).ceil().clamp(2.0, 64.0) as usize;                        path = (1..=n).map(|k| {                            let t = k as f64 / n as f64;                            let a = a0 + sweep * t;                            [c[0] + rad * a.cos(), c[1] + rad * a.sin(), pos[2] + (to[2] - pos[2]) * t]                        }).collect();                    }                }                let extruding = de > 0.0 && dist > 0.0;                let t = if dist > 0.0 { move_time(dist, feed, acc) } else { move_time(de.abs(), feed.min(max_feed), acc) };                r.time += t;                if extruding {                    r.filament += de;                    if last_layer_z.map_or(true, |z| (z - to[2]).abs() > 1e-6) {                        r.layers.push((to[2], 0.0, 0.0));                        last_layer_z = Some(to[2]);                    }                    if let Some(l) = r.layers.last_mut() {                        l.1 += de;                        l.2 += t;                    }                    r.print.add(pos);                    for p in &path {                        r.print.add(*p);                    }                } else if dist > 0.0 {                    r.travel.add(pos);                    r.travel.add(to);                    if let Some(l) = r.layers.last_mut() {                        l.2 += t;                    }                }                if keep_segments && dist > 0.0 && r.segs.len() < 2_000_000 {                    let layer = r.layers.len().saturating_sub(1);                    let mut from = pos;                    for p in &path {                        r.segs.push(Seg { a: from, b: *p, extrude: extruding, layer });                        from = *p;                    }                }                pos = to;            }            ('G', 4) => {                r.time += get('P').map(|ms| ms / 1000.0).or(get('S')).unwrap_or(0.0);            }            ('G', 20) => inch = true,            ('G', 21) => inch = false,            ('G', 28) => {                count(&mut r, "home");                let any = get('X').is_some() || get('Y').is_some() || get('Z').is_some();                for (i, c) in ['X', 'Y', 'Z'].iter().enumerate() {                    if !any || get(*c).is_some() {                        pos[i] = 0.0;                    }                }            }            ('G', 90) => {                abs = true;                e_abs = true;            }            ('G', 91) => {                abs = false;                e_abs = false;            }            ('G', 92) => {                for (i, c) in ['X', 'Y', 'Z'].iter().enumerate() {                    if let Some(v) = get(*c) {                        pos[i] = v * scale;                    }                }                if let Some(v) = get('E') {                    e = v * scale;                }            }            ('M', 82) => e_abs = true,            ('M', 83) => e_abs = false,            ('M', 104) | ('M', 109) => {                if let Some(s) = get('S') {                    r.temps.entry("hotend").or_insert(json!(s));                    r.temps.insert("hotend_last".into(), json!(s));                }            }            ('M', 140) | ('M', 190) => {                if let Some(s) = get('S') {                    r.temps.entry("bed").or_insert(json!(s));                }            }            ('M', 106) => count(&mut r, "fan_on"),            ('M', 107) => count(&mut r, "fan_off"),            ('T', n) => {                if !r.tools.contains(&n) {                    r.tools.push(n);                }                count(&mut r, "tool_changes");            }            ('G', _) | ('M', _) => {                if r.unknown.len() < 30 {                    let c = format!("{}{}", letter, num);                    if !r.unknown.contains(&c) {                        r.unknown.push(c);                    }                }            }            _ => {}        }    }    Ok(r)}fn read(path: &str) -> Result<String, String> {    let b = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?;    if b.len() > 500_000_000 {        return Err("at most 500 MB of G-code".into());    }    Ok(String::from_utf8_lossy(&b).into_owned())}fn hms(s: f64) -> String {    let s = s.round() as i64;    format!("{}h {:02}m {:02}s", s / 3600, (s % 3600) / 60, s % 60)}/// What a G-code file will do: `{ lines, time_s, time (h m s), filament_mm, filament_m, volume_cm3, weight_g, cost, layers, first_layer_height, layer_height, print_bounds, travel_bounds, temperatures, tools, counts, notes (the slicer's), unknown (commands not followed) }`./// `options`: `{ diameter: 1.75 (mm), density: 1.24 (g/cm³, PLA), price_per_kg, acceleration: 1000 (mm/s²; 0 for none), max_speed: 500 (mm/s) }`. The time is an estimate: firmware look-ahead and jerk make real prints a little faster or slower.pub fn gcode_stats(path: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let r = run(&read(path)?, &options, false)?;    let d = options["diameter"].as_f64().unwrap_or(1.75);    let rho = options["density"].as_f64().unwrap_or(1.24);    let vol = PI * (d / 2.0).powi(2) * r.filament / 1000.0;    let weight = vol * rho;    let zs: Vec<f64> = r.layers.iter().map(|l| l.0).collect();    let mut heights: Vec<f64> = zs.windows(2).map(|w| ((w[1] - w[0]) * 1000.0).round() / 1000.0).filter(|h| *h > 0.0).collect();    heights.sort_by(|a, b| a.partial_cmp(b).unwrap());    let typical = heights.get(heights.len() / 2).copied();    Ok(json!({        "lines": r.lines, "time_s": r.time.round(), "time": hms(r.time),        "filament_mm": (r.filament * 10.0).round() / 10.0, "filament_m": (r.filament / 10.0).round() / 100.0,        "volume_cm3": (vol * 100.0).round() / 100.0, "weight_g": (weight * 100.0).round() / 100.0,        "cost": options["price_per_kg"].as_f64().map(|p| (p * weight / 1000.0 * 100.0).round() / 100.0),        "layers": r.layers.len(), "first_layer_height": zs.first().map(|z| (z * 1000.0).round() / 1000.0), "layer_height": typical,        "print_bounds": r.print.json(), "travel_bounds": r.travel.json(), "temperatures": r.temps, "tools": r.tools, "counts": r.counts, "notes": r.notes, "unknown": r.unknown,    }))}/// Layer by layer: `[{ layer, z, filament_mm, time_s }]`, for a progress bar or to find where a print will be at a time.pub fn gcode_layers(path: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    let r = run(&read(path)?, &options, false)?;    Ok(r.layers.iter().enumerate().map(|(i, l)| json!({ "layer": i + 1, "z": (l.0 * 1000.0).round() / 1000.0, "filament_mm": (l.1 * 10.0).round() / 10.0, "time_s": (l.2 * 10.0).round() / 10.0 })).collect())}/// One layer (from 1) drawn from above as SVG, `size` pixels wide: extrusion in dark lines, travel moves dashed when `travel` is true.pub fn gcode_layer_svg(path: &str, layer: i64, size: i64, travel: bool) -> Result<String, String> {    let r = run(&read(path)?, &json!({}), true)?;    if layer < 1 || layer as usize > r.layers.len() {        return Err(format!("there are {} layers", r.layers.len()));    }    let li = layer as usize - 1;    let b = r.print;    if !b.any {        return Err("nothing is printed".into());    }    let (w, h) = ((b.max[0] - b.min[0]).max(1.0), (b.max[1] - b.min[1]).max(1.0));    let px = size.clamp(50, 4000) as f64;    let s = (px - 20.0) / w.max(h);    let (vw, vh) = (w * s + 20.0, h * s + 20.0);    let tx = |x: f64| 10.0 + (x - b.min[0]) * s;    let ty = |y: f64| vh - 10.0 - (y - b.min[1]) * s;    let mut ext = String::new();    let mut trav = String::new();    for g in r.segs.iter().filter(|g| g.layer == li) {        let d = format!("M{:.1} {:.1}L{:.1} {:.1}", tx(g.a[0]), ty(g.a[1]), tx(g.b[0]), ty(g.b[1]));        if g.extrude { ext.push_str(&d) } else if travel { trav.push_str(&d) }    }    Ok(format!(        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{:.0}\" height=\"{:.0}\" viewBox=\"0 0 {:.0} {:.0}\"><rect width=\"100%\" height=\"100%\" fill=\"#fff\"/><path d=\"{}\" fill=\"none\" stroke=\"#c33\" stroke-width=\"0.6\" stroke-dasharray=\"3 3\"/><path d=\"{}\" fill=\"none\" stroke=\"#1b1630\" stroke-width=\"{:.2}\" stroke-linecap=\"round\"/><text x=\"10\" y=\"14\" font-family=\"sans-serif\" font-size=\"12\">layer {} · z {:.2} mm</text></svg>",        vw, vh, vw, vh, trav, ext, (0.45 * s).max(0.5), layer, r.layers[li].0    ))}
3D models models_3d.qwer · 1.0.0STL and OBJ models read: triangles, size, volume, surface area and whether the mesh is closed, a 3D-print quote by weight, and OBJ turned into binary STL.filesuploadsprintingcalculators6 views · 7 downloadsGarmin FIT files fit.qwer · 1.0.0Garmin FIT files from watches and bike computers read: the activity's distance, time, pace, heart rate, cadence, power and ascent, its laps, the track's points, any message as data, and the track as GPX.sportsgpsfilessensorshealth5 views · 7 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.iotsensorsdevicesbinarynetwork8 views · 10 downloadsNMEA GPS nmea.qwer · 1.0.0NMEA 0183 sentences from GPS receivers read: position, time, speed, course, altitude, satellites and fix quality, with checksums checked.gpsdevicesmarinetrackingimport7 views · 8 downloadsOBD-II obd2.qwer · 1.0.0OBD-II answers from an ELM327 adapter decoded: speed, RPM, temperatures, load, fuel level and voltage, trouble codes such as P0133, and the PIDs a car supports.carsdevicesfleetsensorsiot8 views · 8 downloadsReceipt printers escpos.qwer · 1.0.0Receipts for ESC/POS thermal printers: aligned and bold lines, two-column rows, QR codes and barcodes, the cutter and the cash drawer, as base64, a file, or sent to port 9100.printingreceiptsposdevices7 views · 6 downloads
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On G-code for 3D printers: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.