nx_preflight.nx source
↩ module page · 244 lines · 9728 B
1// nx_preflight.nx -- bulletproof pre-print orchestrator.
2//
3// Composes every shipped pre-print sanity primitive into a single
4// SAFE / WARN / UNSAFE verdict that gates G-code emission. Per
5// NISHI_3D_PRINT_BULLETPROOF_PREFLIGHT_ROADMAP -- "push and play"
6// equivalent of Bambu LiDAR/AI/loadcell + Klipper KAMP + Prusa
7// loadcell preflight, sovereign substrate-native.
8//
9// Checks performed (this primitive's contract):
10// 1. nx_mesh_is_manifold (watertight geometry)
11// 2. nx_mesh_fits_machine (bbox <= build volume)
12// 3. nx_polymer_validate_profile (temps within polymer envelope)
13// 4. nx_flow_envelope_check (slicer flow <= min(machine, material))
14//
15// On any UNSAFE: refuse to emit print-start command. On WARN:
16// emit but surface the warning to operator.
17//
18// Per cardinal feedback-no-false-ok-substrate-honesty-audit:
19// claiming "bulletproof" without ALL these checks IS the false-OK
20// class. Future versions add G-code structural sanity, Moonraker
21// ready-state, calibration freshness (queued P-BP-2..P-BP-6).
22//
23// Per cardinal feedback-thymus-sovereignty-audit-soma-germline:
24// every check composes ONLY substrate primitives -- no Bambu/Prusa/
25// vendor APIs, no cloud lookups, no third-party services.
26//
27// license_tier: ORIGINAL
28
29import "nx_syscalls.nx"
30import "nx_mesh.nx"
31import "nx_mesh_print_check.nx"
32import "nx_machine_graph.nx"
33import "nx_material_profile.nx"
34import "nx_polymer_thermal.nx"
35
36// ===== sealed-enum verdicts ========================================
37
38const NX_PREFLIGHT_SAFE: i64 = 0
39const NX_PREFLIGHT_WARN: i64 = 1
40const NX_PREFLIGHT_UNSAFE: i64 = 2
41
42// ===== sealed-enum failure reasons =================================
43
44const NX_PFR_OK: i64 = 0
45const NX_PFR_NOT_MANIFOLD: i64 = 1
46const NX_PFR_MESH_TOO_LARGE: i64 = 2
47const NX_PFR_BAD_THERMAL: i64 = 3
48const NX_PFR_FLOW_EXCEEDED_MACHINE: i64 = 4
49const NX_PFR_FLOW_EXCEEDED_MATERIAL: i64 = 5
50const NX_PFR_BAD_LAYER_HEIGHT: i64 = 6
51const NX_PFR_NULL_INPUT: i64 = 7
52const NX_PFR_LAYER_TOO_THICK: i64 = 8
53const NX_PFR_LAYER_TOO_THIN: i64 = 9
54const NX_PFR_N: i64 = 10
55
56func nx_pfr_name(r: i64) -> *u8 {
57 if r == NX_PFR_OK { return "OK" }
58 if r == NX_PFR_NOT_MANIFOLD { return "NOT_MANIFOLD" }
59 if r == NX_PFR_MESH_TOO_LARGE { return "MESH_TOO_LARGE" }
60 if r == NX_PFR_BAD_THERMAL { return "BAD_THERMAL" }
61 if r == NX_PFR_FLOW_EXCEEDED_MACHINE { return "FLOW_EXCEEDED_MACHINE" }
62 if r == NX_PFR_FLOW_EXCEEDED_MATERIAL { return "FLOW_EXCEEDED_MATERIAL" }
63 if r == NX_PFR_BAD_LAYER_HEIGHT { return "BAD_LAYER_HEIGHT" }
64 if r == NX_PFR_NULL_INPUT { return "NULL_INPUT" }
65 if r == NX_PFR_LAYER_TOO_THICK { return "LAYER_TOO_THICK" }
66 if r == NX_PFR_LAYER_TOO_THIN { return "LAYER_TOO_THIN" }
67 return "UNKNOWN"
68}
69
70// (nx_layer_envelope_check moved below NX_Q14_TO_MM const definition.)
71
72// ===== result struct ===============================================
73
74struct NxPreflightResult {
75 verdict: i64,
76 first_reason: i64,
77 n_checks_run: i64,
78 n_failed: i64,
79}
80
81const NX_PREFLIGHT_BYTES: i64 = 32
82
83// ===== individual check: mesh fits build volume ====================
84//
85// Converts mesh Q14 extent to integer mm + compares against machine
86// build_x_mm/build_y_mm/build_z_mm. Returns SAFE if fits, UNSAFE
87// with MESH_TOO_LARGE if any axis exceeds.
88
89const NX_Q14_TO_MM: i64 = 16384
90
91func nx_mesh_fits_machine(mesh: *NxMesh, machine: *NxMachineGraph) -> i64 {
92 if (mesh as i64) == 0 { return NX_PFR_NULL_INPUT }
93 if (machine as i64) == 0 { return NX_PFR_NULL_INPUT }
94 let bbox: *NxMeshBBox = nx_mesh_bbox_compute(mesh)
95 if bbox.valid != 1 { return NX_PFR_NULL_INPUT }
96
97 // Mesh extent in mm (Q14 / 16384, integer).
98 let ext_x_mm: i64 = (bbox.max_x - bbox.min_x) / NX_Q14_TO_MM
99 let ext_y_mm: i64 = (bbox.max_y - bbox.min_y) / NX_Q14_TO_MM
100 let ext_z_mm: i64 = (bbox.max_z - bbox.min_z) / NX_Q14_TO_MM
101
102 if ext_x_mm > machine.build_x_mm { return NX_PFR_MESH_TOO_LARGE }
103 if ext_y_mm > machine.build_y_mm { return NX_PFR_MESH_TOO_LARGE }
104 if ext_z_mm > machine.build_z_mm { return NX_PFR_MESH_TOO_LARGE }
105 return NX_PFR_OK
106}
107
108// ===== individual check: flow envelope =============================
109//
110// Volumetric flow at slicer's max parameters must be <= min of:
111// machine.max_flow_mm3s (hotend physical capability)
112// material.typical_max_flow_mm3s (material melt + viscosity ceiling)
113//
114// Formula (mm³/s) = line_width_mm × layer_height_mm × max_speed_mms.
115// Inputs are Q14 mm + integer mm/s.
116// In Q14: line_width_q14 * layer_height_q14 / Q14 = mm² in Q14
117// * max_speed_mms / Q14 = mm³/s integer
118//
119// Returns OK, FLOW_EXCEEDED_MACHINE, FLOW_EXCEEDED_MATERIAL, or
120// BAD_LAYER_HEIGHT (degenerate input).
121
122func nx_flow_envelope_check(line_width_q14: i64, layer_height_q14: i64,
123 max_speed_mms: i64,
124 machine: *NxMachineGraph,
125 material: *NxMaterialProfile) -> i64 {
126 if line_width_q14 <= 0 { return NX_PFR_BAD_LAYER_HEIGHT }
127 if layer_height_q14 <= 0 { return NX_PFR_BAD_LAYER_HEIGHT }
128 if max_speed_mms <= 0 { return NX_PFR_BAD_LAYER_HEIGHT }
129 if (machine as i64) == 0 { return NX_PFR_NULL_INPUT }
130 if (material as i64) == 0 { return NX_PFR_NULL_INPUT }
131
132 // Cross-area mm² in Q14 = w_q14 * h_q14 / Q14
133 let cross_q14: i64 = (line_width_q14 * layer_height_q14) / NX_Q14_TO_MM
134 // Volumetric flow mm³/s integer = cross_q14 * speed_mms / Q14
135 let flow_mm3s: i64 = (cross_q14 * max_speed_mms) / NX_Q14_TO_MM
136
137 if flow_mm3s > machine.max_flow_mm3s {
138 return NX_PFR_FLOW_EXCEEDED_MACHINE
139 }
140 if flow_mm3s > material.typical_max_flow_mm3s {
141 return NX_PFR_FLOW_EXCEEDED_MATERIAL
142 }
143 return NX_PFR_OK
144}
145
146// ===== individual check: layer height envelope vs nozzle ==========
147//
148// Industry best practice (Prusa / Bambu / Klipper / OrcaSlicer
149// defaults all agree): layer_height should fall in
150// [0.25 × nozzle_dia, 0.75 × nozzle_dia].
151// Too thick: poor layer bonding (bead can't squish into prior);
152// over-extrusion artifacts.
153// Too thin: under-extrusion gaps + extruder skipping + slow.
154//
155// For 0.4mm nozzle: valid 0.10-0.30 mm. 0.35mm on 0.4mm fails.
156
157func nx_layer_envelope_check(layer_height_q14: i64,
158 machine: *NxMachineGraph) -> i64 {
159 if layer_height_q14 <= 0 { return NX_PFR_BAD_LAYER_HEIGHT }
160 if (machine as i64) == 0 { return NX_PFR_NULL_INPUT }
161 if machine.nozzle_dia_um <= 0 { return NX_PFR_NULL_INPUT }
162
163 let nozzle_q14: i64 = (machine.nozzle_dia_um * NX_Q14_TO_MM) / 1000
164 let max_layer: i64 = (nozzle_q14 * 3) / 4
165 let min_layer: i64 = nozzle_q14 / 4
166
167 if layer_height_q14 > max_layer { return NX_PFR_LAYER_TOO_THICK }
168 if layer_height_q14 < min_layer { return NX_PFR_LAYER_TOO_THIN }
169 return NX_PFR_OK
170}
171
172// ===== orchestrator ================================================
173//
174// Runs every check; returns NxPreflightResult with first failure
175// reason if any. SAFE iff all checks return OK. No WARN tier yet
176// (added in P-BP-1+).
177
178func nx_preflight_check(mesh: *NxMesh,
179 machine: *NxMachineGraph,
180 material: *NxMaterialProfile,
181 layer_height_q14: i64,
182 line_width_q14: i64,
183 max_speed_mms: i64) -> *NxPreflightResult {
184 let r: *NxPreflightResult = (sys_mmap(NX_PREFLIGHT_BYTES)) as *NxPreflightResult
185 r.verdict = NX_PREFLIGHT_SAFE
186 r.first_reason = NX_PFR_OK
187 r.n_checks_run = 0
188 r.n_failed = 0
189
190 // Check 1: manifold
191 r.n_checks_run = r.n_checks_run + 1
192 if nx_mesh_is_manifold(mesh) != NX_MESH_PRINT_OK {
193 r.verdict = NX_PREFLIGHT_UNSAFE
194 r.first_reason = NX_PFR_NOT_MANIFOLD
195 r.n_failed = r.n_failed + 1
196 return r
197 }
198
199 // Check 2: bbox fits
200 r.n_checks_run = r.n_checks_run + 1
201 let fits: i64 = nx_mesh_fits_machine(mesh, machine)
202 if fits != NX_PFR_OK {
203 r.verdict = NX_PREFLIGHT_UNSAFE
204 r.first_reason = fits
205 r.n_failed = r.n_failed + 1
206 return r
207 }
208
209 // Check 3: polymer thermal envelope
210 r.n_checks_run = r.n_checks_run + 1
211 if nx_polymer_validate_profile(material) != NX_POLYTHERM_SAFE {
212 r.verdict = NX_PREFLIGHT_UNSAFE
213 r.first_reason = NX_PFR_BAD_THERMAL
214 r.n_failed = r.n_failed + 1
215 return r
216 }
217
218 // Check 4: layer-height envelope vs nozzle diameter
219 // (runs BEFORE flow envelope: layer-height is the more fundamental
220 // input -- if it's wrong, flow calc is irrelevant. Catching the
221 // simpler/more-likely operator mistake first gives the clearer
222 // diagnostic.)
223 r.n_checks_run = r.n_checks_run + 1
224 let env_v: i64 = nx_layer_envelope_check(layer_height_q14, machine)
225 if env_v != NX_PFR_OK {
226 r.verdict = NX_PREFLIGHT_UNSAFE
227 r.first_reason = env_v
228 r.n_failed = r.n_failed + 1
229 return r
230 }
231
232 // Check 5: flow envelope (multi-input: layer × line × speed)
233 r.n_checks_run = r.n_checks_run + 1
234 let flow_v: i64 = nx_flow_envelope_check(line_width_q14, layer_height_q14,
235 max_speed_mms, machine, material)
236 if flow_v != NX_PFR_OK {
237 r.verdict = NX_PREFLIGHT_UNSAFE
238 r.first_reason = flow_v
239 r.n_failed = r.n_failed + 1
240 return r
241 }
242
243 return r
244}