use thiserror::Error;
pub type Word = u16;
pub const NUMBER_OF_PORTS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PortState {
Close,
Open,
}
impl PortState {
pub fn decode_from_holding_registers(word: Word) -> Self {
if word != 0 { Self::Open } else { Self::Close }
}
}
impl std::fmt::Display for PortState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Close => write!(f, "close"),
Self::Open => write!(f, "open"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PortStates([PortState; NUMBER_OF_PORTS]);
impl PortStates {
pub const ADDRESS: u16 = 0x0001;
pub const QUANTITY: u16 = NUMBER_OF_PORTS as u16;
pub fn decode_from_holding_registers(words: &[Word]) -> Self {
let mut port_states = [PortState::Close; NUMBER_OF_PORTS];
for (i, word) in words.iter().enumerate().take(NUMBER_OF_PORTS) {
port_states[i] = PortState::decode_from_holding_registers(*word);
}
Self(port_states)
}
pub fn iter(&self) -> std::slice::Iter<'_, PortState> {
self.0.iter()
}
pub fn as_array(&self) -> &[PortState; NUMBER_OF_PORTS] {
&self.0
}
}
impl std::fmt::Display for PortStates {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for state in self.iter() {
if !first {
write!(f, ", ")?;
}
write!(f, "{}", state)?;
first = false;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Port(u8);
impl Port {
pub const MIN: u8 = 0;
pub const MAX: u8 = NUMBER_OF_PORTS as u8 - 1;
pub const REG_DATA_SET_PORT_OPEN: Word = 0x0100;
pub const REG_DATA_SET_PORT_CLOSE: Word = 0x0200;
pub const REG_DATA_SET_PORT_TOGGLE: Word = 0x0300;
pub const REG_DATA_SET_PORT_LATCH: Word = 0x0400;
pub const REG_DATA_SET_PORT_MOMENTARY: Word = 0x0500;
pub const REG_DATA_SET_PORT_DELAY: Word = 0x0600;
pub fn address_for_write_register(&self) -> u16 {
(self.0 + 1) as u16
}
pub fn encode_delay_for_write_register(delay: u8) -> Word {
Self::REG_DATA_SET_PORT_DELAY + (delay as Word)
}
}
impl std::ops::Deref for Port {
type Target = u8;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error(
"The port value {0} is outside the valid range of {min} to {max}",
min = Port::MIN,
max = Port::MAX
)]
pub struct ErrorPortOutOfRange(
pub u8,
);
impl TryFrom<u8> for Port {
type Error = ErrorPortOutOfRange;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (Self::MIN..=Self::MAX).contains(&value) {
Ok(Self(value))
} else {
Err(ErrorPortOutOfRange(value))
}
}
}
pub struct PortsAll;
impl PortsAll {
pub const ADDRESS: u16 = 0x0000;
pub const REG_DATA_SET_ALL_OPEN: Word = 0x0700;
pub const REG_DATA_SET_ALL_CLOSE: Word = 0x0800;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Address(u8);
impl std::ops::Deref for Address {
type Target = u8;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for Address {
fn default() -> Self {
Self(0x01)
}
}
impl Address {
pub const ADDRESS: u16 = 0x00FF;
pub const QUANTITY: u16 = 1;
pub const MIN: u8 = 1;
pub const MAX: u8 = 247;
pub const BROADCAST: Address = Address(0xFF);
pub fn decode_from_holding_registers(words: &[Word]) -> Self {
let word_value = *words
.first()
.expect("Register data for address must not be empty");
Self::try_from(word_value as u8).expect("Invalid address value read from device register")
}
pub fn encode_for_write_register(&self) -> Word {
self.0 as u16
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error(
"The address value {0} is outside the valid assignable range of {min} to {max}",
min = Address::MIN,
max = Address::MAX
)]
pub struct ErrorAddressOutOfRange(
pub u8,
);
impl TryFrom<u8> for Address {
type Error = ErrorAddressOutOfRange;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (Self::MIN..=Self::MAX).contains(&value) {
Ok(Self(value))
} else {
Err(ErrorAddressOutOfRange(value))
}
}
}
impl std::fmt::Display for Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "0x{:02x}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn address_try_from_validation() {
assert!(matches!(
Address::try_from(0),
Err(ErrorAddressOutOfRange(0))
));
assert!(matches!(Address::try_from(Address::MIN), Ok(Address(1))));
assert!(matches!(Address::try_from(Address::MAX), Ok(Address(247))));
assert!(matches!(
Address::try_from(Address::MAX + 1),
Err(ErrorAddressOutOfRange(248))
));
assert!(matches!(
Address::try_from(255),
Err(ErrorAddressOutOfRange(255))
));
assert!(matches!(Address::try_from(100), Ok(Address(100))));
}
#[test]
fn address_default() {
assert_eq!(Address::default(), Address(1));
}
#[test]
fn address_encode_decode() {
let addr = Address::try_from(42).unwrap();
let encoded = addr.encode_for_write_register();
assert_eq!(encoded, 42u16);
let decoded = Address::decode_from_holding_registers(&[encoded]);
assert_eq!(decoded, addr);
}
#[test]
#[should_panic(expected = "Register data for address must not be empty")]
fn address_decode_panics_on_empty() {
let _ = Address::decode_from_holding_registers(&[]);
}
#[test]
#[should_panic(expected = "Invalid address value read from device register")]
fn address_decode_panics_on_invalid_value_zero() {
let _ = Address::decode_from_holding_registers(&[0x0000]);
}
#[test]
#[should_panic(expected = "Invalid address value read from device register")]
fn address_decode_panics_on_invalid_value_high() {
let _ = Address::decode_from_holding_registers(&[0x00F8]);
}
#[test]
fn address_decode_valid() {
assert_eq!(
Address::decode_from_holding_registers(&[0x0001]),
Address(1)
);
assert_eq!(
Address::decode_from_holding_registers(&[0x00F7]),
Address(247)
);
}
#[test]
fn port_try_from_validation() {
assert!(matches!(Port::try_from(Port::MIN), Ok(Port(0))));
assert!(matches!(Port::try_from(Port::MAX), Ok(Port(7))));
assert!(matches!(
Port::try_from(Port::MAX + 1),
Err(ErrorPortOutOfRange(8))
));
assert!(matches!(Port::try_from(3), Ok(Port(3))));
}
#[test]
fn port_address_for_write_register_is_one_based() {
assert_eq!(Port::try_from(0).unwrap().address_for_write_register(), 1); assert_eq!(Port::try_from(1).unwrap().address_for_write_register(), 2); assert_eq!(Port::try_from(7).unwrap().address_for_write_register(), 8); }
#[test]
fn port_encode_delay() {
assert_eq!(Port::encode_delay_for_write_register(0), 0x0600);
assert_eq!(Port::encode_delay_for_write_register(10), 0x060A);
assert_eq!(Port::encode_delay_for_write_register(255), 0x06FF);
}
#[test]
fn port_state_decode() {
assert_eq!(
PortState::decode_from_holding_registers(0x0000),
PortState::Close
);
assert_eq!(
PortState::decode_from_holding_registers(0x0001),
PortState::Open
);
assert_eq!(
PortState::decode_from_holding_registers(0xFFFF),
PortState::Open
); }
#[test]
fn port_states_decode() {
let words_all_closed = [0x0000; NUMBER_OF_PORTS];
let words_mixed = [
0x0001, 0x0000, 0xFFFF, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
];
let words_short = [0x0001, 0x0000];
let words_long = [
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x9999,
];
let expected_all_closed = PortStates([PortState::Close; NUMBER_OF_PORTS]);
let expected_mixed = PortStates([
PortState::Open,
PortState::Close,
PortState::Open,
PortState::Close,
PortState::Open,
PortState::Close,
PortState::Open,
PortState::Close,
]);
let mut expected_short_arr = [PortState::Close; NUMBER_OF_PORTS];
expected_short_arr[0] = PortState::Open;
expected_short_arr[1] = PortState::Close;
let expected_short = PortStates(expected_short_arr);
assert_eq!(
PortStates::decode_from_holding_registers(&words_all_closed),
expected_all_closed
);
assert_eq!(
PortStates::decode_from_holding_registers(&words_mixed),
expected_mixed
);
assert_eq!(
PortStates::decode_from_holding_registers(&words_short),
expected_short
);
assert_eq!(
PortStates::decode_from_holding_registers(&words_long),
expected_mixed
); }
#[test]
fn display_formats() {
assert_eq!(PortState::Open.to_string(), "open");
assert_eq!(PortState::Close.to_string(), "close");
assert_eq!(Address(1).to_string(), "0x01");
assert_eq!(Address(247).to_string(), "0xf7");
assert_eq!(Address::BROADCAST.to_string(), "0xff"); let states = PortStates([
PortState::Open,
PortState::Close,
PortState::Open,
PortState::Close,
PortState::Close,
PortState::Close,
PortState::Close,
PortState::Close,
]);
assert_eq!(
states.to_string(),
"open, close, open, close, close, close, close, close"
);
}
}