use crate::device::{
Action, ActionKind, ActionParameters, Device, DeviceKind, Error, Interface, Property,
PropertyKind, Result, Value, private, utils,
};
use alloc::{boxed::Box, string::ToString};
use bitflags_derive::{FlagsDebug, FlagsDisplay, FlagsFromStr};
use core::{str, time::Duration};
use embedded_io_async::{Read, Write};
use strum::{Display, EnumString, FromRepr, VariantNames};
macro_rules! compatible_software_ids {
() => {
419
};
}
pub(super) use compatible_software_ids;
const PROP_ROM_CODE: Property = Property {
kind: PropertyKind::General,
id: "rom_code",
name: "ROM Code",
unit: None,
};
const PROP_OPERATING_TIME: Property = Property {
kind: PropertyKind::General,
id: "operating_time",
name: "Operating Time",
unit: None,
};
const PROP_FAULTS: Property = Property {
kind: PropertyKind::Failure,
id: "faults",
name: "Faults",
unit: None,
};
const PROP_OPERATING_MODE: Property = Property {
kind: PropertyKind::Operation,
id: "operating_mode",
name: "Operating Mode",
unit: None,
};
const PROP_LOAD_LEVEL: Property = Property {
kind: PropertyKind::Operation,
id: "load_level",
name: "Load Level",
unit: None,
};
const PROP_PROGRAM_SELECTOR: Property = Property {
kind: PropertyKind::Operation,
id: "program_selector",
name: "Program Selector",
unit: None,
};
const PROP_PROGRAM_TYPE: Property = Property {
kind: PropertyKind::Operation,
id: "program_type",
name: "Program Type",
unit: None,
};
const PROP_PROGRAM_TEMPERATURE: Property = Property {
kind: PropertyKind::Operation,
id: "program_temperature",
name: "Program Temperature",
unit: Some("°C"),
};
const PROP_PROGRAM_OPTIONS: Property = Property {
kind: PropertyKind::Operation,
id: "program_options",
name: "Program Options",
unit: None,
};
const PROP_PROGRAM_SPIN_SETTING: Property = Property {
kind: PropertyKind::Operation,
id: "program_spin_setting",
name: "Program Spin Setting",
unit: None,
};
const PROP_PROGRAM_PHASE: Property = Property {
kind: PropertyKind::Operation,
id: "program_phase",
name: "Program Phase",
unit: None,
};
const PROP_PROGRAM_LOCKED: Property = Property {
kind: PropertyKind::Operation,
id: "program_locked",
name: "Program Locked",
unit: None,
};
const PROP_ACTIVE_ACTUATORS: Property = Property {
kind: PropertyKind::Io,
id: "active_actuators",
name: "Active Actuators",
unit: None,
};
const PROP_NTC_RESISTANCE: Property = Property {
kind: PropertyKind::Io,
id: "ntc_resistance",
name: "NTC Resistance",
unit: Some("Ω"),
};
const PROP_TEMPERATURE: Property = Property {
kind: PropertyKind::Io,
id: "temperature",
name: "Temperature",
unit: Some("°C"),
};
const PROP_WATER_LEVEL: Property = Property {
kind: PropertyKind::Io,
id: "water_level",
name: "Water Level",
unit: Some("mmH₂O"),
};
const ACTION_SET_PROGRAM_OPTIONS: Action = Action {
kind: ActionKind::Operation,
id: "set_program_options",
name: "Set Program Options",
params: Some(ActionParameters::Flags(&[
"Soak",
"PreWash",
"WaterPlus",
"Short",
])),
};
const ACTION_SET_PROGRAM_SPIN_SETTING: Action = Action {
kind: ActionKind::Operation,
id: "set_program_spin_setting",
name: "Set Program Spin Setting",
params: Some(ActionParameters::Enumeration(SpinSetting::VARIANTS)),
};
const ACTION_START_PROGRAM: Action = Action {
kind: ActionKind::Operation,
id: "start_program",
name: "Start Program",
params: None,
};
bitflags::bitflags! {
#[derive(FlagsDisplay, FlagsDebug, PartialEq, Eq, Copy, Clone)]
pub struct Fault: u8 {
const PressureSensor = 0x01;
const NtcThermistor = 0x02;
const Heater = 0x04;
const TachometerGenerator = 0x08;
const DetergentOverdose = 0x10;
const Inlet = 0x20;
const Drainage = 0x40;
const Eeprom = 0x80;
}
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum OperatingMode {
ProgramIdle = 0x01,
ProgramRunning = 0x02,
ProgramFinished = 0x03,
ServiceProgramming = 0x04,
Service = 0x05,
CustomerProgramming = 0x06,
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum SelectorPosition {
Finish,
Cottons95,
Cottons75,
Cottons60,
Cottons40,
Cottons30,
MinimumIron60,
MinimumIron50,
MinimumIron40,
MinimumIron30,
Delicates40,
Delicates30,
DelicatesCold,
Woolens40,
Woolens30,
WoolensCold,
QuickWash40,
Starch,
Spin,
Drain,
SeparateRinse,
MixedWash40,
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum ProgramType {
None = 0x00,
Cottons = 0x01,
MinimumIron = 0x02,
Delicates = 0x03,
Woolens = 0x04,
QuickWash = 0x05,
Starch = 0x06,
Spin = 0x07,
Drain = 0x08,
SeparateRinse = 0x09,
MixedWash = 0x0a,
}
bitflags::bitflags! {
#[derive(FlagsDisplay, FlagsFromStr, FlagsDebug, PartialEq, Eq, Copy, Clone)]
pub struct ProgramOption: u8 {
const Soak = 0x10;
const PreWash = 0x20;
const WaterPlus = 0x40;
const Short = 0x80;
}
}
#[derive(FromRepr, Display, EnumString, VariantNames, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum SpinSetting {
WithoutSpin,
RinseHold,
SpinMin,
SpinLow,
SpinMed,
SpinHigh,
SpinVeryHigh,
SpinMax,
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum ProgramPhase {
Idle,
Starting,
SoakPreWash1,
SoakPreWash2,
MainWash,
Rinse1,
Rinse2,
Rinse3,
Rinse4,
Rinse5,
RinseHold,
Drain,
FinalSpin,
AntiCreaseFinish,
}
bitflags::bitflags! {
#[derive(FlagsDisplay, FlagsDebug, PartialEq, Eq, Copy, Clone)]
pub struct Actuator: u16 {
const FieldSwitch = 0x0001;
const DrainPump = 0x0002;
const Reverse = 0x0010;
const Heater = 0x0020;
const Softener = 0x0040;
const PreWash = 0x0080;
const MainWash = 0x2000;
const WarmWater = 0x4000;
}
}
#[derive(Debug)]
pub struct WashingMachine<P> {
intf: Interface<P>,
software_id: u16,
}
impl<P: Read + Write> WashingMachine<P> {
pub(crate) async fn initialize(
mut intf: Interface<P>,
software_id: u16,
) -> Result<Self, P::Error> {
intf.enable_dummy_bytes().await?;
intf.unlock_read_access(0xb4ee).await?;
intf.unlock_full_access(0x4e83).await?;
Ok(Self { intf, software_id })
}
pub async fn query_rom_code(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0xffdf).await?)
}
pub async fn query_operating_time(&mut self) -> Result<Duration, P::Error> {
let time: u32 = self.intf.read_memory(0x0014).await?;
let mins = time & 0x0000_00ff;
let hours = utils::decode_bcd_value((time & 0xffff_ff00) >> 8);
Ok(Duration::from_secs(u64::from(hours * 60 * 60 + mins * 60)))
}
pub async fn query_faults(&mut self) -> Result<Fault, P::Error> {
Fault::from_bits(self.intf.read_memory(0x000e).await?).ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_operating_mode(&mut self) -> Result<OperatingMode, P::Error> {
OperatingMode::from_repr(self.intf.read_memory(0x0089).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_selector(&mut self) -> Result<SelectorPosition, P::Error> {
SelectorPosition::from_repr(self.intf.read_memory(0x0071).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_type(&mut self) -> Result<ProgramType, P::Error> {
ProgramType::from_repr(self.intf.read_memory(0x009e).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_temperature(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0x009f).await?)
}
pub async fn query_program_options(&mut self) -> Result<ProgramOption, P::Error> {
ProgramOption::from_bits(self.intf.read_memory(0x0012).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn set_program_options(&mut self, opts: ProgramOption) -> Result<(), P::Error> {
Ok(self.intf.write_memory(0x0012, opts.bits()).await?)
}
pub async fn query_program_spin_setting(&mut self) -> Result<SpinSetting, P::Error> {
SpinSetting::from_repr(self.intf.read_memory(0x0011).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn set_program_spin_setting(&mut self, speed: SpinSetting) -> Result<(), P::Error> {
Ok(self.intf.write_memory(0x0011, speed as u8).await?)
}
pub async fn query_program_phase(&mut self) -> Result<ProgramPhase, P::Error> {
ProgramPhase::from_repr(self.intf.read_memory(0x005e).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_locked(&mut self) -> Result<bool, P::Error> {
let state: u8 = self.intf.read_memory(0x0005).await?;
Ok((state & 0x04) != 0x00)
}
pub async fn query_load_level(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0x000a).await?)
}
pub async fn query_active_actuators(&mut self) -> Result<Actuator, P::Error> {
Actuator::from_bits(self.intf.read_memory(0x0039).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_ntc_resistance(&mut self) -> Result<u32, P::Error> {
let val: u8 = self.intf.read_memory(0x0179).await?;
Ok(utils::ntc_resistance_from_adc(val))
}
pub async fn query_temperature(&mut self) -> Result<(u8, u8), P::Error> {
let [target, current] = self.intf.read_memory(0x0138).await?;
Ok((current, target))
}
pub async fn query_water_level(&mut self) -> Result<(u8, u8), P::Error> {
let [current, target] = self.intf.read_memory(0x003b).await?;
Ok((current, target))
}
pub async fn start_program(&mut self) -> Result<(), P::Error> {
let state: u8 = self.intf.read_memory(0x00a5).await?;
if state == 0x01 {
Ok(self.intf.write_memory(0x00a5, 0x02u8).await?)
} else {
Err(Error::InvalidState)
}
}
}
#[async_trait::async_trait(?Send)]
impl<P: Read + Write> Device<P> for WashingMachine<P> {
async fn connect(port: P) -> Result<Self, P::Error> {
let mut intf = Interface::new(port);
let id = intf.query_software_id().await?;
match id {
compatible_software_ids!() => Self::initialize(intf, id).await,
_ => Err(Error::UnknownSoftwareId(id)),
}
}
fn interface(&mut self) -> &mut Interface<P> {
&mut self.intf
}
fn software_id(&self) -> u16 {
self.software_id
}
fn kind(&self) -> DeviceKind {
DeviceKind::WashingMachine
}
fn properties(&self) -> &'static [Property] {
&[
PROP_ROM_CODE,
PROP_OPERATING_TIME,
PROP_FAULTS,
PROP_OPERATING_MODE,
PROP_PROGRAM_SELECTOR,
PROP_PROGRAM_TYPE,
PROP_PROGRAM_TEMPERATURE,
PROP_PROGRAM_OPTIONS,
PROP_PROGRAM_SPIN_SETTING,
PROP_PROGRAM_PHASE,
PROP_PROGRAM_LOCKED,
PROP_LOAD_LEVEL,
PROP_ACTIVE_ACTUATORS,
PROP_NTC_RESISTANCE,
PROP_TEMPERATURE,
PROP_WATER_LEVEL,
]
}
fn actions(&self) -> &'static [Action] {
&[
ACTION_SET_PROGRAM_OPTIONS,
ACTION_SET_PROGRAM_SPIN_SETTING,
ACTION_START_PROGRAM,
]
}
async fn query_property(&mut self, prop: &Property) -> Result<Value, P::Error> {
match *prop {
PROP_ROM_CODE => Ok(self.query_rom_code().await?.into()),
PROP_OPERATING_TIME => Ok(self.query_operating_time().await?.into()),
PROP_FAULTS => Ok(self.query_faults().await?.to_string().into()),
PROP_OPERATING_MODE => Ok(self.query_operating_mode().await?.to_string().into()),
PROP_PROGRAM_SELECTOR => Ok(self.query_program_selector().await?.to_string().into()),
PROP_PROGRAM_TYPE => Ok(self.query_program_type().await?.to_string().into()),
PROP_PROGRAM_TEMPERATURE => Ok(self.query_program_temperature().await?.into()),
PROP_PROGRAM_OPTIONS => Ok(self.query_program_options().await?.to_string().into()),
PROP_PROGRAM_SPIN_SETTING => {
Ok(self.query_program_spin_setting().await?.to_string().into())
}
PROP_PROGRAM_PHASE => Ok(self.query_program_phase().await?.to_string().into()),
PROP_PROGRAM_LOCKED => Ok(self.query_program_locked().await?.into()),
PROP_LOAD_LEVEL => Ok(self.query_load_level().await?.into()),
PROP_ACTIVE_ACTUATORS => Ok(self.query_active_actuators().await?.to_string().into()),
PROP_NTC_RESISTANCE => Ok(self.query_ntc_resistance().await?.into()),
PROP_TEMPERATURE => Ok(self.query_temperature().await?.into()),
PROP_WATER_LEVEL => Ok(self.query_water_level().await?.into()),
_ => Err(Error::UnknownProperty),
}
}
async fn trigger_action(
&mut self,
action: &Action,
param: Option<Value>,
) -> Result<(), P::Error> {
match *action {
ACTION_SET_PROGRAM_OPTIONS => match param {
Some(Value::String(s)) => self.set_program_options(s.parse()?).await,
_ => Err(Error::InvalidArgument),
},
ACTION_SET_PROGRAM_SPIN_SETTING => match param {
Some(Value::String(s)) => self.set_program_spin_setting(s.parse()?).await,
_ => Err(Error::InvalidArgument),
},
ACTION_START_PROGRAM => match param {
None => self.start_program().await,
_ => Err(Error::InvalidArgument),
},
_ => Err(Error::UnknownAction),
}
}
}
impl<P> private::Sealed for WashingMachine<P> {}