use crate::device::{
Action, ActionKind, ActionParameters, Device, DeviceKind, Error, Interface, Property,
PropertyKind, Result, Value, private, utils,
};
use alloc::{
boxed::Box,
string::{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 {
() => {
629
};
}
pub(super) use compatible_software_ids;
const PROP_SERIAL_NUMBER: Property = Property {
kind: PropertyKind::General,
id: "serial_number",
name: "Serial Number",
unit: None,
};
const PROP_SERIAL_NUMBER_INDEX: Property = Property {
kind: PropertyKind::General,
id: "serial_number_index",
name: "Serial Number Index",
unit: None,
};
const PROP_MODEL_NUMBER: Property = Property {
kind: PropertyKind::General,
id: "model_number",
name: "Model Number",
unit: None,
};
const PROP_BOARD_NUMBER: Property = Property {
kind: PropertyKind::General,
id: "board_number",
name: "Board Number",
unit: None,
};
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_DISPLAY_CONTENTS: Property = Property {
kind: PropertyKind::Operation,
id: "display_contents",
name: "Display Contents",
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_PRESSURE_SENSOR_VALUE: Property = Property {
kind: PropertyKind::Io,
id: "pressure_sensor_value",
name: "Pressure Sensor Value",
unit: None,
};
const PROP_WATER_LEVEL: Property = Property {
kind: PropertyKind::Io,
id: "water_level",
name: "Water Level",
unit: Some("mmHâ‚‚O"),
};
const PROP_MOTOR_PWM_DUTY_CYCLE: Property = Property {
kind: PropertyKind::Io,
id: "motor_pwm_duty_cycle",
name: "Motor PWM Duty Cycle",
unit: Some("%"),
};
const PROP_TACHOMETER_SPEED: Property = Property {
kind: PropertyKind::Io,
id: "tachometer_speed",
name: "Tachometer Speed",
unit: Some("rpm"),
};
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",
"IntensiveShort",
])),
};
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: u16 {
const PressureSensor = 0x0001;
const NtcThermistor = 0x0002;
const Heater = 0x0004;
const TachometerGenerator = 0x0008;
const DetergentOverdose = 0x0010;
const Inlet = 0x0020;
const Drainage = 0x0040;
const SpinCycle = 0x0080;
const Eeprom = 0x0100;
}
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum OperatingMode {
ProgramIdle = 0x01,
ProgramRunning = 0x02,
ProgramFinished = 0x03,
ServiceProgramming = 0x04,
CustomerProgramming = 0x05,
Service = 0x06,
Demo = 0x08,
}
#[derive(FromRepr, Display, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum SelectorPosition {
Finish,
Cottons95,
Cottons75,
Cottons60,
Cottons40,
Cottons30,
MinimumIron60,
MinimumIron50,
MinimumIron40,
MinimumIron30,
DrainSpin,
SeparateRinse,
Starch,
AutomaticMixedWash40,
QuickWash40,
WoolensCold,
Woolens30,
Woolens40,
Silks30,
DelicatesCold,
Delicates30,
Delicates40,
}
#[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,
DrainSpin = 0x07,
SeparateRinse = 0x09,
AutomaticMixedWash = 0x0a,
Silks = 0x0b,
}
bitflags::bitflags! {
#[derive(FlagsDisplay, FlagsFromStr, FlagsDebug, PartialEq, Eq, Copy, Clone)]
pub struct ProgramOption: u8 {
const Soak = 0x10;
const PreWash = 0x20;
const WaterPlus = 0x40;
const IntensiveShort = 0x80;
}
}
#[derive(FromRepr, Display, EnumString, VariantNames, PartialEq, Eq, Copy, Clone, Debug)]
#[repr(u8)]
pub enum SpinSetting {
WithoutSpin,
RinseHold,
SpinMin,
SpinLow,
SpinMed,
SpinHigh,
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 DrainPump = 0x0004;
const DrumLights = 0x0008;
const Reverse = 0x0010;
const Heater = 0x0020;
const Softener = 0x0040;
const PreWash = 0x0080;
const FieldSwitch = 0x0100;
const WarmWater = 0x0200;
const MainWash = 0x0400;
}
}
#[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.unlock_read_access(0x43ea).await?;
intf.unlock_full_access(0x1f02).await?;
intf.write_memory(0x02c2, 0x01u8).await?;
Ok(Self { intf, software_id })
}
pub async fn query_serial_number(&mut self) -> Result<String, P::Error> {
let data: [u8; 10] = self.intf.read_eeprom(0x01ba).await?;
let serial = str::from_utf8(&data[1..9]).map_err(|_| Error::UnexpectedMemoryValue)?;
Ok(serial.to_string())
}
pub async fn query_serial_number_index(&mut self) -> Result<String, P::Error> {
let data: [u8; 4] = self.intf.read_eeprom(0x01be).await?;
let idx = str::from_utf8(&data[1..3]).map_err(|_| Error::UnexpectedMemoryValue)?;
Ok(idx.to_string())
}
pub async fn query_model_number(&mut self) -> Result<String, P::Error> {
let data: [u8; 16] = self.intf.read_eeprom(0x01bf).await?;
let model = str::from_utf8(&data[1..]).map_err(|_| Error::UnexpectedMemoryValue)?;
Ok(model.trim_end().to_string())
}
pub async fn query_board_number(&mut self) -> Result<String, P::Error> {
let data: [u8; 8] = self.intf.read_eeprom(0x01ca).await?;
let board = str::from_utf8(&data).map_err(|_| Error::UnexpectedMemoryValue)?;
Ok(board.to_string())
}
pub async fn query_rom_code(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0xffdb).await?)
}
pub async fn query_operating_time(&mut self) -> Result<Duration, P::Error> {
let time: u32 = self.intf.read_memory(0x0052).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(0x004e).await?).ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_operating_mode(&mut self) -> Result<OperatingMode, P::Error> {
OperatingMode::from_repr(self.intf.read_memory(0x00cd).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_selector(&mut self) -> Result<SelectorPosition, P::Error> {
SelectorPosition::from_repr(self.intf.read_memory(0x00b5).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_type(&mut self) -> Result<ProgramType, P::Error> {
ProgramType::from_repr(self.intf.read_memory(0x00de).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_temperature(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0x00df).await?)
}
pub async fn query_program_options(&mut self) -> Result<ProgramOption, P::Error> {
ProgramOption::from_bits(self.intf.read_memory(0x0058).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn set_program_options(&mut self, opts: ProgramOption) -> Result<(), P::Error> {
Ok(self.intf.write_memory(0x0058, opts.bits()).await?)
}
pub async fn query_program_spin_setting(&mut self) -> Result<SpinSetting, P::Error> {
SpinSetting::from_repr(self.intf.read_memory(0x0057).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn set_program_spin_setting(&mut self, speed: SpinSetting) -> Result<(), P::Error> {
Ok(self.intf.write_memory(0x0057, speed as u8).await?)
}
pub async fn query_program_phase(&mut self) -> Result<ProgramPhase, P::Error> {
ProgramPhase::from_repr(self.intf.read_memory(0x00a2).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_program_locked(&mut self) -> Result<bool, P::Error> {
let state: u8 = self.intf.read_memory(0x0045).await?;
Ok((state & 0x04) != 0x00)
}
pub async fn query_load_level(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0x004a).await?)
}
pub async fn query_display_contents(&mut self) -> Result<String, P::Error> {
let display: u32 = self.intf.read_memory(0x009e).await?;
let points = (display & 0x0070_0000) >> 20;
let d1_code = (display & 0x0000_000f) as u8;
let d2_code = ((display & 0x0000_00f0) >> 4) as u8;
let d3_code = ((display & 0x0000_0f00) >> 8) as u8;
let d1_special = (display & 0x0200_0000) != 0x0000_0000;
let d2_special = (display & 0x0400_0000) != 0x0000_0000;
let d3_special = (display & 0x0800_0000) != 0x0000_0000;
let d1_point = points == 0x01 || points == 0x07;
let d2_point = points == 0x02 || points == 0x07;
let d3_point = points == 0x03 || points == 0x07;
Ok([
utils::decode_mc14489_digit(d1_code, d1_special),
if d1_point { Some('.') } else { None },
utils::decode_mc14489_digit(d2_code, d2_special),
if d2_point { Some('.') } else { None },
utils::decode_mc14489_digit(d3_code, d3_special),
if d3_point { Some('.') } else { None },
]
.iter()
.flatten()
.collect())
}
pub async fn query_active_actuators(&mut self) -> Result<Actuator, P::Error> {
Actuator::from_bits(self.intf.read_memory(0x007d).await?)
.ok_or(Error::UnexpectedMemoryValue)
}
pub async fn query_ntc_resistance(&mut self) -> Result<u32, P::Error> {
let val: u8 = self.intf.read_memory(0x01bf).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(0x0136).await?;
Ok((current, target))
}
pub async fn query_pressure_sensor_value(&mut self) -> Result<u8, P::Error> {
Ok(self.intf.read_memory(0x02be).await?)
}
pub async fn query_water_level(&mut self) -> Result<(u8, u8), P::Error> {
let [current, target] = self.intf.read_memory(0x007f).await?;
Ok((current, target))
}
pub async fn query_motor_pwm_duty_cycle(&mut self) -> Result<u8, P::Error> {
let duty: u8 = self.intf.read_memory(0x02b9).await?;
Ok((u16::from(duty) * 100 / 0xff).try_into()?)
}
pub async fn query_tachometer_speed(&mut self) -> Result<(u16, u16), P::Error> {
let speed: u32 = self.intf.read_memory(0x01a4).await?;
let target = (speed & 0xffff) as u16;
let current = (speed >> 16) as u16;
Ok((current, target))
}
pub async fn start_program(&mut self) -> Result<(), P::Error> {
let state: u8 = self.intf.read_memory(0x00e7).await?;
if state == 0x01 {
Ok(self.intf.write_memory(0x00e7, 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_SERIAL_NUMBER,
PROP_SERIAL_NUMBER_INDEX,
PROP_MODEL_NUMBER,
PROP_BOARD_NUMBER,
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_DISPLAY_CONTENTS,
PROP_ACTIVE_ACTUATORS,
PROP_NTC_RESISTANCE,
PROP_TEMPERATURE,
PROP_PRESSURE_SENSOR_VALUE,
PROP_WATER_LEVEL,
PROP_MOTOR_PWM_DUTY_CYCLE,
PROP_TACHOMETER_SPEED,
]
}
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_SERIAL_NUMBER => Ok(self.query_serial_number().await?.into()),
PROP_SERIAL_NUMBER_INDEX => Ok(self.query_serial_number_index().await?.into()),
PROP_MODEL_NUMBER => Ok(self.query_model_number().await?.into()),
PROP_BOARD_NUMBER => Ok(self.query_board_number().await?.into()),
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_DISPLAY_CONTENTS => Ok(self.query_display_contents().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_PRESSURE_SENSOR_VALUE => Ok(self.query_pressure_sensor_value().await?.into()),
PROP_WATER_LEVEL => Ok(self.query_water_level().await?.into()),
PROP_MOTOR_PWM_DUTY_CYCLE => Ok(self.query_motor_pwm_duty_cycle().await?.into()),
PROP_TACHOMETER_SPEED => Ok(self.query_tachometer_speed().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> {}