diff --git a/fan-controller/Cargo.toml b/fan-controller/Cargo.toml index 8f836c6..f1283a4 100644 --- a/fan-controller/Cargo.toml +++ b/fan-controller/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "fan-controller" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] cortex-m = { version = "0.7.7", features = ["inline-asm"] } diff --git a/fan-controller/src/main.rs b/fan-controller/src/main.rs index b8c3a2f..e59cfac 100644 --- a/fan-controller/src/main.rs +++ b/fan-controller/src/main.rs @@ -13,11 +13,11 @@ use embassy_futures::join::join; use embassy_net::Stack; use embassy_rp::gpio::{Input, Level, Output, Pull}; use embassy_rp::peripherals::{ - DMA_CH0, PIN_18, PIN_20, PIN_21, PIN_23, PIN_25, PIN_4, PIO0, UART0, + DMA_CH0, PIN_4, PIN_18, PIN_20, PIN_21, PIN_23, PIN_25, PIO0, UART0, }; use embassy_rp::pio::{InterruptHandler as PioInterruptHandler, Pio, PioPin}; use embassy_rp::uart::BufferedInterruptHandler; -use embassy_rp::{bind_interrupts, Peripherals}; +use embassy_rp::{Peripherals, bind_interrupts}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::{self, Channel}; use embassy_sync::mutex::Mutex; @@ -31,11 +31,11 @@ use static_cell::StaticCell; use {defmt_rtt as _, panic_probe as _}; -use crate::fan::set_point::{ParseSetPointError, SetPoint}; use crate::fan::Fan; +use crate::fan::set_point::{ParseSetPointError, SetPoint}; use crate::mqtt::packet::ping_request::PingRequest; use crate::mqtt::packet::publish; -use crate::task::{set_up_network_stack, MqttBrokerConfiguration, Publish}; +use crate::task::{MqttBrokerConfiguration, Publish, set_up_network_stack}; mod async_callback; mod configuration; @@ -196,7 +196,7 @@ async fn input(pin_18: PIN_18) { #[deprecated(note = "There are now two separate tasks that update the fan independently")] /// Update fans whenenver the fan setting or on state changes #[embassy_executor::task] -async fn update_fans(fans: &'static FansOnceLock) { +async fn update_fans(fans: &'static ModbusOnceLock) { let Some(mut receiver) = FAN_CONTROLLER.fan_states.0.receiver() else { // Not using asserts because they are hard to debug on embedded where it crashed error!("No receiver for fan is on state. This should never happen."); @@ -245,8 +245,8 @@ async fn update_fans(fans: &'static FansOnceLock) { } } -type FansMutex = Mutex>; -type FansOnceLock = OnceLock; +type ModbusMutex = Mutex>; +type ModbusOnceLock = OnceLock; /// Fan state can have a setting while being off although and we emulate that behavior because /// fan devices actually don't have that behavior @@ -492,23 +492,63 @@ async fn mqtt_brain_routine( #[embassy_executor::task] async fn fan_control_routine( fan_address: modbus::device::Address, - fan_state: &'static Signal, - fans: &'static FansOnceLock, + fan_speed: &'static Signal, + fans: &'static ModbusOnceLock, ) { + let modbus_mutex = fans.get().await; + + let mut current_speed: Option = None; loop { - let state = fan_state.wait().await; - //TODO retry logic + let mut speed = fan_speed.wait().await; + if current_speed.is_some_and(|speed| speed == speed) { + //TODO consider to update fan display state nontheless + info!("Fan state update received but has same state"); + continue; + } + info!("Received fan state"); + // Instruct modbus to send update - //TODO restart waiting for lock when the fan state gets updated before the lock is released - let fans = fans.get().await; - let mut fans = fans.lock().await; + let mut modbus = modbus_mutex.lock().await; + // Check we have the latest state in case it was updated while waiting for the lock + speed = fan_speed.try_take().unwrap_or(speed); + let function = modbus::function::WriteHoldingRegister::new( fan_address, fan::holding_registers::REFERENCE_SET_POINT, - *state, + *speed, ); - fans.send_3(function).await; + + const MAX_ATTEMPTS: u8 = 3; + let mut attempt = 1; + while let Err(error) = modbus.send_3(&function).await + && attempt <= MAX_ATTEMPTS + { + error!("Failed to send fan state update"); + attempt += 1; + + // Release lock so other tasks get a chance to access modbus for sending messages to devices + drop(modbus); + + // Exponential backoff + // Safe power of 2 because maximum value is 3 (900ms max) + Timer::after_millis(u64::from(attempt).pow(2) * 100).await; + modbus = modbus_mutex.lock().await; + } + + info!("Fan state updated after {} attempts", attempt); + + if attempt > MAX_ATTEMPTS { + error!( + "Failed to send fan state update after {} retries", + MAX_ATTEMPTS + ); + + // TODO set other fan to current fan speed to avoid them getting out of sync and creating over or underpressure in the house + } + + // TODO on success send update to fan display logic unit + current_speed = Some(speed); } } @@ -560,7 +600,7 @@ async fn main(spawner: Spawner) { fan::get_configuration(), ); - static FANS: FansOnceLock = FansOnceLock::new(); + static FANS: ModbusOnceLock = ModbusOnceLock::new(); // Just initialize it _ = FANS.get_or_init(|| client.into()); diff --git a/fan-controller/src/modbus/client.rs b/fan-controller/src/modbus/client.rs index f96bb5d..f61f9a3 100644 --- a/fan-controller/src/modbus/client.rs +++ b/fan-controller/src/modbus/client.rs @@ -1,20 +1,19 @@ use defmt::{error, info}; use embassy_rp::{ - dma, + Peripheral, dma, gpio::{Level, Output, Pin}, interrupt::typelevel::Binding, uart::{self, BufferedInterruptHandler, BufferedUart, RxPin, TxPin}, - Peripheral, }; -use embassy_time::{block_for, with_timeout, Duration, TimeoutError, Timer}; +use embassy_time::{Duration, TimeoutError, Timer, block_for, with_timeout}; use embedded_io_async::{Read, Write}; use crate::{ configuration, - fan::{self, address, holding_registers, set_point::SetPoint, Fan, FanResponse, BAUD_RATE}, + fan::{self, BAUD_RATE, Fan, FanResponse, address, holding_registers, set_point::SetPoint}, modbus::{ self, - function::{read_input_register::ReadInputRegister, WriteHoldingRegister}, + function::{WriteHoldingRegister, read_input_register::ReadInputRegister}, }, }; @@ -121,7 +120,7 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { Ok(response) } - pub(crate) async fn send_3(&mut self, message: WriteHoldingRegister) -> Result<(), Error> { + pub(crate) async fn send_3(&mut self, message: &WriteHoldingRegister) -> Result<(), Error> { // Write then read // Set pin setting DE (driver enable) to on (high) on the MAX845 to send data self.driver_enable.set_high(); diff --git a/fan-controller/src/modbus/mod.rs b/fan-controller/src/modbus/mod.rs index 65ee110..92b6d5a 100644 --- a/fan-controller/src/modbus/mod.rs +++ b/fan-controller/src/modbus/mod.rs @@ -3,9 +3,10 @@ pub(crate) mod device; pub(crate) mod function; pub(crate) mod register; -use crc::{Crc, CRC_16_MODBUS}; +use crc::{CRC_16_MODBUS, Crc}; -use crate::modbus::function::{read_input_register::ReadInputRegister, Function}; +pub(crate) use client::Client; +use function::{Function, read_input_register::ReadInputRegister}; /// Used to create CRC checksums when forming modbus messages pub(super) const CRC: Crc = Crc::::new(&CRC_16_MODBUS); diff --git a/fan-controller/src/task.rs b/fan-controller/src/task.rs index ede1847..4c5ba0e 100644 --- a/fan-controller/src/task.rs +++ b/fan-controller/src/task.rs @@ -1,3 +1,4 @@ +use crate::PingRequest; use crate::fan::set_point::SetPoint; use crate::mqtt::packet::connect::Connect; use crate::mqtt::packet::disconnect::Disconnect; @@ -7,17 +8,16 @@ use crate::mqtt::packet::subscribe::{Subscribe, Subscription}; use crate::mqtt::packet::subscribe_acknowledgement::SubscribeAcknowledgement; use crate::mqtt::task::send; use crate::mqtt::{self}; -use crate::mqtt::{non_zero_u16, TryDecode}; -use crate::PingRequest; -use crate::{configuration, fan, gain_control, FanState}; -use crate::{modbus, FansOnceLock}; +use crate::mqtt::{TryDecode, non_zero_u16}; +use crate::{FanState, configuration, fan, gain_control}; +use crate::{ModbusOnceLock, modbus}; use ::mqtt::QualityOfService; use core::future::poll_fn; use core::num::NonZeroU16; use core::ops::DerefMut; use core::task::Poll; use cyw43::{Control, NetDriver}; -use defmt::{error, info, unwrap, warn, Format}; +use defmt::{Format, error, info, unwrap, warn}; use embassy_executor::Spawner; use embassy_futures::join::{join, join4}; use embassy_net::dns::{DnsQueryType, DnsSocket}; @@ -32,7 +32,7 @@ use embassy_sync::channel::{self, Channel}; use embassy_sync::mutex::Mutex; use embassy_sync::signal::Signal; use embassy_sync::waitqueue::AtomicWaker; -use embassy_time::{with_deadline, with_timeout, Duration, Instant, Timer}; +use embassy_time::{Duration, Instant, Timer, with_deadline, with_timeout}; use embedded_io_async::Read; use rand::RngCore; use static_cell::StaticCell; @@ -104,7 +104,9 @@ async fn handle_publish<'f>( let payload = match core::str::from_utf8(publish.payload) { Ok(payload) => payload, Err(error) => { - warn!("Expected percentage_command_topic payload (speed percentage) to be a valid UTF-8 string with a number"); + warn!( + "Expected percentage_command_topic payload (speed percentage) to be a valid UTF-8 string with a number" + ); return; } }; @@ -266,7 +268,9 @@ async fn listen< Ok(response) => response, // Matching to get compiler error if this changes Err(_) => { - defmt::unreachable!("Ping response is always empty so decode should always succeed if the protocol did not change") + defmt::unreachable!( + "Ping response is always empty so decode should always succeed if the protocol did not change" + ) } }; @@ -561,7 +565,7 @@ async fn update_homeassistant( } } -async fn poll_sensors(fans: FansOnceLock) { +async fn poll_sensors(fans: ModbusOnceLock) { loop { let mut fans = fans.get().await.lock().await; let fans = fans.deref_mut();