diff --git a/Cargo.lock b/Cargo.lock index df6087a..a37eddb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,6 +1100,7 @@ dependencies = [ "embassy-time", "embedded-io-async", "embedded-nal-async", + "heapless 0.8.0", "nb 1.1.0", "panic-probe", "portable-atomic", diff --git a/fan-controller/Cargo.toml b/fan-controller/Cargo.toml index 9d59173..016a20d 100644 --- a/fan-controller/Cargo.toml +++ b/fan-controller/Cargo.toml @@ -39,6 +39,7 @@ embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", package = "e embassy-time = { version = "0.3.1", features = ["defmt"] } embedded-io-async = "0.6.1" embedded-nal-async = "0.7.1" +heapless = "0.8.0" nb = "1.1.0" panic-probe = "0.3.2" portable-atomic = { version = "1.7", features = ["critical-section"] } diff --git a/fan-controller/src/configuration.rs b/fan-controller/src/configuration.rs index b51384b..f3162c4 100644 --- a/fan-controller/src/configuration.rs +++ b/fan-controller/src/configuration.rs @@ -36,7 +36,7 @@ pub(crate) const MQTT_BROKER_CREDENTIALS: MqttBrokerCredentials = MqttBrokerCred //TODO make configurable /// Prefix is "homeassistant", but it can be changed in home assistant configuration -pub(crate) const DISCOVERY_TOPIC: &str = "homeassistant/fan/testfan/config"; +pub(crate) const DISCOVERY_TOPIC: &str = "homeassistant/fan/fancontroller/config"; /// The keep alive interval defines the maximum time between messages sent to the broker. /// The broker will disconnect the client if no message is received within 1.5 times of the keep alive interval. diff --git a/fan-controller/src/fan.rs b/fan-controller/src/fan.rs index 4f9ac18..0e648fc 100644 --- a/fan-controller/src/fan.rs +++ b/fan-controller/src/fan.rs @@ -1,6 +1,7 @@ //! ebm-pabst [RadiCal centrifugal fans in scroll housings for residential ventilation](https://www.ebmpapst.com/us/en/campaigns/product-campaigns/centrifugal-fans/radical-with-scroll-housing.html) //! specific configuration and constants +use crate::string_buffer::StringBuffer; use crate::{configuration, modbus}; use cortex_m::prelude::_embedded_hal_serial_Write; use defmt::{error, info, Format}; @@ -9,7 +10,7 @@ use embassy_rp::gpio::{Level, Output, Pin}; use embassy_rp::interrupt::typelevel::Binding; use embassy_rp::uart::{Async, DataBits, InterruptHandler, Parity, RxPin, StopBits, TxPin, Uart}; use embassy_rp::{uart, Peripheral}; -use embassy_time::{with_timeout, Duration, TimeoutError, Timer}; +use embassy_time::{block_for, with_timeout, Duration, TimeoutError, Timer}; pub(crate) const BAUD_RATE: u32 = 19_200; pub(crate) fn get_configuration() -> uart::Config { @@ -24,20 +25,9 @@ pub(crate) fn get_configuration() -> uart::Config { } pub(crate) const MAX_SET_POINT: u16 = 64_000; -/// Like a string with length and capacity of 5. Used for sending publish packets to Home Assistant through MQTT -pub(crate) struct SetFanStatePayload { - buffer: [u8; 5], - start_index: usize, -} - -impl SetFanStatePayload { - pub(crate) fn as_bytes(&self) -> &[u8] { - &self.buffer[self.start_index..] - } -} #[derive(Debug, Format, Clone, Copy, PartialEq)] -pub(crate) struct Setting(u16); +pub(crate) struct Setting(pub(crate) u16); #[derive(Debug, Format)] pub(crate) struct SetPointOutOfBoundsError; @@ -55,28 +45,6 @@ impl Setting { const fn get(&self) -> u16 { self.0 } - - pub(crate) const fn to_string_buffer(&self) -> SetFanStatePayload { - // The largest value the set point can assume is 64000 which is 5 characters long - let mut buffer = [0; 5]; - let mut index = 4; - let mut remainder = self.0; - loop { - let digit = remainder % 10; - // Convert digit to ASCII (which is also valid utf-8) - buffer[index] = digit as u8 + b'0'; - remainder /= 10; - if remainder <= 0 { - break; - } - index -= 1; - } - - SetFanStatePayload { - buffer, - start_index: index, - } - } } #[derive(Default, Format, Debug)] @@ -97,6 +65,20 @@ pub(super) mod holding_registers { pub(crate) const REFERENCE_SET_POINT: [u8; 2] = 0xd001_u16.to_be_bytes(); } +mod input_registers { + pub(super) const TEMPERATURE_SENSOR_1: [u8; 2] = 0xd02e_u16.to_be_bytes(); + pub(super) const HUMIDITY_SENSOR_1: [u8; 2] = 0xd02f_u16.to_be_bytes(); + pub(super) const TEMPERATURE_SENSOR_2: [u8; 2] = 0xd030_u16.to_be_bytes(); + pub(super) const HUMIDITY_SENSOR_2: [u8; 2] = 0xd031_u16.to_be_bytes(); +} + +pub(crate) enum Fan { + One, + Two, +} + +/// Modbus messages are sent through UART to MAX845 to control fans. +/// The pin is used to enable the DE pin to switch between reading and writing pub(crate) struct Client<'a, UART: uart::Instance, PIN: Pin> { uart: Uart<'a, UART, Async>, driver_enable: Output<'a, PIN>, @@ -121,6 +103,57 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { } } + async fn send(&mut self, message: &[u8; 8]) -> Result<(), TimeoutError> { + // Write then read + // Set pin setting DE (driver enable) to on (high) on the MAX845 to send data + self.driver_enable.set_high(); + + // As ref because &[u8; 8] is not the same as &[u8] + let result = with_timeout( + configuration::FAN_TIMEOUT, + self.uart.write(message.as_ref()), + ) + .await?; + // let result = self.uart.blocking_write(message.as_ref()); + info!("uart write result: {:?}", result); + + // Before closing we need to flush the buffer to ensure that all data is written + // This requires blocking or we get a WouldBlock error. I don't understand why (TODO) + let result = self.uart.blocking_flush(); + if let Err(error) = result { + error!("uart flush error"); + } + + // In addition to flushing we need to wait for some time before turning off data in on the + // MAX845 because we might be too fast and cut off the last byte or more. (This happened) + // I saw someone using 120 microseconds (https://youtu.be/i46jdhvRej4?t=886). + // This number is based on trial and error. Don't feel bad to change it if it doesn't work. + // Also timings in microseconds are not accurate. + // I assume this should be below the modbus message delay + // Timer::after(Duration::from_micros(1_000)).await; + // Using await timer breaks this too. Probably because it yields to the scheduler + block_for(Duration::from_micros(1_000)); + + // Close sending data to enable receiving data + self.driver_enable.set_low(); + + // Read + // Read response from fan + let mut response_buffer: [u8; 8] = [0; 8]; + info!("Waiting for response from fan"); + let response = with_timeout( + configuration::FAN_TIMEOUT, + self.uart.read(&mut response_buffer), + ) + .await?; + // let response = self.uart.blocking_read(&mut response_buffer); + + info!("response from fan: {:?} {:?}", response, response_buffer); + //TODO validate response from fan + Ok(()) + } + + /// The mutable reference to self here is important as there can only be one writer to the (mod)bus at a time pub(crate) async fn set_set_point( &mut self, Setting(set_point): &Setting, @@ -150,38 +183,11 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { message[7] = checksum[0]; info!("Sending message to fan 1: {:?}", message); - // Set pin setting DE (driver enable) to on (high) on the MAX845 to send data - self.driver_enable.set_high(); - let result = self.uart.write(&message).await; - info!("uart write result: {:?}", result); - - // Before closing we need to flush the buffer to ensure that all data is written - // This requires blocking or we get a WouldBlock error. I don't understand why (TODO) - let result = self.uart.blocking_flush(); - if let Err(error) = result { - error!("uart flush error"); - } - - // Wait to avoid cutting off last byte when turning off driver enable - Timer::after(Duration::from_micros(190)).await; - - // Close sending data to enable receiving data - self.driver_enable.set_low(); - - // Read response from fan 1 - let mut response_buffer: [u8; 8] = [0; 8]; - info!("Waiting for response from fan 1"); - let response = with_timeout( - configuration::FAN_TIMEOUT, - self.uart.read(&mut response_buffer), - ) - .await?; - - info!("response from fan 1: {:?} {:?}", response, response_buffer); - //TODO validate response from fan 1 + self.send(&message).await?; /// Messsage delay between modbus messages in microseconds const MESSAGE_DELAY: u64 = modbus::get_message_delay(BAUD_RATE); + info!("Message delay {}", MESSAGE_DELAY); Timer::after_micros(MESSAGE_DELAY).await; // Form message to fan 2 @@ -191,36 +197,70 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { let checksum = modbus::CRC.checksum(&message[..6]).to_be_bytes(); message[6] = checksum[1]; message[7] = checksum[0]; + info!("sending message to fan 2: {:?}", message); + self.send(&message).await?; + Ok(()) + } + + pub(crate) async fn get_temperature(&mut self, fan: Fan) -> Result { + let mut message: [u8; 8] = [ + // Device address + match fan { + Fan::One => address::FAN_1, + Fan::Two => address::FAN_2, + }, + // Modbus function code + modbus::function_code::READ_INPUT_REGISTER, + // Input register address + input_registers::TEMPERATURE_SENSOR_1[0], + input_registers::TEMPERATURE_SENSOR_1[1], + // Number of registers to read + 0, + 1, + // CRC is set later + 0, + 0, + ]; + + let checksum = modbus::CRC.checksum(&message[..6]).to_be_bytes(); + + // They come out reversed (or is us using to_be_bytes reversed?) + message[6] = checksum[1]; + message[7] = checksum[0]; + info!("Sending read temperature message {:?}", message); // Set pin setting DE (driver enable) to on (high) on the MAX845 to send data self.driver_enable.set_high(); let result = self.uart.write(&message).await; - info!("uart result: {:?}", result); - + info!("uart write result: {:?}", result); // Before closing we need to flush the buffer to ensure that all data is written + // This requires blocking or we get a WouldBlock error. I don't understand why (TODO) let result = self.uart.blocking_flush(); if let Err(error) = result { - info!("uart flush error"); + error!("uart flush error"); } - // In addition to flushing we need to wait for some time before turning off data in on the - // MAX845 because we might be too fast and cut off the last byte or more. (This happened) - // I saw someone using 120 microseconds (https://youtu.be/i46jdhvRej4?t=886). This number - // is based on trial and error. Don't feel bad to change it if it doesn't work. + // Wait to avoid cutting off last byte when turning off driver enable Timer::after(Duration::from_micros(190)).await; // Close sending data to enable receiving data self.driver_enable.set_low(); - //TODO validate response from fan 2 - // Read response from fan 2 + // Read response + let mut response_buffer: [u8; 8] = [0; 8]; + info!("Waiting for response from fan 1"); let response = with_timeout( configuration::FAN_TIMEOUT, self.uart.read(&mut response_buffer), ) .await?; - info!("response from fan 2: {:?} {:?}", response, response_buffer); - Ok(()) + + info!("response from fan 1: {:?} {:?}", response, response_buffer); + let length = response_buffer[2]; + let temperature = u16::from_be_bytes([response_buffer[3], response_buffer[4]]); + info!("Temperature (divide by 10): {}", temperature); + + Ok(temperature) } } diff --git a/fan-controller/src/main.rs b/fan-controller/src/main.rs index 50832b8..311c53b 100644 --- a/fan-controller/src/main.rs +++ b/fan-controller/src/main.rs @@ -3,6 +3,7 @@ //TODO remove this 🥴 #![allow(warnings)] +use configuration::DISCOVERY_TOPIC; use core::convert::Infallible; use core::future::{poll_fn, Future}; use core::num::NonZeroU16; @@ -45,6 +46,7 @@ use mqtt::{Encode, TryDecode}; use rand::RngCore; use reqwless::client::{TlsConfig, TlsVerify}; use static_cell::StaticCell; +use string_buffer::StringBuffer; use {defmt_rtt as _, panic_probe as _}; @@ -70,6 +72,7 @@ mod configuration; mod fan; mod modbus; mod mqtt; +mod string_buffer; bind_interrupts!(struct Irqs { PIO0_IRQ_0 => PioInterruptHandler; @@ -404,7 +407,7 @@ async fn mqtt_task( use mqtt::task; let packet = Connect { - client_identifier: "testfan", + client_identifier: "fancontroller", username: configuration::MQTT_BROKER_CREDENTIALS.username, password: configuration::MQTT_BROKER_CREDENTIALS.password, keep_alive_seconds: configuration::KEEP_ALIVE.as_secs() as u16, @@ -480,7 +483,7 @@ async fn mqtt_task( // This part is not MQTT and application specific match publish.topic_name { - "testfan/speed/percentage" => { + "fancontroller/speed/percentage" => { let payload = match core::str::from_utf8(publish.payload) { Ok(payload) => payload, Err(error) => { @@ -518,7 +521,7 @@ async fn mqtt_task( }); // Home assistant and fan update will be done by receiver } - "testfan/on/set" => { + "fancontroller/on/set" => { info!("Received fan set on command from homeassistant"); info!( "Payload: {:?}", @@ -650,8 +653,19 @@ async fn mqtt_task( let listen = listen(&mut reader); enum PredefinedPublish { - PublishPercentageState { setting: fan::Setting }, - PublishOnState { is_on: bool }, + FanPercentageState { + setting: fan::Setting, + }, + FanOnState { + is_on: bool, + }, + SensorTemperature { + /// Celsius temperature as read from the sensor. This is the raw value. To get the actual temperature, divide by 10. + /// e.g. 234 means 23.4 degrees Celsius + temperature: u16, + /// The fan the sensor is on + fan: fan::Fan, + }, } enum Message<'a> { @@ -687,11 +701,11 @@ async fn mqtt_task( } } Message::PredefinedPublish(publish) => match publish { - PredefinedPublish::PublishPercentageState { setting } => { + PredefinedPublish::FanPercentageState { setting } => { info!("Sending percentage state publish {}", setting); - let buffer = setting.to_string_buffer(); + let buffer: StringBuffer<5> = setting.into(); let packet = Publish { - topic_name: "testfan/speed/percentage_state", + topic_name: "fancontroller/speed/percentage_state", payload: buffer.as_bytes(), }; @@ -700,16 +714,16 @@ async fn mqtt_task( continue; } } - PredefinedPublish::PublishOnState { is_on } => { + PredefinedPublish::FanOnState { is_on } => { //TODO update state let packet = if is_on { Publish { - topic_name: "testfan/on/state", + topic_name: "fancontroller/on/state", payload: b"ON", } } else { Publish { - topic_name: "testfan/on/state", + topic_name: "fancontroller/on/state", payload: b"OFF", } }; @@ -720,6 +734,31 @@ async fn mqtt_task( continue; } } + PredefinedPublish::SensorTemperature { temperature, fan } => { + // The value is divided by 10 in the value template that is submitted with home assistant discovery + let formatted = match heapless::String::<3>::try_from(temperature) { + Ok(formatted) => formatted, + Err(error) => { + error!( + "Error writing temperature {} to heapless::String", + temperature + ); + continue; + } + }; + let packet = Publish { + topic_name: match fan { + fan::Fan::One => "fancontroller/sensor/fan-1/temperature", + fan::Fan::Two => "fancontroller/sensor/fan-2/temperature", + }, + payload: formatted.as_bytes(), + }; + info!("Sending sensor temperature publish"); + if let Err(error) = send(&mut *writer, packet).await { + error!("Error sending predefined publish: {:?}", error); + continue; + } + } }, } @@ -736,7 +775,7 @@ async fn mqtt_task( // Subscribe to home assistant topics const SUBSCRIPTIONS: [Subscription; 2] = [ Subscription { - topic_filter: "testfan/on/set", + topic_filter: "fancontroller/on/set", options: mqtt::packet::subscribe::Options::new( QualityOfService::AtMostOnceDelivery, false, @@ -746,7 +785,7 @@ async fn mqtt_task( ), }, Subscription { - topic_filter: "testfan/speed/percentage", + topic_filter: "fancontroller/speed/percentage", options: mqtt::packet::subscribe::Options::new( QualityOfService::AtMostOnceDelivery, false, @@ -781,8 +820,10 @@ async fn mqtt_task( //TODO add diagnostic entity like IP address //TODO availability topic //TODO remove whitespace at compile time through macro, build script or const fn + //TODO support availability with will message https://www.home-assistant.io/integrations/mqtt/#using-availability-topics // Using abbreviations to save space of binary and on the wire (haven't measured the effect though...) + // See https://www.home-assistant.io/integrations/mqtt/ or https://github.com/home-assistant/core/blob/dev/homeassistant/components/mqtt/abbreviations.py // name -> name // uniq_id -> unique_id // stat_t -> state_topic @@ -790,17 +831,34 @@ async fn mqtt_task( // pct_stat_t -> percentage_state_topic // pct_cmd_t -> percentage_command_topic // spd_rng_max -> speed_range_max + // dev -> device + // mf -> manufacturer + // o -> origin (recommended https://www.home-assistant.io/integrations/mqtt/) + // unit_of_meas -> unit_of_measurement // Don't need to set speed_range_min because it is 1 by default + const DISCOVERY_PAYLOAD: &[u8] = br#"{ - "name": "Fan", - "uniq_id": "testfan", - "stat_t": "testfan/on/state", - "cmd_t": "testfan/on/set", - "pct_stat_t": "testfan/speed/percentage_state", - "pct_cmd_t": "testfan/speed/percentage", - "spd_rng_max": 64000 + "name": "Fans", + "uniq_id": "fancontroller", + "stat_t": "fancontroller/on/state", + "cmd_t": "fancontroller/on/set", + "pct_stat_t": "fancontroller/speed/percentage_state", + "pct_cmd_t": "fancontroller/speed/percentage", + "spd_rng_max": 64000, + "dev": { + "ids": "fancontroller-device", + "name": "Fan Controller", + "model": "Raspberry Pi Pico W 1" + } }"#; + // "~": "fancontroller", + // + // "o": { + // "name": "Fan Controller", + // "url": "github.com/santaclaas/embedded-fan-control/" + // } + const DISCOVERY_PUBLISH: Message = Message::Publish(Publish { topic_name: configuration::DISCOVERY_TOPIC, payload: DISCOVERY_PAYLOAD, @@ -808,6 +866,26 @@ async fn mqtt_task( OUTGOING.send(DISCOVERY_PUBLISH).await; //TODO wait for packet acknowledgement + return; + + // Needs to be string because "°C" is not ASCII + const DISCOVERY_TEMPERATURE_SENSOR_1: &str = r#"{ + "name": "Fan 1 Temperature Sensor", + "uniq_id": "fan-1-temperature-sensor", + "dev": { + "ids": "fancontroller-device" + }, + "stat_t": "fancontroller/fan-1/temperature", + "value_template": "{{ value | float / 10 }}", + "unit_of_meas": "°C" + }"#; + + const DISCOVERY_TEMPERATURE_SENSOR_1_PUBLISH: Message = Message::Publish(Publish { + topic_name: "homeassistant/sensor/fan-1-temperature-sensor/config", + payload: DISCOVERY_TEMPERATURE_SENSOR_1.as_bytes(), + }); + + OUTGOING.send(DISCOVERY_TEMPERATURE_SENSOR_1_PUBLISH).await; } // Future 3 @@ -871,7 +949,7 @@ async fn mqtt_task( // Future 4 let keep_alive = keep_alive(&writer); - // Future 5 update homeassistant when manual change with button occurs + // Future 5 update homeassistant when change occurs async fn update_homeassistant() { let Some(mut receiver) = FAN_STATE.receiver() else { error!("Fan state receiver was not set up. Cannot update Homeassistant"); @@ -892,14 +970,13 @@ async fn mqtt_task( "UPDATING HOMEASSISTANT {}", if state.is_on { "ON" } else { "OFF" } ); - let message = Message::PredefinedPublish(PredefinedPublish::PublishOnState { - is_on: state.is_on, - }); + let message = + Message::PredefinedPublish(PredefinedPublish::FanOnState { is_on: state.is_on }); OUTGOING.send(message).await; // Update setting before is on state for smoother transition in homeassistant UI info!("UPDATING HOMEASSISTANT {}", state.setting); - let message = Message::PredefinedPublish(PredefinedPublish::PublishPercentageState { + let message = Message::PredefinedPublish(PredefinedPublish::FanPercentageState { setting: state.setting, }); OUTGOING.send(message).await; @@ -908,74 +985,29 @@ async fn mqtt_task( } } - //TODO cancel all tasks when client loses connection - join5(listen, talk, keep_alive, set_up, update_homeassistant()).await; -} - -async fn send_discovery_and_keep_alive( - mut writer: TcpWriter<'_>, - mut send_buffer: [u8; 256], - mut keep_alive: Ticker, -) -> Result<(), MqttError> { - // Send discovery packet - // Configuration is like the YAML configuration that would be added in Home Assistant but as JSON - // Command topic: The MQTT topic to publish commands to change the state of the fan - //TODO set firmware version from Cargo.toml package version - //TODO think about setting hardware version, support url, and manufacturer - //TODO create single home assistant device with multiple entities for sensors in fan and the bypass - //TODO add diagnostic entity like IP address - //TODO availability topic - //TODO remove whitespace at compile time through macro, build script or const fn - - // Using abbreviations to save space of binary and on the wire (haven't measured the effect though...) - // name -> name - // uniq_id -> unique_id - // stat_t -> state_topic - // cmd_t -> command_topic - // pct_stat_t -> percentage_state_topic - // pct_cmd_t -> percentage_command_topic - // spd_rng_max -> speed_range_max - // Don't need to set speed_range_min because it is 1 by default - const DISCOVERY_PAYLOAD: &[u8] = br#"{ - "name": "Fan", - "uniq_id": "testfan", - "stat_t": "testfan/on/state", - "cmd_t": "testfan/on/set", - "pct_stat_t": "testfan/speed/percentage_state", - "pct_cmd_t": "testfan/speed/percentage", - "spd_rng_max": 64000 - }"#; - - const DISCOVERY_PUBLISH: Publish = Publish { - topic_name: configuration::DISCOVERY_TOPIC, - payload: DISCOVERY_PAYLOAD, - }; - - let mut offset = 0; - DISCOVERY_PUBLISH - .try_encode(&mut send_buffer, &mut offset) - .map_err(MqttError::WritePublishError)?; - - writer - .write_all(&send_buffer[..offset]) - .await - .map_err(MqttError::WriteError)?; - writer.flush().await.map_err(MqttError::FlushError)?; + // Future 6 + async fn poll_sensors() { + loop { + let mut fans = FANS.lock().await; + let Some(fans) = fans.deref_mut() else { + error!("Fans were not set up. Cannot poll sensors"); + return; + }; - keep_alive.reset(); - // No acknowledgement to read for quality of service 0 + let temperature = match fans.get_temperature(fan::Fan::One).await { + Ok(temperature) => temperature, + Err(TimeoutError) => { + error!("Timeout getting temperature for fan 1"); + return; + } + }; - loop { - keep_alive.next().await; - // Send keep alive ping request - PingRequest.encode(&mut send_buffer, &mut offset); - writer - .write_all(&send_buffer[..offset]) - .await - .map_err(MqttError::WriteError)?; - writer.flush().await.map_err(MqttError::FlushError)?; - //TODO read ping response or disconnect after "a reasonable amount of time" + Timer::after_secs(10).await; + } } + + //TODO cancel all tasks when client loses connection + join5(listen, talk, keep_alive, set_up, update_homeassistant()).await; } /// This task handles inputs from physical buttons to change the fan speed @@ -1047,21 +1079,23 @@ async fn update_fans() { return; }; - let mut previous_is_on = FAN_STATE - .try_get() - .map(|state| state.is_on) - .unwrap_or(false); + // Only comparing on state causes button triggers to be ignored + let mut previous = FAN_STATE.try_get().unwrap_or(FanState { + is_on: false, + setting: fan::Setting::ZERO, + }); loop { let state = receiver.changed().await; - if state.is_on == previous_is_on { + if state == previous { continue; } // Update previous before continue - previous_is_on = state.is_on; + previous = state.clone(); + info!("Updating fans"); let mut fans = FANS.lock().await; let Some(fans) = fans.deref_mut() else { warn!("No fan client found"); @@ -1088,7 +1122,7 @@ static FANS: Fans = Mutex::new(None); /// Fan state can have a setting while being off although and we emulate that behavior because /// fan devices actually don't have that behavior -#[derive(Clone)] +#[derive(PartialEq, Clone)] struct FanState { is_on: bool, setting: fan::Setting, diff --git a/fan-controller/src/mqtt/task.rs b/fan-controller/src/mqtt/task.rs index 764fd66..2267921 100644 --- a/fan-controller/src/mqtt/task.rs +++ b/fan-controller/src/mqtt/task.rs @@ -30,7 +30,7 @@ where { info!("Sending packet"); let mut offset = 0; - let mut send_buffer = [0; 512]; + let mut send_buffer = [0; 1024]; packet .try_encode(&mut send_buffer, &mut offset) .map_err(SendError::EncodeError)?; diff --git a/fan-controller/src/string_buffer.rs b/fan-controller/src/string_buffer.rs new file mode 100644 index 0000000..b979d92 --- /dev/null +++ b/fan-controller/src/string_buffer.rs @@ -0,0 +1,37 @@ +use crate::fan; + +/// Like a string with length and capacity of 5. Used for sending publish packets to Home Assistant through MQTT +pub(crate) struct StringBuffer { + buffer: [u8; N], + start_index: usize, +} + +impl StringBuffer { + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.buffer[self.start_index..] + } +} + +impl From for StringBuffer<5> { + fn from(setting: fan::Setting) -> Self { + // The largest value the set point can assume is 64000 which is 5 characters long + let mut buffer = [0; 5]; + let mut index = 4; + let mut remainder = setting.0; + loop { + let digit = remainder % 10; + // Convert digit to ASCII (which is also valid utf-8) + buffer[index] = digit as u8 + b'0'; + remainder /= 10; + if remainder <= 0 { + break; + } + index -= 1; + } + + Self { + buffer, + start_index: index, + } + } +}