From b080ac1f0b3213ac2764a0fdbe18f3aab9751680 Mon Sep 17 00:00:00 2001 From: Claas Date: Fri, 15 Nov 2024 23:31:18 +0100 Subject: [PATCH] Disable temperature experimentation set to max 50% as we run the fan at lower speed than allowed so it lasts longer --- Cargo.lock | 34 +++---- debug-listener/src/main.rs | 3 +- fan-controller/src/fan.rs | 170 ++++++++++++++++++++++++++--------- fan-controller/src/main.rs | 36 +++++--- fan-controller/src/modbus.rs | 69 ++++++++++++++ 5 files changed, 241 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a37eddb..4114085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.71", + "syn 2.0.87", "which", ] @@ -524,7 +524,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -535,7 +535,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -572,7 +572,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -729,7 +729,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -1193,7 +1193,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -1955,7 +1955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -2298,7 +2298,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -2451,9 +2451,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -2494,22 +2494,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -2547,7 +2547,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] [[package]] @@ -2785,5 +2785,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.87", ] diff --git a/debug-listener/src/main.rs b/debug-listener/src/main.rs index bc3ee8e..b6cd4e5 100644 --- a/debug-listener/src/main.rs +++ b/debug-listener/src/main.rs @@ -2,7 +2,8 @@ use serialport::{DataBits, Parity, SerialPort, StopBits}; use std::time::Duration; // This might change depending on your system. Could put this in an environment variable -const PORT_NAME: &str = "/dev/cu.usbserial-150"; +// const PORT_NAME: &str = "/dev/cu.usbserial-150"; +const PORT_NAME: &str = "/dev/cu.usbserial-1150"; fn open_serial_port() -> serialport::Result> { serialport::new(PORT_NAME, 19_200) diff --git a/fan-controller/src/fan.rs b/fan-controller/src/fan.rs index 949cc11..677039c 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::modbus::ReadInputRegister; use crate::{configuration, modbus}; use cortex_m::prelude::_embedded_hal_serial_Write; use defmt::{error, info, Format}; @@ -27,6 +28,8 @@ pub(crate) fn get_configuration() -> uart::Config { configuration } +const BLOCK_FOR: Duration = Duration::from_micros(5_000); + pub(crate) const MAX_SET_POINT: u16 = 64_000; #[derive(Debug, Format, Clone, Copy, PartialEq)] @@ -87,6 +90,39 @@ pub(crate) struct Client<'a, UART: uart::Instance, PIN: Pin> { driver_enable: Output<'a, PIN>, } +pub(crate) enum Error { + Timeout(TimeoutError), + Uart(uart::Error), +} + +// Is there a way to implement the From trait with a macro like thiserror +impl From for Error { + fn from(error: TimeoutError) -> Self { + Self::Timeout(error) + } +} + +impl From for Error { + fn from(error: uart::Error) -> Self { + Self::Uart(error) + } +} + +struct FanResponse { + data: [u8; N], + length: usize, +} + +impl FanResponse { + fn new(data: [u8; N], length: usize) -> Self { + Self { data, length } + } + + fn as_slice(&self) -> &[u8] { + &self.data[..self.length] + } +} + impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { pub(crate) fn new( uart: impl Peripheral

+ 'a, @@ -108,18 +144,76 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { } } - async fn send(&mut self, message: &[u8; 8]) -> Result<(), TimeoutError> { + async fn send_2( + &mut self, + message: impl modbus::ToBytes, + ) -> 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(); + + let bytes = message.to_bytes(); + info!("Sending message to fan: {:?}", bytes); + // As ref because &[u8; 8] is not the same as &[u8] + let result = with_timeout(configuration::FAN_TIMEOUT, self.uart.write_all(&bytes)).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"); + } + + // 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(BLOCK_FOR); + + // Close sending data to enable receiving data + self.driver_enable.set_low(); + + // Read + // Read response from fan. The response can vary in length + let mut response_buffer: [u8; RESPONSE] = [0; RESPONSE]; + info!("Waiting for response from fan"); + let bytes_read = with_timeout( + configuration::FAN_TIMEOUT, + //TODO test this does not wait for bytes to fill the buffer + // leading to a timeout because the response is only 7 bytes but the buffer is 8 and it waits for the last byte to arrive + self.uart.read(&mut response_buffer), + ) + .await??; + + info!("response from fan: {:?} {:?}", bytes_read, response_buffer); + let response = FanResponse::new(response_buffer, bytes_read); + + //TODO validate response from fan + // Read the correct number of bytes + Ok(response) + } + async fn send( + &mut self, + message: impl AsRef<[u8]>, + ) -> 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(); + info!("Sending message to fan: {:?}", message.as_ref()); // As ref because &[u8; 8] is not the same as &[u8] let result = with_timeout( configuration::FAN_TIMEOUT, self.uart.write_all(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 @@ -137,32 +231,36 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { // 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)); + block_for(BLOCK_FOR); // 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]; + // Read response from fan. The response can vary in length + let mut response_buffer: [u8; N] = [0; N]; info!("Waiting for response from fan"); - let response = with_timeout( + let bytes_read = with_timeout( configuration::FAN_TIMEOUT, + //TODO test this does not wait for bytes to fill the buffer + // leading to a timeout because the response is only 7 bytes but the buffer is 8 and it waits for the last byte to arrive self.uart.read(&mut response_buffer), ) - .await?; - // let response = self.uart.blocking_read(&mut response_buffer); + .await??; + + info!("response from fan: {:?} {:?}", bytes_read, response_buffer); + let response = FanResponse::new(response_buffer, bytes_read); - info!("response from fan: {:?} {:?}", response, response_buffer); //TODO validate response from fan - Ok(()) + // Read the correct number of bytes + Ok(response) } /// 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, - ) -> Result<(), TimeoutError> { + ) -> Result<(), Error> { // Send update through UART to MAX845 to modbus fans // Form message to fan 1 let mut message: [u8; 8] = [ @@ -188,11 +286,12 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { message[7] = checksum[0]; info!("Sending message to fan 1: {:?}", message); - self.send(&message).await?; + let _ = self.send::<8>(&message).await?; /// Messsage delay between modbus messages in microseconds const MESSAGE_DELAY: u64 = modbus::get_message_delay(BAUD_RATE); info!("Message delay {}", MESSAGE_DELAY); + // We can yield the future here because the wait time between messages is a minimum and can be longer Timer::after_micros(MESSAGE_DELAY).await; // Form message to fan 2 @@ -204,11 +303,21 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { message[7] = checksum[0]; info!("sending message to fan 2: {:?}", message); - self.send(&message).await?; + let _ = self.send::<8>(&message).await?; Ok(()) } - pub(crate) async fn get_temperature(&mut self, fan: Fan) -> Result { + pub(crate) async fn get_temperature(&mut self, fan: Fan) -> Result { + let message = modbus::Message::new( + match fan { + Fan::One => address::FAN_1, + Fan::Two => address::FAN_2, + }, + ReadInputRegister::new(0xd02e, 1), + ); + + let test: FanResponse<7> = self.send_2(message).await?; + let mut message: [u8; 8] = [ // Device address match fan { @@ -235,35 +344,12 @@ impl<'a, UART: uart::Instance, PIN: Pin> Client<'a, UART, PIN> { 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 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 - 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?; + let response = self.send::<7>(&message).await?; + let response = response.as_slice(); - 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]]); + //TODO read the correct number of bytes + let length = response[2]; + let temperature = u16::from_be_bytes([response[3], response[4]]); info!("Temperature (divide by 10): {}", temperature); Ok(temperature) diff --git a/fan-controller/src/main.rs b/fan-controller/src/main.rs index 0dca458..22b8220 100644 --- a/fan-controller/src/main.rs +++ b/fan-controller/src/main.rs @@ -7,7 +7,7 @@ use configuration::DISCOVERY_TOPIC; use core::convert::Infallible; use core::future::{poll_fn, Future}; use core::num::NonZeroU16; -use core::ops::{Deref, DerefMut, Sub}; +use core::ops::{Deref, DerefMut, Div, Sub}; use core::pin::pin; use core::sync::atomic::AtomicBool; use core::task::Poll; @@ -843,6 +843,7 @@ async fn mqtt_task( // unit_of_meas -> unit_of_measurement // Don't need to set speed_range_min because it is 1 by default + // Speed set to max 32000 which is 50% of what the fans can do but more is not needed. This way the fans last longer const DISCOVERY_PAYLOAD: &[u8] = br#"{ "name": "Fans", "uniq_id": "fancontroller", @@ -850,7 +851,7 @@ async fn mqtt_task( "cmd_t": "fancontroller/on/set", "pct_stat_t": "fancontroller/speed/percentage_state", "pct_cmd_t": "fancontroller/speed/percentage", - "spd_rng_max": 64000, + "spd_rng_max": 32000, "dev": { "ids": "fancontroller-device", "name": "Fan Controller", @@ -1002,10 +1003,14 @@ async fn mqtt_task( let temperature = match fans.get_temperature(fan::Fan::One).await { Ok(temperature) => temperature, - Err(TimeoutError) => { + Err(fan::Error::Timeout(TimeoutError)) => { error!("Timeout getting temperature for fan 1"); return; } + Err(fan::Error::Uart(error)) => { + error!("Uart error getting temperature for fan 1: {:?}", error); + return; + } }; Timer::after_secs(10).await; @@ -1019,7 +1024,8 @@ async fn mqtt_task( keep_alive, set_up, // Join because there is only a join with max 5 arguments 😬 - join(update_homeassistant(), poll_sensors()), + // join(update_homeassistant(), poll_sensors()), + update_homeassistant(), ) .await; } @@ -1063,10 +1069,11 @@ async fn input_task(pin_18: PIN_18) { }); continue; } - // 10% - fan::State::Low => fan::MAX_SET_POINT / 10, - // 25% - fan::State::Medium => fan::MAX_SET_POINT / 4, + // Setting speeds based + // 64000 / 3.3 + fan::State::Low => 19_393, + // 64000 / 2.4 = + fan::State::Medium => 26_666, // 50% fan::State::High => fan::MAX_SET_POINT / 2, }; @@ -1123,9 +1130,16 @@ async fn update_fans() { fan::Setting::ZERO }; - if let Err(TimeoutError) = fans.set_set_point(&setting).await { - error!("Timeout setting fan speed from button press"); - continue; + match fans.set_set_point(&setting).await { + Ok(_) => {} + Err(fan::Error::Timeout(TimeoutError)) => { + error!("Timeout setting fan speed"); + continue; + } + Err(fan::Error::Uart(error)) => { + error!("Uart error setting fan speed: {:?}", error); + continue; + } } } } diff --git a/fan-controller/src/modbus.rs b/fan-controller/src/modbus.rs index d29778d..408641a 100644 --- a/fan-controller/src/modbus.rs +++ b/fan-controller/src/modbus.rs @@ -29,3 +29,72 @@ pub(super) const fn get_message_delay(baud_rate: u32) -> u64 { // Round up as it is better to wait longer than too short MICROSECONDS_FOR_BITS.div_ceil(baud_rate as u64) } + +pub(crate) trait Function { + const CODE: u8; +} + +pub(crate) struct ReadInputRegister { + address: u16, + number_of_registers: u16, +} + +impl ReadInputRegister { + pub(crate) fn new(address: u16, number_of_registers: u16) -> Self { + Self { + address, + number_of_registers, + } + } +} + +impl Function for ReadInputRegister { + const CODE: u8 = function_code::READ_INPUT_REGISTER; +} + +pub(crate) struct Message { + address: u8, + function: F, +} + +pub(crate) trait ToBytes { + fn to_bytes(&self) -> [u8; LENGTH]; +} + +impl ToBytes<8> for Message { + fn to_bytes(&self) -> [u8; 8] { + let address_bytes = self.function.address.to_be_bytes(); + let length_bytes = self.function.number_of_registers.to_be_bytes(); + let mut buffer = [ + // Device address + self.address, + // Modbus function code + self.code(), + // Starting address + address_bytes[0], + address_bytes[1], + // Number of registers to read + length_bytes[0], + length_bytes[1], + // CRC checksum (placeholder) + 0, + 0, + ]; + + let checksum = CRC.checksum(&buffer[..6]).to_be_bytes(); + // They come out reversed (or is us using to_be_bytes reversed?) + buffer[6] = checksum[1]; + buffer[7] = checksum[0]; + buffer + } +} + +impl Message { + pub(crate) fn new(address: u8, function: F) -> Self { + Self { address, function } + } + + const fn code(&self) -> u8 { + F::CODE + } +} -- 2.51.2