Skip to main content

lumen/
json.rs

1//! Typed models for the JSON chunks: `META` (with its `sectors` entries),
2//! `PROF` and `LROV`.
3//!
4//! META, a `META.sectors` entry, a `PROF` profile's `settings` block and an
5//! `LROV` payload all draw on the same field namespace, so they share [`Timing`].
6//! Unknown JSON keys are preserved in `Timing::extra` rather than dropped, which
7//! is what lets this crate re-emit a chunk it did not fully understand
8//! ([`spec/16-versioning.md`] section 10.2).
9//!
10//! Durations are whole milliseconds, lengths are integer micrometers and speeds
11//! are integer micrometers per minute, so all three are modelled as integers and
12//! nothing below one millisecond is expressible. Floating-point JSON syntax is
13//! accepted for every field the specification calls an ordinary number, which is
14//! what a temperature, an energy, a percentage or a density is.
15
16use serde::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18
19/// The ten fields META must carry ([`spec/17-validation.md`] section 11.2).
20pub const REQUIRED_META_FIELDS: [&str; 10] = [
21    "meta_version",
22    "normal_exposure_ms",
23    "bottom_exposure_ms",
24    "bottom_layer_count",
25    "transition_layer_count",
26    "layer_height_um",
27    "lift_slow_distance_um",
28    "lift_slow_speed_um_min",
29    "retract_fast_distance_um",
30    "retract_fast_speed_um_min",
31];
32
33/// The timing, motion, PWM and temperature namespace shared by META, a
34/// `META.sectors` entry, `PROF.settings` and an `LROV` payload.
35///
36/// Every field is optional: META requires some of them, a sector entry or an
37/// `LROV` payload supplies only what it changes, and a `PROF` supplies all of
38/// them. A `bottom_*` field that is absent equals its normal counterpart
39/// ([`spec/14-layer-timing.md`] section 8).
40#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
41pub struct Timing {
42    /// Default layer thickness in micrometers.
43    #[serde(skip_serializing_if = "Option::is_none", default)]
44    pub layer_height_um: Option<u32>,
45    /// Exposure time for normal layers, in milliseconds.
46    #[serde(skip_serializing_if = "Option::is_none", default)]
47    pub normal_exposure_ms: Option<u32>,
48    /// Exposure time for bottom layers, in milliseconds.
49    #[serde(skip_serializing_if = "Option::is_none", default)]
50    pub bottom_exposure_ms: Option<u32>,
51    /// How many layers the bottom range holds.
52    #[serde(skip_serializing_if = "Option::is_none", default)]
53    pub bottom_layer_count: Option<u32>,
54    /// How many layers interpolate between bottom and normal.
55    #[serde(skip_serializing_if = "Option::is_none", default)]
56    pub transition_layer_count: Option<u32>,
57
58    /// Lift, slow segment: the peel. Distance of that one segment.
59    #[serde(skip_serializing_if = "Option::is_none", default)]
60    pub lift_slow_distance_um: Option<u32>,
61    /// Lift, slow segment speed.
62    #[serde(skip_serializing_if = "Option::is_none", default)]
63    pub lift_slow_speed_um_min: Option<u32>,
64    /// Lift, fast segment: the remainder. `0` makes the lift single-stage.
65    #[serde(skip_serializing_if = "Option::is_none", default)]
66    pub lift_fast_distance_um: Option<u32>,
67    /// Lift, fast segment speed.
68    #[serde(skip_serializing_if = "Option::is_none", default)]
69    pub lift_fast_speed_um_min: Option<u32>,
70    /// Retract, fast segment: most of the return. Distance of that one segment.
71    #[serde(skip_serializing_if = "Option::is_none", default)]
72    pub retract_fast_distance_um: Option<u32>,
73    /// Retract, fast segment speed.
74    #[serde(skip_serializing_if = "Option::is_none", default)]
75    pub retract_fast_speed_um_min: Option<u32>,
76    /// Retract, slow segment: the final approach. `0` makes the retract single-stage.
77    #[serde(skip_serializing_if = "Option::is_none", default)]
78    pub retract_slow_distance_um: Option<u32>,
79    /// Retract, slow segment speed.
80    #[serde(skip_serializing_if = "Option::is_none", default)]
81    pub retract_slow_speed_um_min: Option<u32>,
82
83    /// Bottom-layer lift, slow segment distance.
84    #[serde(skip_serializing_if = "Option::is_none", default)]
85    pub bottom_lift_slow_distance_um: Option<u32>,
86    /// Bottom-layer lift, slow segment speed.
87    #[serde(skip_serializing_if = "Option::is_none", default)]
88    pub bottom_lift_slow_speed_um_min: Option<u32>,
89    /// Bottom-layer lift, fast segment distance.
90    #[serde(skip_serializing_if = "Option::is_none", default)]
91    pub bottom_lift_fast_distance_um: Option<u32>,
92    /// Bottom-layer lift, fast segment speed.
93    #[serde(skip_serializing_if = "Option::is_none", default)]
94    pub bottom_lift_fast_speed_um_min: Option<u32>,
95    /// Bottom-layer retract, fast segment distance.
96    #[serde(skip_serializing_if = "Option::is_none", default)]
97    pub bottom_retract_fast_distance_um: Option<u32>,
98    /// Bottom-layer retract, fast segment speed.
99    #[serde(skip_serializing_if = "Option::is_none", default)]
100    pub bottom_retract_fast_speed_um_min: Option<u32>,
101    /// Bottom-layer retract, slow segment distance.
102    #[serde(skip_serializing_if = "Option::is_none", default)]
103    pub bottom_retract_slow_distance_um: Option<u32>,
104    /// Bottom-layer retract, slow segment speed.
105    #[serde(skip_serializing_if = "Option::is_none", default)]
106    pub bottom_retract_slow_speed_um_min: Option<u32>,
107
108    /// Pause before curing, in milliseconds.
109    #[serde(skip_serializing_if = "Option::is_none", default)]
110    pub wait_time_before_cure_ms: Option<u32>,
111    /// Pause after curing, in milliseconds.
112    #[serde(skip_serializing_if = "Option::is_none", default)]
113    pub wait_time_after_cure_ms: Option<u32>,
114    /// Pause after the lift, in milliseconds.
115    #[serde(skip_serializing_if = "Option::is_none", default)]
116    pub wait_time_after_lift_ms: Option<u32>,
117    /// Bottom-layer pause before curing, in milliseconds.
118    #[serde(skip_serializing_if = "Option::is_none", default)]
119    pub bottom_wait_time_before_cure_ms: Option<u32>,
120    /// Bottom-layer pause after curing, in milliseconds.
121    #[serde(skip_serializing_if = "Option::is_none", default)]
122    pub bottom_wait_time_after_cure_ms: Option<u32>,
123    /// Bottom-layer pause after the lift, in milliseconds.
124    #[serde(skip_serializing_if = "Option::is_none", default)]
125    pub bottom_wait_time_after_lift_ms: Option<u32>,
126
127    /// Light PWM for normal layers, `0`-`255`.
128    #[serde(skip_serializing_if = "Option::is_none", default)]
129    pub light_pwm: Option<u32>,
130    /// Light PWM for bottom layers, `0`-`255`.
131    #[serde(skip_serializing_if = "Option::is_none", default)]
132    pub bottom_light_pwm: Option<u32>,
133
134    /// Chamber target temperature in Celsius.
135    #[serde(skip_serializing_if = "Option::is_none", default)]
136    pub chamber_temperature_c: Option<f64>,
137    /// Vat target temperature in Celsius.
138    #[serde(skip_serializing_if = "Option::is_none", default)]
139    pub vat_temperature_c: Option<f64>,
140
141    /// Experimental resin working curve.
142    #[serde(skip_serializing_if = "Option::is_none", default)]
143    pub cure_curve: Option<CureCurve>,
144
145    /// Unknown keys, preserved verbatim.
146    ///
147    /// In a META payload the schema's own `extra` object also lands here, under
148    /// the key `"extra"`; see [`Timing::vendor_extra`].
149    #[serde(flatten)]
150    pub extra: Map<String, Value>,
151}
152
153impl Timing {
154    /// The META or PROF `extra` object, if the payload carried one.
155    pub fn vendor_extra(&self) -> Option<&Value> {
156        self.extra.get("extra")
157    }
158
159    /// Every field name this namespace defines, in schema order.
160    pub fn field_names() -> &'static [&'static str] {
161        &[
162            "layer_height_um",
163            "normal_exposure_ms",
164            "bottom_exposure_ms",
165            "bottom_layer_count",
166            "transition_layer_count",
167            "lift_slow_distance_um",
168            "lift_slow_speed_um_min",
169            "lift_fast_distance_um",
170            "lift_fast_speed_um_min",
171            "retract_fast_distance_um",
172            "retract_fast_speed_um_min",
173            "retract_slow_distance_um",
174            "retract_slow_speed_um_min",
175            "bottom_lift_slow_distance_um",
176            "bottom_lift_slow_speed_um_min",
177            "bottom_lift_fast_distance_um",
178            "bottom_lift_fast_speed_um_min",
179            "bottom_retract_fast_distance_um",
180            "bottom_retract_fast_speed_um_min",
181            "bottom_retract_slow_distance_um",
182            "bottom_retract_slow_speed_um_min",
183            "wait_time_before_cure_ms",
184            "wait_time_after_cure_ms",
185            "wait_time_after_lift_ms",
186            "bottom_wait_time_before_cure_ms",
187            "bottom_wait_time_after_cure_ms",
188            "bottom_wait_time_after_lift_ms",
189            "light_pwm",
190            "bottom_light_pwm",
191            "chamber_temperature_c",
192            "vat_temperature_c",
193            "cure_curve",
194        ]
195    }
196
197    /// Whether every field in `names` is present in the raw payload.
198    ///
199    /// `self` alone cannot answer this: a field that was absent and a field that
200    /// was explicitly `null` deserialize alike, so presence must be read from
201    /// the raw JSON object.
202    pub fn missing_from(object: &Map<String, Value>, names: &[&str]) -> Vec<String> {
203        names
204            .iter()
205            .filter(|name| !object.contains_key(**name))
206            .map(|name| (*name).to_string())
207            .collect()
208    }
209}
210
211/// The experimental resin working curve: Beer-Lambert parameters.
212#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
213pub struct CureCurve {
214    /// Penetration depth in micrometers.
215    pub dp_um: f64,
216    /// Critical exposure in mJ/cm^2.
217    pub ec_mj_cm2: f64,
218    /// Base energy in mJ/cm^2.
219    pub e0_mj_cm2: f64,
220}
221
222/// One entry of the material library.
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct Material {
225    /// Material name. Required and non-empty when the array is present.
226    #[serde(default)]
227    pub name: String,
228    /// Brand, e.g. `DragonFruit`.
229    #[serde(skip_serializing_if = "Option::is_none", default)]
230    pub brand: Option<String>,
231    /// Family, e.g. `standard`.
232    #[serde(skip_serializing_if = "Option::is_none", default)]
233    pub family: Option<String>,
234    /// Density in grams per millilitre.
235    #[serde(skip_serializing_if = "Option::is_none", default)]
236    pub density_g_ml: Option<f64>,
237    /// Display color.
238    #[serde(skip_serializing_if = "Option::is_none", default)]
239    pub color_rgba: Option<[u8; 4]>,
240    /// Bottle price, vendor metadata.
241    #[serde(skip_serializing_if = "Option::is_none", default)]
242    pub bottle_price: Option<f64>,
243    /// Bottle capacity in millilitres, vendor metadata.
244    #[serde(skip_serializing_if = "Option::is_none", default)]
245    pub bottle_capacity_ml: Option<f64>,
246    /// Unknown keys, preserved verbatim.
247    #[serde(flatten)]
248    pub extra: Map<String, Value>,
249}
250
251/// Printer identity, as carried by META and `PROF.compatible_printers`.
252#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
253pub struct Printer {
254    /// Printer or model name.
255    #[serde(skip_serializing_if = "Option::is_none", default)]
256    pub name: Option<String>,
257    /// Manufacturer.
258    #[serde(skip_serializing_if = "Option::is_none", default)]
259    pub manufacturer: Option<String>,
260    /// Firmware version.
261    #[serde(skip_serializing_if = "Option::is_none", default)]
262    pub firmware_version: Option<String>,
263    /// Serial number.
264    #[serde(skip_serializing_if = "Option::is_none", default)]
265    pub serial: Option<String>,
266    /// Display width in pixels.
267    #[serde(skip_serializing_if = "Option::is_none", default)]
268    pub display_width_px: Option<u32>,
269    /// Display height in pixels.
270    #[serde(skip_serializing_if = "Option::is_none", default)]
271    pub display_height_px: Option<u32>,
272    /// Pixel pitch in micrometers.
273    #[serde(skip_serializing_if = "Option::is_none", default)]
274    pub pixel_size_um: Option<u32>,
275    /// Build plate X dimension in micrometers.
276    #[serde(skip_serializing_if = "Option::is_none", default)]
277    pub build_width_um: Option<u32>,
278    /// Build plate Y dimension in micrometers.
279    #[serde(skip_serializing_if = "Option::is_none", default)]
280    pub build_depth_um: Option<u32>,
281    /// Build plate Z dimension in micrometers.
282    #[serde(skip_serializing_if = "Option::is_none", default)]
283    pub build_height_um: Option<u32>,
284    /// Panel bit depth.
285    #[serde(skip_serializing_if = "Option::is_none", default)]
286    pub bit_depth: Option<u32>,
287    /// Unknown keys, preserved verbatim.
288    #[serde(flatten)]
289    pub extra: Map<String, Value>,
290}
291
292/// A printer a profile is known to fit, as a model pattern plus display hints.
293#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
294pub struct CompatiblePrinter {
295    /// Manufacturer.
296    #[serde(skip_serializing_if = "Option::is_none", default)]
297    pub manufacturer: Option<String>,
298    /// Glob-like model pattern, e.g. `Ares 12K*`.
299    #[serde(skip_serializing_if = "Option::is_none", default)]
300    pub model_pattern: Option<String>,
301    /// Display width in pixels.
302    #[serde(skip_serializing_if = "Option::is_none", default)]
303    pub display_width_px: Option<u32>,
304    /// Display height in pixels.
305    #[serde(skip_serializing_if = "Option::is_none", default)]
306    pub display_height_px: Option<u32>,
307    /// Pixel pitch in micrometers.
308    #[serde(skip_serializing_if = "Option::is_none", default)]
309    pub pixel_size_um: Option<u32>,
310    /// Unknown keys, preserved verbatim.
311    #[serde(flatten)]
312    pub extra: Map<String, Value>,
313}
314
315/// Anti-aliasing settings, informational to a printer.
316#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
317pub struct AntiAliasing {
318    /// Whether AA was applied.
319    #[serde(skip_serializing_if = "Option::is_none", default)]
320    pub enabled: Option<bool>,
321    /// AA level.
322    #[serde(skip_serializing_if = "Option::is_none", default)]
323    pub level: Option<u32>,
324    /// AA mode, e.g. `blur`.
325    #[serde(skip_serializing_if = "Option::is_none", default)]
326    pub mode: Option<String>,
327    /// Floor applied to AA alpha, in percent.
328    #[serde(skip_serializing_if = "Option::is_none", default)]
329    pub minimum_alpha_percent: Option<f64>,
330    /// Blur brush radius in pixels.
331    #[serde(skip_serializing_if = "Option::is_none", default)]
332    pub blur_brush_radius_px: Option<u32>,
333    /// Blur brush kernel.
334    #[serde(skip_serializing_if = "Option::is_none", default)]
335    pub blur_brush_kernel: Option<String>,
336    /// Blur brush X sigma.
337    #[serde(skip_serializing_if = "Option::is_none", default)]
338    pub blur_brush_sigma_x: Option<f64>,
339    /// Blur brush Y sigma.
340    #[serde(skip_serializing_if = "Option::is_none", default)]
341    pub blur_brush_sigma_y: Option<f64>,
342    /// Z-blend look-back span.
343    #[serde(skip_serializing_if = "Option::is_none", default)]
344    pub z_blend_look_back: Option<u32>,
345    /// Z-blend fade distance in pixels.
346    #[serde(skip_serializing_if = "Option::is_none", default)]
347    pub z_blend_fade_px: Option<u32>,
348    /// Whether dithering was applied.
349    #[serde(skip_serializing_if = "Option::is_none", default)]
350    pub dither_enabled: Option<bool>,
351    /// Unknown keys, preserved verbatim.
352    #[serde(flatten)]
353    pub extra: Map<String, Value>,
354}
355
356/// Per-axis scale compensation in percent.
357#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
358pub struct ScaleCompensation {
359    /// X axis.
360    #[serde(default)]
361    pub x: f64,
362    /// Y axis.
363    #[serde(default)]
364    pub y: f64,
365    /// Z axis.
366    #[serde(default)]
367    pub z: f64,
368}
369
370/// Slicer attribution.
371#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
372pub struct Slicer {
373    /// Slicer name.
374    #[serde(skip_serializing_if = "Option::is_none", default)]
375    pub name: Option<String>,
376    /// Slicer version.
377    #[serde(skip_serializing_if = "Option::is_none", default)]
378    pub version: Option<String>,
379    /// Slicer URL.
380    #[serde(skip_serializing_if = "Option::is_none", default)]
381    pub url: Option<String>,
382    /// Unknown keys, preserved verbatim.
383    #[serde(flatten)]
384    pub extra: Map<String, Value>,
385}
386
387/// The `META` chunk: the resolved parameters for this print job.
388///
389/// Timing fields are flattened in, exactly as they sit at the top level of the
390/// JSON object; see [`Timing`].
391#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
392pub struct Meta {
393    /// Schema version of the META payload.
394    #[serde(skip_serializing_if = "Option::is_none", default)]
395    pub meta_version: Option<u32>,
396    /// Timing, motion, PWM and temperature fields.
397    #[serde(flatten)]
398    pub timing: Timing,
399    /// Authoritative material library for this print.
400    #[serde(skip_serializing_if = "Option::is_none", default)]
401    pub materials: Option<Vec<Material>>,
402    /// One entry per non-zero sector this print uses, each carrying that
403    /// sector's identity and the base timing fields it changes.
404    #[serde(skip_serializing_if = "Option::is_none", default)]
405    pub sectors: Option<Vec<Sector>>,
406    /// Printer identity.
407    #[serde(skip_serializing_if = "Option::is_none", default)]
408    pub printer: Option<Printer>,
409    /// Anti-aliasing settings, informational.
410    #[serde(skip_serializing_if = "Option::is_none", default)]
411    pub anti_aliasing: Option<AntiAliasing>,
412    /// Per-axis scale compensation.
413    #[serde(skip_serializing_if = "Option::is_none", default)]
414    pub scale_compensation_pct: Option<ScaleCompensation>,
415    /// Estimated print time in whole seconds, informational.
416    #[serde(skip_serializing_if = "Option::is_none", default)]
417    pub estimated_print_time_sec: Option<u32>,
418    /// Estimated resin volume in millilitres, informational.
419    #[serde(skip_serializing_if = "Option::is_none", default)]
420    pub estimated_resin_volume_ml: Option<f64>,
421    /// Slicer attribution.
422    #[serde(skip_serializing_if = "Option::is_none", default)]
423    pub slicer: Option<Slicer>,
424}
425
426impl Meta {
427    /// The META `extra` object, if the payload carried one.
428    pub fn extra(&self) -> Option<&Value> {
429        self.timing.vendor_extra()
430    }
431}
432
433/// The `PROF` chunk: a named, versioned, reusable print profile.
434#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
435pub struct Profile {
436    /// Profile name. Required and non-empty.
437    #[serde(default)]
438    pub profile_name: String,
439    /// Profile version. Required and non-empty.
440    #[serde(default)]
441    pub profile_version: String,
442    /// One of `material`, `printer`, `combined`. Required.
443    #[serde(skip_serializing_if = "Option::is_none", default)]
444    pub profile_type: Option<String>,
445    /// Unique identifier for deduplication.
446    #[serde(skip_serializing_if = "Option::is_none", default)]
447    pub profile_uuid: Option<String>,
448    /// Attribution.
449    #[serde(skip_serializing_if = "Option::is_none", default)]
450    pub author: Option<String>,
451    /// Creation timestamp.
452    #[serde(skip_serializing_if = "Option::is_none", default)]
453    pub created_unix_sec: Option<u64>,
454    /// Free-form description.
455    #[serde(skip_serializing_if = "Option::is_none", default)]
456    pub description: Option<String>,
457    /// Printer compatibility hints.
458    #[serde(skip_serializing_if = "Option::is_none", default)]
459    pub compatible_printers: Option<Vec<CompatiblePrinter>>,
460    /// Printer definition, for a `printer` or `combined` profile.
461    #[serde(skip_serializing_if = "Option::is_none", default)]
462    pub printer: Option<Printer>,
463    /// The profile's settings, in META's field names. Required.
464    #[serde(default)]
465    pub settings: Timing,
466    /// Anti-aliasing preferences.
467    #[serde(skip_serializing_if = "Option::is_none", default)]
468    pub anti_aliasing: Option<AntiAliasing>,
469    /// Material library, same schema and indexing as `META.materials`.
470    #[serde(skip_serializing_if = "Option::is_none", default)]
471    pub materials: Option<Vec<Material>>,
472    /// Per-axis scale compensation.
473    #[serde(skip_serializing_if = "Option::is_none", default)]
474    pub scale_compensation_pct: Option<ScaleCompensation>,
475    /// Vendor metadata.
476    #[serde(skip_serializing_if = "Option::is_none", default)]
477    pub extra: Option<Value>,
478}
479
480/// A `META.sectors` entry: one non-zero sector's identity and base timing.
481///
482/// The entry carries only what differs from META's own values: every timing
483/// field it leaves out is inherited from META's, field by field, and a sector
484/// with no entry at all - sector 0, the primary one, always - resolves from META
485/// alone ([`spec/14-layer-timing.md`] section 8). A sector carries no material
486/// until its entry names one.
487#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
488pub struct Sector {
489    /// Sector identifier. `0` is reserved for the implicit primary sector, so an
490    /// entry carries `>= 1`.
491    #[serde(default)]
492    pub sector_id: u32,
493    /// Display name.
494    #[serde(skip_serializing_if = "Option::is_none", default)]
495    pub name: Option<String>,
496    /// Index into `META.materials`. Absent means no material.
497    #[serde(skip_serializing_if = "Option::is_none", default)]
498    pub material_index: Option<u32>,
499    /// Display hint overriding the material's color for this sector.
500    #[serde(skip_serializing_if = "Option::is_none", default)]
501    pub color_rgba: Option<[u8; 4]>,
502    /// Base timing for this sector; absent fields inherit from META.
503    #[serde(flatten)]
504    pub timing: Timing,
505}