Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling Implementation

In this section, we will implement error handling for our DS3231 driver.

We will start by defining a custom error enum that covers all possible error types we may encounter when working with the DS3231 driver.

#![allow(unused)]
fn main() {
use rtc_hal::datetime::DateTimeError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    I2c(I2cError),
    InvalidAddress,
    UnsupportedSqwFrequency,
    DateTime(DateTimeError),
    InvalidBaseCentury,
}
}

The main difference between the DS1307 and DS3231 here is that we don't have the NvramOutOfBounds variant and added InvalidBaseCentury .

Next, we will implement the Display trait for our error enum to provide clear error messages when debugging or handling errors:

#![allow(unused)]
fn main() {
impl<I2cError> core::fmt::Display for Error<I2cError>
where
    I2cError: core::fmt::Debug + core::fmt::Display,
{
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Error::I2c(e) => write!(f, "I2C communication error: {e}"),
            Error::InvalidAddress => write!(f, "Invalid register address"),
            Error::DateTime(e) => write!(f, "Invalid date/time values: {e}"),
            Error::UnsupportedSqwFrequency => write!(f, "Unsupported square wave frequency"),
            Error::InvalidBaseCentury => write!(f, "Base century must be 19 or greater"),
        }
    }
}
}

Implementing Rust's core Error trait

We will implement Rust's core Error trait for our custom error type. Since our error enum already implements Debug and Display, we can use an empty implementation block. Don't confuse this with our rtc-hal's Error trait, which we will implement shortly.

From docs: Implementing the Error trait only requires that Debug and Display are implemented too.

#![allow(unused)]
fn main() {
impl<I2cError> core::error::Error for Error<I2cError> where
    I2cError: core::fmt::Debug + core::fmt::Display
{
}
}

Implementing RTC HAL trait

We will implement the rtc-hal's Error trait for our custom error type. As you already know, this trait requires us to implement the kind method that maps our error variants to the standard error kinds we defined in the RTC HAL.

#![allow(unused)]
fn main() {
impl<I2cError> rtc_hal::error::Error for Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    fn kind(&self) -> rtc_hal::error::ErrorKind {
        match self {
            Error::I2c(_) => rtc_hal::error::ErrorKind::Bus,
            Error::InvalidAddress => rtc_hal::error::ErrorKind::InvalidAddress,
            Error::DateTime(_) => rtc_hal::error::ErrorKind::InvalidDateTime,
            Error::UnsupportedSqwFrequency => rtc_hal::error::ErrorKind::UnsupportedSqwFrequency,
            Error::InvalidBaseCentury => rtc_hal::error::ErrorKind::InvalidDateTime,
        }
    }
}
}

If you have noticed, we have mapped the InvalidBaseCentury error to the InvalidDateTime error of the RTC HAL; RTC HAL uses common errors, not specific ones for each device.

Converting I2C Errors

We finally implement the From trait to automatically convert I2C errors into our custom error type. This allows us to use the ? operator when working with I2C operations.

#![allow(unused)]
fn main() {
impl<I2cError> From<I2cError> for Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    fn from(value: I2cError) -> Self {
        Error::I2c(value)
    }
}
}

The full code for the Error module (error.rs)

#![allow(unused)]
fn main() {
//! Error type definitions for the DS3231 RTC driver.
//!
//! This module defines the `Error` enum and helper functions
//! for classifying and handling DS3231-specific failures.

use rtc_hal::datetime::DateTimeError;

/// DS3231 driver errors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    /// I2C communication error
    I2c(I2cError),
    /// Invalid register address
    InvalidAddress,
    /// The specified square wave frequency is not supported by the RTC
    UnsupportedSqwFrequency,
    /// Invalid date/time parameters provided by user
    DateTime(DateTimeError),
    /// Invalid Base Century (It should be either 19,20,21)
    InvalidBaseCentury,
}

impl<I2cError> core::fmt::Display for Error<I2cError>
where
    I2cError: core::fmt::Debug + core::fmt::Display,
{
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Error::I2c(e) => write!(f, "I2C communication error: {e}"),
            Error::InvalidAddress => write!(f, "Invalid register address"),
            Error::DateTime(e) => write!(f, "Invalid date/time values: {e}"),
            Error::UnsupportedSqwFrequency => write!(f, "Unsupported square wave frequency"),
            Error::InvalidBaseCentury => write!(f, "Base century must be 19 or greater"),
        }
    }
}

impl<I2cError> core::error::Error for Error<I2cError> where
    I2cError: core::fmt::Debug + core::fmt::Display
{
}

// /// Converts an [`I2cError`] into an [`Error`] by wrapping it in the
// /// [`Error::I2c`] variant.
// ///
impl<I2cError> From<I2cError> for Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    fn from(value: I2cError) -> Self {
        Error::I2c(value)
    }
}

impl<I2cError> rtc_hal::error::Error for Error<I2cError>
where
    I2cError: core::fmt::Debug,
{
    fn kind(&self) -> rtc_hal::error::ErrorKind {
        match self {
            Error::I2c(_) => rtc_hal::error::ErrorKind::Bus,
            Error::InvalidAddress => rtc_hal::error::ErrorKind::InvalidAddress,
            Error::DateTime(_) => rtc_hal::error::ErrorKind::InvalidDateTime,
            Error::UnsupportedSqwFrequency => rtc_hal::error::ErrorKind::UnsupportedSqwFrequency,
            Error::InvalidBaseCentury => rtc_hal::error::ErrorKind::InvalidDateTime,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rtc_hal::datetime::DateTimeError;
    use rtc_hal::error::{Error as RtcError, ErrorKind};

    #[test]
    fn test_from_i2c_error() {
        #[derive(Debug, PartialEq, Eq)]
        struct DummyI2cError(u8);

        let e = Error::from(DummyI2cError(42));
        assert_eq!(e, Error::I2c(DummyI2cError(42)));
    }

    #[test]
    fn test_error_kind_mappings() {
        // I2c variant
        let e: Error<&str> = Error::I2c("oops");
        assert_eq!(e.kind(), ErrorKind::Bus);

        // InvalidAddress
        let e: Error<&str> = Error::InvalidAddress;
        assert_eq!(e.kind(), ErrorKind::InvalidAddress);

        // DateTime
        let e: Error<&str> = Error::DateTime(DateTimeError::InvalidDay);
        assert_eq!(e.kind(), ErrorKind::InvalidDateTime);

        // UnsupportedSqwFrequency
        let e: Error<&str> = Error::UnsupportedSqwFrequency;
        assert_eq!(e.kind(), ErrorKind::UnsupportedSqwFrequency);

        // InvalidBaseCentury
        let e: Error<&str> = Error::InvalidBaseCentury;
        assert_eq!(e.kind(), ErrorKind::InvalidDateTime);
    }

    #[derive(Debug, PartialEq, Eq)]
    struct MockI2cError {
        code: u8,
        message: &'static str,
    }

    impl core::fmt::Display for MockI2cError {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
            write!(f, "I2C Error {}: {}", self.code, self.message)
        }
    }

    #[test]
    fn test_display_all_variants() {
        let errors = vec![
            (
                Error::I2c(MockI2cError {
                    code: 1,
                    message: "test",
                }),
                "I2C communication error: I2C Error 1: test",
            ),
            (Error::InvalidAddress, "Invalid register address"),
            (
                Error::DateTime(DateTimeError::InvalidMonth),
                "Invalid date/time values: invalid month",
            ),
            (
                Error::UnsupportedSqwFrequency,
                "Unsupported square wave frequency",
            ),
            (
                Error::InvalidBaseCentury,
                "Base century must be 19 or greater",
            ),
        ];

        for (error, expected) in errors {
            assert_eq!(format!("{error}"), expected);
        }
    }
}
}