aquarium_control/recorder/
recorder_data_frame.rs

1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9use chrono::NaiveDateTime;
10use std::fmt;
11
12#[derive(Debug, PartialEq, Clone)]
13/// Contains the data to be logged wrapped into Option.
14/// For each signal, where the data could not be acquired, the None value is used.
15/// The None value is represented as NULL in the SQL database.
16/// The struct only contains minimal implementation for development purposes.
17#[derive(Default)]
18pub struct RecorderDataFrame {
19    // timestamp when the frame was captured
20    pub timestamp: NaiveDateTime,
21
22    // measured water temperature in °C
23    pub water_temperature: Option<f32>,
24
25    // filtered water temperature in °C
26    pub water_temperature_filtered: Option<f32>,
27
28    // measured pH value of water
29    pub ph: Option<f32>,
30
31    // filtered pH value of water
32    pub ph_filtered: Option<f32>,
33
34    // measured conductivity of water in uS/cm
35    pub conductivity: Option<f32>,
36
37    // filtered conductivity of water in uS/cm
38    pub conductivity_filtered: Option<f32>,
39
40    // conductivity in uS/cm compensated for temperature deviation
41    pub conductivity_compensated: Option<f32>,
42
43    // status bit indicating that fresh water refill is ongoing
44    pub refill_in_progress: Option<bool>,
45
46    // measured tank level switch position (true=high, false=low)
47    pub tank_level_switch_position: Option<bool>,
48
49    // calculated tank level switch stabilized position (true=high, false=low)
50    pub tank_level_switch_position_stabilized: Option<bool>,
51
52    // tank level switch validity bit (true=invalid, false=valid)
53    pub tank_level_switch_invalid: Option<bool>,
54
55    // status bit indicating that ventilation is switched on
56    pub surface_ventilation_status: Option<bool>,
57
58    // measured ambient temperature in °C
59    pub ambient_temperature: Option<f32>,
60
61    // measured ambient humidity in %
62    pub ambient_humidity: Option<f32>,
63
64    // status bit indicating that the heater is switched on
65    pub heater_status: Option<bool>,
66}
67
68impl fmt::Display for RecorderDataFrame {
69    /// Provides a multi-line, human-readable output of the struct's values for debugging purposes.
70    ///
71    /// This implementation formats all fields of the `RecorderDataFrame` into a detailed
72    /// string representation. `Option` fields are displayed as their contained value or
73    /// the string "None" if empty. Boolean `Option` fields are converted to descriptive
74    /// words like "yes"/"no", "high"/"low", etc. This is particularly useful for
75    /// inspecting the contents of data frames during development and logging.
76    ///
77    /// # Arguments
78    /// * `f` - A mutable reference to the formatter, as required by the `fmt::Display` trait.
79    ///
80    /// # Returns
81    /// A `fmt::Result` indicating whether the formatting operation was successful.
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        let none_string = "None".to_string();
84        let water_temperature_string = match self.water_temperature {
85            Some(water_temperature) => water_temperature.to_string(),
86            None => none_string.clone(),
87        };
88        let water_temperature_filtered_string = match self.water_temperature_filtered {
89            Some(water_temperature_filtered) => water_temperature_filtered.to_string(),
90            None => none_string.clone(),
91        };
92        let ph_string = match self.ph {
93            Some(ph) => ph.to_string(),
94            None => none_string.clone(),
95        };
96        let ph_filtered_string = match self.ph_filtered {
97            Some(ph_filtered) => ph_filtered.to_string(),
98            None => none_string.clone(),
99        };
100        let conductivity_string = match self.conductivity {
101            Some(conductivity) => conductivity.to_string(),
102            None => none_string.clone(),
103        };
104        let conductivity_filtered_string = match self.conductivity_filtered {
105            Some(conductivity_filtered) => conductivity_filtered.to_string(),
106            None => none_string.clone(),
107        };
108        let conductivity_compensated_string = match self.conductivity_compensated {
109            Some(conductivity_compensated) => conductivity_compensated.to_string(),
110            None => none_string.clone(),
111        };
112        let refill_in_progress_string = match self.refill_in_progress {
113            Some(refill_in_progress) => match refill_in_progress {
114                true => "yes".to_string(),
115                false => "no".to_string(),
116            },
117            None => none_string.clone(),
118        };
119        let tank_level_switch_position_string = match self.tank_level_switch_position {
120            Some(tank_level_switch_position) => match tank_level_switch_position {
121                true => "high".to_string(),
122                false => "low".to_string(),
123            },
124            None => none_string.clone(),
125        };
126        let tank_level_switch_position_stabilized_string =
127            match self.tank_level_switch_position_stabilized {
128                Some(tank_level_switch_position_stabilized) => {
129                    match tank_level_switch_position_stabilized {
130                        true => "high".to_string(),
131                        false => "low".to_string(),
132                    }
133                }
134                None => none_string.clone(),
135            };
136        let tank_level_switch_invalid_string = match self.tank_level_switch_invalid {
137            Some(tank_level_switch_invalid) => match tank_level_switch_invalid {
138                true => "invalid".to_string(),
139                false => "valid".to_string(),
140            },
141            None => none_string.clone(),
142        };
143        let surface_ventilation_status_string = match self.surface_ventilation_status {
144            Some(surface_ventilation_status) => match surface_ventilation_status {
145                true => "on".to_string(),
146                false => "off".to_string(),
147            },
148            None => none_string.clone(),
149        };
150        let ambient_temperature_string = match self.ambient_temperature {
151            Some(ambient_temperature) => ambient_temperature.to_string(),
152            None => none_string.clone(),
153        };
154        let ambient_humidity_string = match self.ambient_humidity {
155            Some(ambient_humidity) => ambient_humidity.to_string(),
156            None => none_string.clone(),
157        };
158        let heater_status_string = match self.heater_status {
159            Some(heater_status) => match heater_status {
160                true => "on".to_string(),
161                false => "off".to_string(),
162            },
163            None => none_string.clone(),
164        };
165        write!(
166            f,
167            "\n**************************************\ntimestamp: {}\
168            \nwater_temperature: {},\nwater_temperature_filtered: {}, \
169            \nph: {},\nph_filtered: {}, \
170            \nconductivity: {},\nconductivity_filtered: {},\nconductivity_compensated: {}, \
171            \nrefill_in_progress: {}, \
172            \ntank_level_switch_position: {},\ntank_level_switch_position_stabilized: {}\ntank_level_switch_invalid: {},\
173            \nsurface_ventilation_status: {},\
174            \nambient_temperature: {},\nambient_humidity: {}, \
175            \nheater_status: {} \n
176            **************************************\n",
177            self.timestamp,
178            water_temperature_string,
179            water_temperature_filtered_string,
180            ph_string,
181            ph_filtered_string,
182            conductivity_string,
183            conductivity_filtered_string,
184            conductivity_compensated_string,
185            refill_in_progress_string,
186            tank_level_switch_position_string,
187            tank_level_switch_position_stabilized_string,
188            tank_level_switch_invalid_string,
189            surface_ventilation_status_string,
190            ambient_temperature_string,
191            ambient_humidity_string,
192            heater_status_string,
193        )
194    }
195}
196
197#[cfg(test)]
198impl RecorderDataFrame {
199    // Asserts that all fields of this `RecorderDataFrame` match a set of target values.
200    //
201    // This helper function is used in unit tests to perform comprehensive
202    // equality checks on a `RecorderDataFrame` instance. It compares each
203    // `Option<T>` field of the current frame against the corresponding
204    // `target_X` `Option<T>` value provided.
205    //
206    // # Arguments
207    // * `target_water_temperature` - Expected `Option<f32>` for water temperature.
208    // * `target_water_temperature_filtered` - Expected `Option<f32>` for filtered water temperature.
209    // * `target_water_ph` - Expected `Option<f32>` for pH.
210    // * `target_water_ph_filtered` - Expected `Option<f32>` for filtered pH.
211    // * `target_water_conductivity` - Expected `Option<f32>` for conductivity.
212    // * `target_water_conductivity_filtered` - Expected `Option<f32>` for filtered conductivity.
213    // * `target_water_conductivity_compensated` - Expected `Option<f32>` for compensated conductivity.
214    // * `target_refill_in_progress` - Expected `Option<bool>` for refill in progress status.
215    // * `target_tank_level_switch_position` - Expected `Option<bool>` for tank level switch position.
216    // * `target_tank_level_switch_position_stabilized` - Expected `Option<bool>` for stabilized tank level switch position.
217    // * `target_tank_level_switch_invalid` - Expected `Option<bool>` for tank level switch invalid status.
218    // * `target_surface_ventilation_status` - Expected `Option<bool>` for surface ventilation status.
219    // * `target_ambient_temperature` - Expected `Option<f32>` for ambient temperature.
220    // * `target_ambient_humidity` - Expected `Option<f32>` for ambient humidity.
221    // * `target_heater_status` - Expected `Option<bool>` for heater status.
222    // * `target_water_temperature_ds18b20` - Expected `Option<f32>` for DS18B20 water temperature.
223    // * `target_ambient_temperature_ds18b20` - Expected `Option<f32>` for DS18B20 ambient temperature.
224    //
225    // # Panics
226    // This function will panic if any field of `self` does not strictly
227    // equal its corresponding `target_X` value (including matching `Some(value)`
228    // with `Some(value)` or `None` with `None`).
229    pub fn assert(
230        &self,
231        target_water_temperature: Option<f32>,
232        target_water_temperature_filtered: Option<f32>,
233        target_water_ph: Option<f32>,
234        target_water_ph_filtered: Option<f32>,
235        target_water_conductivity: Option<f32>,
236        target_water_conductivity_filtered: Option<f32>,
237        target_water_conductivity_compensated: Option<f32>,
238        target_refill_in_progress: Option<bool>,
239        target_tank_level_switch_position: Option<bool>,
240        target_tank_level_switch_position_stabilized: Option<bool>,
241        target_tank_level_switch_invalid: Option<bool>,
242        target_surface_ventilation_status: Option<bool>,
243        target_ambient_temperature: Option<f32>,
244        target_ambient_humidity: Option<f32>,
245        target_heater_status: Option<bool>,
246    ) {
247        assert_eq!(self.water_temperature, target_water_temperature);
248        assert_eq!(
249            self.water_temperature_filtered,
250            target_water_temperature_filtered,
251        );
252        assert_eq!(self.ph, target_water_ph);
253        assert_eq!(self.ph_filtered, target_water_ph_filtered);
254        assert_eq!(self.conductivity, target_water_conductivity);
255        assert_eq!(
256            self.conductivity_filtered,
257            target_water_conductivity_filtered
258        );
259        assert_eq!(
260            self.conductivity_compensated,
261            target_water_conductivity_compensated
262        );
263        assert_eq!(self.refill_in_progress, target_refill_in_progress);
264        assert_eq!(
265            self.tank_level_switch_position,
266            target_tank_level_switch_position
267        );
268        assert_eq!(
269            self.tank_level_switch_position_stabilized,
270            target_tank_level_switch_position_stabilized
271        );
272        assert_eq!(
273            self.tank_level_switch_invalid,
274            target_tank_level_switch_invalid
275        );
276        assert_eq!(
277            self.surface_ventilation_status,
278            target_surface_ventilation_status
279        );
280        assert_eq!(self.ambient_temperature, target_ambient_temperature);
281        assert_eq!(self.ambient_humidity, target_ambient_humidity);
282        assert_eq!(self.heater_status, target_heater_status);
283    }
284}