switch to bulk measurements
This commit is contained in:
@@ -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 = [
|
||||||
|
@@ -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
14
rust/canapi/Cargo.toml
Normal 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
138
rust/canapi/src/lib.rs
Normal 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 {}
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
|
@@ -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());
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
}
|
@@ -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,108 +381,24 @@ 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;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
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 general_fault(&mut self, enable: bool) {
|
async fn general_fault(&mut self, enable: bool) {
|
||||||
hold_disable(6);
|
hold_disable(6);
|
||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,13 +51,162 @@ 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 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]);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
SensorImpl::CanBus {
|
||||||
|
twai_config,
|
||||||
|
can_power,
|
||||||
|
} => {
|
||||||
|
can_power.set_high();
|
||||||
|
let config = twai_config.take().expect("twai config not set");
|
||||||
|
let mut twai = config.start();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let rec = twai.receive();
|
||||||
|
match rec {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
info!("Error receiving CAN message: {:?}", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer::after_millis(10).await;
|
||||||
|
let can = Self::inner_can(&mut twai).await;
|
||||||
|
|
||||||
|
can_power.set_low();
|
||||||
|
|
||||||
|
let config = twai.stop();
|
||||||
|
twai_config.replace(config);
|
||||||
|
|
||||||
|
let value = can?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl SensorImpl {
|
||||||
|
pub async fn autodetect(&mut self) -> FatResult<DetectionResult> {
|
||||||
|
match self {
|
||||||
|
SensorImpl::PulseCounter { .. } => {
|
||||||
|
bail!("Only CAN bus implementation supports autodetection")
|
||||||
|
}
|
||||||
|
SensorImpl::CanBus {
|
||||||
|
twai_config,
|
||||||
|
can_power,
|
||||||
|
} => {
|
||||||
|
// Power on CAN transceiver and start controller
|
||||||
|
can_power.set_high();
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Send a few test messages per potential sensor node
|
||||||
|
for plant in 0..crate::hal::PLANT_COUNT {
|
||||||
|
for sensor in [Sensor::A, Sensor::B] {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = DetectionResult::default();
|
||||||
|
loop {
|
||||||
|
match as_async.receive_async().with_deadline(Instant::from_millis(100)).await {
|
||||||
|
Ok(or) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("Received CAN message: {:?}", or);
|
||||||
|
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Timeout receiving CAN message: {:?}", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = as_async.stop().into_blocking();
|
||||||
|
can_power.set_low();
|
||||||
|
twai_config.replace(config);
|
||||||
|
|
||||||
|
info!("Autodetection result: {:?}", result);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
|
||||||
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
||||||
for repeat in 0..REPEAT_MOIST_MEASURE {
|
for repeat in 0..REPEAT_MOIST_MEASURE {
|
||||||
signal_counter.pause();
|
signal_counter.pause();
|
||||||
@@ -131,138 +282,11 @@ impl SensorInteraction for SensorImpl {
|
|||||||
Ok(median)
|
Ok(median)
|
||||||
}
|
}
|
||||||
|
|
||||||
SensorImpl::CanBus {
|
|
||||||
twai_config,
|
|
||||||
can_power,
|
|
||||||
} => {
|
|
||||||
can_power.set_high();
|
|
||||||
let config = twai_config.take().expect("twai config not set");
|
|
||||||
let mut twai = config.start();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let rec = twai.receive();
|
|
||||||
match rec {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
info!("Error receiving CAN message: {:?}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer::after_millis(10).await;
|
|
||||||
let can = Self::inner_can(plant, sensor, &mut twai).await;
|
|
||||||
|
|
||||||
can_power.set_low();
|
|
||||||
|
|
||||||
let config = twai.stop();
|
|
||||||
twai_config.replace(config);
|
|
||||||
|
|
||||||
let value = can?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SensorImpl {
|
|
||||||
pub async fn autodetect(&mut self) -> FatResult<[(bool, bool); crate::hal::PLANT_COUNT]> {
|
|
||||||
match self {
|
|
||||||
SensorImpl::PulseCounter { .. } => {
|
|
||||||
bail!("Only CAN bus implementation supports autodetection")
|
|
||||||
}
|
|
||||||
SensorImpl::CanBus {
|
|
||||||
twai_config,
|
|
||||||
can_power,
|
|
||||||
} => {
|
|
||||||
// Power on CAN transceiver and start controller
|
|
||||||
can_power.set_high();
|
|
||||||
let mut 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;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Error building CAN frame");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Error creating address for sensor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Poll for messages for ~100 ms
|
|
||||||
let detect_timeout = Instant::now()
|
|
||||||
.checked_add(embassy_time::Duration::from_millis(100))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match as_async.receive() {
|
|
||||||
Ok(or) => {
|
|
||||||
info!("Received CAN message: {:?}", or);
|
|
||||||
}
|
|
||||||
Err(nb::Error::WouldBlock) => {
|
|
||||||
if Instant::now() > detect_timeout {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Timer::after_millis(10).await;
|
|
||||||
}
|
|
||||||
Err(nb::Error::Other(err)) => {
|
|
||||||
info!("Error receiving CAN message: {:?}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = as_async.stop().into_blocking();
|
|
||||||
can_power.set_low();
|
|
||||||
twai_config.replace(config);
|
|
||||||
|
|
||||||
// Stub: return no detections yet
|
|
||||||
let mut result = [(false, false); crate::hal::PLANT_COUNT];
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn inner_can(
|
async fn inner_can(
|
||||||
plant: usize,
|
|
||||||
sensor: Sensor,
|
|
||||||
twai: &mut Twai<'static, Blocking>,
|
twai: &mut Twai<'static, Blocking>,
|
||||||
) -> FatResult<f32> {
|
) -> FatResult<Moistures> {
|
||||||
let can_sensor: Sensor = sensor.into();
|
[0_u8; 8];
|
||||||
//let request = RequestMoisture { sensor: can_sensor };
|
config::standard();
|
||||||
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)
|
|
||||||
.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 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) => {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,14 +115,10 @@ 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)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(raw) => match map_range_moisture(
|
|
||||||
raw,
|
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,22 +128,14 @@ 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)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(raw) => match map_range_moisture(
|
|
||||||
raw,
|
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,
|
||||||
@@ -158,10 +145,6 @@ 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
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user