nx_machine_graph.nx source
↩ module page · 373 lines · 15479 B
1// nx_machine_graph.nx -- data-driven printer description: build
2// volume, kinematics, motion envelope, thermal limits, hardware
3// geometry. No magic numbers anywhere in the slicer (Cardinal 11)
4// -- every printer-specific value lives here, parameterized.
5//
6// Per cardinal NISHI_3D_PRINT_ROADMAP §2.8 + bits-up-exceed axis
7// "cross-printer profiles don't port": adding a new printer to the
8// substrate's slicer is DESCRIBING its NxMachineGraph, NOT rewriting
9// any slicer code. This is the architecture promise that makes
10// P6/P7 multi-printer proofs achievable, not aspirational.
11//
12// Distinct from nx_motion.nx (which is Lucas-Kanade optical flow
13// for video -- totally different concept; printer "motion" here is
14// the kinematic envelope, not frame-to-frame estimation).
15//
16// Unit conventions per field (documented at struct definition):
17// - Build volume in integer millimetres (no sub-mm precision needed)
18// - Nozzle / filament diameter in integer micrometres
19// - Velocities in integer mm/s
20// - Accelerations in integer mm/s²
21// - Temperatures in integer °C
22// - Q14 fixed-point only where ratios are needed (none here yet)
23//
24// Why mixed integer units rather than uniform Q14:
25// Real-world printer specs are quoted in mm/s, mm/s², °C -- the
26// integer units match operator + manufacturer conventions. Slicer
27// composes nx_fp32_q14 + nx_machine_graph at code points where
28// it needs cross-unit math, with explicit conversion.
29//
30// Operator-verified defaults: nx_machine_graph_qidi_xmax3() returns
31// values verified 2026-05-19 against official Qidi US store specs.
32// Operators MUST verify against their actual unit before any real
33// print -- printer-to-printer variance exists even within model.
34//
35// license_tier: ORIGINAL
36
37import "nx_syscalls.nx"
38import "nx_print_process.nx"
39
40// ===== sealed-enum: kinematics_type ================================
41
42const NX_KIN_CARTESIAN: i64 = 0
43const NX_KIN_COREXY: i64 = 1
44const NX_KIN_DELTA: i64 = 2
45const NX_KIN_SCARA: i64 = 3
46const NX_KIN_HBOT: i64 = 4
47// Non-FDM-typical kinematics added for machine-agnostic architecture
48// (2026-05-20): the substrate slicer + emit stack is reusable across
49// any additive machinery, not just Cartesian/CoreXY FDM printers.
50const NX_KIN_ROBOT_6DOF: i64 = 5 // arm-mounted nozzle (DIW concrete,
51 // robotic FDM, multi-axis SLA)
52const NX_KIN_GANTRY: i64 = 6 // large-format gantry (concrete
53 // construction-scale printers,
54 // overhead crane DIW)
55const NX_KIN_CONVEYOR: i64 = 7 // belt printer (Creality CR-30,
56 // White Knight; infinite-Z via
57 // angled gantry on conveyor)
58const NX_KIN_N: i64 = 8
59
60func nx_kin_is_valid(k: i64) -> i64 {
61 if k < 0 { return 0 }
62 if k >= NX_KIN_N { return 0 }
63 return 1
64}
65
66// ===== kinematics-class predicates (architecture-reusable) =========
67//
68// Each predicate is O(1) so the slicer can dispatch on kinematics
69// physics without hardcoding per-machine assumptions. Predicates
70// answer the "what does THIS kinematics allow?" questions that any
71// reusable architecture needs.
72
73// 1 if the kinematics has a finite Z build envelope (false for
74// CONVEYOR which has effectively unbounded Z via belt advancement).
75func nx_kinematics_finite_z(k: i64) -> i64 {
76 if k == NX_KIN_CONVEYOR { return 0 }
77 return 1
78}
79
80// 1 if X and Y motors move independently (Cartesian/Gantry/SCARA/
81// Robot). CoreXY/HBot/Delta couple motors so jerk envelopes differ.
82func nx_kinematics_xy_decoupled(k: i64) -> i64 {
83 if k == NX_KIN_CARTESIAN { return 1 }
84 if k == NX_KIN_GANTRY { return 1 }
85 if k == NX_KIN_SCARA { return 1 }
86 if k == NX_KIN_ROBOT_6DOF { return 1 }
87 if k == NX_KIN_CONVEYOR { return 1 } // gantry on conveyor
88 return 0
89}
90
91// 1 if the kinematics has >= 5 degrees of freedom (multi-axis enables
92// non-planar slicing, overhang-free toolpaths, robotic-arm-mounted
93// FDM/DIW). Only Robot-6DOF qualifies for now.
94func nx_kinematics_5_axis_or_more(k: i64) -> i64 {
95 if k == NX_KIN_ROBOT_6DOF { return 1 }
96 return 0
97}
98
99// 1 if the build platform moves (belt for CONVEYOR; some Cartesians
100// have a moving bed in Y). Useful for the slicer when computing
101// cooling-airflow direction relative to the deposited filament.
102func nx_kinematics_bed_translates(k: i64) -> i64 {
103 if k == NX_KIN_CONVEYOR { return 1 }
104 if k == NX_KIN_CARTESIAN { return 1 } // Prusa-style moving bed
105 return 0
106}
107
108// ===== sealed-enum: extruder_type ==================================
109
110const NX_EXT_BOWDEN: i64 = 0
111const NX_EXT_DIRECT_DRIVE: i64 = 1
112const NX_EXT_TOOLCHANGER: i64 = 2
113const NX_EXT_N: i64 = 3
114
115// ===== struct ======================================================
116
117struct NxMachineGraph {
118 // Build volume (mm)
119 build_x_mm: i64,
120 build_y_mm: i64,
121 build_z_mm: i64,
122
123 // Kinematics
124 kinematics: i64, // NX_KIN_*
125
126 // Motion envelope
127 max_velocity_xy_mms: i64, // mm/s
128 max_velocity_z_mms: i64,
129 max_velocity_e_mms: i64, // extruder
130 max_accel_mms2: i64, // mm/s²
131 max_jerk_mms: i64, // mm/s (junction velocity)
132
133 // Thermal envelope
134 hotend_max_c: i64,
135 hotend_min_c: i64,
136 bed_max_c: i64,
137 chamber_max_c: i64, // 0 if no heated chamber
138
139 // Hardware geometry
140 nozzle_dia_um: i64, // 400 = 0.4 mm
141 filament_dia_um: i64, // 1750 = 1.75 mm
142 extruder_type: i64, // NX_EXT_*
143 max_flow_mm3s: i64, // hotend high-flow ceiling
144
145 // Probing + features
146 has_auto_bed_mesh: i64, // 0 or 1
147 has_z_probe: i64,
148 has_input_shaping: i64, // Klipper / Marlin 2.1+
149
150 // Process class -- which additive-manufacturing process this
151 // machine performs (FDM polymer / DIW cement / DMLS / etc.).
152 // See runtime/nx_print_process.nx for the sealed enum. Set on
153 // every factory so the slicer can dispatch to the right emit
154 // backend without per-machine specialization. (Architecture-
155 // reusable axis 2026-05-20: substrate is machine-agnostic;
156 // Qidi X-Max 3 happens to be the FDM-polymer reference impl.)
157 process_class: i64,
158}
159
160const NX_MACHINE_BYTES: i64 = 176 // 22 fields × 8
161
162// ===== allocation ==================================================
163
164func nx_machine_graph_alloc() -> *NxMachineGraph {
165 return (sys_mmap(NX_MACHINE_BYTES)) as *NxMachineGraph
166}
167
168// ===== validation ==================================================
169
170const NX_MACHINE_OK: i64 = 0
171const NX_MACHINE_ERR_BAD_KIN: i64 = 1
172const NX_MACHINE_ERR_BAD_EXT: i64 = 2
173const NX_MACHINE_ERR_BAD_BUILD: i64 = 3
174const NX_MACHINE_ERR_BAD_THERMAL: i64 = 4
175const NX_MACHINE_ERR_BAD_MOTION: i64 = 5
176const NX_MACHINE_ERR_BAD_HARDWARE: i64 = 6
177
178// Returns NX_MACHINE_OK or first verdict that fails. Defends the
179// slicer boundary (Cardinal 12) from operator-typo machine graphs.
180func nx_machine_graph_validate(g: *NxMachineGraph) -> i64 {
181 if nx_kin_is_valid(g.kinematics) != 1 { return NX_MACHINE_ERR_BAD_KIN }
182 if g.extruder_type < 0 { return NX_MACHINE_ERR_BAD_EXT }
183 if g.extruder_type >= NX_EXT_N { return NX_MACHINE_ERR_BAD_EXT }
184
185 if g.build_x_mm <= 0 { return NX_MACHINE_ERR_BAD_BUILD }
186 if g.build_y_mm <= 0 { return NX_MACHINE_ERR_BAD_BUILD }
187 if g.build_z_mm <= 0 { return NX_MACHINE_ERR_BAD_BUILD }
188
189 if g.hotend_max_c <= g.hotend_min_c { return NX_MACHINE_ERR_BAD_THERMAL }
190 if g.hotend_max_c <= 0 { return NX_MACHINE_ERR_BAD_THERMAL }
191 if g.bed_max_c < 0 { return NX_MACHINE_ERR_BAD_THERMAL }
192 if g.chamber_max_c < 0 { return NX_MACHINE_ERR_BAD_THERMAL }
193
194 if g.max_velocity_xy_mms <= 0 { return NX_MACHINE_ERR_BAD_MOTION }
195 if g.max_velocity_z_mms <= 0 { return NX_MACHINE_ERR_BAD_MOTION }
196 if g.max_velocity_e_mms <= 0 { return NX_MACHINE_ERR_BAD_MOTION }
197 if g.max_accel_mms2 <= 0 { return NX_MACHINE_ERR_BAD_MOTION }
198 if g.max_jerk_mms < 0 { return NX_MACHINE_ERR_BAD_MOTION }
199
200 if g.nozzle_dia_um <= 0 { return NX_MACHINE_ERR_BAD_HARDWARE }
201 if g.filament_dia_um <= 0 { return NX_MACHINE_ERR_BAD_HARDWARE }
202 if g.max_flow_mm3s <= 0 { return NX_MACHINE_ERR_BAD_HARDWARE }
203
204 return NX_MACHINE_OK
205}
206
207// ===== factory: Qidi X-Max 3 =======================================
208//
209// Verified against https://qidi3d.com/pages/x-max3 2026-05-19.
210// Build volume + accel + hotend + chamber + max_flow all match
211// official spec. Z velocity, jerk, min temp are conservative
212// substrate defaults (operator should refine via nx_calibrate when
213// P5 ships).
214
215func nx_machine_graph_qidi_xmax3() -> *NxMachineGraph {
216 let g: *NxMachineGraph = nx_machine_graph_alloc()
217 g.build_x_mm = 325 // verified
218 g.build_y_mm = 325 // verified
219 g.build_z_mm = 315 // verified
220 g.kinematics = NX_KIN_COREXY
221 g.max_velocity_xy_mms = 600 // verified (advertised peak)
222 g.max_velocity_z_mms = 30 // conservative
223 g.max_velocity_e_mms = 100 // conservative
224 g.max_accel_mms2 = 20000 // verified
225 g.max_jerk_mms = 12 // Klipper square_corner_velocity equiv
226 g.hotend_max_c = 350 // verified
227 g.hotend_min_c = 170 // PLA minimum
228 g.bed_max_c = 120 // typical for class
229 g.chamber_max_c = 65 // verified
230 g.nozzle_dia_um = 400 // default 0.4 mm
231 g.filament_dia_um = 1750 // 1.75 mm
232 g.extruder_type = NX_EXT_DIRECT_DRIVE
233 g.max_flow_mm3s = 35 // verified (high-flow hotend)
234 g.has_auto_bed_mesh = 1
235 g.has_z_probe = 1
236 g.has_input_shaping = 1 // Klipper firmware
237 g.process_class = NX_PROCESS_FDM_POLYMER
238 return g
239}
240
241// ===== factory: Prusa MK4 / MK4S ===================================
242//
243// Verified specs (Prusa Research 2026 public data): i3-class bed-
244// slinger (NOT CoreXY -- bed is Y-axis), Nextruder loadcell (gold-
245// standard first-layer), input shaping in 5.0.0+ firmware, high-flow
246// optional 0.6mm nozzle, all-metal hotend. no heated chamber.
247//
248// Prusa-MK4-specific feature: loadcell first-layer means
249// has_z_probe=1 reflects PHYSICAL touch-the-bed measurement, NOT
250// inductive probe -- the most accurate first-layer in the industry
251// per Prusa Research's loadcell research.
252
253func nx_machine_graph_prusa_mk4() -> *NxMachineGraph {
254 let g: *NxMachineGraph = nx_machine_graph_alloc()
255 g.build_x_mm = 250
256 g.build_y_mm = 210
257 g.build_z_mm = 220
258 g.kinematics = NX_KIN_CARTESIAN
259 g.max_velocity_xy_mms = 200
260 g.max_velocity_z_mms = 12
261 g.max_velocity_e_mms = 80
262 g.max_accel_mms2 = 2000
263 g.max_jerk_mms = 5
264 g.hotend_max_c = 290
265 g.hotend_min_c = 170
266 g.bed_max_c = 120
267 g.chamber_max_c = 0 // no chamber (open-frame i3)
268 g.nozzle_dia_um = 400
269 g.filament_dia_um = 1750
270 g.extruder_type = NX_EXT_DIRECT_DRIVE
271 g.max_flow_mm3s = 35 // Nextruder high-flow capability
272 g.has_auto_bed_mesh = 1
273 g.has_z_probe = 1 // loadcell first-layer
274 g.has_input_shaping = 1 // firmware 5.0.0+
275 g.process_class = NX_PROCESS_FDM_POLYMER
276 return g
277}
278
279// ===== factory: Voron 2.4 (300mm canonical) ========================
280//
281// Voron-design canonical 300mm CoreXY build. Operator-built so
282// specifics vary; this captures the most common config (Stealthburner
283// + Phaetus Rapido + 300mm bed + optional 60°C chamber). Klipper
284// firmware (Voron canonical) -- input shaping + bed mesh standard.
285
286func nx_machine_graph_voron_2_4() -> *NxMachineGraph {
287 let g: *NxMachineGraph = nx_machine_graph_alloc()
288 g.build_x_mm = 300
289 g.build_y_mm = 300
290 g.build_z_mm = 300
291 g.kinematics = NX_KIN_COREXY
292 g.max_velocity_xy_mms = 500
293 g.max_velocity_z_mms = 30
294 g.max_velocity_e_mms = 100
295 g.max_accel_mms2 = 10000
296 g.max_jerk_mms = 12
297 g.hotend_max_c = 300 // Rapido HF typical
298 g.hotend_min_c = 170
299 g.bed_max_c = 120
300 g.chamber_max_c = 60 // operator-installed; common build
301 g.nozzle_dia_um = 400
302 g.filament_dia_um = 1750
303 g.extruder_type = NX_EXT_DIRECT_DRIVE
304 g.max_flow_mm3s = 25 // conservative; HF hotend goes higher
305 g.has_auto_bed_mesh = 1
306 g.has_z_probe = 1
307 g.has_input_shaping = 1
308 g.process_class = NX_PROCESS_FDM_POLYMER
309 return g
310}
311
312// ===== factory: Bambu Lab X1 Carbon ================================
313//
314// Verified vs https://bambulab.com/en-us/x1 (2026-05). CoreXY,
315// active vibration compensation, micro-LiDAR (closed-loop first-
316// layer + flow calibration, hardware-bound), AMS multi-material
317// integration. Substrate composes the Klipper-equivalent endpoints
318// only -- LiDAR + AMS are operator-side proprietary features.
319
320func nx_machine_graph_bambu_x1c() -> *NxMachineGraph {
321 let g: *NxMachineGraph = nx_machine_graph_alloc()
322 g.build_x_mm = 256
323 g.build_y_mm = 256
324 g.build_z_mm = 256
325 g.kinematics = NX_KIN_COREXY
326 g.max_velocity_xy_mms = 500
327 g.max_velocity_z_mms = 30
328 g.max_velocity_e_mms = 100
329 g.max_accel_mms2 = 20000
330 g.max_jerk_mms = 12
331 g.hotend_max_c = 300 // hardened steel default
332 g.hotend_min_c = 170
333 g.bed_max_c = 110
334 g.chamber_max_c = 60 // passive (no heater); reaches via hotend + bed
335 g.nozzle_dia_um = 400
336 g.filament_dia_um = 1750
337 g.extruder_type = NX_EXT_DIRECT_DRIVE
338 g.max_flow_mm3s = 35
339 g.has_auto_bed_mesh = 1 // LiDAR-based
340 g.has_z_probe = 1 // LiDAR + force sensors
341 g.has_input_shaping = 1 // active vibration compensation
342 g.process_class = NX_PROCESS_FDM_POLYMER
343 return g
344}
345
346// ===== process_class setter (architecture-reusable) ================
347//
348// Allow callers to construct a machine graph for a non-FDM process
349// (e.g., DIW concrete printer) by composing the existing struct
350// plus the process_class field. No FDM-specific assumption is
351// baked into the substrate.
352
353func nx_machine_set_process(g: *NxMachineGraph, process_class: i64) -> i64 {
354 if (g as i64) == 0 { return -1 }
355 if nx_process_is_valid(process_class) == 0 { return -1 }
356 g.process_class = process_class
357 return 0
358}
359
360// ===== sealed-enum verdict names ==================================
361//
362// Used by audit dashboards + smokes for human-readable failures.
363
364func nx_machine_verdict_name(v: i64) -> *u8 {
365 if v == NX_MACHINE_OK { return "OK" }
366 if v == NX_MACHINE_ERR_BAD_KIN { return "BAD_KIN" }
367 if v == NX_MACHINE_ERR_BAD_EXT { return "BAD_EXT" }
368 if v == NX_MACHINE_ERR_BAD_BUILD { return "BAD_BUILD" }
369 if v == NX_MACHINE_ERR_BAD_THERMAL { return "BAD_THERMAL" }
370 if v == NX_MACHINE_ERR_BAD_MOTION { return "BAD_MOTION" }
371 if v == NX_MACHINE_ERR_BAD_HARDWARE { return "BAD_HARDWARE" }
372 return "UNKNOWN"
373}