code wiki / (root) / nx_preflight.nx

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}