micropelt 1.1.8

Micropelt device communication definitions
Documentation
use std::fmt;
use std::io::{Error, ErrorKind, Result};

use crate::utils::check_payload_length;

const UPLINK_N_BYTES: usize = 6;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Uplink {
    revision: Version,
    hardware: Version,
    firmware: FirmwareVersion,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Version {
    major: u8,
    minor: u8,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FirmwareVersion {
    year_since_2000: u8,
    month: u8,
    day: u8,
    minor: u8,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:01x}.{:01x}", self.major, self.minor)
    }
}

impl fmt::Display for FirmwareVersion {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}.{:02}.{:02}.{:02}",
            self.year_since_2000 as u16 + 2000,
            self.month,
            self.day,
            self.minor,
        )
    }
}

impl Uplink {
    pub(crate) fn deserialise(input: &[u8]) -> Result<Self> {
        check_payload_length(input, UPLINK_N_BYTES)?;

        let revision = Version::from_bin(input[0]);
        let hardware = Version::from_bin(input[1]);
        let year_since_2000 = input[2];
        let month = bin_to_month(input[3])?;
        let day = bin_to_day(month, input[4])?;
        let minor = input[5];

        Ok(Self {
            revision,
            hardware,
            firmware: FirmwareVersion {
                year_since_2000,
                month,
                day,
                minor,
            },
        })
    }

    pub fn revision(&self) -> &Version {
        &self.revision
    }

    pub fn hardware_version(&self) -> &Version {
        &self.hardware
    }

    pub fn firmware_version(&self) -> &FirmwareVersion {
        &self.firmware
    }
}

impl Version {
    #[rustfmt::skip]
    pub fn from_bin(value: u8) -> Self {
        Self {
            major:  value       & 0b1111,
            minor: (value >> 4) & 0b1111,
        }
    }
}

fn bin_to_month(value: u8) -> Result<u8> {
    if 0 < value && value <= 12 {
        Ok(value)
    } else {
        Err(Error::new(
            ErrorKind::InvalidInput,
            format!("There are 12 months in a year, but received month {value}"),
        ))
    }
}

fn bin_to_day(month: u8, value: u8) -> Result<u8> {
    let n_days = month_get_n_days(month)?;
    if 0 < value && value <= n_days {
        Ok(value)
    } else {
        Err(Error::new(
            ErrorKind::InvalidInput,
            format!("Month {month} contains {n_days} days, but received day {value}",),
        ))
    }
}

fn month_get_n_days(month: u8) -> Result<u8> {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => Ok(31),
        4 | 6 | 9 | 11 => Ok(30),
        2 => Ok(29),
        _ => Err(Error::new(
            ErrorKind::InvalidInput,
            format!("There are 12 months in a year, but received month {month}"),
        )),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn version_to_string() {
        assert_eq!("8.3", Version { major: 8, minor: 3 }.to_string());
        assert_eq!(
            "2.a",
            Version {
                major: 2,
                minor: 10
            }
            .to_string()
        );
    }

    #[test]
    fn firmware_version_to_string() {
        assert_eq!(
            "2098.07.06.05",
            FirmwareVersion {
                year_since_2000: 98,
                month: 7,
                day: 6,
                minor: 5,
            }
            .to_string()
        );

        assert_eq!(
            "2000.12.25.00",
            FirmwareVersion {
                year_since_2000: 0,
                month: 12,
                day: 25,
                minor: 0,
            }
            .to_string()
        );
    }

    #[test]
    fn deserialise() {
        let input = [0x9a, 0xc6, 210, 9, 3, 24];

        let uplink = Uplink::deserialise(&input).unwrap();

        assert_eq!(
            Version {
                major: 0xa,
                minor: 9
            },
            uplink.revision
        );
        assert_eq!(
            Version {
                major: 6,
                minor: 0xc
            },
            uplink.hardware
        );
        assert_eq!(
            FirmwareVersion {
                year_since_2000: 210,
                month: 9,
                day: 3,
                minor: 24,
            },
            uplink.firmware
        );
    }

    #[test]
    fn bin_to_version() {
        assert_eq!(
            Version {
                major: 11,
                minor: 7
            },
            Version::from_bin(123)
        );
    }

    #[test]
    fn bin_to_month_ok() {
        assert_eq!(6, bin_to_month(6).unwrap());
    }

    #[test]
    fn bin_to_month_err() {
        assert!(bin_to_month(0).is_err());
        assert!(bin_to_month(13).is_err());
        assert!(bin_to_month(200).is_err());
    }

    #[test]
    fn bin_to_day_ok() {
        assert_eq!(1, bin_to_day(12, 1).unwrap());
    }

    #[test]
    fn bin_to_day_err() {
        assert!(bin_to_day(0, 1).is_err());
        assert!(bin_to_day(1, 0).is_err());
        assert!(bin_to_day(1, 32).is_err());
        assert!(bin_to_day(2, 30).is_err());
        assert!(bin_to_day(4, 31).is_err());
    }

    #[test]
    fn month_get_n_days_err() {
        assert!(month_get_n_days(0).is_err());
        assert!(month_get_n_days(13).is_err());
        assert!(month_get_n_days(17).is_err());
    }
}