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}