aquarium_control/utilities/
config.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*/
9
10#[cfg(not(test))]
11use log::info;
12
13use serde_derive::Deserialize;
14use std::fs;
15
16#[cfg(all(target_os = "linux", feature = "target_hw"))]
17use crate::beacon::ws2812b::Ws2812BConfig;
18
19use crate::food::feed_config::FeedConfig;
20use crate::mineral::balling_config::BallingConfig;
21use crate::recorder::data_logger_config::DataLoggerConfig;
22use crate::relays::actuate_controllino_config::ActuateControllinoConfig;
23use crate::relays::relay_manager_config::RelayManagerConfig;
24#[allow(unused)] // used in conditionally compiled code
25use crate::sensors::atlas_scientific_config::AtlasScientificConfig;
26use crate::sensors::dht_config::DhtConfig;
27use crate::sensors::gpio_handler_config::GpioHandlerConfig;
28use crate::sensors::sensor_manager_config::SensorManagerConfig;
29use crate::thermal::heating_config::HeatingConfig;
30#[cfg(test)]
31use std::env;
32
33#[cfg(feature = "target_hw")]
34use crate::sensors::i2c_interface_config::I2cInterfaceConfig;
35
36use crate::sensors::tank_level_switch_config::TankLevelSwitchConfig;
37
38#[cfg(target_os = "linux")]
39use crate::dispatch::messaging_config::MessagingConfig;
40
41use crate::database::sql_interface_config::SqlInterfaceConfig;
42use crate::permission::schedule_check_config::ScheduleCheckConfig;
43use crate::sensors::ds18b20_config::Ds18b20Config;
44use crate::simulator::tcp_communication_config::TcpCommunicationConfig;
45use crate::thermal::ventilation_config::VentilationConfig;
46use crate::utilities::config_error::ConfigError;
47use crate::utilities::logger_config::LoggerConfig;
48use crate::utilities::publish_pid_config::PublishPidConfig;
49use crate::watchmen::memory_config::MemoryConfig;
50use crate::watchmen::monitors_config::MonitorsConfig;
51use crate::watchmen::watchdog_config::WatchdogConfig;
52use crate::water::refill_config::RefillConfig;
53
54/// Top level struct to hold the configuration data read from .toml file.
55/// It does not contain any implementation.
56#[derive(Deserialize)]
57pub struct ConfigData {
58    /// configuration of the SQL database interface
59    pub sql_interface: SqlInterfaceConfig,
60
61    /// configuration of the TCP connection (e.g., for simulator)
62    pub tcp_communication: TcpCommunicationConfig,
63
64    /// configuration of the data logger
65    pub data_logger: DataLoggerConfig,
66
67    /// configuration for AtlasScientific
68    #[allow(unused)] // used in conditionally compiled code
69    pub atlas_scientific: AtlasScientificConfig,
70
71    /// configuration for Ambient
72    pub sensor_manager: SensorManagerConfig,
73
74    /// configuration GPIO handler
75    pub gpio_handler: GpioHandlerConfig,
76
77    /// configuration of the DHT driver
78    #[allow(unused)] // used in conditionally compiled code
79    pub dht: DhtConfig,
80
81    /// configuration of the fresh water refill control
82    pub refill: RefillConfig,
83
84    /// configuration of the tank level switch calculation
85    pub tank_level_switch: TankLevelSwitchConfig,
86
87    /// configuration of the heating control
88    pub heating: HeatingConfig,
89
90    /// configuration of the ventilation control
91    pub ventilation: VentilationConfig,
92
93    /// configuration of the monitors
94    pub monitors: MonitorsConfig,
95
96    /// configuration of the Balling dosing control
97    pub balling: BallingConfig,
98
99    /// configuration of the feed control
100    pub feed: FeedConfig,
101
102    /// configuration of the relay manager
103    pub relay_manager: RelayManagerConfig,
104
105    /// configuration of the Controllino relays
106    pub controllino_relay: ActuateControllinoConfig,
107
108    /// configuration for schedule check
109    pub schedule_check: ScheduleCheckConfig,
110
111    #[cfg(feature = "target_hw")]
112    /// configuration of I2C interface
113    pub i2c_interface: I2cInterfaceConfig,
114
115    #[cfg(target_os = "linux")]
116    /// configuration of messaging (IPC)
117    pub messaging: MessagingConfig,
118
119    /// configuration of PID publication
120    pub publish_pid: PublishPidConfig,
121
122    /// configuration of Watchdog communication
123    pub watchdog: WatchdogConfig,
124
125    /// configuration of DS18B20 temperature sensor communication
126    pub ds18b20: Ds18b20Config,
127
128    /// configuration of WS2812B RGB LED
129    #[cfg(all(target_os = "linux", feature = "target_hw"))]
130    pub ws2812b: Ws2812BConfig,
131
132    /// check of memory configuration
133    pub memory: MemoryConfig,
134
135    /// configuration of the general log file
136    pub logger: LoggerConfig,
137}
138
139#[cfg(not(test))]
140/// Loads configuration data from a specified `.toml` file into a `ConfigData` struct.
141///
142/// This version of the function is intended for production environments. It reads
143/// the entire content of the `.toml` file and then attempts to parse it into the
144/// `ConfigData` structure.
145///
146/// # Arguments
147/// * `filename` - The fully qualified path (including directory and file name)
148///   of the `.toml` configuration file to be read.
149///
150/// # Returns
151/// A `Result` containing a `ConfigData` struct with the parsed configuration on success.
152///
153/// # Errors
154/// Returns a `ConfigError` variant if any part of the process fails:
155/// - `ConfigError::FileRead`: If the specified file cannot be read from the filesystem.
156///   This can be caused by the file not existing, or the application lacking the
157///   necessary permissions to access it.
158/// - `ConfigError::TomlParse`: If the file's content is not valid TOML, or if its
159///   structure does not match the `ConfigData` struct, preventing deserialization.
160pub fn read_config_file(filename: String) -> Result<ConfigData, ConfigError> {
161    #[cfg(not(test))]
162    info!(target: module_path!(), "reading configuration from {filename}");
163
164    // Read the contents of the file and return error in case of failure.
165    let contents = fs::read_to_string(filename.clone())
166        .map_err(|e| ConfigError::FileRead(filename.clone(), e))?;
167
168    // Parse the configuration data and return error in case of failure.
169    let data = toml::from_str(&contents).map_err(|e| ConfigError::TomlParse(filename, e))?;
170
171    Ok(data)
172}
173
174#[cfg(test)]
175// Loads configuration data from a specified `.toml` file into a `ConfigData` struct for testing.
176//
177// This version of the function is specifically designed for test environments (`#[cfg(test)]`).
178// It constructs the full file path by prepending the `CARGO_MANIFEST_DIR`
179// environment variable (which points to the root of the current Cargo package)
180// to the provided `filename`. It then reads and attempts to parse the TOML content.
181//
182// # Arguments
183// * `filename` - The relative path to the configuration file, expected to be located
184//   within a subfolder of the Cargo base directory.
185//
186// # Returns
187// A `ConfigData` struct containing the parsed configuration if successful.
188//
189// # Panics
190// This function will panic in the following scenarios:
191// - If the `CARGO_MANIFEST_DIR` environment variable is not set.
192// - If the specified file cannot be read (e.g., the file is not found, permissions issues).
193// - If the file content is not valid TOML or cannot be deserialized into `ConfigData`.
194pub fn read_config_file(filename: String) -> Result<ConfigData, ConfigError> {
195    let pathname_test =
196        env::var("CARGO_MANIFEST_DIR").map_err(|_| ConfigError::CargoManifestDir)?;
197
198    let filename_test = pathname_test + "/" + filename.as_str();
199    // inspired by https://codingpackets.com/blog/rust-load-a-toml-file/
200    // Read the contents of the file using a `match` block
201    // to return the `data: Ok(c)` as a `String`
202    // or handle any `errors: Err(_)`.
203    let contents = match fs::read_to_string(filename_test.clone()) {
204        // If successful, return the file's text as `contents`.
205        // `c` is a local variable.
206        Ok(c) => c,
207        // Handle the `error` case.
208        Err(_) => {
209            // Write `msg` to `stderr`.
210            panic!("Could not read file `{}`", filename);
211        }
212    };
213
214    // Use a `match` block to return the
215    // file `contents` as a `Data struct: Ok(d)`
216    // or handle any `errors: Err(_)`.
217    let result = match toml::from_str(&contents) {
218        // If successful, return data as `Data` struct.
219        // `d` is a local variable.
220        Ok(d) => d,
221        // Handle the `error` case.
222        Err(e) => {
223            // Write `msg` to `stderr`.
224            panic!(
225                "{}: unable to load data from `{filename}` ({e})",
226                module_path!()
227            );
228        }
229    };
230    Ok(result)
231}
232
233#[cfg(test)]
234// Loads configuration data from a `.toml` file and overrides the database name for testing.
235//
236// This helper function is exclusively used in test environments. It reads the
237// configuration from a specified `.toml` file (relative to `CARGO_MANIFEST_DIR`)
238// and then *replaces* the `db_name` within the `SqlInterfaceConfig` to point
239// to a unique test database, ensuring test isolation.
240//
241// # Arguments
242// * `relative_path` - The relative path to the configuration file within the Cargo base folder.
243// * `test_db_number` - A unique number (between 1 and 99, inclusive) used to
244//   construct the specific test database name (e.g., `aquarium_test_01`, `aquarium_test_28`).
245//
246// # Returns
247// A `ConfigData` struct with the database name specifically set for testing.
248//
249// # Panics
250// This function will panic in the following scenarios:
251// - If `test_db_number` is 0 or greater than 99.
252// - If `read_config_file` (the underlying function it calls) panics
253//   (e.g., file not found, invalid TOML, `CARGO_MANIFEST_DIR` not set).
254pub fn read_config_file_with_test_database(
255    relative_path: String,
256    test_db_number: u32,
257) -> ConfigData {
258    if test_db_number == 0 || test_db_number > 99 {
259        panic!(
260            "{}: number for test database ({}) is out of bounds.",
261            module_path!(),
262            test_db_number
263        )
264    }
265
266    // assemble the test database name with prefix and index.
267    let test_db_name: String = "aquarium_test_".to_string() + &format!("{:02}", test_db_number);
268
269    let mut config = read_config_file(relative_path).unwrap();
270
271    // replace the database name from the configuration file with the test database name
272    // required by the test case
273    config.sql_interface.db_name = test_db_name;
274
275    config
276}