diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1c4412b..ffae5fe 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -37,6 +37,8 @@ partition_table = "partitions.csv" [dependencies] +# Shared CAN API +canapi = { path = "canapi" } #ESP stuff esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] } esp-hal = { version = "=1.0.0-rc.0", features = [ diff --git a/rust/build.rs b/rust/build.rs index e0ddcaf..2cc5449 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -1,5 +1,3 @@ -use std::process::Command; - use vergen::EmitBuilder; fn linker_be_nice() { @@ -50,72 +48,6 @@ fn linker_be_nice() { } fn main() { - //webpack(); linker_be_nice(); let _ = EmitBuilder::builder().all_git().all_build().emit(); } - -fn webpack() { - //println!("cargo:rerun-if-changed=./src/src_webpack"); - Command::new("rm") - .arg("./src/webserver/bundle.js.gz") - .output() - .unwrap(); - - match Command::new("cmd").spawn() { - Ok(_) => { - println!("Assuming build on windows"); - let output = Command::new("cmd") - .arg("/K") - .arg("npx") - .arg("webpack") - .current_dir("./src_webpack") - .output() - .unwrap(); - println!("status: {}", output.status); - println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); - println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - assert!(output.status.success()); - - // move webpack results to rust webserver src - let _ = Command::new("cmd") - .arg("/K") - .arg("move") - .arg("./src_webpack/bundle.js.gz") - .arg("./src/webserver") - .output() - .unwrap(); - let _ = Command::new("cmd") - .arg("/K") - .arg("move") - .arg("./src_webpack/index.html.gz") - .arg("./src/webserver") - .output() - .unwrap(); - } - Err(_) => { - println!("Assuming build on linux"); - let output = Command::new("npx") - .arg("webpack") - .current_dir("./src_webpack") - .output() - .unwrap(); - println!("status: {}", output.status); - println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); - println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - assert!(output.status.success()); - - // move webpack results to rust webserver src - let _ = Command::new("mv") - .arg("./src_webpack/bundle.js.gz") - .arg("./src/webserver") - .output() - .unwrap(); - let _ = Command::new("mv") - .arg("./src_webpack/index.html.gz") - .arg("./src/webserver") - .output() - .unwrap(); - } - } -} diff --git a/rust/canapi/Cargo.toml b/rust/canapi/Cargo.toml new file mode 100644 index 0000000..7176692 --- /dev/null +++ b/rust/canapi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "canapi" +version = "0.1.0" +edition = "2021" + +[lib] +name = "canapi" +path = "src/lib.rs" + +[features] +default = [] + +[dependencies] +bincode = { version = "2.0.1", default-features = false, features = ["derive"] } diff --git a/rust/canapi/src/lib.rs b/rust/canapi/src/lib.rs new file mode 100644 index 0000000..bb93cf2 --- /dev/null +++ b/rust/canapi/src/lib.rs @@ -0,0 +1,138 @@ +#![no_std] +//! CAN bus API shared crate for PlantCtrl sensors and controller. +//! Addressing and messages are defined here to be reused by all bus participants. + +use bincode::{Decode, Encode}; + +/// Total plants supported by addressing (0..=15) +pub const MAX_PLANTS: u8 = 16; + +/// Sensors per plant: 0..=1 => A/B +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +#[repr(u8)] +pub enum SensorSlot { + A = 0, + B = 1, +} + +impl SensorSlot { + pub const fn from_index(idx: u8) -> Option { + match idx { + 0 => Some(SensorSlot::A), + 1 => Some(SensorSlot::B), + _ => None, + } + } +} + +/// Legacy sensor base address kept for compatibility with existing code. +/// Each plant uses SENSOR_BASE_ADDRESS + plant_index (0..PLANT_COUNT-1). +/// 11-bit standard ID space, safe range. +pub const SENSOR_BASE_ADDRESS: u16 = 1000; + +/// Typed topics within the SENSOR_BASE space. +/// Additional offsets allow distinct message semantics while keeping plant-indexed layout. +pub mod id { + use crate::{SensorSlot, MAX_PLANTS, SENSOR_BASE_ADDRESS}; + + /// Number of plants addressable per sensor slot group + pub const PLANTS_PER_GROUP: u16 = MAX_PLANTS as u16; // 16 + /// Offset applied for SensorSlot::B within a message group + pub const B_OFFSET: u16 = PLANTS_PER_GROUP; // 16 + + // Message group base offsets relative to SENSOR_BASE_ADDRESS + pub const MOISTURE_DATA_OFFSET: u16 = 0; // periodic data from sensor (sensor -> controller) + pub const IDENTIFY_CMD_OFFSET: u16 = 32; // identify LED command (controller -> sensor) + + // Convenience constants for per-slot base offsets + pub const IDENTIFY_CMD_OFFSET_A: u16 = IDENTIFY_CMD_OFFSET + 0; + pub const IDENTIFY_CMD_OFFSET_B: u16 = IDENTIFY_CMD_OFFSET + B_OFFSET; + + #[inline] + pub const fn plant_id(message_type_offset: u16, sensor: SensorSlot, plant: u16) -> u16 { + match sensor { + SensorSlot::A => SENSOR_BASE_ADDRESS + message_type_offset + plant, + SensorSlot::B => SENSOR_BASE_ADDRESS + message_type_offset + B_OFFSET + plant, + } + } + + /// Kinds of message spaces recognized by the addressing scheme. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum MessageKind { + MoistureData, // sensor -> controller + IdentifyCmd, // controller -> sensor + } + + /// Try to classify a received 11-bit standard ID into a known message kind and extract plant and sensor slot. + /// Returns (kind, plant, slot) on success. + #[inline] + pub const fn classify(id: u16) -> Option<(MessageKind, u8, SensorSlot)> { + // Ensure the ID is within our base space + if id < SENSOR_BASE_ADDRESS { + return None; + } + let rel = id - SENSOR_BASE_ADDRESS; + + // Helper: decode within a given group offset + const fn decode_in_group(rel: u16, group_base: u16) -> Option<(u8, SensorSlot)> { + if rel < group_base { return None; } + let inner = rel - group_base; + if inner < PLANTS_PER_GROUP { // A slot + Some((inner as u8, SensorSlot::A)) + } else if inner >= B_OFFSET && inner < B_OFFSET + PLANTS_PER_GROUP { // B slot + Some(((inner - B_OFFSET) as u8, SensorSlot::B)) + } else { + None + } + } + + // Check known groups in order + if let Some((plant, slot)) = decode_in_group(rel, MOISTURE_DATA_OFFSET) { + return Some((MessageKind::MoistureData, plant, slot)); + } + if let Some((plant, slot)) = decode_in_group(rel, IDENTIFY_CMD_OFFSET) { + return Some((MessageKind::IdentifyCmd, plant, slot)); + } + None + } + + /// Returns Some((plant, slot)) regardless of message kind, if the id falls into any known group; otherwise None. + #[inline] + pub const fn extract_plant_slot(id: u16) -> Option<(u8, SensorSlot)> { + match classify(id) { + Some((_kind, plant, slot)) => Some((plant, slot)), + None => None, + } + } + + /// Check if an id corresponds exactly to the given message kind, plant and slot. + #[inline] + pub const fn is_identify_for(id: u16, plant: u8, slot: SensorSlot) -> bool { + id == plant_id(IDENTIFY_CMD_OFFSET, slot, plant as u16) + } + + #[inline] + pub const fn is_moisture_data_for(id: u16, plant: u8, slot: SensorSlot) -> bool { + id == plant_id(MOISTURE_DATA_OFFSET, slot, plant as u16) + } +} + +/// Periodic moisture data sent by sensors. +/// Fits into 5 bytes with bincode-v2 (no varint): u8 + u8 + u16 = 4, alignment may keep 4. +#[derive(Debug, Clone, Copy, Encode, Decode)] +pub struct MoistureData { + pub plant: u8, // 0..MAX_PLANTS-1 + pub sensor: SensorSlot, // A/B + pub hz: u16, // measured frequency of moisture sensor +} + +/// Request a sensor to report immediately (controller -> sensor). +#[derive(Debug, Clone, Copy, Encode, Decode)] +pub struct MoistureRequest { + pub plant: u8, + pub sensor: SensorSlot, // target sensor (sensor filters by this) +} + +/// Control a sensor's identify LED, if received by sensor, blink for a few seconds +#[derive(Debug, Clone, Copy, Encode, Decode)] +pub struct IdentifyLed {} diff --git a/rust/src/hal/battery.rs b/rust/src/hal/battery.rs index 15eea22..d55e1df 100644 --- a/rust/src/hal/battery.rs +++ b/rust/src/hal/battery.rs @@ -1,6 +1,5 @@ use crate::fat_error::{FatError, FatResult}; use crate::hal::Box; -use alloc::string::String; use async_trait::async_trait; use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags}; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; @@ -37,12 +36,6 @@ pub struct BatteryInfo { pub temperature: u16, } -#[derive(Debug, Serialize)] -pub enum BatteryError { - NoBatteryMonitor, - CommunicationError(String), -} - #[derive(Debug, Serialize)] pub enum BatteryState { Unknown, diff --git a/rust/src/hal/esp.rs b/rust/src/hal/esp.rs index c79e54a..33fc26d 100644 --- a/rust/src/hal/esp.rs +++ b/rust/src/hal/esp.rs @@ -595,8 +595,7 @@ impl Esp<'_> { if duration_in_ms == 0 { software_reset(); } else { - ///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms)); - let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000)); + let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms)); let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] = [(&mut self.wake_gpio1, WakeupLevel::Low)]; let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); diff --git a/rust/src/hal/initial_hal.rs b/rust/src/hal/initial_hal.rs index ec14533..b59bf0e 100644 --- a/rust/src/hal/initial_hal.rs +++ b/rust/src/hal/initial_hal.rs @@ -3,7 +3,7 @@ use crate::fat_error::{FatError, FatResult}; use crate::hal::esp::Esp; use crate::hal::rtc::{BackupHeader, RTCModuleInteraction}; use crate::hal::water::TankSensor; -use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS}; +use crate::hal::{BoardInteraction, FreePeripherals, Moistures, TIME_ACCESS}; use crate::{ bail, config::PlantControllerConfig, @@ -117,14 +117,11 @@ impl<'a> BoardInteraction<'a> for Initial<'a> { bail!("Please configure board revision") } - async fn measure_moisture_hz( - &mut self, - _plant: usize, - _sensor: Sensor, - ) -> Result { + async fn measure_moisture_hz(&mut self) -> Result { bail!("Please configure board revision") } + async fn general_fault(&mut self, enable: bool) { self.general_fault.set_level(enable.into()); } diff --git a/rust/src/hal/mod.rs b/rust/src/hal/mod.rs index 10e2f1c..6baf93a 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -1,5 +1,5 @@ pub(crate) mod battery; -mod can_api; +// mod can_api; // replaced by external canapi crate pub mod esp; mod initial_hal; mod little_fs2storage_adapter; @@ -7,7 +7,7 @@ pub(crate) mod rtc; mod v3_hal; mod v3_shift_register; mod v4_hal; -mod v4_sensor; +pub(crate) mod v4_sensor; mod water; use crate::alloc::string::ToString; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; @@ -60,6 +60,7 @@ use bincode::{Decode, Encode}; use bq34z100::Bq34z100g1Driver; use chrono::{DateTime, FixedOffset, Utc}; use core::cell::RefCell; +use canapi::SensorSlot; use ds323x::ic::DS3231; use ds323x::interface::I2cInterface; use ds323x::{DateTimeAccess, Ds323x}; @@ -105,6 +106,8 @@ use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; use littlefs2::object_safe::DynStorage; use log::{error, info, warn}; use portable_atomic::AtomicBool; +use serde::Serialize; + pub static TIME_ACCESS: OnceLock> = OnceLock::new(); @@ -124,6 +127,15 @@ pub enum Sensor { B, } +impl Into for Sensor { + fn into(self) -> SensorSlot { + match self { + Sensor::A => SensorSlot::A, + Sensor::B => SensorSlot::B, + } + } +} + pub struct PlantHal {} pub struct HAL<'a> { @@ -142,19 +154,19 @@ pub trait BoardInteraction<'a> { fn is_day(&self) -> bool; //should be multsampled - async fn light(&mut self, enable: bool) -> Result<(), FatError>; - async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>; - async fn pump_current(&mut self, plant: usize) -> Result; - async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>; - async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result; + async fn light(&mut self, enable: bool) -> FatResult<()>; + async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()>; + async fn pump_current(&mut self, plant: usize) -> FatResult; + async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()>; + async fn measure_moisture_hz(&mut self) -> Result; async fn general_fault(&mut self, enable: bool); - async fn test(&mut self) -> Result<(), FatError>; + async fn test(&mut self) -> FatResult<()>; fn set_config(&mut self, config: PlantControllerConfig); - async fn get_mptt_voltage(&mut self) -> Result; - async fn get_mptt_current(&mut self) -> Result; + async fn get_mptt_voltage(&mut self) -> FatResult; + async fn get_mptt_current(&mut self) -> FatResult; // Return JSON string with autodetected sensors per plant. Default: not supported. - async fn detect_sensors(&mut self) -> Result { + async fn detect_sensors(&mut self) -> FatResult { bail!("Autodetection is only available on v4 HAL with CAN bus"); } @@ -685,3 +697,19 @@ pub async fn esp_set_time(time: DateTime) -> FatResult<()> { .set_rtc_time(&time.to_utc()) .await } + +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)] +pub struct Moistures{ + pub sensor_a_hz: [f32; PLANT_COUNT], + pub sensor_b_hz: [f32; PLANT_COUNT], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] +pub struct DetectionResult { + plant: [DetectionSensorResult; crate::hal::PLANT_COUNT] +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] +pub struct DetectionSensorResult{ + sensor_a: bool, + sensor_b: bool, +} \ No newline at end of file diff --git a/rust/src/hal/v3_hal.rs b/rust/src/hal/v3_hal.rs index 6582e3c..9f90f1b 100644 --- a/rust/src/hal/v3_hal.rs +++ b/rust/src/hal/v3_hal.rs @@ -4,7 +4,7 @@ use crate::hal::esp::{hold_disable, hold_enable}; use crate::hal::rtc::RTCModuleInteraction; use crate::hal::v3_shift_register::ShiftRegister40; use crate::hal::water::TankSensor; -use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS}; +use crate::hal::{BoardInteraction, FreePeripherals, Moistures, Sensor, PLANT_COUNT, TIME_ACCESS}; use crate::log::{LogMessage, LOG_ACCESS}; use crate::{ config::PlantControllerConfig, @@ -170,6 +170,112 @@ pub(crate) fn create_v3( })) } +impl V3<'_> { + + async fn inner_measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { + let mut results = [0_f32; REPEAT_MOIST_MEASURE]; + for repeat in 0..REPEAT_MOIST_MEASURE { + self.signal_counter.pause(); + self.signal_counter.clear(); + //Disable all + { + let shift_register = self.shift_register.lock().await; + shift_register.decompose()[MS_4].set_high()?; + } + + let sensor_channel = match sensor { + Sensor::A => match plant { + 0 => SENSOR_A_1, + 1 => SENSOR_A_2, + 2 => SENSOR_A_3, + 3 => SENSOR_A_4, + 4 => SENSOR_A_5, + 5 => SENSOR_A_6, + 6 => SENSOR_A_7, + 7 => SENSOR_A_8, + _ => bail!("Invalid plant id {}", plant), + }, + Sensor::B => match plant { + 0 => SENSOR_B_1, + 1 => SENSOR_B_2, + 2 => SENSOR_B_3, + 3 => SENSOR_B_4, + 4 => SENSOR_B_5, + 5 => SENSOR_B_6, + 6 => SENSOR_B_7, + 7 => SENSOR_B_8, + _ => bail!("Invalid plant id {}", plant), + }, + }; + + let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; + { + let shift_register = self.shift_register.lock().await; + let pin_0 = &mut shift_register.decompose()[MS_0]; + let pin_1 = &mut shift_register.decompose()[MS_1]; + let pin_2 = &mut shift_register.decompose()[MS_2]; + let pin_3 = &mut shift_register.decompose()[MS_3]; + if is_bit_set(0) { + pin_0.set_high()?; + } else { + pin_0.set_low()?; + } + if is_bit_set(1) { + pin_1.set_high()?; + } else { + pin_1.set_low()?; + } + if is_bit_set(2) { + pin_2.set_high()?; + } else { + pin_2.set_low()?; + } + if is_bit_set(3) { + pin_3.set_high()?; + } else { + pin_3.set_low()?; + } + + shift_register.decompose()[MS_4].set_low()?; + shift_register.decompose()[SENSOR_ON].set_high()?; + } + let measurement = 100; //how long to measure and then extrapolate to hz + let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz + + //give some time to stabilize + Timer::after_millis(10).await; + self.signal_counter.resume(); + Timer::after_millis(measurement).await; + self.signal_counter.pause(); + { + let shift_register = self.shift_register.lock().await; + shift_register.decompose()[MS_4].set_high()?; + shift_register.decompose()[SENSOR_ON].set_low()?; + } + Timer::after_millis(10).await; + let unscaled = self.signal_counter.value(); + let hz = unscaled as f32 * factor; + LOG_ACCESS + .lock() + .await + .log( + LogMessage::RawMeasure, + unscaled as u32, + hz as u32, + &plant.to_string(), + &format!("{sensor:?}"), + ) + .await; + results[repeat] = hz; + } + results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord + + let mid = results.len() / 2; + let median = results[mid]; + Ok(median) + } +} + #[async_trait(?Send)] impl<'a> BoardInteraction<'a> for V3<'a> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> { @@ -275,109 +381,25 @@ impl<'a> BoardInteraction<'a> for V3<'a> { Ok(()) } - async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { - let mut results = [0_f32; REPEAT_MOIST_MEASURE]; - for repeat in 0..REPEAT_MOIST_MEASURE { - self.signal_counter.pause(); - self.signal_counter.clear(); - //Disable all - { - let shift_register = self.shift_register.lock().await; - shift_register.decompose()[MS_4].set_high()?; - } - - let sensor_channel = match sensor { - Sensor::A => match plant { - 0 => SENSOR_A_1, - 1 => SENSOR_A_2, - 2 => SENSOR_A_3, - 3 => SENSOR_A_4, - 4 => SENSOR_A_5, - 5 => SENSOR_A_6, - 6 => SENSOR_A_7, - 7 => SENSOR_A_8, - _ => bail!("Invalid plant id {}", plant), - }, - Sensor::B => match plant { - 0 => SENSOR_B_1, - 1 => SENSOR_B_2, - 2 => SENSOR_B_3, - 3 => SENSOR_B_4, - 4 => SENSOR_B_5, - 5 => SENSOR_B_6, - 6 => SENSOR_B_7, - 7 => SENSOR_B_8, - _ => bail!("Invalid plant id {}", plant), - }, - }; - - let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; - { - let shift_register = self.shift_register.lock().await; - let pin_0 = &mut shift_register.decompose()[MS_0]; - let pin_1 = &mut shift_register.decompose()[MS_1]; - let pin_2 = &mut shift_register.decompose()[MS_2]; - let pin_3 = &mut shift_register.decompose()[MS_3]; - if is_bit_set(0) { - pin_0.set_high()?; - } else { - pin_0.set_low()?; - } - if is_bit_set(1) { - pin_1.set_high()?; - } else { - pin_1.set_low()?; - } - if is_bit_set(2) { - pin_2.set_high()?; - } else { - pin_2.set_low()?; - } - if is_bit_set(3) { - pin_3.set_high()?; - } else { - pin_3.set_low()?; - } - - shift_register.decompose()[MS_4].set_low()?; - shift_register.decompose()[SENSOR_ON].set_high()?; - } - let measurement = 100; //how long to measure and then extrapolate to hz - let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz - - //give some time to stabilize - Timer::after_millis(10).await; - self.signal_counter.resume(); - Timer::after_millis(measurement).await; - self.signal_counter.pause(); - { - let shift_register = self.shift_register.lock().await; - shift_register.decompose()[MS_4].set_high()?; - shift_register.decompose()[SENSOR_ON].set_low()?; - } - Timer::after_millis(10).await; - let unscaled = self.signal_counter.value(); - let hz = unscaled as f32 * factor; + async fn measure_moisture_hz(&mut self) -> Result { + let mut result = Moistures::default(); + for plant in 0..PLANT_COUNT { + let a = self.inner_measure_moisture_hz(plant, Sensor::A).await; + let b = self.inner_measure_moisture_hz(plant, Sensor::B).await; + let aa = a.unwrap_or_else(|_| u32::MAX as f32); + let bb = b.unwrap_or_else(|_| u32::MAX as f32); LOG_ACCESS .lock() .await - .log( - LogMessage::RawMeasure, - unscaled as u32, - hz as u32, - &plant.to_string(), - &format!("{sensor:?}"), - ) + .log(LogMessage::TestSensor, aa as u32, bb as u32, &plant.to_string(), "") .await; - results[repeat] = hz; + result.sensor_a_hz[plant] = aa; + result.sensor_b_hz[plant] = bb; } - results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord - - let mid = results.len() / 2; - let median = results[mid]; - Ok(median) + Ok(result) } + async fn general_fault(&mut self, enable: bool) { hold_disable(6); if enable { @@ -410,23 +432,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> { self.pump(i, false).await?; Timer::after_millis(100).await; } - for plant in 0..PLANT_COUNT { - let a = self.measure_moisture_hz(plant, Sensor::A).await; - let b = self.measure_moisture_hz(plant, Sensor::B).await; - let aa = match a { - Ok(a) => a as u32, - Err(_) => u32::MAX, - }; - let bb = match b { - Ok(b) => b as u32, - Err(_) => u32::MAX, - }; - LOG_ACCESS - .lock() - .await - .log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "") - .await; - } + self.measure_moisture_hz().await?; Timer::after_millis(10).await; Ok(()) } diff --git a/rust/src/hal/v4_hal.rs b/rust/src/hal/v4_hal.rs index 734d646..cd2e46a 100644 --- a/rust/src/hal/v4_hal.rs +++ b/rust/src/hal/v4_hal.rs @@ -6,7 +6,7 @@ use crate::hal::esp::{hold_disable, hold_enable, Esp}; use crate::hal::rtc::RTCModuleInteraction; use crate::hal::v4_sensor::{SensorImpl, SensorInteraction}; use crate::hal::water::TankSensor; -use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS}; +use crate::hal::{BoardInteraction, DetectionResult, FreePeripherals, Moistures, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS}; use crate::log::{LogMessage, LOG_ACCESS}; use alloc::boxed::Box; use alloc::string::ToString; @@ -387,8 +387,8 @@ impl<'a> BoardInteraction<'a> for V4<'a> { Ok(()) } - async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result { - self.sensor.measure_moisture_hz(plant, sensor).await + async fn measure_moisture_hz(&mut self) -> Result { + self.sensor.measure_moisture_hz().await } async fn general_fault(&mut self, enable: bool) { @@ -426,21 +426,14 @@ impl<'a> BoardInteraction<'a> for V4<'a> { self.pump(i, false).await?; Timer::after_millis(100).await; } + let moisture = self.measure_moisture_hz().await?; for plant in 0..PLANT_COUNT { - let a = self.measure_moisture_hz(plant, Sensor::A).await; - let b = self.measure_moisture_hz(plant, Sensor::B).await; - let aa = match a { - Ok(a) => a as u32, - Err(_) => u32::MAX, - }; - let bb = match b { - Ok(b) => b as u32, - Err(_) => u32::MAX, - }; + let a = moisture.sensor_a_hz[plant] as u32; + let b = moisture.sensor_b_hz[plant] as u32; LOG_ACCESS .lock() .await - .log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "") + .log(LogMessage::TestSensor, a, b, &plant.to_string(), "") .await; } Timer::after_millis(10).await; @@ -451,30 +444,15 @@ impl<'a> BoardInteraction<'a> for V4<'a> { self.config = config; } - async fn get_mptt_voltage(&mut self) -> Result { + async fn get_mptt_voltage(&mut self) -> FatResult { self.charger.get_mptt_voltage() } - async fn get_mptt_current(&mut self) -> Result { + async fn get_mptt_current(&mut self) -> FatResult { self.charger.get_mppt_current() } - async fn detect_sensors(&mut self) -> Result { - // Delegate to sensor autodetect and build JSON - let detected = self.sensor.autodetect().await?; - // Build JSON manually to avoid exposing internal types - let mut s = alloc::string::String::from("{\"plants\":["); - for (i, (a, b)) in detected.iter().enumerate() { - if i != 0 { - s.push(','); - } - s.push_str("{\"a\":"); - s.push_str(if *a { "true" } else { "false" }); - s.push_str(",\"b\":"); - s.push_str(if *b { "true" } else { "false" }); - s.push('}'); - } - s.push_str("]}"); - Ok(s) + async fn detect_sensors(&mut self) -> FatResult { + self.sensor.autodetect().await } } diff --git a/rust/src/hal/v4_sensor.rs b/rust/src/hal/v4_sensor.rs index 23a85ee..3fb8996 100644 --- a/rust/src/hal/v4_sensor.rs +++ b/rust/src/hal/v4_sensor.rs @@ -1,31 +1,33 @@ +use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET}; use crate::bail; use crate::fat_error::{ContextExt, FatError, FatResult}; -use crate::hal::can_api::ResponseMoisture; -use crate::hal::Sensor; -use crate::hal::{can_api, Box}; +use canapi::{SensorSlot}; +use crate::hal::{DetectionResult, Moistures, Sensor}; +use crate::hal::Box; use crate::log::{LogMessage, LOG_ACCESS}; use alloc::format; use alloc::string::ToString; use async_trait::async_trait; use bincode::config; -use bincode::error::DecodeError; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_time::{Instant, Timer, WithTimeout}; -use embedded_can::Frame; +use embedded_can::{Frame, Id}; use esp_hal::gpio::Output; use esp_hal::i2c::master::I2c; use esp_hal::pcnt::unit::Unit; use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration}; -use esp_hal::{Async, Blocking}; -use log::info; +use esp_hal::{Blocking}; +use log::{error, info, warn}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; const REPEAT_MOIST_MEASURE: usize = 10; + + #[async_trait(?Send)] pub trait SensorInteraction { - async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult; + async fn measure_moisture_hz(&mut self) -> FatResult; } const MS0: u8 = 1_u8; @@ -49,86 +51,21 @@ pub enum SensorImpl { #[async_trait(?Send)] impl SensorInteraction for SensorImpl { - async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult { + async fn measure_moisture_hz(&mut self) -> FatResult { match self { SensorImpl::PulseCounter { signal_counter, sensor_expander, .. } => { - let mut results = [0_f32; REPEAT_MOIST_MEASURE]; - for repeat in 0..REPEAT_MOIST_MEASURE { - signal_counter.pause(); - signal_counter.clear(); - - //Disable all - sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; - - let sensor_channel = match sensor { - Sensor::A => plant as u32, - Sensor::B => (15 - plant) as u32, - }; - - let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; - if is_bit_set(0) { - sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?; - } else { - sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; - } - if is_bit_set(1) { - sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?; - } else { - sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; - } - if is_bit_set(2) { - sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?; - } else { - sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; - } - if is_bit_set(3) { - sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?; - } else { - sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; - } - - sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?; - sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?; - - let measurement = 100; // TODO what is this scaling factor? what is its purpose? - let factor = 1000f32 / measurement as f32; - - //give some time to stabilize - Timer::after_millis(10).await; - signal_counter.resume(); - Timer::after_millis(measurement).await; - signal_counter.pause(); - sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; - sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?; - sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; - sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; - sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; - sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; - Timer::after_millis(10).await; - let unscaled = 1337; //signal_counter.get_counter_value()? as i32; - let hz = unscaled as f32 * factor; - LOG_ACCESS - .lock() - .await - .log( - LogMessage::RawMeasure, - unscaled as u32, - hz as u32, - &plant.to_string(), - &format!("{sensor:?}"), - ) - .await; - results[repeat] = hz; + let mut result = Moistures::default(); + for plant in 0..crate::hal::PLANT_COUNT{ + result.sensor_a_hz[plant] = Self::inner_pulse(plant, Sensor::A, signal_counter, sensor_expander).await?; + info!("Sensor {} {:?}: {}", plant, Sensor::A, result.sensor_a_hz[plant]); + result.sensor_b_hz[plant] = Self::inner_pulse(plant, Sensor::B, signal_counter, sensor_expander).await?; + info!("Sensor {} {:?}: {}", plant, Sensor::B, result.sensor_b_hz[plant]); } - results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord - - let mid = results.len() / 2; - let median = results[mid]; - Ok(median) + Ok(result) } SensorImpl::CanBus { @@ -151,7 +88,7 @@ impl SensorInteraction for SensorImpl { } Timer::after_millis(10).await; - let can = Self::inner_can(plant, sensor, &mut twai).await; + let can = Self::inner_can(&mut twai).await; can_power.set_low(); @@ -165,8 +102,10 @@ impl SensorInteraction for SensorImpl { } } + + impl SensorImpl { - pub async fn autodetect(&mut self) -> FatResult<[(bool, bool); crate::hal::PLANT_COUNT]> { + pub async fn autodetect(&mut self) -> FatResult { match self { SensorImpl::PulseCounter { .. } => { bail!("Only CAN bus implementation supports autodetection") @@ -177,7 +116,7 @@ impl SensorImpl { } => { // Power on CAN transceiver and start controller can_power.set_high(); - let mut config = twai_config.take().expect("twai config not set"); + let config = twai_config.take().expect("twai config not set"); let mut as_async = config.into_async().start(); // Give CAN some time to stabilize Timer::after_millis(10).await; @@ -185,52 +124,72 @@ impl SensorImpl { // Send a few test messages per potential sensor node for plant in 0..crate::hal::PLANT_COUNT { for sensor in [Sensor::A, Sensor::B] { - // Reuse CAN addressing scheme from moisture request - let can_buffer = [0_u8; 8]; - let cfg = config::standard(); - if let Some(address) = - StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16) - { - if let Some(frame) = EspTwaiFrame::new(address, &can_buffer) { - // Try a few times; we intentionally ignore rx here and rely on stub logic - let resu = as_async.transmit_async(&frame).await; - match resu { - Ok(_) => { - info!( - "Sent test message to plant {} sensor {:?}", - plant, sensor - ); - } - Err(err) => { - info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err); - } + let target = StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, sensor.into(), plant as u16)).context(">> Could not create address for sensor! (plant: {}) <<")?; + let can_buffer = [0_u8; 0]; + if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) { + // Try a few times; we intentionally ignore rx here and rely on stub logic + let resu = as_async.transmit_async(&frame).await; + match resu { + Ok(_) => { + info!( + "Sent test message to plant {} sensor {:?}", + plant, sensor + ); + } + Err(err) => { + info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err); } - } else { - info!("Error building CAN frame"); } } else { - info!("Error creating address for sensor"); + info!("Error building CAN frame"); } + } } - // Poll for messages for ~100 ms - let detect_timeout = Instant::now() - .checked_add(embassy_time::Duration::from_millis(100)) - .unwrap(); + let mut result = DetectionResult::default(); loop { - match as_async.receive() { + match as_async.receive_async().with_deadline(Instant::from_millis(100)).await { Ok(or) => { - info!("Received CAN message: {:?}", or); - } - Err(nb::Error::WouldBlock) => { - if Instant::now() > detect_timeout { - break; + match or { + Ok(can_frame) => { + match can_frame.id() { + Id::Standard(id) => { + let rawid = id.as_raw(); + match classify(rawid) { + None => {} + Some(msg) => { + if msg.0 == MessageKind::MoistureData { + let plant = msg.1 as usize; + let sensor = msg.2; + match sensor { + SensorSlot::A => { + result.plant[plant].sensor_a = true; + } + SensorSlot::B => { + result.plant[plant].sensor_b = true; + } + } + } + } + } + } + Id::Extended(ext) => { + warn!("Received extended ID: {:?}", ext); + } + } + + } + Err(err ) => { + error!("Error receiving CAN message: {:?}", err); + break; + } } - Timer::after_millis(10).await; + info!("Received CAN message: {:?}", or); + } - Err(nb::Error::Other(err)) => { - info!("Error receiving CAN message: {:?}", err); + Err(err) => { + error!("Timeout receiving CAN message: {:?}", err); break; } } @@ -240,29 +199,94 @@ impl SensorImpl { can_power.set_low(); twai_config.replace(config); - // Stub: return no detections yet - let mut result = [(false, false); crate::hal::PLANT_COUNT]; + info!("Autodetection result: {:?}", result); Ok(result) } } } - async fn inner_can( - plant: usize, - sensor: Sensor, - twai: &mut Twai<'static, Blocking>, - ) -> FatResult { - let can_sensor: Sensor = sensor.into(); - //let request = RequestMoisture { sensor: can_sensor }; - let can_buffer = [0_u8; 8]; - let config = config::standard(); - //let encoded = bincode::encode_into_slice(&request, &mut can_buffer, config)?; + pub async fn inner_pulse(plant: usize, sensor: Sensor, signal_counter: &mut Unit<'_, 0>, sensor_expander: &mut Pca9535Immediate>>) -> FatResult { - let address = StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16) - .context(">> Could not create address for sensor! (plant: {}) <<")?; - let request = - EspTwaiFrame::new(address, &can_buffer[0..8]).context("Error building CAN frame")?; - twai.transmit(&request)?; + let mut results = [0_f32; REPEAT_MOIST_MEASURE]; + for repeat in 0..REPEAT_MOIST_MEASURE { + signal_counter.pause(); + signal_counter.clear(); + + //Disable all + sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; + + let sensor_channel = match sensor { + Sensor::A => plant as u32, + Sensor::B => (15 - plant) as u32, + }; + + let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 }; + if is_bit_set(0) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; + } + if is_bit_set(1) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; + } + if is_bit_set(2) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; + } + if is_bit_set(3) { + sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?; + } else { + sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; + } + + sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?; + sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?; + + let measurement = 100; // TODO what is this scaling factor? what is its purpose? + let factor = 1000f32 / measurement as f32; + + //give some time to stabilize + Timer::after_millis(10).await; + signal_counter.resume(); + Timer::after_millis(measurement).await; + signal_counter.pause(); + sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?; + sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?; + Timer::after_millis(10).await; + let unscaled = 1337; //signal_counter.get_counter_value()? as i32; + let hz = unscaled as f32 * factor; + LOG_ACCESS + .lock() + .await + .log( + LogMessage::RawMeasure, + unscaled as u32, + hz as u32, + &plant.to_string(), + &format!("{sensor:?}"), + ) + .await; + results[repeat] = hz; + } + results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord + + let mid = results.len() / 2; + let median = results[mid]; + Ok(median) +} + + async fn inner_can( + twai: &mut Twai<'static, Blocking>, + ) -> FatResult { + [0_u8; 8]; + config::standard(); let timeout = Instant::now() .checked_add(embassy_time::Duration::from_millis(100)) @@ -271,26 +295,7 @@ impl SensorImpl { let answer = twai.receive(); match answer { Ok(answer) => { - let data = EspTwaiFrame::data(&answer); - let response: Result<(ResponseMoisture, usize), DecodeError> = - bincode::decode_from_slice(&data, config); - info!("Can answer {response:?}"); - let value = response?.0; - if (value.plant as usize) != plant { - bail!( - "Received answer for wrong plant! Expected: {}, got: {}", - plant, - value.plant - ); - } - if value.sensor != can_sensor { - bail!( - "Received answer for wrong sensor! Expected: {:?}, got: {:?}", - can_sensor, - value.sensor - ); - } - return Ok(value.hz as f32); + info!("Received CAN message: {:?}", answer); } Err(error) => match error { nb::Error::Other(error) => { diff --git a/rust/src/main.rs b/rust/src/main.rs index 18864e2..1e130fa 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -399,15 +399,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { publish_tank_state(&mut board, &tank_state, water_temp).await; + let moisture = board.board_hal.measure_moisture_hz().await?; + let plantstate: [PlantState; PLANT_COUNT] = [ - PlantState::read_hardware_state(0, &mut board).await, - PlantState::read_hardware_state(1, &mut board).await, - PlantState::read_hardware_state(2, &mut board).await, - PlantState::read_hardware_state(3, &mut board).await, - PlantState::read_hardware_state(4, &mut board).await, - PlantState::read_hardware_state(5, &mut board).await, - PlantState::read_hardware_state(6, &mut board).await, - PlantState::read_hardware_state(7, &mut board).await, + PlantState::read_hardware_state(moisture,0, &mut board).await, + PlantState::read_hardware_state(moisture,1, &mut board).await, + PlantState::read_hardware_state(moisture,2, &mut board).await, + PlantState::read_hardware_state(moisture,3, &mut board).await, + PlantState::read_hardware_state(moisture,4, &mut board).await, + PlantState::read_hardware_state(moisture,5, &mut board).await, + PlantState::read_hardware_state(moisture,6, &mut board).await, + PlantState::read_hardware_state(moisture,7, &mut board).await, ]; publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index 26c7288..0384df5 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -1,9 +1,9 @@ +use crate::hal::Moistures; use crate::{ config::PlantConfig, - hal::{Sensor, HAL}, + hal::HAL, in_time_range, }; -use alloc::string::{String, ToString}; use chrono::{DateTime, TimeDelta, Utc}; use chrono_tz::Tz; use serde::{Deserialize, Serialize}; @@ -15,7 +15,6 @@ const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, thi pub enum MoistureSensorError { ShortCircuit { hz: f32, max: f32 }, OpenLoop { hz: f32, min: f32 }, - BoardError(String), } #[derive(Debug, PartialEq, Serialize)] @@ -116,15 +115,11 @@ fn map_range_moisture( } impl PlantState { - pub async fn read_hardware_state(plant_id: usize, board: &mut HAL<'_>) -> Self { + pub async fn read_hardware_state(moistures: Moistures, plant_id: usize, board: &mut HAL<'_>) -> Self { let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a { - match board - .board_hal - .measure_moisture_hz(plant_id, Sensor::A) - .await - { - Ok(raw) => match map_range_moisture( - raw, + let raw = moistures.sensor_a_hz[plant_id]; + match map_range_moisture( + raw, board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, ) { @@ -133,35 +128,23 @@ impl PlantState { moisture_percent, }, Err(err) => MoistureSensorState::SensorError(err), - }, - Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( - err.to_string(), - )), - } + } } else { MoistureSensorState::Disabled }; let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b { - match board - .board_hal - .measure_moisture_hz(plant_id, Sensor::B) - .await - { - Ok(raw) => match map_range_moisture( - raw, - board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, - board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, - ) { - Ok(moisture_percent) => MoistureSensorState::MoistureValue { - raw_hz: raw, - moisture_percent, - }, - Err(err) => MoistureSensorState::SensorError(err), + let raw = moistures.sensor_b_hz[plant_id]; + match map_range_moisture( + raw, + board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, + board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, + ) { + Ok(moisture_percent) => MoistureSensorState::MoistureValue { + raw_hz: raw, + moisture_percent, }, - Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( - err.to_string(), - )), + Err(err) => MoistureSensorState::SensorError(err), } } else { MoistureSensorState::Disabled diff --git a/rust/src/webserver/get_json.rs b/rust/src/webserver/get_json.rs index 1ef991d..673a569 100644 --- a/rust/src/webserver/get_json.rs +++ b/rust/src/webserver/get_json.rs @@ -37,9 +37,10 @@ where T: Read + Write, { let mut board = BOARD_ACCESS.get().await.lock().await; + let moistures = board.board_hal.measure_moisture_hz().await?; let mut plant_state = Vec::new(); for i in 0..PLANT_COUNT { - plant_state.push(PlantState::read_hardware_state(i, &mut board).await); + plant_state.push(PlantState::read_hardware_state(moistures, i, &mut board).await); } let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a { MoistureSensorState::Disabled => "disabled".to_string(), diff --git a/rust/src/webserver/post_json.rs b/rust/src/webserver/post_json.rs index 1686468..90ba87a 100644 --- a/rust/src/webserver/post_json.rs +++ b/rust/src/webserver/post_json.rs @@ -52,7 +52,8 @@ pub(crate) async fn board_test() -> FatResult> { pub(crate) async fn detect_sensors() -> FatResult> { let mut board = BOARD_ACCESS.get().await.lock().await; - let json = board.board_hal.detect_sensors().await?; + let result = board.board_hal.detect_sensors().await?; + let json = serde_json::to_string(&result)?; Ok(Some(json)) }