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}