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}