Skip to main content

sen6x/
types.rs

1use crate::io::{FromBytes, ValueWrapper};
2use crate::{Error, io};
3use bitrs::layout;
4use fixed_str::FixedStr;
5use sen6x_macros::SenRead;
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9pub(crate) type Milliseconds = u16;
10
11/// A particulate-matter mass concentration, in micrograms per cubic metre (µg/m³).
12///
13/// Obtain the physical value with `f32::from` (or `.into()`).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct MicrogramsPerCubicMeter {
17    value: u16,
18}
19/// Converts to µg/m³ (the raw register value divided by 10).
20impl From<MicrogramsPerCubicMeter> for f32 {
21    fn from(value: MicrogramsPerCubicMeter) -> f32 {
22        value.value as f32 / 10f32
23    }
24}
25
26impl ValueWrapper for MicrogramsPerCubicMeter {
27    type Inner = u16;
28    fn wrap(value: u16) -> Self {
29        MicrogramsPerCubicMeter { value }
30    }
31    fn unwrap(&self) -> Self::Inner {
32        self.value
33    }
34}
35
36/// A relative humidity, in percent (%RH).
37///
38/// Obtain the physical value with `f32::from` (or `.into()`).
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub struct Percent {
42    value: i16,
43}
44
45/// Converts to percent (the raw register value divided by 100).
46impl From<Percent> for f32 {
47    fn from(value: Percent) -> f32 {
48        value.value as f32 / 100f32
49    }
50}
51
52impl ValueWrapper for Percent {
53    type Inner = i16;
54    fn wrap(value: i16) -> Self {
55        Percent { value }
56    }
57    fn unwrap(&self) -> Self::Inner {
58        self.value
59    }
60}
61
62/// A temperature, in degrees Celsius (°C).
63///
64/// Obtain the physical value with `f32::from` (or `.into()`).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub struct DegCelsius {
68    value: i16,
69}
70/// Converts to °C (the raw register value divided by 200).
71impl From<DegCelsius> for f32 {
72    fn from(value: DegCelsius) -> f32 {
73        value.value as f32 / 200f32
74    }
75}
76
77impl ValueWrapper for DegCelsius {
78    type Inner = i16;
79    fn wrap(value: i16) -> Self {
80        DegCelsius { value }
81    }
82    fn unwrap(&self) -> Self::Inner {
83        self.value
84    }
85}
86
87/// A gas-index reading (VOC or NOx index points, nominal range 1–500, 100 ≈ typical).
88///
89/// Obtain the physical value with `f32::from` (or `.into()`).
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92pub struct Index {
93    value: i16,
94}
95
96/// Converts to index points (the raw register value divided by 10).
97impl From<Index> for f32 {
98    fn from(value: Index) -> f32 {
99        value.value as f32 / 10f32
100    }
101}
102
103impl ValueWrapper for Index {
104    type Inner = i16;
105    fn wrap(value: i16) -> Self {
106        Index { value }
107    }
108    fn unwrap(&self) -> Self::Inner {
109        self.value
110    }
111}
112
113/// A gas concentration, in parts per million (ppm) — used for CO₂.
114///
115/// Obtain the physical value with `f32::from` (or `.into()`).
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
118pub struct Ppm {
119    value: i16,
120}
121
122impl Ppm {
123    /// Creates a concentration of `value` ppm.
124    ///
125    /// ```
126    /// use sen6x::types::Ppm;
127    /// assert_eq!(f32::from(Ppm::new(1013)), 1013.0);
128    /// ```
129    pub fn new(value: i16) -> Self {
130        Ppm { value }
131    }
132}
133
134/// Converts to ppm (the raw register value, unscaled).
135impl From<Ppm> for f32 {
136    fn from(value: Ppm) -> f32 {
137        value.value as f32
138    }
139}
140
141impl ValueWrapper for Ppm {
142    type Inner = i16;
143    fn wrap(value: i16) -> Self {
144        Ppm { value }
145    }
146    fn unwrap(&self) -> Self::Inner {
147        self.value
148    }
149}
150
151/// An unsigned gas concentration, in parts per million (ppm).
152///
153/// Used as the reference CO₂ concentration passed to forced recalibration.
154/// Obtain the physical value with `f32::from` (or `.into()`).
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
157pub struct PpmU16 {
158    value: u16,
159}
160
161impl PpmU16 {
162    /// Creates a concentration of `value` ppm.
163    ///
164    /// ```
165    /// use sen6x::types::PpmU16;
166    /// assert_eq!(f32::from(PpmU16::new(1013)), 1013.0);
167    /// ```
168    pub fn new(value: u16) -> Self {
169        PpmU16 { value }
170    }
171}
172
173/// Converts to ppm (the raw value, unscaled).
174impl From<PpmU16> for f32 {
175    fn from(value: PpmU16) -> f32 {
176        value.value as f32
177    }
178}
179
180impl ValueWrapper for PpmU16 {
181    type Inner = u16;
182    fn wrap(value: u16) -> Self {
183        PpmU16 { value }
184    }
185
186    fn unwrap(&self) -> Self::Inner {
187        self.value
188    }
189}
190
191/// A gas concentration, in parts per billion (ppb) — used for formaldehyde (HCHO).
192///
193/// Obtain the physical value with `f32::from` (or `.into()`).
194#[derive(Debug, Copy, Clone, PartialEq, Eq)]
195#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
196pub struct Ppb {
197    value: i16,
198}
199
200/// Converts to ppb (the raw register value divided by 10).
201impl From<Ppb> for f32 {
202    fn from(value: Ppb) -> f32 {
203        value.value as f32 / 10f32
204    }
205}
206
207impl ValueWrapper for Ppb {
208    type Inner = i16;
209    fn wrap(value: i16) -> Self {
210        Ppb { value }
211    }
212    fn unwrap(&self) -> Self::Inner {
213        self.value
214    }
215}
216
217/// A particle number concentration, in particles per cubic centimetre (#/cm³).
218///
219/// Obtain the physical value with `f32::from` (or `.into()`).
220#[derive(Debug, Copy, Clone, PartialEq, Eq)]
221#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
222pub struct ParticlesPerCm3 {
223    value: u16,
224}
225
226/// Converts to particles per cm³ (the raw register value divided by 10).
227impl From<ParticlesPerCm3> for f32 {
228    fn from(value: ParticlesPerCm3) -> f32 {
229        value.value as f32 / 10f32
230    }
231}
232
233impl ValueWrapper for ParticlesPerCm3 {
234    type Inner = u16;
235    fn wrap(value: u16) -> Self {
236        ParticlesPerCm3 { value }
237    }
238    fn unwrap(&self) -> Self::Inner {
239        self.value
240    }
241}
242
243/// An air pressure, in hectopascals (hPa).
244///
245/// Used to set the ambient pressure for the CO₂ sensor. Obtain the physical
246/// value with `f32::from` (or `.into()`).
247#[derive(Debug, Copy, Clone, PartialEq, Eq)]
248#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
249pub struct Hpa {
250    value: u16,
251}
252
253impl Hpa {
254    /// Creates a pressure of `value` hectopascals.
255    ///
256    /// ```
257    /// use sen6x::types::Hpa;
258    /// assert_eq!(f32::from(Hpa::new(1013)), 1013.0);
259    /// ```
260    pub fn new(value: u16) -> Self {
261        Hpa { value }
262    }
263}
264
265/// Converts to hPa (the raw value, unscaled).
266impl From<Hpa> for f32 {
267    fn from(value: Hpa) -> f32 {
268        value.value as f32
269    }
270}
271
272impl ValueWrapper for Hpa {
273    type Inner = u16;
274    fn wrap(value: u16) -> Self {
275        Hpa { value }
276    }
277    fn unwrap(&self) -> Self::Inner {
278        self.value
279    }
280}
281
282/// A length, in metres (m).
283///
284/// Used to set the sensor altitude for the CO₂ sensor's pressure compensation.
285/// Obtain the physical value with `f32::from` (or `.into()`).
286#[derive(Debug, Copy, Clone, PartialEq, Eq)]
287#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
288pub struct Meters {
289    value: u16,
290}
291
292impl Meters {
293    /// Creates a `value` meters.
294    ///
295    /// ```
296    /// use sen6x::types::Meters;
297    /// assert_eq!(f32::from(Meters::new(2000)), 2000.0);
298    /// ```
299    pub fn new(value: u16) -> Self {
300        Meters { value }
301    }
302}
303
304/// Converts to metres (the raw value, unscaled).
305impl From<Meters> for f32 {
306    fn from(value: Meters) -> f32 {
307        value.value as f32
308    }
309}
310
311impl ValueWrapper for Meters {
312    type Inner = u16;
313    fn wrap(value: u16) -> Self {
314        Meters { value }
315    }
316    fn unwrap(&self) -> Self::Inner {
317        self.value
318    }
319}
320
321/// The device's product name, as a fixed-capacity (32-byte) string.
322pub type ProductName = FixedStr<32>;
323/// The device's serial number, as a fixed-capacity (32-byte) string.
324pub type SerialNumber = FixedStr<32>;
325
326impl FromBytes<48, FixedStr<32>> for FixedStr<32> {
327    fn from_bytes_with_crc<E>(bytes: &[u8; 48]) -> Result<FixedStr<32>, Error<E>> {
328        io::check_crc::<32, E>(bytes).map(|v| FixedStr::<32>::from_slice(&v))
329    }
330}
331
332/// Whether new measurement results are available to read.
333#[derive(Debug, SenRead, Clone, Copy, PartialEq, Eq)]
334#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
335pub struct DataReady {
336    /// `true` if new data is ready. `false` if not, or when no measurement is running.
337    pub data_ready: bool,
338}
339
340/// Measured values returned by a SEN62.
341///
342/// A field is `None` when that value is unavailable (for example, when no
343/// measurement has been running for at least one second).
344#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
345#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
346pub struct MeasuredValuesSen62 {
347    ///Mass Concentration PM1.0
348    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
349    ///Mass Concentration PM2.5
350    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
351    ///Mass Concentration PM4.0
352    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
353    /// Mass Concentration PM10.0
354    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
355    /// Ambient Humidity
356    pub ambient_humidity: Option<Percent>,
357    /// Ambient Temperature
358    pub ambient_temperature: Option<DegCelsius>,
359}
360
361/// Measured values returned by a SEN63C (adds CO₂ over the SEN62).
362///
363/// A field is `None` when that value is unavailable (for example, when no
364/// measurement has been running for at least one second).
365#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
366#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
367pub struct MeasuredValuesSen63c {
368    ///Mass Concentration PM1.0
369    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
370    ///Mass Concentration PM2.5
371    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
372    ///Mass Concentration PM4.0
373    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
374    /// Mass Concentration PM10.0
375    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
376    /// Ambient Humidity
377    pub ambient_humidity: Option<Percent>,
378    /// Ambient Temperature
379    pub ambient_temperature: Option<DegCelsius>,
380    /// CO2 concentration
381    pub co2: Option<Ppm>,
382}
383/// Measured values returned by a SEN65 (adds VOC and NOx indices over the SEN62).
384///
385/// A field is `None` when that value is unavailable (for example, when no
386/// measurement has been running for at least one second).
387#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
388#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
389pub struct MeasuredValuesSen65 {
390    ///Mass Concentration PM1.0
391    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
392    ///Mass Concentration PM2.5
393    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
394    ///Mass Concentration PM4.0
395    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
396    /// Mass Concentration PM10.0
397    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
398    /// Ambient Humidity
399    pub ambient_humidity: Option<Percent>,
400    /// Ambient Temperature
401    pub ambient_temperature: Option<DegCelsius>,
402    /// VOC Index
403    pub voc_index: Option<Index>,
404    /// NOx Index
405    pub nox_index: Option<Index>,
406}
407/// Measured values returned by a SEN66 (PM, RH/T, VOC, NOx and CO₂).
408///
409/// A field is `None` when that value is unavailable (for example, when no
410/// measurement has been running for at least one second).
411#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
412#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
413pub struct MeasuredValuesSen66 {
414    ///Mass Concentration PM1.0
415    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
416    ///Mass Concentration PM2.5
417    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
418    ///Mass Concentration PM4.0
419    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
420    /// Mass Concentration PM10.0
421    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
422    /// Ambient Humidity
423    pub ambient_humidity: Option<Percent>,
424    /// Ambient Temperature
425    pub ambient_temperature: Option<DegCelsius>,
426    /// VOC Index
427    pub voc_index: Option<Index>,
428    /// NOx Index
429    pub nox_index: Option<Index>,
430    /// CO2 concentration
431    pub co2: Option<Ppm>,
432}
433
434/// Measured values returned by a SEN68 (adds formaldehyde over the SEN65).
435///
436/// A field is `None` when that value is unavailable (for example, when no
437/// measurement has been running for at least one second).
438#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
439#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
440pub struct MeasuredValuesSen68 {
441    ///Mass Concentration PM1.0
442    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
443    ///Mass Concentration PM2.5
444    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
445    ///Mass Concentration PM4.0
446    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
447    /// Mass Concentration PM10.0
448    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
449    /// Ambient Humidity
450    pub ambient_humidity: Option<Percent>,
451    /// Ambient Temperature
452    pub ambient_temperature: Option<DegCelsius>,
453    /// VOC Index
454    pub voc_index: Option<Index>,
455    /// NOx Index
456    pub nox_index: Option<Index>,
457    /// Formaldehyde concentration
458    pub hcho: Option<Ppb>,
459}
460
461/// Measured values returned by a SEN69C (PM, RH/T, VOC, NOx, formaldehyde and CO₂).
462///
463/// A field is `None` when that value is unavailable (for example, when no
464/// measurement has been running for at least one second).
465#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
466#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
467pub struct MeasuredValuesSen69c {
468    ///Mass Concentration PM1.0
469    pub pm_1_0: Option<MicrogramsPerCubicMeter>,
470    ///Mass Concentration PM2.5
471    pub pm_2_5: Option<MicrogramsPerCubicMeter>,
472    ///Mass Concentration PM4.0
473    pub pm_4_0: Option<MicrogramsPerCubicMeter>,
474    /// Mass Concentration PM10.0
475    pub pm_10_0: Option<MicrogramsPerCubicMeter>,
476    /// Ambient Humidity
477    pub ambient_humidity: Option<Percent>,
478    /// Ambient Temperature
479    pub ambient_temperature: Option<DegCelsius>,
480    /// VOC Index
481    pub voc_index: Option<Index>,
482    /// NOx Index
483    pub nox_index: Option<Index>,
484    /// Formaldehyde concentration
485    pub hcho: Option<Ppb>,
486    /// CO2 concentration
487    pub co2: Option<Ppm>,
488}
489
490/// Raw (uncompensated) values from a SEN62 or SEN63C.
491///
492/// A field is `None` when that value is unavailable.
493#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
494#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
495pub struct RawValuesSen62Sen63c {
496    /// Ambient Humidity
497    pub ambient_humidity: Option<Percent>,
498    /// Ambient Temperature
499    pub ambient_temperature: Option<DegCelsius>,
500}
501
502/// Raw (uncompensated) values from a SEN65, SEN68 or SEN69C.
503///
504/// A field is `None` when that value is unavailable.
505#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
506#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
507pub struct RawValuesSen65Sen68Sen69c {
508    /// Ambient Humidity
509    pub ambient_humidity: Option<Percent>,
510    /// Ambient Temperature
511    pub ambient_temperature: Option<DegCelsius>,
512    /// VOC Index
513    pub voc_index: Option<Index>,
514    /// NOx Index
515    pub nox_index: Option<Index>,
516}
517
518/// Raw (uncompensated) values from a SEN66.
519///
520/// A field is `None` when that value is unavailable.
521#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
522#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
523pub struct RawValuesSen66 {
524    /// Ambient Humidity
525    pub ambient_humidity: Option<Percent>,
526    /// Ambient Temperature
527    pub ambient_temperature: Option<DegCelsius>,
528    /// VOC Index
529    pub voc_index: Option<Index>,
530    /// NOx Index
531    pub nox_index: Option<Index>,
532    /// CO2 concentration
533    pub co2: Option<Ppm>,
534}
535
536/// Particle number concentrations, cumulative per size bin.
537///
538/// Each field is the number concentration of particles up to the given
539/// aerodynamic diameter. A field is `None` when the value is unavailable.
540#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
541#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
542pub struct NumberConcentrationValues {
543    /// Number concentration of particles ≤ 0.5 µm.
544    pub pm_0_5: Option<ParticlesPerCm3>,
545    /// Number concentration of particles ≤ 1.0 µm.
546    pub pm_1_0: Option<ParticlesPerCm3>,
547    /// Number concentration of particles ≤ 2.5 µm.
548    pub pm_2_5: Option<ParticlesPerCm3>,
549    /// Number concentration of particles ≤ 4.0 µm.
550    pub pm_4_0: Option<ParticlesPerCm3>,
551    /// Number concentration of particles ≤ 10 µm.
552    pub pm_10: Option<ParticlesPerCm3>,
553}
554
555/// Custom temperature-offset parameters used to compensate the ambient
556/// temperature reading for the host design.
557#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
558#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
559pub struct TemperatureOffsetParameters {
560    /// Constant temperature offset to subtract.
561    pub offset: DegCelsius,
562    /// Normalized slope of the offset versus the measured temperature.
563    pub slope: i16,
564    /// Time constant of the offset filter, in seconds.
565    pub time_constant: u16,
566    /// Offset slot (0–4) being configured; the device blends all active slots.
567    pub slot: u16,
568}
569
570/// Custom temperature-acceleration parameters of the RH/T engine, overriding
571/// the device defaults. See the datasheet for the exact transfer function.
572#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
573#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
574pub struct TemperatureAccelerationParameters {
575    /// Filter constant `K`.
576    pub k: u16,
577    /// Filter constant `P`.
578    pub p: u16,
579    /// First time constant `T1`.
580    pub t1: u16,
581    /// Second time constant `T2`.
582    pub t2: u16,
583}
584
585layout! {
586    /// Device status register, decoded into individual flags.
587    ///
588    /// Each accessor returns a single status bit. Error flags are *sticky* — they
589    /// stay set until cleared (see `read_and_clear_device_status` or a reset).
590    /// The available flags are:
591    ///
592    /// - `speed_warning` — fan speed is outside the target range.
593    /// - `co2_1_error`, `co2_2_error` — CO₂ sensor errors.
594    /// - `pm_error` — particulate-matter sensor error.
595    /// - `hcho_error` — formaldehyde sensor error.
596    /// - `gas_error` — VOC/NOx gas sensor error.
597    /// - `rh_t_error` — humidity/temperature sensor error.
598    /// - `fan_error` — fan is mechanically blocked or broken.
599    pub struct DeviceStatus(u32);
600    {
601        let __ @ 31..22;
602        let speed_warning @ 21;
603        let __ @ 20..13;
604        let co2_1_error @ 12;
605        let pm_error @ 11;
606        let hcho_error @ 10;
607        let co2_2_error @ 9;
608        let __ @ 8;
609        let gas_error @ 7;
610        let rh_t_error @ 6;
611        let __ @ 5;
612        let fan_error @ 4;
613        let __ @ 3..0;
614    }
615}
616
617impl ValueWrapper for DeviceStatus {
618    type Inner = u32;
619    fn wrap(value: u32) -> Self {
620        DeviceStatus::from(value)
621    }
622    fn unwrap(&self) -> Self::Inner {
623        self.0
624    }
625}
626
627/// Device firmware version.
628#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
629#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
630pub struct Version {
631    /// Major firmware version.
632    pub major: u8,
633    /// Minor firmware version.
634    pub minor: u8,
635}
636
637/// Humidity and temperature measured by the SHT sensor at the end of a heater cycle.
638#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
639#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
640pub struct ShtHeaterMeasurements {
641    /// Relative humidity reported by the SHT sensor, or `None` if unavailable.
642    pub sht_relative_humidity: Option<Percent>,
643    /// Temperature reported by the SHT sensor, or `None` if unavailable.
644    pub sht_temperature: Option<DegCelsius>,
645}
646
647/// Tuning parameters for the VOC gas-index algorithm.
648///
649/// See Sensirion's
650/// [VOC Index for Indoor Air Applications](https://sensirion.com/media/documents/02232963/6294E043/Info_Note_VOC_Index.pdf)
651/// for the meaning and valid ranges of each parameter.
652#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
653#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
654pub struct VocAlgorithmTuningParameters {
655    /// Index value the algorithm maps the average condition to (default 100).
656    pub index_offset: i16,
657    /// Time constant (hours) for the offset's adaptive learning.
658    pub learning_time_offset_hours: i16,
659    /// Time constant (hours) for the gain's adaptive learning.
660    pub learning_time_gain_hours: i16,
661    /// Maximum duration (minutes) that gating may stall learning.
662    pub gating_max_duration_minutes: i16,
663    /// Initial standard deviation used to estimate the gain.
664    pub std_initial: i16,
665    /// Gain factor applied to the normalized signal.
666    pub gain_factor: i16,
667}
668
669/// Opaque backup of the VOC algorithm's internal state.
670///
671/// Read it to persist learning across a power cycle or reset, and write it back
672/// before the next measurement to skip the initial learning phase.
673#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
674#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
675pub struct VocAlgorithmState {
676    /// The 8 raw state bytes, treated as an opaque blob.
677    pub state: [u8; 8],
678}
679
680impl FromBytes<12, [u8; 8]> for [u8; 8] {
681    fn from_bytes_with_crc<E>(bytes: &[u8; 12]) -> Result<[u8; 8], Error<E>> {
682        io::check_crc::<8, E>(bytes)
683    }
684}
685
686/// Tuning parameters for the NOx gas-index algorithm.
687///
688/// See Sensirion's
689/// [NOx Index for Indoor Air Applications](https://sensirion.com/media/documents/9F289B95/6294DFFC/Info_Note_NOx_Index.pdf)
690/// for the meaning and valid ranges of each parameter.
691#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
692#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
693pub struct NoxAlgorithmTuningParameters {
694    /// Index value the algorithm maps the average condition to (default 1).
695    pub index_offset: i16,
696    /// Time constant (hours) for the offset's adaptive learning.
697    pub learning_time_offset_hours: i16,
698    /// Time constant (hours) for the gain's adaptive learning (unused for NOx).
699    pub learning_time_gain_hours: i16,
700    /// Maximum duration (minutes) that gating may stall learning.
701    pub gating_max_duration_minutes: i16,
702    /// Initial standard deviation used to estimate the gain.
703    pub std_initial: i16,
704    /// Gain factor applied to the normalized signal.
705    pub gain_factor: i16,
706}
707
708/// Result of a forced CO₂ recalibration (FRC).
709#[derive(SenRead, Debug, Clone, PartialEq, Eq)]
710#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
711pub struct Co2Correction {
712    /// Raw FRC result word, or `None` if the recalibration failed (`0xFFFF`).
713    ///
714    /// Prefer [`Co2Correction::value`], which decodes this into a correction in ppm.
715    pub result: Option<u16>,
716}
717
718impl Co2Correction {
719    /// The applied CO₂ correction, in ppm, or `None` if the recalibration failed.
720    ///
721    /// The raw result is offset-encoded around `0x8000`; this subtracts the offset.
722    ///
723    /// ```
724    /// use sen6x::types::Co2Correction;
725    /// // 0x8000 encodes a zero correction.
726    /// let c = Co2Correction { result: Some(0x8000) };
727    /// assert_eq!(f32::from(c.value().unwrap()), 0.0);
728    /// ```
729    pub fn value(&self) -> Option<Ppm> {
730        self.result
731            .map(|v| <Ppm as ValueWrapper>::wrap(((v as i32) - 0x8000i32) as i16))
732    }
733}