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
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 errorWhat 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 errorLayer 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 errorOne 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.qweOr 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.gcodeThen, in your program, all of it or only what you call:
use gcode.qweruse gcode_stats, gcode_layers from gcode // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// 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
//! 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 ))}Related
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.6 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.5 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.8 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.7 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.8 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.7 views · 6 downloadsThread 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.