1use crate::check::Check;
9use crate::error::{Error, Result};
10use crate::json::{CureCurve, Meta, Sector, Timing, REQUIRED_META_FIELDS};
11
12const DEFAULT_LIGHT_PWM: u32 = 255;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct Resolved {
18 pub layer_height_um: u32,
20 pub exposure_ms: u32,
22 pub lift_slow_distance_um: u32,
24 pub lift_slow_speed_um_min: u32,
26 pub lift_fast_distance_um: u32,
28 pub lift_fast_speed_um_min: u32,
30 pub retract_fast_distance_um: u32,
32 pub retract_fast_speed_um_min: u32,
34 pub retract_slow_distance_um: u32,
36 pub retract_slow_speed_um_min: u32,
38 pub wait_time_before_cure_ms: u32,
40 pub wait_time_after_cure_ms: u32,
42 pub wait_time_after_lift_ms: u32,
44 pub light_pwm: u32,
46 pub chamber_temperature_c: Option<f64>,
48 pub vat_temperature_c: Option<f64>,
50 pub cure_curve: Option<CureCurve>,
52 pub is_bottom: bool,
54 pub is_transition: bool,
56}
57
58macro_rules! pair {
60 ($meta:expr, $sector:expr, $normal:ident, $bottom:ident, $default:expr) => {
61 field_pair($meta, $sector, |t| t.$normal, |t| t.$bottom, $default)
62 };
63}
64
65pub fn resolve(
71 meta: &Meta,
72 sector: Option<&Sector>,
73 overrides: Option<&Timing>,
74 layer: u32,
75 sector_id: u32,
76) -> Result<Resolved> {
77 let missing: Vec<&str> = REQUIRED_META_FIELDS
81 .iter()
82 .copied()
83 .filter(|name| *name != "meta_version")
84 .filter(|name| !meta_carries(meta, name))
85 .collect();
86 if !missing.is_empty() {
87 return Err(Error::new(
88 Check::MetaRequiredFields,
89 format!("META is missing {}", missing.join(", ")),
90 ));
91 }
92
93 let timing = &meta.timing;
94 let sect = match sector_id {
97 0 => None,
98 _ => sector.map(|definition| &definition.timing),
99 };
100
101 let layer_height_um = merged(timing, sect, |t| t.layer_height_um).unwrap_or_default();
104 let bottom_layer_count = merged(timing, sect, |t| t.bottom_layer_count).unwrap_or_default();
105 let transition_layer_count =
106 merged(timing, sect, |t| t.transition_layer_count).unwrap_or_default();
107 let light_pwm = merged(timing, sect, |t| t.light_pwm).unwrap_or(DEFAULT_LIGHT_PWM);
108 let bottom_light_pwm = merged(timing, sect, |t| t.bottom_light_pwm).unwrap_or(light_pwm);
109
110 let exposure = pair!(timing, sect, normal_exposure_ms, bottom_exposure_ms, 0);
114 let lift_slow_distance = pair!(
115 timing,
116 sect,
117 lift_slow_distance_um,
118 bottom_lift_slow_distance_um,
119 0
120 );
121 let lift_slow_speed = pair!(
122 timing,
123 sect,
124 lift_slow_speed_um_min,
125 bottom_lift_slow_speed_um_min,
126 0
127 );
128 let lift_fast_distance = pair!(
129 timing,
130 sect,
131 lift_fast_distance_um,
132 bottom_lift_fast_distance_um,
133 0
134 );
135 let lift_fast_speed = pair!(
136 timing,
137 sect,
138 lift_fast_speed_um_min,
139 bottom_lift_fast_speed_um_min,
140 0
141 );
142 let retract_fast_distance = pair!(
143 timing,
144 sect,
145 retract_fast_distance_um,
146 bottom_retract_fast_distance_um,
147 0
148 );
149 let retract_fast_speed = pair!(
150 timing,
151 sect,
152 retract_fast_speed_um_min,
153 bottom_retract_fast_speed_um_min,
154 0
155 );
156 let retract_slow_distance = pair!(
157 timing,
158 sect,
159 retract_slow_distance_um,
160 bottom_retract_slow_distance_um,
161 0
162 );
163 let retract_slow_speed = pair!(
164 timing,
165 sect,
166 retract_slow_speed_um_min,
167 bottom_retract_slow_speed_um_min,
168 0
169 );
170 let wait_before_cure = pair!(
171 timing,
172 sect,
173 wait_time_before_cure_ms,
174 bottom_wait_time_before_cure_ms,
175 0
176 );
177 let wait_after_cure = pair!(
178 timing,
179 sect,
180 wait_time_after_cure_ms,
181 bottom_wait_time_after_cure_ms,
182 0
183 );
184 let wait_after_lift = pair!(
185 timing,
186 sect,
187 wait_time_after_lift_ms,
188 bottom_wait_time_after_lift_ms,
189 0
190 );
191
192 let bottom = u64::from(bottom_layer_count);
194 let transition = u64::from(transition_layer_count);
195 let index = u64::from(layer);
196 let stage = if index < bottom {
197 Stage::Bottom
198 } else if index < bottom + transition {
199 Stage::Transition {
205 k: index - bottom + 1,
206 n: transition + 1,
207 }
208 } else {
209 Stage::Normal
210 };
211
212 let mut resolved = Resolved {
213 layer_height_um,
214 exposure_ms: blend_u32(exposure, stage),
215 lift_slow_distance_um: blend_u32(lift_slow_distance, stage),
216 lift_slow_speed_um_min: blend_u32(lift_slow_speed, stage),
217 lift_fast_distance_um: blend_u32(lift_fast_distance, stage),
218 lift_fast_speed_um_min: blend_u32(lift_fast_speed, stage),
219 retract_fast_distance_um: blend_u32(retract_fast_distance, stage),
220 retract_fast_speed_um_min: blend_u32(retract_fast_speed, stage),
221 retract_slow_distance_um: blend_u32(retract_slow_distance, stage),
222 retract_slow_speed_um_min: blend_u32(retract_slow_speed, stage),
223 wait_time_before_cure_ms: blend_u32(wait_before_cure, stage),
224 wait_time_after_cure_ms: blend_u32(wait_after_cure, stage),
225 wait_time_after_lift_ms: blend_u32(wait_after_lift, stage),
226 light_pwm: match stage {
229 Stage::Bottom => bottom_light_pwm,
230 _ => light_pwm,
231 },
232 chamber_temperature_c: merged(timing, sect, |t| t.chamber_temperature_c),
233 vat_temperature_c: merged(timing, sect, |t| t.vat_temperature_c),
234 cure_curve: merged(timing, sect, |t| t.cure_curve),
235 is_bottom: matches!(stage, Stage::Bottom),
236 is_transition: matches!(stage, Stage::Transition { .. }),
237 };
238
239 if let Some(overrides) = overrides {
243 fold_entry(&mut resolved, overrides);
244 }
245
246 Ok(resolved)
247}
248
249#[derive(Clone, Copy)]
251enum Stage {
252 Bottom,
254 Normal,
256 Transition { k: u64, n: u64 },
258}
259
260#[derive(Clone, Copy)]
263struct Blend<T> {
264 normal: T,
266 bottom: T,
268}
269
270impl<T: Copy> Blend<T> {
271 fn new(normal: Option<T>, bottom: Option<T>, default: T) -> Self {
274 let normal = normal.unwrap_or(default);
275 Blend {
276 normal,
277 bottom: bottom.unwrap_or(normal),
278 }
279 }
280}
281
282fn merged<T: Copy>(
286 meta: &Timing,
287 sector: Option<&Timing>,
288 get: impl Fn(&Timing) -> Option<T>,
289) -> Option<T> {
290 sector.and_then(&get).or_else(|| get(meta))
291}
292
293fn field_pair<T: Copy>(
295 meta: &Timing,
296 sector: Option<&Timing>,
297 normal: impl Fn(&Timing) -> Option<T>,
298 bottom: impl Fn(&Timing) -> Option<T>,
299 default: T,
300) -> Blend<T> {
301 Blend::new(
302 merged(meta, sector, normal),
303 merged(meta, sector, bottom),
304 default,
305 )
306}
307
308fn meta_carries(meta: &Meta, name: &str) -> bool {
314 let timing = &meta.timing;
315 match name {
316 "normal_exposure_ms" => timing.normal_exposure_ms.is_some(),
317 "bottom_exposure_ms" => timing.bottom_exposure_ms.is_some(),
318 "bottom_layer_count" => timing.bottom_layer_count.is_some(),
319 "transition_layer_count" => timing.transition_layer_count.is_some(),
320 "layer_height_um" => timing.layer_height_um.is_some(),
321 "lift_slow_distance_um" => timing.lift_slow_distance_um.is_some(),
322 "lift_slow_speed_um_min" => timing.lift_slow_speed_um_min.is_some(),
323 "retract_fast_distance_um" => timing.retract_fast_distance_um.is_some(),
324 "retract_fast_speed_um_min" => timing.retract_fast_speed_um_min.is_some(),
325 _ => false,
326 }
327}
328
329fn blend_u32(field: Blend<u32>, stage: Stage) -> u32 {
346 match stage {
347 Stage::Bottom => field.bottom,
348 Stage::Normal => field.normal,
349 Stage::Transition { k, n } => {
350 let bottom = u128::from(field.bottom);
351 let normal = u128::from(field.normal);
352 let numerator = bottom * (n - k) as u128 + normal * k as u128;
353 ((2 * numerator + n as u128) / (2 * n as u128)) as u32
358 }
359 }
360}
361
362fn fold_entry(resolved: &mut Resolved, over: &Timing) {
369 if let Some(value) = over.layer_height_um {
370 resolved.layer_height_um = value;
371 }
372 if let Some(value) = over.normal_exposure_ms.or(over.bottom_exposure_ms) {
373 resolved.exposure_ms = value;
374 }
375 if let Some(value) = over
376 .lift_slow_distance_um
377 .or(over.bottom_lift_slow_distance_um)
378 {
379 resolved.lift_slow_distance_um = value;
380 }
381 if let Some(value) = over
382 .lift_slow_speed_um_min
383 .or(over.bottom_lift_slow_speed_um_min)
384 {
385 resolved.lift_slow_speed_um_min = value;
386 }
387 if let Some(value) = over
388 .lift_fast_distance_um
389 .or(over.bottom_lift_fast_distance_um)
390 {
391 resolved.lift_fast_distance_um = value;
392 }
393 if let Some(value) = over
394 .lift_fast_speed_um_min
395 .or(over.bottom_lift_fast_speed_um_min)
396 {
397 resolved.lift_fast_speed_um_min = value;
398 }
399 if let Some(value) = over
400 .retract_fast_distance_um
401 .or(over.bottom_retract_fast_distance_um)
402 {
403 resolved.retract_fast_distance_um = value;
404 }
405 if let Some(value) = over
406 .retract_fast_speed_um_min
407 .or(over.bottom_retract_fast_speed_um_min)
408 {
409 resolved.retract_fast_speed_um_min = value;
410 }
411 if let Some(value) = over
412 .retract_slow_distance_um
413 .or(over.bottom_retract_slow_distance_um)
414 {
415 resolved.retract_slow_distance_um = value;
416 }
417 if let Some(value) = over
418 .retract_slow_speed_um_min
419 .or(over.bottom_retract_slow_speed_um_min)
420 {
421 resolved.retract_slow_speed_um_min = value;
422 }
423 if let Some(value) = over
424 .wait_time_before_cure_ms
425 .or(over.bottom_wait_time_before_cure_ms)
426 {
427 resolved.wait_time_before_cure_ms = value;
428 }
429 if let Some(value) = over
430 .wait_time_after_cure_ms
431 .or(over.bottom_wait_time_after_cure_ms)
432 {
433 resolved.wait_time_after_cure_ms = value;
434 }
435 if let Some(value) = over
436 .wait_time_after_lift_ms
437 .or(over.bottom_wait_time_after_lift_ms)
438 {
439 resolved.wait_time_after_lift_ms = value;
440 }
441 if let Some(value) = over.light_pwm.or(over.bottom_light_pwm) {
442 resolved.light_pwm = value;
443 }
444 if let Some(value) = over.chamber_temperature_c {
445 resolved.chamber_temperature_c = Some(value);
446 }
447 if let Some(value) = over.vat_temperature_c {
448 resolved.vat_temperature_c = Some(value);
449 }
450 if let Some(value) = over.cure_curve {
451 resolved.cure_curve = Some(value);
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 fn meta() -> Meta {
462 Meta {
463 meta_version: Some(1),
464 timing: Timing {
465 layer_height_um: Some(50),
466 normal_exposure_ms: Some(2500),
467 bottom_exposure_ms: Some(30000),
468 bottom_layer_count: Some(4),
469 transition_layer_count: Some(8),
470 lift_slow_distance_um: Some(5000),
471 lift_slow_speed_um_min: Some(65000),
472 lift_fast_distance_um: Some(3000),
473 lift_fast_speed_um_min: Some(180000),
474 retract_fast_distance_um: Some(5000),
475 retract_fast_speed_um_min: Some(150000),
476 retract_slow_distance_um: Some(3000),
477 retract_slow_speed_um_min: Some(180000),
478 wait_time_before_cure_ms: Some(1000),
479 wait_time_after_cure_ms: Some(0),
480 wait_time_after_lift_ms: Some(500),
481 light_pwm: Some(255),
482 ..Timing::default()
483 },
484 ..Meta::default()
485 }
486 }
487
488 fn at(meta: &Meta, layer: u32) -> Resolved {
490 resolve(meta, None, None, layer, 0).unwrap()
491 }
492
493 #[test]
494 fn exposure_blends_bottom_transition_and_normal() {
495 let meta = meta();
496
497 assert_eq!(at(&meta, 0).exposure_ms, 30000);
498 assert_eq!(at(&meta, 3).exposure_ms, 30000);
499 assert_eq!(at(&meta, 4).exposure_ms, 26944);
502 assert_eq!(at(&meta, 11).exposure_ms, 5556);
504 assert_eq!(at(&meta, 12).exposure_ms, 2500);
505
506 assert_eq!(
507 (at(&meta, 0).is_bottom, at(&meta, 0).is_transition),
508 (true, false)
509 );
510 assert_eq!(
511 (at(&meta, 3).is_bottom, at(&meta, 3).is_transition),
512 (true, false)
513 );
514 assert_eq!(
515 (at(&meta, 4).is_bottom, at(&meta, 4).is_transition),
516 (false, true)
517 );
518 assert_eq!(
519 (at(&meta, 11).is_bottom, at(&meta, 11).is_transition),
520 (false, true)
521 );
522 assert_eq!(
523 (at(&meta, 12).is_bottom, at(&meta, 12).is_transition),
524 (false, false)
525 );
526 assert_eq!(
527 (at(&meta, 400).is_bottom, at(&meta, 400).is_transition),
528 (false, false)
529 );
530 }
531
532 #[test]
533 fn wait_times_interpolate_between_their_two_forms() {
534 let mut meta = meta();
535 meta.timing.bottom_wait_time_before_cure_ms = Some(10000);
536
537 assert_eq!(at(&meta, 0).wait_time_before_cure_ms, 10000);
538 assert_eq!(at(&meta, 4).wait_time_before_cure_ms, 9000);
540 assert_eq!(at(&meta, 12).wait_time_before_cure_ms, 1000);
541 assert_eq!(at(&meta, 0).wait_time_after_lift_ms, 500);
543 assert_eq!(at(&meta, 5).wait_time_after_lift_ms, 500);
544 }
545
546 #[test]
547 fn absent_bottom_form_inherits_its_normal_value() {
548 let meta = meta();
549 assert_eq!(at(&meta, 0).lift_fast_distance_um, 3000);
552 assert_eq!(at(&meta, 6).lift_fast_distance_um, 3000);
553 assert_eq!(at(&meta, 40).lift_fast_distance_um, 3000);
554
555 let mut without = meta.clone();
557 without.timing.lift_fast_distance_um = None;
558 assert_eq!(at(&without, 40).lift_fast_distance_um, 0);
559 }
560
561 #[test]
562 fn interpolation_rounds_half_away_from_zero() {
563 let mut meta = meta();
564 meta.timing.bottom_lift_slow_distance_um = Some(6000);
565
566 assert_eq!(at(&meta, 0).lift_slow_distance_um, 6000);
567 assert_eq!(at(&meta, 4).lift_slow_distance_um, 5889);
569 assert_eq!(at(&meta, 12).lift_slow_distance_um, 5000);
570
571 let mut half = meta.clone();
575 half.timing.bottom_layer_count = Some(1);
576 half.timing.transition_layer_count = Some(1);
577 half.timing.lift_slow_distance_um = Some(5000);
578 half.timing.bottom_lift_slow_distance_um = Some(5001);
579 half.timing.normal_exposure_ms = Some(2500);
580 half.timing.bottom_exposure_ms = Some(2501);
581 half.timing.wait_time_after_lift_ms = Some(100);
582 half.timing.bottom_wait_time_after_lift_ms = Some(101);
583
584 assert_eq!(at(&half, 1).lift_slow_distance_um, 5001);
585 assert_eq!(at(&half, 1).exposure_ms, 2501);
586 assert_eq!(at(&half, 1).wait_time_after_lift_ms, 101);
587 assert_eq!(at(&half, 0).exposure_ms, 2501);
589 assert_eq!(at(&half, 2).exposure_ms, 2500);
590
591 let mut extreme = meta.clone();
598 extreme.timing.bottom_layer_count = Some(0);
599 extreme.timing.transition_layer_count = Some(u32::MAX);
600 extreme.timing.normal_exposure_ms = Some(0);
601 extreme.timing.bottom_exposure_ms = Some(u32::MAX);
602
603 assert_eq!(at(&extreme, u32::MAX - 1).exposure_ms, 1);
604 assert!(at(&extreme, u32::MAX - 1).is_transition);
605 assert_eq!(at(&extreme, u32::MAX).exposure_ms, 0);
606 }
607
608 #[test]
609 fn light_pwm_switches_at_the_bottom_boundary_without_interpolating() {
610 let mut meta = meta();
611 meta.timing.bottom_light_pwm = Some(200);
612
613 assert_eq!(at(&meta, 3).light_pwm, 200);
614 assert_eq!(at(&meta, 4).light_pwm, 255);
617
618 let mut no_bottom_pwm = meta.clone();
620 no_bottom_pwm.timing.bottom_light_pwm = None;
621 no_bottom_pwm.timing.light_pwm = Some(180);
622 assert_eq!(at(&no_bottom_pwm, 0).light_pwm, 180);
623 assert_eq!(at(&no_bottom_pwm, 4).light_pwm, 180);
624
625 let mut no_pwm = meta.clone();
627 no_pwm.timing.light_pwm = None;
628 no_pwm.timing.bottom_light_pwm = None;
629 assert_eq!(at(&no_pwm, 0).light_pwm, 255);
630 assert_eq!(at(&no_pwm, 40).light_pwm, 255);
631 }
632
633 #[test]
634 fn a_sector_entry_overrides_its_own_sector_only() {
635 let meta = meta();
636 let sector = Sector {
637 sector_id: 1,
638 timing: Timing {
639 normal_exposure_ms: Some(3000),
640 bottom_exposure_ms: Some(35000),
641 ..Timing::default()
642 },
643 ..Sector::default()
644 };
645
646 assert_eq!(
648 resolve(&meta, Some(§or), None, 0, 0)
649 .unwrap()
650 .exposure_ms,
651 30000
652 );
653 assert_eq!(
654 resolve(&meta, Some(§or), None, 12, 0)
655 .unwrap()
656 .exposure_ms,
657 2500
658 );
659
660 assert_eq!(
662 resolve(&meta, Some(§or), None, 0, 1)
663 .unwrap()
664 .exposure_ms,
665 35000
666 );
667 assert_eq!(
668 resolve(&meta, Some(§or), None, 12, 1)
669 .unwrap()
670 .exposure_ms,
671 3000
672 );
673
674 assert_eq!(
676 resolve(&meta, Some(§or), None, 12, 1)
677 .unwrap()
678 .lift_slow_distance_um,
679 5000
680 );
681 assert_eq!(resolve(&meta, None, None, 12, 1).unwrap().exposure_ms, 2500);
683
684 let sector = Sector {
686 sector_id: 1,
687 timing: Timing {
688 bottom_layer_count: Some(6),
689 transition_layer_count: Some(0),
690 ..Timing::default()
691 },
692 ..Sector::default()
693 };
694 assert!(resolve(&meta, Some(§or), None, 5, 1).unwrap().is_bottom);
695 assert!(
696 resolve(&meta, Some(§or), None, 5, 0)
697 .unwrap()
698 .is_transition
699 );
700 }
701
702 #[test]
703 fn temperatures_and_cure_curve_come_from_meta_or_a_sector_entry_unblended() {
704 let mut meta = meta();
705 meta.timing.chamber_temperature_c = Some(30.0);
706 meta.timing.cure_curve = Some(CureCurve {
707 dp_um: 120.0,
708 ec_mj_cm2: 7.5,
709 e0_mj_cm2: 3.0,
710 });
711
712 let resolved = at(&meta, 0);
713 assert_eq!(resolved.chamber_temperature_c, Some(30.0));
714 assert_eq!(resolved.vat_temperature_c, None);
715 assert_eq!(
716 resolved.cure_curve,
717 Some(CureCurve {
718 dp_um: 120.0,
719 ec_mj_cm2: 7.5,
720 e0_mj_cm2: 3.0,
721 })
722 );
723
724 let sector = Sector {
725 sector_id: 1,
726 timing: Timing {
727 chamber_temperature_c: Some(40.0),
728 ..Timing::default()
729 },
730 ..Sector::default()
731 };
732 let for_sector_1 = resolve(&meta, Some(§or), None, 0, 1).unwrap();
733 assert_eq!(for_sector_1.chamber_temperature_c, Some(40.0));
734 assert_eq!(for_sector_1.vat_temperature_c, None);
735 assert_eq!(for_sector_1.cure_curve.unwrap().dp_um, 120.0);
736 assert_eq!(at(&meta, 0).chamber_temperature_c, Some(30.0));
737 }
738
739 #[test]
740 fn an_lrov_payload_replaces_only_the_fields_it_carries() {
741 let meta = meta();
742 let overrides = Timing {
743 normal_exposure_ms: Some(9000),
744 lift_slow_distance_um: Some(7000),
745 ..Timing::default()
746 };
747
748 let resolved = resolve(&meta, None, Some(&overrides), 12, 0).unwrap();
749 assert_eq!(resolved.exposure_ms, 9000);
750 assert_eq!(resolved.lift_slow_distance_um, 7000);
751 assert_eq!(resolved.lift_slow_speed_um_min, 65_000);
753
754 assert_eq!(resolve(&meta, None, None, 16, 0).unwrap().exposure_ms, 2500);
759 assert_eq!(resolve(&meta, None, None, 12, 1).unwrap().exposure_ms, 2500);
760 }
761
762 #[test]
763 fn an_lrov_payload_overrides_pwm_and_optional_motion() {
764 let meta = meta();
765 let overrides = Timing {
766 normal_exposure_ms: Some(9000),
767 light_pwm: Some(120),
768 bottom_light_pwm: Some(130),
769 lift_fast_distance_um: Some(2500),
770 chamber_temperature_c: Some(35.0),
771 ..Timing::default()
772 };
773
774 let resolved = resolve(&meta, None, Some(&overrides), 1, 0).unwrap();
775 assert_eq!(resolved.light_pwm, 120);
776 assert_eq!(resolved.lift_fast_distance_um, 2500);
777 assert_eq!(resolved.chamber_temperature_c, Some(35.0));
778 assert_eq!(
780 resolve(&meta, None, Some(&overrides), 0, 0)
781 .unwrap()
782 .light_pwm,
783 120
784 );
785 assert_eq!(resolve(&meta, None, None, 0, 0).unwrap().light_pwm, 255);
786 }
787
788 #[test]
789 fn missing_required_meta_field_is_reported() {
790 let mut meta = meta();
791 meta.timing.bottom_exposure_ms = None;
792 meta.timing.lift_slow_speed_um_min = None;
793
794 let err = resolve(&meta, None, None, 0, 0).unwrap_err();
795 assert_eq!(err.check(), Check::MetaRequiredFields);
796 let detail = err.detail();
797 assert!(detail.contains("bottom_exposure_ms"), "{detail}");
798 assert!(detail.contains("lift_slow_speed_um_min"), "{detail}");
799 assert!(!detail.contains("normal_exposure_ms"), "{detail}");
800
801 let mut no_version = meta.clone();
803 no_version.meta_version = None;
804 no_version.timing.bottom_exposure_ms = Some(30000);
805 no_version.timing.lift_slow_speed_um_min = Some(65000);
806 assert!(resolve(&no_version, None, None, 0, 0).is_ok());
807 }
808}