aquarium_control/sensors/i2c_interface.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(all(target_os = "linux", feature = "target_hw"))]
11use log::info;
12use spin_sleep::SpinSleeper;
13use std::fmt;
14
15#[cfg(all(target_os = "linux", feature = "target_hw"))]
16use crate::launch::channels::AquaChannelError;
17
18use crate::sensors::i2c_error::I2cError;
19use crate::sensors::i2c_interface_config::I2cInterfaceConfig;
20
21cfg_if::cfg_if! {
22 if #[cfg(all(target_os = "linux", feature = "target_hw"))] {
23 use log::error;
24 use rppal::i2c::I2c; // required for communication with AtlasScientific sensors
25
26 #[cfg(feature = "debug_i2c_interface")]
27 use log::debug;
28
29 use std::time::Duration;
30 use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
31 use crate::sensors::i2c_error::I2cError::{InitializationFailure, SetTimeoutError};
32 use crate::sensors::i2c_interface_channels::I2cInterfaceChannels;
33 use nix::unistd::gettid;
34 }
35}
36
37/// A constant for the maximum command length considering future applications as well.
38const MAX_I2C_COMMAND_LENGTH: usize = 16;
39
40/// A constant for the maximum response length from an I2C device.
41const MAX_I2C_RESPONSE_LENGTH: usize = 64;
42
43pub struct I2cRequest {
44 #[allow(unused)]
45 /// I2C address of the bus participant
46 pub i2c_address: u16,
47
48 #[allow(unused)]
49 /// input sequence to be sent to the bus participant
50 pub(crate) send_buf: [u8; MAX_I2C_COMMAND_LENGTH],
51
52 #[allow(unused)]
53 /// The actual length of the command in the buffer.
54 pub(crate) send_len: usize,
55
56 #[allow(unused)]
57 /// sleep time in milliseconds before retrieving an answer from the bus participant
58 pub(crate) sleep_time_millis: u64,
59
60 #[allow(unused)]
61 /// length of the expected response in byte
62 pub(crate) expected_response_length: usize,
63}
64
65impl I2cRequest {
66 /// Creates a new `I2cRequest` after validating the command length.
67 ///
68 /// This constructor prepares a command for sending over the I2C bus. It takes a
69 /// command as a slice, copies it into a fixed-size buffer, and packages it
70 /// with the necessary metadata for the I2C interface to execute the request.
71 ///
72 /// # Arguments
73 /// * `i2c_address` - The 7-bit I2C address of the target device.
74 /// * `send_slice` - A byte slice (`&[u8]`) containing the command to be sent.
75 /// * `sleep_time_millis` - The duration in milliseconds to wait after sending the
76 /// command before attempting to read a response.
77 /// * `expected_response_length` - The number of bytes expected in the response from the device.
78 ///
79 /// # Returns
80 /// A `Result` containing a new `I2cRequest` struct on success.
81 ///
82 /// # Errors
83 /// Returns an `I2cError` if the provided command is too large for the buffer:
84 /// - `I2cError::CommandTooLong`: If the length of `send_slice` exceeds the
85 /// `MAX_I2C_COMMAND_LENGTH` constant.
86 #[allow(unused)] // used in conditionally compiled code
87 pub fn new(
88 i2c_address: u16,
89 send_slice: &[u8],
90 sleep_time_millis: u64,
91 expected_response_length: usize,
92 ) -> Result<I2cRequest, I2cError> {
93 // Ensure the command fits into our fixed-size buffer.
94 if send_slice.len() > MAX_I2C_COMMAND_LENGTH {
95 return Err(I2cError::CommandTooLong {
96 location: module_path!().to_string(),
97 max_len: MAX_I2C_COMMAND_LENGTH,
98 actual_len: send_slice.len(),
99 });
100 }
101
102 // Create a zero-initialized array and copy the command into it.
103 let mut send_buf = [0u8; MAX_I2C_COMMAND_LENGTH];
104 send_buf[..send_slice.len()].copy_from_slice(send_slice);
105
106 Ok(I2cRequest {
107 i2c_address,
108 send_buf,
109 send_len: send_slice.len(),
110 sleep_time_millis,
111 expected_response_length,
112 })
113 }
114}
115
116#[derive(Debug, Clone)]
117pub struct I2cResponse {
118 /// A fixed-size buffer holding the sequence received from the bus participant.
119 pub read_buf: [u8; MAX_I2C_RESPONSE_LENGTH],
120
121 #[allow(unused)]
122 /// length of a message as informed by i2c::read
123 pub length: usize,
124}
125
126impl fmt::Display for I2cResponse {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 // descriptive prefix
129 write!(f, "I2cResponse [")?;
130
131 // Use self.length to iterate over only the valid part of the buffer.
132 let valid_slice = &self.read_buf[..self.length];
133
134 // Format the read_buf content
135 // Iterate over the bytes and format each as two-digit hexadecimal
136 for (i, &byte) in valid_slice.iter().enumerate() {
137 write!(f, "{byte:02x}")?; // :02x ensures two hexadecimal digits, padding with leading zero if necessary
138 if i < valid_slice.len() - 1 {
139 write!(f, " ")?; // Add a space between bytes for readability
140 }
141 }
142
143 // Conditionally include the length field if it's compiled in
144 #[cfg(any(test, all(target_os = "linux", feature = "target_hw")))]
145 {
146 // Only show length if it's different from the actual buffer length,
147 // or if it's explicitly useful. In many cases, length == read_buf.len().
148 write!(f, "] (length: {})", self.length)?;
149 }
150
151 #[cfg(not(any(test, all(target_os = "linux", feature = "target_hw"))))]
152 {
153 write!(f, "]")?; // Close the bracket if length field is not present
154 }
155
156 Ok(())
157 }
158}
159
160#[allow(dead_code)]
161impl I2cResponse {
162 /// Creates a new `I2cResponse` by copying data from a slice.
163 ///
164 /// This constructor is used to package the raw data received from an I2C device
165 /// into a structured response. It copies the contents of the provided slice
166 /// into a fixed-size internal buffer.
167 ///
168 /// # Arguments
169 /// * `read_data` - A byte slice (`&[u8]`) containing the data read from the I2C bus.
170 /// The length of this slice determines the `length` field of the new struct.
171 ///
172 /// # Returns
173 /// A new `I2cResponse` struct containing the sensor data.
174 pub fn new(read_data: &[u8]) -> I2cResponse {
175 let mut read_buf = [0u8; MAX_I2C_RESPONSE_LENGTH];
176
177 // The length of read_data is the number of bytes we want to copy.
178 // We assume `len` will not be greater than `MAX_I2C_RESPONSE_LENGTH` because
179 // the calling function `get_response_from_i2c` uses a slice of the correct size.
180 let len = read_data.len();
181 read_buf[..len].copy_from_slice(read_data);
182
183 I2cResponse {
184 read_buf,
185 length: len,
186 }
187 }
188}
189
190pub type I2cResult = Result<I2cResponse, I2cError>;
191
192#[allow(dead_code)]
193/// Contains the configuration and the implementation for communication via I2C **.
194/// Sensor data is read periodically upon request via the channel and communicated back to the caller.
195pub struct I2cInterface {
196 /// I2C interface of rppal library
197 #[cfg(all(target_os = "linux", feature = "target_hw"))]
198 i2c: I2c,
199
200 /// configuration for I2C interface
201 config: I2cInterfaceConfig,
202
203 /// spin sleeper for waiting response of sensor unit
204 spin_sleeper: SpinSleeper,
205}
206
207impl I2cInterface {
208 #[cfg(all(target_os = "linux", feature = "target_hw"))]
209 /// Creates a new `I2cInterface` instance, establishing and configuring the I2C bus.
210 ///
211 /// This constructor initializes the I2C interface on the target hardware. It opens the I2C bus
212 /// provided by the `rppal` library and sets a global transaction timeout based on the
213 /// provided configuration.
214 ///
215 /// # Arguments
216 /// * `config` - Configuration data for the I2C interface, including the transaction timeout.
217 ///
218 /// # Returns
219 /// A `Result` containing a new `I2cInterface` struct on success, ready to communicate
220 /// with devices on the I2C bus.
221 ///
222 /// # Errors
223 /// Returns an `I2cError` if the I2C bus cannot be initialized or configured:
224 /// - `I2cError::InitializationFailure`: If the underlying call to `I2c::new()` fails,
225 /// which can be caused by permission issues (not running as root or not in the `i2c` group),
226 /// or if the I2C hardware is not enabled on the system.
227 /// - `I2cError::SetTimeoutError`: If setting the I2C transaction timeout fails.
228 pub fn new(config: I2cInterfaceConfig) -> Result<I2cInterface, I2cError> {
229 let i2c = I2c::new().map_err(|e| InitializationFailure {
230 location: module_path!().to_string(),
231 source: e,
232 })?;
233 i2c.set_timeout(config.timeout_transaction)
234 .map_err(|e| SetTimeoutError {
235 location: module_path!().to_string(),
236 source: e,
237 })?;
238
239 Ok(I2cInterface {
240 i2c,
241 spin_sleeper: SpinSleeper::default(),
242 config,
243 })
244 }
245
246 #[cfg(all(target_os = "linux", feature = "target_hw"))]
247 /// Performs a complete I2C write/read transaction with a sensor.
248 ///
249 /// This function orchestrates a full I2C communication cycle:
250 /// 1. Sets the I2C slave address for the target device.
251 /// 2. Writes the command from the `i2c_request`.
252 /// 3. Pauses for the specified duration to allow the sensor to process the command.
253 /// 4. Reads the response from the sensor into the provided buffer.
254 ///
255 /// # Arguments
256 /// * `i2c_request` - A struct containing the I2C address, command, and timing details.
257 /// * `buffer` - A mutable byte slice that will be used to store the data read from the sensor.
258 ///
259 /// # Returns
260 /// A `Result` containing an `I2cResponse` with the successfully read sensor data.
261 ///
262 /// # Errors
263 /// Returns an `I2cError` variant if any stage of the communication fails:
264 /// - `I2cError::ProvidedBufferTooSmall`: If the provided `buffer` is not large enough
265 /// to hold the `expected_response_length`.
266 /// - `I2cError::SetAddressError`: If setting the I2C slave address fails.
267 /// - `I2cError::WriteFailure`: If the command cannot be written to the sensor.
268 /// - `I2cError::InsufficientWrite`: If not all bytes of the command could be written.
269 /// - `I2cError::InsufficientRead`: If the full response could not be read from the sensor.
270 /// - `I2cError::IncorrectResponseLength`: If the number of bytes read does not match
271 /// the `expected_response_length`.
272 pub fn get_response_from_i2c(
273 &mut self,
274 i2c_request: I2cRequest,
275 buffer: &mut [u8],
276 ) -> I2cResult {
277 let sleep_duration = Duration::from_millis(i2c_request.sleep_time_millis);
278
279 // Check the buffer's capacity against the request's needs.
280 if buffer.len() < i2c_request.expected_response_length {
281 return Err(I2cError::ProvidedBufferTooSmall {
282 location: module_path!().to_string(),
283 buffer_size: buffer.len(),
284 expected_size: i2c_request.expected_response_length,
285 });
286 }
287
288 self.i2c
289 .set_slave_address(i2c_request.i2c_address)
290 .map_err(|e| I2cError::SetAddressError {
291 location: module_path!().to_string(),
292 address: i2c_request.i2c_address,
293 source: e,
294 })?;
295
296 let realized_write_len = self
297 .i2c
298 .write(&i2c_request.send_buf[..i2c_request.send_len])
299 .map_err(|e| I2cError::WriteFailure {
300 location: module_path!().to_string(),
301 source: e,
302 })?;
303 if realized_write_len != i2c_request.send_len {
304 return Err(I2cError::InsufficientWrite(
305 module_path!().to_string(),
306 i2c_request.send_len,
307 realized_write_len,
308 ));
309 }
310
311 // give the I2C bus participant time to process the request
312 self.spin_sleeper.sleep(sleep_duration);
313
314 // Use a sub-slice of the provided buffer for the read operation.
315 let read_slice = &mut buffer[..i2c_request.expected_response_length];
316
317 let received_message_length =
318 self.i2c
319 .read(read_slice)
320 .map_err(|e| I2cError::InsufficientRead {
321 location: module_path!().to_string(),
322 source: e,
323 })?;
324 if received_message_length != i2c_request.expected_response_length {
325 Err(I2cError::IncorrectResponseLength(
326 module_path!().to_string(),
327 i2c_request.expected_response_length,
328 received_message_length,
329 ))
330 } else {
331 Ok(I2cResponse::new(read_slice))
332 }
333 }
334
335 #[cfg(all(target_os = "linux", feature = "target_hw"))]
336 /// Executes the main loop for the I2C interface thread, acting as a request/response service.
337 ///
338 /// This function runs indefinitely, processing I2C requests received from other modules.
339 /// It listens on the `rx_i2c_interface_from_atlas_scientific` channel for an `I2cRequest`. Upon
340 /// receiving a request, it calls `get_response_from_i2c` to perform the hardware
341 /// communication and sends the resulting `I2cResult` back to the caller via the
342 /// channel.
343 ///
344 /// The loop also listens for a `Quit` command on the `rx_from_signal_handler` channel
345 /// to enable graceful shutdown. It uses internal lock flags to prevent log flooding
346 /// from repeated channel communication errors.
347 ///
348 /// # Arguments
349 /// * `i2c_interface_channels` - A mutable reference to the struct containing the channels.
350 pub fn execute(&mut self, i2c_interface_channels: &mut I2cInterfaceChannels) {
351 #[cfg(not(test))]
352 info!(target: module_path!(), "Thread started with TID: {}", gettid());
353
354 let mut lock_error_receive_atlas_scientific = false;
355 let mut lock_error_send_atlas_scientific = false;
356 let sleep_time = Duration::from_millis(100);
357 let spin_sleeper = SpinSleeper::default();
358
359 // Allocate the read buffer once, here on the stack, using the correct size.
360 let mut read_buffer = [0u8; MAX_I2C_RESPONSE_LENGTH];
361
362 loop {
363 let (quit_command_received, _, _) = self.process_external_request(
364 &mut i2c_interface_channels.rx_i2c_interface_from_signal_handler,
365 None,
366 );
367 if quit_command_received {
368 break;
369 }
370
371 match i2c_interface_channels.receive_from_atlas_scientific() {
372 Ok(request) => {
373 lock_error_receive_atlas_scientific = false;
374 let response = self.get_response_from_i2c(request, &mut read_buffer);
375 match i2c_interface_channels.send_to_atlas_scientific(response) {
376 Ok(_) => {
377 lock_error_send_atlas_scientific = false;
378 }
379 Err(e) => {
380 if !lock_error_send_atlas_scientific {
381 error!(target: module_path!(), "Error occurred when sending to atlas_scientific {:?}", e);
382 lock_error_send_atlas_scientific = true;
383 }
384 }
385 }
386 }
387 Err(e) => {
388 match e {
389 #[cfg(feature = "debug_channels")]
390 AquaChannelError::Full => { /* not applicable, do nothin */ }
391 AquaChannelError::Empty => { /* empty buffer - no action required */ }
392 AquaChannelError::Disconnected => {
393 if !lock_error_receive_atlas_scientific {
394 error!(target: module_path!(), "Error occurred when trying to receive from atlas_scientific {:?}", e);
395 lock_error_receive_atlas_scientific = true;
396 }
397 }
398 }
399 }
400 }
401 spin_sleeper.sleep(sleep_time);
402 }
403 }
404}