Skip to main content

printobserver_types/
reported.rs

1//! Reported numbers, and the plausibility ranges this crate declares for them.
2//!
3//! A numeric field this crate declares a range for is typed [`Reported<f64>`],
4//! which serializes as an object carrying a `value` and a boolean
5//! `out_of_range` that is true exactly when the reported `value` is non-finite
6//! or lies outside the range this crate declares for that field. A value
7//! outside the range is carried through as reported with that flag set, never
8//! coerced, never clamped and never dropped.
9//!
10//! The non-finite half of that definition is deliberate, because `f64` admits
11//! inhabitants an ordinary range comparison answers wrongly. `NaN` compares
12//! neither below a minimum nor above a maximum, so `value < min || value > max`
13//! would report the most implausible reading this type can hold as a plausible
14//! one; the two infinities lie outside every finite range. All three are what a
15//! disconnected thermistor or a division by a zero-valued reading produces,
16//! which is exactly the class the flag exists to carry, so all three are
17//! `out_of_range`.
18//!
19//! On the wire the type is closed instead: the JSON number grammar denotes no
20//! non-finite value, so a `Reported<f64>` never arrives by parse carrying one,
21//! and this crate refuses a non-finite `value` on emission rather than taking
22//! the serializer's own default of emitting `null` — which would parse back as
23//! a field the source did not report and turn an implausible reading into no
24//! reading at all.
25//!
26//! **These are validity ranges on what a printer can plausibly report, and they
27//! are not the operator's safety envelope.** A reading outside one of them says
28//! the source reported something implausible — a disconnected thermistor, a
29//! firmware that answers in another unit. What an actor is *allowed to ask for*
30//! is [`SafetyEnvelope`](crate::SafetyEnvelope), which is server configuration
31//! and is narrower by orders of magnitude; nothing in this crate reads these
32//! ranges as a bound on an action, and the two are never intersected, compared
33//! or substituted for one another.
34
35use core::fmt;
36use std::borrow::Cow;
37
38use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
39use serde::ser::SerializeStruct as _;
40use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _, ser::Error as _};
41
42/// An inclusive pair of 64-bit floats.
43#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
44#[serde(deny_unknown_fields)]
45pub struct Range {
46    /// The lowest value the range admits, inclusive.
47    pub min: f64,
48    /// The highest value the range admits, inclusive.
49    pub max: f64,
50}
51
52impl Range {
53    /// The range from `min` to `max`, both inclusive.
54    #[must_use]
55    pub const fn new(min: f64, max: f64) -> Self {
56        Self { min, max }
57    }
58
59    /// Whether `value` lies inside this range.
60    ///
61    /// A non-finite value lies inside no range: `NaN` compares neither below
62    /// the minimum nor above the maximum, and the two infinities lie outside
63    /// every finite range.
64    #[must_use]
65    pub fn contains(&self, value: f64) -> bool {
66        value.is_finite() && value >= self.min && value <= self.max
67    }
68}
69
70/// The plausibility range of `PrinterSnapshot::feedrate_factor`.
71///
72/// A multiplier where one means one hundred percent, spanning what the
73/// firmware's own speed command accepts. A validity range on what a printer can
74/// plausibly report, not a bound on what an actor may ask for.
75pub const FEEDRATE_FACTOR_RANGE: Range = Range::new(0.1, 10.0);
76
77/// The plausibility range of `PrinterSnapshot::flowrate_factor`.
78///
79/// A multiplier where one means one hundred percent, spanning what the
80/// firmware's own flow command accepts. A validity range on what a printer can
81/// plausibly report, not a bound on what an actor may ask for.
82pub const FLOWRATE_FACTOR_RANGE: Range = Range::new(0.1, 10.0);
83
84/// The plausibility range of `PrinterSnapshot::fan_percent`, in percent.
85///
86/// A validity range on what a printer can plausibly report, not a bound on what
87/// an actor may ask for.
88pub const FAN_PERCENT_RANGE: Range = Range::new(0.0, 100.0);
89
90/// The plausibility range of `JobSnapshot::completion`, as a fraction.
91///
92/// A validity range on what a printer can plausibly report, not a bound on what
93/// an actor may ask for.
94pub const COMPLETION_RANGE: Range = Range::new(0.0, 1.0);
95
96/// The plausibility range of `HeaterSnapshot::actual_c`, in degrees Celsius.
97///
98/// A validity range on what a printer can plausibly report, not a bound on what
99/// an actor may ask for.
100pub const HEATER_ACTUAL_C_RANGE: Range = Range::new(-20.0, 500.0);
101
102/// The plausibility range of `HeaterSnapshot::target_c`, in degrees Celsius.
103///
104/// A validity range on what a printer can plausibly report, not a bound on what
105/// an actor may ask for.
106pub const HEATER_TARGET_C_RANGE: Range = Range::new(0.0, 500.0);
107
108/// The plausibility range of `HeaterSnapshot::offset_c`, in degrees Celsius.
109///
110/// A validity range on what a printer can plausibly report, not a bound on what
111/// an actor may ask for.
112pub const HEATER_OFFSET_C_RANGE: Range = Range::new(-50.0, 50.0);
113
114/// A value a source reported, carrying whether it is outside the range this
115/// crate declares for the field it was reported in.
116///
117/// The value is carried exactly as reported: it is never coerced, clamped or
118/// dropped, whatever the flag says.
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub struct Reported<T> {
121    /// The value exactly as the source reported it.
122    value: T,
123    /// Whether the value is non-finite or outside the declared range.
124    out_of_range: bool,
125}
126
127impl Reported<f64> {
128    /// The value as reported, flagged against the range declared for its field.
129    #[must_use]
130    pub fn new(value: f64, range: Range) -> Self {
131        Self {
132            value,
133            out_of_range: !range.contains(value),
134        }
135    }
136
137    /// The value exactly as the source reported it.
138    #[must_use]
139    pub const fn value(&self) -> f64 {
140        self.value
141    }
142
143    /// Whether the value is non-finite or outside its field's declared range.
144    #[must_use]
145    pub const fn out_of_range(&self) -> bool {
146        self.out_of_range
147    }
148}
149
150/// The wire shape of a [`Reported<f64>`]: the pair, and nothing else.
151#[derive(Deserialize)]
152#[serde(deny_unknown_fields)]
153struct ReportedWire {
154    value: f64,
155    out_of_range: bool,
156}
157
158impl Serialize for Reported<f64> {
159    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
160        if !self.value.is_finite() {
161            return Err(S::Error::custom(format!(
162                "a non-finite reported value ({}) has no JSON number spelling, and emitting it \
163                 as `null` would parse back as a field the source did not report",
164                self.value
165            )));
166        }
167        let mut state = serializer.serialize_struct("Reported", 2)?;
168        state.serialize_field("value", &self.value)?;
169        state.serialize_field("out_of_range", &self.out_of_range)?;
170        state.end()
171    }
172}
173
174impl<'de> Deserialize<'de> for Reported<f64> {
175    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
176        let wire = ReportedWire::deserialize(deserializer)?;
177        Ok(Self {
178            value: wire.value,
179            out_of_range: wire.out_of_range,
180        })
181    }
182}
183
184impl JsonSchema for Reported<f64> {
185    fn schema_name() -> Cow<'static, str> {
186        Cow::Borrowed("Reported")
187    }
188
189    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
190        json_schema!({
191            "type": "object",
192            "title": "Reported",
193            "description": "A value as a source reported it, flagged when it is outside the \
194                            plausibility range this crate declares for its field.",
195            "properties": {
196                "value": { "type": "number" },
197                "out_of_range": { "type": "boolean" }
198            },
199            "required": ["value", "out_of_range"],
200            "additionalProperties": false
201        })
202    }
203}
204
205impl fmt::Display for Reported<f64> {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        if self.out_of_range {
208            write!(formatter, "{} (out of range)", self.value)
209        } else {
210            write!(formatter, "{}", self.value)
211        }
212    }
213}
214
215/// Read an optional reported value, refusing a flag that disagrees with the
216/// value under the range declared for the field it was read in.
217fn read_ranged<'de, D: Deserializer<'de>>(
218    deserializer: D,
219    field: &str,
220    range: Range,
221) -> Result<Option<Reported<f64>>, D::Error> {
222    let Some(reported) = Option::<Reported<f64>>::deserialize(deserializer)? else {
223        return Ok(None);
224    };
225    let expected = !range.contains(reported.value);
226    if reported.out_of_range != expected {
227        return Err(D::Error::custom(format!(
228            "`{field}` reported value {} with out_of_range {}, but the declared range \
229             {} to {} makes it {expected}",
230            reported.value, reported.out_of_range, range.min, range.max
231        )));
232    }
233    Ok(Some(reported))
234}
235
236/// Read `PrinterSnapshot::feedrate_factor` against its declared range.
237pub(crate) fn de_feedrate_factor<'de, D: Deserializer<'de>>(
238    deserializer: D,
239) -> Result<Option<Reported<f64>>, D::Error> {
240    read_ranged(deserializer, "feedrate_factor", FEEDRATE_FACTOR_RANGE)
241}
242
243/// Read `PrinterSnapshot::flowrate_factor` against its declared range.
244pub(crate) fn de_flowrate_factor<'de, D: Deserializer<'de>>(
245    deserializer: D,
246) -> Result<Option<Reported<f64>>, D::Error> {
247    read_ranged(deserializer, "flowrate_factor", FLOWRATE_FACTOR_RANGE)
248}
249
250/// Read `PrinterSnapshot::fan_percent` against its declared range.
251pub(crate) fn de_fan_percent<'de, D: Deserializer<'de>>(
252    deserializer: D,
253) -> Result<Option<Reported<f64>>, D::Error> {
254    read_ranged(deserializer, "fan_percent", FAN_PERCENT_RANGE)
255}
256
257/// Read `JobSnapshot::completion` against its declared range.
258pub(crate) fn de_completion<'de, D: Deserializer<'de>>(
259    deserializer: D,
260) -> Result<Option<Reported<f64>>, D::Error> {
261    read_ranged(deserializer, "completion", COMPLETION_RANGE)
262}
263
264/// Read `HeaterSnapshot::actual_c` against its declared range.
265pub(crate) fn de_heater_actual_c<'de, D: Deserializer<'de>>(
266    deserializer: D,
267) -> Result<Option<Reported<f64>>, D::Error> {
268    read_ranged(deserializer, "actual_c", HEATER_ACTUAL_C_RANGE)
269}
270
271/// Read `HeaterSnapshot::target_c` against its declared range.
272pub(crate) fn de_heater_target_c<'de, D: Deserializer<'de>>(
273    deserializer: D,
274) -> Result<Option<Reported<f64>>, D::Error> {
275    read_ranged(deserializer, "target_c", HEATER_TARGET_C_RANGE)
276}
277
278/// Read `HeaterSnapshot::offset_c` against its declared range.
279pub(crate) fn de_heater_offset_c<'de, D: Deserializer<'de>>(
280    deserializer: D,
281) -> Result<Option<Reported<f64>>, D::Error> {
282    read_ranged(deserializer, "offset_c", HEATER_OFFSET_C_RANGE)
283}