switch to bulk measurements

This commit is contained in:
2025-10-10 19:40:57 +02:00
parent 083573de4a
commit 45e948636b
15 changed files with 520 additions and 441 deletions

View File

@@ -37,6 +37,8 @@ partition_table = "partitions.csv"
[dependencies] [dependencies]
# Shared CAN API
canapi = { path = "canapi" }
#ESP stuff #ESP stuff
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] } esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
esp-hal = { version = "=1.0.0-rc.0", features = [ esp-hal = { version = "=1.0.0-rc.0", features = [

View File

@@ -1,5 +1,3 @@
use std::process::Command;
use vergen::EmitBuilder; use vergen::EmitBuilder;
fn linker_be_nice() { fn linker_be_nice() {
@@ -50,72 +48,6 @@ fn linker_be_nice() {
} }
fn main() { fn main() {
//webpack();
linker_be_nice(); linker_be_nice();
let _ = EmitBuilder::builder().all_git().all_build().emit(); 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();
}
}
}

14
rust/canapi/Cargo.toml Normal file
View File

@@ -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"] }

138
rust/canapi/src/lib.rs Normal file
View File

@@ -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<Self> {
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 {}

View File

@@ -1,6 +1,5 @@
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::hal::Box; use crate::hal::Box;
use alloc::string::String;
use async_trait::async_trait; use async_trait::async_trait;
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags}; use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
@@ -37,12 +36,6 @@ pub struct BatteryInfo {
pub temperature: u16, pub temperature: u16,
} }
#[derive(Debug, Serialize)]
pub enum BatteryError {
NoBatteryMonitor,
CommunicationError(String),
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub enum BatteryState { pub enum BatteryState {
Unknown, Unknown,

View File

@@ -595,8 +595,7 @@ impl Esp<'_> {
if duration_in_ms == 0 { if duration_in_ms == 0 {
software_reset(); software_reset();
} else { } else {
///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms)); let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000));
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] = let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
[(&mut self.wake_gpio1, WakeupLevel::Low)]; [(&mut self.wake_gpio1, WakeupLevel::Low)];
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);

View File

@@ -3,7 +3,7 @@ use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::Esp; use crate::hal::esp::Esp;
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction}; use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS}; use crate::hal::{BoardInteraction, FreePeripherals, Moistures, TIME_ACCESS};
use crate::{ use crate::{
bail, bail,
config::PlantControllerConfig, config::PlantControllerConfig,
@@ -117,14 +117,11 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
async fn measure_moisture_hz( async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
&mut self,
_plant: usize,
_sensor: Sensor,
) -> Result<f32, FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
async fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
self.general_fault.set_level(enable.into()); self.general_fault.set_level(enable.into());
} }

View File

@@ -1,5 +1,5 @@
pub(crate) mod battery; pub(crate) mod battery;
mod can_api; // mod can_api; // replaced by external canapi crate
pub mod esp; pub mod esp;
mod initial_hal; mod initial_hal;
mod little_fs2storage_adapter; mod little_fs2storage_adapter;
@@ -7,7 +7,7 @@ pub(crate) mod rtc;
mod v3_hal; mod v3_hal;
mod v3_shift_register; mod v3_shift_register;
mod v4_hal; mod v4_hal;
mod v4_sensor; pub(crate) mod v4_sensor;
mod water; mod water;
use crate::alloc::string::ToString; use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
@@ -60,6 +60,7 @@ use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver; use bq34z100::Bq34z100g1Driver;
use chrono::{DateTime, FixedOffset, Utc}; use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell; use core::cell::RefCell;
use canapi::SensorSlot;
use ds323x::ic::DS3231; use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface; use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x}; use ds323x::{DateTimeAccess, Ds323x};
@@ -105,6 +106,8 @@ use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage; use littlefs2::object_safe::DynStorage;
use log::{error, info, warn}; use log::{error, info, warn};
use portable_atomic::AtomicBool; use portable_atomic::AtomicBool;
use serde::Serialize;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new(); pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
@@ -124,6 +127,15 @@ pub enum Sensor {
B, B,
} }
impl Into<SensorSlot> for Sensor {
fn into(self) -> SensorSlot {
match self {
Sensor::A => SensorSlot::A,
Sensor::B => SensorSlot::B,
}
}
}
pub struct PlantHal {} pub struct PlantHal {}
pub struct HAL<'a> { pub struct HAL<'a> {
@@ -142,19 +154,19 @@ pub trait BoardInteraction<'a> {
fn is_day(&self) -> bool; fn is_day(&self) -> bool;
//should be multsampled //should be multsampled
async fn light(&mut self, enable: bool) -> Result<(), FatError>; async fn light(&mut self, enable: bool) -> FatResult<()>;
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>; async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()>;
async fn pump_current(&mut self, plant: usize) -> Result<Current, FatError>; async fn pump_current(&mut self, plant: usize) -> FatResult<Current>;
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>; async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()>;
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError>; async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError>;
async fn general_fault(&mut self, enable: bool); 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); fn set_config(&mut self, config: PlantControllerConfig);
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>; async fn get_mptt_voltage(&mut self) -> FatResult<Voltage>;
async fn get_mptt_current(&mut self) -> Result<Current, FatError>; async fn get_mptt_current(&mut self) -> FatResult<Current>;
// Return JSON string with autodetected sensors per plant. Default: not supported. // Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> { async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
bail!("Autodetection is only available on v4 HAL with CAN bus"); bail!("Autodetection is only available on v4 HAL with CAN bus");
} }
@@ -685,3 +697,19 @@ pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
.set_rtc_time(&time.to_utc()) .set_rtc_time(&time.to_utc())
.await .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,
}

View File

@@ -4,7 +4,7 @@ use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40; use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor; 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::log::{LogMessage, LOG_ACCESS};
use crate::{ use crate::{
config::PlantControllerConfig, 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<f32, FatError> {
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)] #[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for V3<'a> { impl<'a> BoardInteraction<'a> for V3<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
@@ -275,109 +381,25 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Ok(()) Ok(())
} }
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> { async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
let mut results = [0_f32; REPEAT_MOIST_MEASURE]; let mut result = Moistures::default();
for repeat in 0..REPEAT_MOIST_MEASURE { for plant in 0..PLANT_COUNT {
self.signal_counter.pause(); let a = self.inner_measure_moisture_hz(plant, Sensor::A).await;
self.signal_counter.clear(); let b = self.inner_measure_moisture_hz(plant, Sensor::B).await;
//Disable all let aa = a.unwrap_or_else(|_| u32::MAX as f32);
{ let bb = b.unwrap_or_else(|_| u32::MAX as f32);
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 LOG_ACCESS
.lock() .lock()
.await .await
.log( .log(LogMessage::TestSensor, aa as u32, bb as u32, &plant.to_string(), "")
LogMessage::RawMeasure,
unscaled as u32,
hz as u32,
&plant.to_string(),
&format!("{sensor:?}"),
)
.await; .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 Ok(result)
let mid = results.len() / 2;
let median = results[mid];
Ok(median)
} }
async fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
hold_disable(6); hold_disable(6);
if enable { if enable {
@@ -410,23 +432,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
self.pump(i, false).await?; self.pump(i, false).await?;
Timer::after_millis(100).await; Timer::after_millis(100).await;
} }
for plant in 0..PLANT_COUNT { self.measure_moisture_hz().await?;
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;
}
Timer::after_millis(10).await; Timer::after_millis(10).await;
Ok(()) Ok(())
} }

View File

@@ -6,7 +6,7 @@ use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction}; use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use crate::hal::water::TankSensor; 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 crate::log::{LogMessage, LOG_ACCESS};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::string::ToString; use alloc::string::ToString;
@@ -387,8 +387,8 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
Ok(()) Ok(())
} }
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> { async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
self.sensor.measure_moisture_hz(plant, sensor).await self.sensor.measure_moisture_hz().await
} }
async fn general_fault(&mut self, enable: bool) { async fn general_fault(&mut self, enable: bool) {
@@ -426,21 +426,14 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
self.pump(i, false).await?; self.pump(i, false).await?;
Timer::after_millis(100).await; Timer::after_millis(100).await;
} }
let moisture = self.measure_moisture_hz().await?;
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, Sensor::A).await; let a = moisture.sensor_a_hz[plant] as u32;
let b = self.measure_moisture_hz(plant, Sensor::B).await; let b = moisture.sensor_b_hz[plant] as u32;
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 LOG_ACCESS
.lock() .lock()
.await .await
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "") .log(LogMessage::TestSensor, a, b, &plant.to_string(), "")
.await; .await;
} }
Timer::after_millis(10).await; Timer::after_millis(10).await;
@@ -451,30 +444,15 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
self.config = config; self.config = config;
} }
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> { async fn get_mptt_voltage(&mut self) -> FatResult<Voltage> {
self.charger.get_mptt_voltage() self.charger.get_mptt_voltage()
} }
async fn get_mptt_current(&mut self) -> Result<Current, FatError> { async fn get_mptt_current(&mut self) -> FatResult<Current> {
self.charger.get_mppt_current() self.charger.get_mppt_current()
} }
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> { async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
// Delegate to sensor autodetect and build JSON self.sensor.autodetect().await
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)
} }
} }

View File

@@ -1,31 +1,33 @@
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use crate::bail; use crate::bail;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::can_api::ResponseMoisture; use canapi::{SensorSlot};
use crate::hal::Sensor; use crate::hal::{DetectionResult, Moistures, Sensor};
use crate::hal::{can_api, Box}; use crate::hal::Box;
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{LogMessage, LOG_ACCESS};
use alloc::format; use alloc::format;
use alloc::string::ToString; use alloc::string::ToString;
use async_trait::async_trait; use async_trait::async_trait;
use bincode::config; use bincode::config;
use bincode::error::DecodeError;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Instant, Timer, WithTimeout}; use embassy_time::{Instant, Timer, WithTimeout};
use embedded_can::Frame; use embedded_can::{Frame, Id};
use esp_hal::gpio::Output; use esp_hal::gpio::Output;
use esp_hal::i2c::master::I2c; use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::unit::Unit; use esp_hal::pcnt::unit::Unit;
use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration}; use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
use esp_hal::{Async, Blocking}; use esp_hal::{Blocking};
use log::info; use log::{error, info, warn};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const REPEAT_MOIST_MEASURE: usize = 10; const REPEAT_MOIST_MEASURE: usize = 10;
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait SensorInteraction { pub trait SensorInteraction {
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32>; async fn measure_moisture_hz(&mut self) -> FatResult<Moistures>;
} }
const MS0: u8 = 1_u8; const MS0: u8 = 1_u8;
@@ -49,86 +51,21 @@ pub enum SensorImpl {
#[async_trait(?Send)] #[async_trait(?Send)]
impl SensorInteraction for SensorImpl { impl SensorInteraction for SensorImpl {
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32> { async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
match self { match self {
SensorImpl::PulseCounter { SensorImpl::PulseCounter {
signal_counter, signal_counter,
sensor_expander, sensor_expander,
.. ..
} => { } => {
let mut results = [0_f32; REPEAT_MOIST_MEASURE]; let mut result = Moistures::default();
for repeat in 0..REPEAT_MOIST_MEASURE { for plant in 0..crate::hal::PLANT_COUNT{
signal_counter.pause(); result.sensor_a_hz[plant] = Self::inner_pulse(plant, Sensor::A, signal_counter, sensor_expander).await?;
signal_counter.clear(); 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?;
//Disable all info!("Sensor {} {:?}: {}", plant, Sensor::B, result.sensor_b_hz[plant]);
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 Ok(result)
let mid = results.len() / 2;
let median = results[mid];
Ok(median)
} }
SensorImpl::CanBus { SensorImpl::CanBus {
@@ -151,7 +88,7 @@ impl SensorInteraction for SensorImpl {
} }
Timer::after_millis(10).await; 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(); can_power.set_low();
@@ -165,8 +102,10 @@ impl SensorInteraction for SensorImpl {
} }
} }
impl SensorImpl { impl SensorImpl {
pub async fn autodetect(&mut self) -> FatResult<[(bool, bool); crate::hal::PLANT_COUNT]> { pub async fn autodetect(&mut self) -> FatResult<DetectionResult> {
match self { match self {
SensorImpl::PulseCounter { .. } => { SensorImpl::PulseCounter { .. } => {
bail!("Only CAN bus implementation supports autodetection") bail!("Only CAN bus implementation supports autodetection")
@@ -177,7 +116,7 @@ impl SensorImpl {
} => { } => {
// Power on CAN transceiver and start controller // Power on CAN transceiver and start controller
can_power.set_high(); 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(); let mut as_async = config.into_async().start();
// Give CAN some time to stabilize // Give CAN some time to stabilize
Timer::after_millis(10).await; Timer::after_millis(10).await;
@@ -185,52 +124,72 @@ impl SensorImpl {
// Send a few test messages per potential sensor node // Send a few test messages per potential sensor node
for plant in 0..crate::hal::PLANT_COUNT { for plant in 0..crate::hal::PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] { for sensor in [Sensor::A, Sensor::B] {
// Reuse CAN addressing scheme from moisture request 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; 8]; let can_buffer = [0_u8; 0];
let cfg = config::standard(); if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
if let Some(address) = // Try a few times; we intentionally ignore rx here and rely on stub logic
StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16) let resu = as_async.transmit_async(&frame).await;
{ match resu {
if let Some(frame) = EspTwaiFrame::new(address, &can_buffer) { Ok(_) => {
// Try a few times; we intentionally ignore rx here and rely on stub logic info!(
let resu = as_async.transmit_async(&frame).await; "Sent test message to plant {} sensor {:?}",
match resu { plant, sensor
Ok(_) => { );
info!( }
"Sent test message to plant {} sensor {:?}", Err(err) => {
plant, sensor info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err);
);
}
Err(err) => {
info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err);
}
} }
} else {
info!("Error building CAN frame");
} }
} else { } 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 { loop {
match as_async.receive() { match as_async.receive_async().with_deadline(Instant::from_millis(100)).await {
Ok(or) => { Ok(or) => {
info!("Received CAN message: {:?}", or); match or {
} Ok(can_frame) => {
Err(nb::Error::WouldBlock) => { match can_frame.id() {
if Instant::now() > detect_timeout { Id::Standard(id) => {
break; 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)) => { Err(err) => {
info!("Error receiving CAN message: {:?}", err); error!("Timeout receiving CAN message: {:?}", err);
break; break;
} }
} }
@@ -240,29 +199,94 @@ impl SensorImpl {
can_power.set_low(); can_power.set_low();
twai_config.replace(config); twai_config.replace(config);
// Stub: return no detections yet info!("Autodetection result: {:?}", result);
let mut result = [(false, false); crate::hal::PLANT_COUNT];
Ok(result) Ok(result)
} }
} }
} }
async fn inner_can( pub async fn inner_pulse(plant: usize, sensor: Sensor, signal_counter: &mut Unit<'_, 0>, sensor_expander: &mut Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>) -> FatResult<f32> {
plant: usize,
sensor: Sensor,
twai: &mut Twai<'static, Blocking>,
) -> FatResult<f32> {
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)?;
let address = StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16) let mut results = [0_f32; REPEAT_MOIST_MEASURE];
.context(">> Could not create address for sensor! (plant: {}) <<")?; for repeat in 0..REPEAT_MOIST_MEASURE {
let request = signal_counter.pause();
EspTwaiFrame::new(address, &can_buffer[0..8]).context("Error building CAN frame")?; signal_counter.clear();
twai.transmit(&request)?;
//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<Moistures> {
[0_u8; 8];
config::standard();
let timeout = Instant::now() let timeout = Instant::now()
.checked_add(embassy_time::Duration::from_millis(100)) .checked_add(embassy_time::Duration::from_millis(100))
@@ -271,26 +295,7 @@ impl SensorImpl {
let answer = twai.receive(); let answer = twai.receive();
match answer { match answer {
Ok(answer) => { Ok(answer) => {
let data = EspTwaiFrame::data(&answer); info!("Received CAN message: {:?}", 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);
} }
Err(error) => match error { Err(error) => match error {
nb::Error::Other(error) => { nb::Error::Other(error) => {

View File

@@ -399,15 +399,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
publish_tank_state(&mut board, &tank_state, water_temp).await; publish_tank_state(&mut board, &tank_state, water_temp).await;
let moisture = board.board_hal.measure_moisture_hz().await?;
let plantstate: [PlantState; PLANT_COUNT] = [ let plantstate: [PlantState; PLANT_COUNT] = [
PlantState::read_hardware_state(0, &mut board).await, PlantState::read_hardware_state(moisture,0, &mut board).await,
PlantState::read_hardware_state(1, &mut board).await, PlantState::read_hardware_state(moisture,1, &mut board).await,
PlantState::read_hardware_state(2, &mut board).await, PlantState::read_hardware_state(moisture,2, &mut board).await,
PlantState::read_hardware_state(3, &mut board).await, PlantState::read_hardware_state(moisture,3, &mut board).await,
PlantState::read_hardware_state(4, &mut board).await, PlantState::read_hardware_state(moisture,4, &mut board).await,
PlantState::read_hardware_state(5, &mut board).await, PlantState::read_hardware_state(moisture,5, &mut board).await,
PlantState::read_hardware_state(6, &mut board).await, PlantState::read_hardware_state(moisture,6, &mut board).await,
PlantState::read_hardware_state(7, &mut board).await, PlantState::read_hardware_state(moisture,7, &mut board).await,
]; ];
publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await;

View File

@@ -1,9 +1,9 @@
use crate::hal::Moistures;
use crate::{ use crate::{
config::PlantConfig, config::PlantConfig,
hal::{Sensor, HAL}, hal::HAL,
in_time_range, in_time_range,
}; };
use alloc::string::{String, ToString};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -15,7 +15,6 @@ const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, thi
pub enum MoistureSensorError { pub enum MoistureSensorError {
ShortCircuit { hz: f32, max: f32 }, ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 }, OpenLoop { hz: f32, min: f32 },
BoardError(String),
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
@@ -116,15 +115,11 @@ fn map_range_moisture(
} }
impl PlantState { 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 { let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
match board let raw = moistures.sensor_a_hz[plant_id];
.board_hal match map_range_moisture(
.measure_moisture_hz(plant_id, Sensor::A) raw,
.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_min_frequency,
board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency,
) { ) {
@@ -133,35 +128,23 @@ impl PlantState {
moisture_percent, moisture_percent,
}, },
Err(err) => MoistureSensorState::SensorError(err), Err(err) => MoistureSensorState::SensorError(err),
}, }
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
err.to_string(),
)),
}
} else { } else {
MoistureSensorState::Disabled MoistureSensorState::Disabled
}; };
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b { let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
match board let raw = moistures.sensor_b_hz[plant_id];
.board_hal match map_range_moisture(
.measure_moisture_hz(plant_id, Sensor::B) raw,
.await 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(raw) => match map_range_moisture( ) {
raw, Ok(moisture_percent) => MoistureSensorState::MoistureValue {
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency, raw_hz: raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_max_frequency, moisture_percent,
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
}, },
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( Err(err) => MoistureSensorState::SensorError(err),
err.to_string(),
)),
} }
} else { } else {
MoistureSensorState::Disabled MoistureSensorState::Disabled

View File

@@ -37,9 +37,10 @@ where
T: Read + Write, T: Read + Write,
{ {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
let moistures = board.board_hal.measure_moisture_hz().await?;
let mut plant_state = Vec::new(); let mut plant_state = Vec::new();
for i in 0..PLANT_COUNT { 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 { let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a {
MoistureSensorState::Disabled => "disabled".to_string(), MoistureSensorState::Disabled => "disabled".to_string(),

View File

@@ -52,7 +52,8 @@ pub(crate) async fn board_test() -> FatResult<Option<String>> {
pub(crate) async fn detect_sensors() -> FatResult<Option<String>> { pub(crate) async fn detect_sensors() -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await; 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)) Ok(Some(json))
} }