From 9c3ba569366e7ab167fcec3d419af1f43a39a0a1 Mon Sep 17 00:00:00 2001 From: Claas Date: Wed, 9 Jul 2025 22:01:50 +0200 Subject: [PATCH] Abstract constants for reuse --- Cargo.lock | 5 +++++ Cargo.toml | 1 + fan-controller/Cargo.toml | 2 ++ fan-controller/build.rs | 26 ++++++++++++++++---------- fan-controller/src/fan.rs | 10 ++++++++++ fan-controller/src/main.rs | 38 +++++++++++++++++--------------------- mqtt/README.md | 2 ++ topic/Cargo.toml | 6 ++++++ topic/README.md | 3 +++ topic/src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 10 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 mqtt/README.md create mode 100644 topic/Cargo.toml create mode 100644 topic/README.md create mode 100644 topic/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a2ed4ea..3d3c756 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,6 +1105,7 @@ dependencies = [ "serde_json", "static_cell", "thiserror 2.0.12", + "topic", ] [[package]] @@ -2755,6 +2756,10 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "topic" +version = "0.1.0" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index cf5266a..9407448 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "exponential_distribution", "fan-controller", "mqtt", + "topic", ] resolver = "3" diff --git a/fan-controller/Cargo.toml b/fan-controller/Cargo.toml index 52a8f7f..afba911 100644 --- a/fan-controller/Cargo.toml +++ b/fan-controller/Cargo.toml @@ -47,6 +47,7 @@ portable-atomic = { version = "1.7", features = ["critical-section"] } rand = { version = "0.8.5", default-features = false } reqwless = { version = "0.12.1", features = ["defmt"] } static_cell = "2" +topic = { version = "0.1.0", path = "../topic" } [profile.release] debug = 2 @@ -59,3 +60,4 @@ ruff_python_parser = { git = "https://github.com/astral-sh/ruff", rev = "9bee837 serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" thiserror = "2.0.12" +topic = { version = "0.1.0", path = "../topic" } diff --git a/fan-controller/build.rs b/fan-controller/build.rs index d5fe60a..965a890 100644 --- a/fan-controller/build.rs +++ b/fan-controller/build.rs @@ -7,6 +7,7 @@ //! Cargo re-run the build script whenever `memory.x` is changed, //! updating `memory.x` ensures a rebuild of the application with the //! new memory settings. +#![allow(unused)] use std::collections::HashMap; use std::env::{self, VarError}; @@ -353,10 +354,12 @@ fn set_discovery_payload() { Component::Fan { name: Some("Fan 1"), unique_id: Some("fancontroller/fan-1"), - state_topic: Some("fancontroller/fan-1/on/state"), - command_topic: "fancontroller/fan-1/on/set", - percentage_state_topic: Some("fancontroller/fan-1/speed/percentage_state"), - percentage_command_topic: Some("fancontroller/fan-1/speed/percentage"), + state_topic: Some(topic::fan_controller::fan_1::STATE), + command_topic: topic::fan_controller::fan_1::COMMAND, + percentage_state_topic: Some(topic::fan_controller::fan_1::percentage::STATE), + percentage_command_topic: Some( + topic::fan_controller::fan_1::percentage::COMMAND, + ), speed_range_max: Some(32_000), }, ), @@ -366,21 +369,24 @@ fn set_discovery_payload() { Component::Fan { name: Some("Fan 2"), unique_id: Some("fancontroller/fan-2"), - state_topic: Some("fancontroller/fan-2/on/state"), - command_topic: "fancontroller/fan-2/on/set", - percentage_state_topic: Some("fancontroller/fan-2/speed/percentage_state"), - percentage_command_topic: Some("fancontroller/fan-2/speed/percentage"), + state_topic: Some(topic::fan_controller::fan_2::STATE), + command_topic: topic::fan_controller::fan_2::COMMAND, + percentage_state_topic: Some(topic::fan_controller::fan_2::percentage::STATE), + percentage_command_topic: Some( + topic::fan_controller::fan_2::percentage::COMMAND, + ), speed_range_max: Some(32_000), }, ), ]), quality_of_service: None, - state_topic: Some("fancontroller/on/state"), - command_topic: Some("fancontroller/on/set"), + state_topic: Some(topic::fan_controller::STATE), + command_topic: Some(topic::fan_controller::COMMAND), encoding: None, }; let payload = serde_json::to_string(&payload).unwrap(); + println!("cargo:rustc-env=FAN_CONTROLLER_DISCOVERY_PAYLOAD={payload}",); } diff --git a/fan-controller/src/fan.rs b/fan-controller/src/fan.rs index 1c58b23..bb62ba6 100644 --- a/fan-controller/src/fan.rs +++ b/fan-controller/src/fan.rs @@ -89,6 +89,16 @@ pub(crate) enum State { Medium, High, } +impl State { + pub(crate) const fn next(&self) -> Self { + match self { + Self::Off => Self::Low, + Self::Low => Self::Medium, + Self::Medium => Self::High, + Self::High => Self::Off, + } + } +} pub(crate) mod address { pub(crate) const FAN_1: u8 = 0x02; diff --git a/fan-controller/src/main.rs b/fan-controller/src/main.rs index be51b23..a8cf81d 100644 --- a/fan-controller/src/main.rs +++ b/fan-controller/src/main.rs @@ -1,5 +1,6 @@ #![no_std] #![no_main] +#![allow(unused)] use configuration::DISCOVERY_TOPIC; use core::convert::Infallible; @@ -461,8 +462,8 @@ async fn mqtt_task( if *packet { Poll::Ready(()) } else { - // Waker needs to be overwritten on each poll. Read the Rust async book on wakers - // for more details + // Waker needs to be overwritten on each poll. + // Read the Rust async book on wakers for more details let waker = context.waker(); WAKER.register(waker); Poll::Pending @@ -478,7 +479,7 @@ async fn mqtt_task( /// A handler that takes MQTT publishes and sets the fan settings accordingly async fn handle_publish<'f>(publish: &'f Publish<'f>) { info!("Handling publish"); - let sender = FAN_STATE.sender(); + let sender = FAN_1_STATE.sender(); // This part is not MQTT and application specific match publish.topic_name { @@ -520,7 +521,7 @@ async fn mqtt_task( }); // Home assistant and fan update will be done by receiver } - "fancontroller/on/set" => { + topic::fan_controller::COMMAND => { info!("Received fan set on command from homeassistant"); info!( "Payload: {:?}", @@ -782,7 +783,7 @@ async fn mqtt_task( // Subscribe to home assistant topics const SUBSCRIPTIONS: [Subscription; 2] = [ Subscription { - topic_filter: "fancontroller/on/set", + topic_filter: topic::fan_controller::COMMAND, options: mqtt::packet::subscribe::Options::new( QualityOfService::AtMostOnceDelivery, false, @@ -948,7 +949,7 @@ async fn mqtt_task( // Future 5 update homeassistant when change occurs async fn update_homeassistant() { - let Some(mut receiver) = FAN_STATE.receiver() else { + let Some(mut receiver) = FAN_1_STATE.receiver() else { error!("Fan state receiver was not set up. Cannot update Homeassistant"); return; }; @@ -1028,7 +1029,7 @@ async fn input_task(pin_18: PIN_18) { let mut button = Debouncer::new(Input::new(pin_18, Pull::Up), Duration::from_millis(250)); let mut fan_state = fan::State::default(); - let sender = FAN_STATE.sender(); + let sender = FAN_1_STATE.sender(); loop { // Falling edge for our button -> button down (pressing down @@ -1040,18 +1041,11 @@ async fn input_task(pin_18: PIN_18) { let start = Instant::now(); // Advance to next fan state - // Could make this a state machine with phantom data, but chose not to as long as it is this simple - fan_state = match fan_state { - fan::State::Off => fan::State::Low, - fan::State::Low => fan::State::Medium, - fan::State::Medium => fan::State::High, - fan::State::High => fan::State::Off, - }; + fan_state = fan_state.next(); - // Setting values low on purpose for testing let state = match fan_state { fan::State::Off => { - let setting = FAN_STATE + let setting = FAN_1_STATE .try_get() .map(|state| state.setting) .unwrap_or(fan::Setting::ZERO); @@ -1078,17 +1072,18 @@ async fn input_task(pin_18: PIN_18) { sender.send(state); } } + /// Update fans whenenver the fan setting or on state changes #[embassy_executor::task] async fn update_fans() { - let Some(mut receiver) = FAN_STATE.receiver() else { + let Some(mut receiver) = FAN_1_STATE.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."); return; }; // Only comparing on state causes button triggers to be ignored - let mut previous = FAN_STATE.try_get().unwrap_or(FanState { + let mut previous = FAN_1_STATE.try_get().unwrap_or(FanState { is_on: false, setting: fan::Setting::ZERO, }); @@ -1153,7 +1148,8 @@ struct FanState { /// Receivers: /// - Fan /// - MQTT (client to server) -static FAN_STATE: Watch = Watch::new(); +static FAN_1_STATE: Watch = Watch::new(); +static FAN_2_STATE: Watch = Watch::new(); /// Displays fan status with 2 LEDs: /// Off Off -> Fans Off @@ -1174,14 +1170,14 @@ async fn display_status(pin_21: PIN_21, pin_20: PIN_20) { led_1.set_low(); led_2.set_low(); - let Some(mut receiver) = FAN_STATE.receiver() else { + let Some(mut receiver) = FAN_1_STATE.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."); return; }; // Set initial state - let mut current_state = FAN_STATE.try_get().unwrap_or(FanState { + let mut current_state = FAN_1_STATE.try_get().unwrap_or(FanState { is_on: false, setting: fan::Setting::ZERO, }); diff --git a/mqtt/README.md b/mqtt/README.md new file mode 100644 index 0000000..1dddbe4 --- /dev/null +++ b/mqtt/README.md @@ -0,0 +1,2 @@ +# MQTT +General abstractions and code for MQTT that can be reused and is not specific to the fan controller project diff --git a/topic/Cargo.toml b/topic/Cargo.toml new file mode 100644 index 0000000..d315350 --- /dev/null +++ b/topic/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "topic" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/topic/README.md b/topic/README.md new file mode 100644 index 0000000..55694a2 --- /dev/null +++ b/topic/README.md @@ -0,0 +1,3 @@ +# Topics +Application specific Home Assistant MQTT topics for the fan controller. +Library is used to share variables between projects and build scripts diff --git a/topic/src/lib.rs b/topic/src/lib.rs new file mode 100644 index 0000000..88a5c90 --- /dev/null +++ b/topic/src/lib.rs @@ -0,0 +1,33 @@ +pub mod fan_controller { + /// The topic to publish the on/off state of the fan controller. + pub const STATE: &str = "fancontroller/on/state"; + /// The topic to subscribe to for setting the on/off state of the fan controller. + /// This topic is used by Home Assistant to notify the fan controller to turn on or off. + pub const COMMAND: &str = "fancontroller/on/set"; + + pub mod fan_1 { + /// The topic to publish the on/off state of the fan 1 on the fan controller. + pub const STATE: &str = "fancontroller/fan-1/on/state"; + /// The topic to subscribe to for setting the on/off state of the fan 1 on the fan controller. + /// This topic is used by Home Assistant to notify the fan controller to turn on or off the fan. + pub const COMMAND: &str = "fancontroller/fan-1/on/set"; + + pub mod percentage { + pub const STATE: &str = "fancontroller/fan-2/speed/percentage_state"; + pub const COMMAND: &str = "fancontroller/fan-2/speed/percentage"; + } + } + + pub mod fan_2 { + /// The topic to publish the on/off state of the fan 2 on the fan controller. + pub const STATE: &str = "fancontroller/fan-2/on/state"; + /// The topic to subscribe to for setting the on/off state of the fan 2 on the fan controller. + /// This topic is used by Home Assistant to notify the fan controller to turn on or off the fan. + pub const COMMAND: &str = "fancontroller/fan-2/on/set"; + + pub mod percentage { + pub const STATE: &str = "fancontroller/fan-2/speed/percentage_state"; + pub const COMMAND: &str = "fancontroller/fan-2/speed/percentage"; + } + } +} -- 2.51.2