pub mod id360;
pub mod id419;
pub mod id605;
pub mod id629;
use crate::{Error as ProtocolError, Interface, Read, Write};
use alloc::{boxed::Box, string::String};
use core::{
fmt::{Display, Formatter},
num::TryFromIntError,
time::Duration,
};
pub type Result<T, E> = core::result::Result<T, Error<E>>;
#[non_exhaustive]
#[derive(PartialEq, Eq, Debug)]
pub enum Error<E> {
UnknownSoftwareId(u16),
InvalidArgument,
InvalidState,
UnexpectedMemoryValue,
UnknownProperty,
UnknownAction,
Protocol(ProtocolError<E>),
}
impl<E: core::error::Error> Display for Error<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownSoftwareId(id) => write!(f, "unknown software ID: {id}"),
Self::InvalidArgument => write!(f, "invalid argument"),
Self::InvalidState => write!(f, "invalid state"),
Self::UnexpectedMemoryValue => write!(f, "unexpected memory value"),
Self::UnknownProperty => write!(f, "unknown property"),
Self::UnknownAction => write!(f, "unknown action"),
Self::Protocol(err) => write!(f, "protocol error: {err}"),
}
}
}
impl<E: core::error::Error> core::error::Error for Error<E> {}
impl<E> From<ProtocolError<E>> for Error<E> {
fn from(err: ProtocolError<E>) -> Self {
Self::Protocol(err)
}
}
impl<E> From<TryFromIntError> for Error<E> {
fn from(_err: TryFromIntError) -> Self {
Self::UnexpectedMemoryValue
}
}
impl<E> From<bitflags::parser::ParseError> for Error<E> {
fn from(_err: bitflags::parser::ParseError) -> Self {
Self::InvalidArgument
}
}
impl<E> From<strum::ParseError> for Error<E> {
fn from(_err: strum::ParseError) -> Self {
Self::InvalidArgument
}
}
#[non_exhaustive]
#[derive(strum::Display, PartialEq, Eq, Copy, Clone, Debug)]
#[strum(serialize_all = "title_case")]
pub enum DeviceKind {
WashingMachine,
TumbleDryer,
WasherDryer,
Dishwasher,
CoffeeMachine,
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum PropertyKind {
General,
Failure,
Operation,
Io,
}
#[derive(PartialEq, Eq, Debug)]
pub struct Property {
pub kind: PropertyKind,
pub id: &'static str,
pub name: &'static str,
pub unit: Option<&'static str>,
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum ActionKind {
Operation,
Calibration,
}
#[derive(PartialEq, Eq, Debug)]
pub enum ActionParameters {
Enumeration(&'static [&'static str]),
Flags(&'static [&'static str]),
}
#[derive(PartialEq, Eq, Debug)]
pub struct Action {
pub kind: ActionKind,
pub id: &'static str,
pub name: &'static str,
pub params: Option<ActionParameters>,
}
#[derive(PartialEq, Eq, Debug)]
pub enum Value {
Bool(bool),
Number(u32),
Sensor(u32, u32),
String(String),
Duration(Duration),
}
impl From<bool> for Value {
fn from(val: bool) -> Self {
Self::Bool(val)
}
}
impl From<u8> for Value {
fn from(val: u8) -> Self {
Self::Number(val.into())
}
}
impl From<u16> for Value {
fn from(val: u16) -> Self {
Self::Number(val.into())
}
}
impl From<u32> for Value {
fn from(val: u32) -> Self {
Self::Number(val)
}
}
impl From<(u8, u8)> for Value {
fn from(vals: (u8, u8)) -> Self {
Self::Sensor(vals.0.into(), vals.1.into())
}
}
impl From<(u16, u16)> for Value {
fn from(vals: (u16, u16)) -> Self {
Self::Sensor(vals.0.into(), vals.1.into())
}
}
impl From<(u32, u32)> for Value {
fn from(vals: (u32, u32)) -> Self {
Self::Sensor(vals.0, vals.1)
}
}
impl From<String> for Value {
fn from(string: String) -> Self {
Self::String(string)
}
}
impl From<Duration> for Value {
fn from(dur: Duration) -> Self {
Self::Duration(dur)
}
}
#[async_trait::async_trait(?Send)]
pub trait Device<P: Read + Write>: private::Sealed {
async fn connect(port: P) -> Result<Self, P::Error>
where
Self: Sized;
fn software_id(&self) -> u16;
fn kind(&self) -> DeviceKind;
fn properties(&self) -> &'static [Property];
fn actions(&self) -> &'static [Action];
async fn query_property(&mut self, prop: &Property) -> Result<Value, P::Error>;
async fn trigger_action(
&mut self,
action: &Action,
param: Option<Value>,
) -> Result<(), P::Error>;
fn interface(&mut self) -> &mut Interface<P>;
}
pub async fn connect<'a, P: 'a + Read + Write>(
port: P,
) -> Result<Box<dyn Device<P> + 'a>, P::Error> {
let mut intf = Interface::new(port);
let id = intf.query_software_id().await?;
match id {
id360::compatible_software_ids!() => {
Ok(Box::new(id360::WashingMachine::initialize(intf, id).await?) as Box<dyn Device<P>>)
}
id419::compatible_software_ids!() => {
Ok(Box::new(id419::WashingMachine::initialize(intf, id).await?) as Box<dyn Device<P>>)
}
id605::compatible_software_ids!() => {
Ok(Box::new(id605::Dishwasher::initialize(intf, id).await?) as Box<dyn Device<P>>)
}
id629::compatible_software_ids!() => {
Ok(Box::new(id629::WashingMachine::initialize(intf, id).await?) as Box<dyn Device<P>>)
}
_ => Err(Error::UnknownSoftwareId(id)),
}
}
mod utils {
pub(super) fn decode_bcd_value(mut val: u32) -> u32 {
let mut mul = 1;
let mut res = 0;
while val > 0 {
let digit = val & 0x0f;
if digit <= 9 {
res += digit * mul;
}
mul *= 10;
val >>= 4;
}
res
}
pub(super) fn ntc_resistance_from_adc(val: u8) -> u32 {
(2150 * u32::from(val)) / (256 - u32::from(val))
}
pub(super) fn decode_mc14489_digit(code: u8, special: bool) -> Option<char> {
match (code, special) {
(0x00, false) => Some('0'),
(0x01, false) => Some('1'),
(0x02, false) => Some('2'),
(0x03, false) => Some('3'),
(0x04, false) => Some('4'),
(0x05, false) => Some('5'),
(0x06, false) => Some('6'),
(0x07, false) => Some('7'),
(0x08, false) => Some('8'),
(0x09, false) => Some('9'),
(0x0a, false) => Some('A'),
(0x0b, false) => Some('b'),
(0x0c, false) => Some('C'),
(0x0d, false) => Some('d'),
(0x0e, false) => Some('E'),
(0x0f, false) => Some('F'),
(0x01, true) => Some('c'),
(0x02, true) => Some('H'),
(0x03, true) => Some('h'),
(0x04, true) => Some('J'),
(0x05, true) => Some('L'),
(0x06, true) => Some('n'),
(0x07, true) => Some('o'),
(0x08, true) => Some('P'),
(0x09, true) => Some('r'),
(0x0a, true) => Some('U'),
(0x0b, true) => Some('u'),
(0x0c, true) => Some('y'),
(0x0d, true) => Some('-'),
(0x0e, true) => Some('='),
(0x0f, true) => Some('°'),
_ => None,
}
}
}
mod private {
pub trait Sealed {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::init_logger;
use alloc::collections::vec_deque::VecDeque;
use core::convert::Infallible;
#[tokio::test]
async fn connect_to_device() -> Result<(), Infallible> {
init_logger();
let mut deque = VecDeque::from([0x00, 0x75, 0x02, 0x77, 0x00, 0x00, 0x00, 0x00]);
{
let dev = connect(&mut deque).await?;
assert_eq!(dev.software_id(), 629, "software ID should be correct");
assert_eq!(
dev.kind(),
DeviceKind::WashingMachine,
"device kind should be correct"
);
}
assert_eq!(
deque,
[
0x11, 0x00, 0x00, 0x02, 0x13, 0x00, 0x20, 0xea, 0x43, 0x00, 0x4d, 0x32, 0x02, 0x1f,
0x00, 0x53, 0x40, 0xc2, 0x02, 0x01, 0x05, 0x01, 0x01,
],
"deque contents should be correct"
);
Ok(())
}
#[tokio::test]
async fn error_unknown_software_id() -> Result<(), Infallible> {
init_logger();
let mut deque = VecDeque::from([0x00, 0xff, 0xff, 0xfe, 0x00, 0x00]);
let res = connect(&mut deque).await;
assert!(
matches!(res, Err(Error::UnknownSoftwareId(0xffff))),
"result should be unknown software ID error"
);
Ok(())
}
}