sensor sweep tester
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
use crate::hal::Sensor;
|
||||
use bincode::{Decode, Encode};
|
||||
|
||||
pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000;
|
||||
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
||||
pub(crate) struct RequestMoisture {
|
||||
pub(crate) sensor: Sensor,
|
||||
}
|
||||
pub(crate) struct AutoDetectRequest {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
||||
pub(crate) struct ResponseMoisture {
|
||||
@@ -12,9 +11,3 @@ pub(crate) struct ResponseMoisture {
|
||||
pub sensor: Sensor,
|
||||
pub hz: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Eq)]
|
||||
pub(crate) enum Sensor {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
@@ -56,6 +56,7 @@ use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bincode::{Decode, Encode};
|
||||
use bq34z100::Bq34z100g1Driver;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use core::cell::RefCell;
|
||||
@@ -117,7 +118,7 @@ pub static I2C_DRIVER: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
|
||||
pub enum Sensor {
|
||||
A,
|
||||
B,
|
||||
@@ -152,6 +153,11 @@ pub trait BoardInteraction<'a> {
|
||||
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
|
||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
|
||||
|
||||
// Return JSON string with autodetected sensors per plant. Default: not supported.
|
||||
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> {
|
||||
bail!("Autodetection is only available on v4 HAL with CAN bus");
|
||||
}
|
||||
|
||||
async fn progress(&mut self, counter: u32) {
|
||||
// Indicate progress is active to suppress default wait_infinity blinking
|
||||
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
|
@@ -201,8 +201,8 @@ pub(crate) async fn create_v4(
|
||||
log::info!("Can bus mode ");
|
||||
let twai_config = Some(twai::TwaiConfiguration::new(
|
||||
peripherals.twai,
|
||||
peripherals.gpio0,
|
||||
peripherals.gpio2,
|
||||
peripherals.gpio0,
|
||||
TWAI_BAUDRATE,
|
||||
TwaiMode::Normal,
|
||||
));
|
||||
@@ -458,4 +458,24 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
|
||||
self.charger.get_mppt_current()
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> {
|
||||
// Delegate to sensor autodetect and build JSON
|
||||
use alloc::string::ToString;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -9,15 +9,17 @@ use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use bincode::config;
|
||||
use bincode::error::DecodeError;
|
||||
use can_api::RequestMoisture;
|
||||
use core::mem;
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::{Instant, Timer};
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_time::{Instant, Timer, WithTimeout};
|
||||
use embedded_can::nb::Can;
|
||||
use embedded_can::Frame;
|
||||
use esp_hal::gpio::Output;
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
|
||||
use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
|
||||
use esp_hal::Blocking;
|
||||
use log::info;
|
||||
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
|
||||
@@ -140,9 +142,22 @@ impl SensorInteraction for SensorImpl {
|
||||
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);
|
||||
|
||||
@@ -154,21 +169,104 @@ impl SensorInteraction for SensorImpl {
|
||||
}
|
||||
|
||||
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 config = twai_config.take().expect("twai config not set");
|
||||
let mut twai = config.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 = twai.transmit(&frame);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let rec = twai
|
||||
.receive()
|
||||
.with_timeout(embassy_time::Duration::from_millis(100))
|
||||
.await;
|
||||
match rec {
|
||||
Ok(msg) => match msg {
|
||||
Ok(or) => {
|
||||
info!("Received CAN message: {:?}", or);
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Error receiving CAN message: {:?}", err);
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
info!("Error receiving CAN message: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for acknowledgements on the bus (stub: just wait 5 seconds)
|
||||
Timer::after_millis(5_000).await;
|
||||
// Stop CAN and power down
|
||||
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(
|
||||
plant: usize,
|
||||
sensor: Sensor,
|
||||
twai: &mut Twai<'static, Blocking>,
|
||||
) -> FatResult<f32> {
|
||||
let can_sensor: can_api::Sensor = sensor.into();
|
||||
let request = RequestMoisture { sensor: can_sensor };
|
||||
let mut can_buffer = [0_u8; 8];
|
||||
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 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..encoded])
|
||||
.context("Error building CAN frame")?;
|
||||
let request =
|
||||
EspTwaiFrame::new(address, &can_buffer[0..8]).context("Error building CAN frame")?;
|
||||
twai.transmit(&request)?;
|
||||
|
||||
let timeout = Instant::now()
|
||||
@@ -214,12 +312,3 @@ impl SensorImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sensor> for can_api::Sensor {
|
||||
fn from(value: Sensor) -> Self {
|
||||
match value {
|
||||
Sensor::A => can_api::Sensor::A,
|
||||
Sensor::B => can_api::Sensor::B,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ use crate::webserver::get_log::get_log;
|
||||
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
||||
use crate::webserver::ota::ota_operations;
|
||||
use crate::webserver::post_json::{
|
||||
board_test, night_lamp_test, pump_test, set_config, wifi_scan, write_time,
|
||||
board_test, night_lamp_test, pump_test, set_config, wifi_scan, write_time, detect_sensors,
|
||||
};
|
||||
use crate::{bail, BOARD_ACCESS};
|
||||
use alloc::borrow::ToOwned;
|
||||
@@ -151,6 +151,7 @@ impl Handler for HTTPRequestRouter {
|
||||
"/pumptest" => Some(pump_test(conn).await),
|
||||
"/lamptest" => Some(night_lamp_test(conn).await),
|
||||
"/boardtest" => Some(board_test().await),
|
||||
"/detect_sensors" => Some(detect_sensors().await),
|
||||
"/reboot" => {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.get_esp().set_restart_to_conf(true);
|
||||
|
@@ -50,6 +50,12 @@ pub(crate) async fn board_test() -> FatResult<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn detect_sensors() -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let json = board.board_hal.detect_sensors().await?;
|
||||
Ok(Some(json))
|
||||
}
|
||||
|
||||
pub(crate) async fn pump_test<T, const N: usize>(
|
||||
request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>>
|
||||
|
@@ -173,6 +173,15 @@ export interface BatteryState {
|
||||
state_of_health: string
|
||||
}
|
||||
|
||||
export interface DetectionPlant {
|
||||
a: boolean,
|
||||
b: boolean
|
||||
}
|
||||
|
||||
export interface DetectionResult {
|
||||
plants: DetectionPlant[]
|
||||
}
|
||||
|
||||
export interface TankInfo {
|
||||
/// is there enough water in the tank
|
||||
enough_water: boolean,
|
||||
|
@@ -163,6 +163,7 @@
|
||||
|
||||
<h3>Plants:</h3>
|
||||
<button id="measure_moisture">Measure Moisture</button>
|
||||
<button id="detect_sensors" style="display:none">Detect/Test Sensors</button>
|
||||
<div id="plants" class="plantlist"></div>
|
||||
|
||||
<div class="flexcontainer-rev">
|
||||
|
@@ -358,6 +358,36 @@ export class Controller {
|
||||
)
|
||||
}
|
||||
|
||||
async detectSensors() {
|
||||
let counter = 0
|
||||
let limit = 5
|
||||
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
|
||||
|
||||
let timerId: string | number | NodeJS.Timeout | undefined
|
||||
|
||||
function updateProgress() {
|
||||
counter++;
|
||||
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
}
|
||||
|
||||
timerId = setTimeout(updateProgress, 1000);
|
||||
|
||||
fetch(PUBLIC_URL + "/detect_sensors", { method: "POST" })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("detect_sensors");
|
||||
const pretty = JSON.stringify(json);
|
||||
toast.info("Detection result: " + pretty);
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timerId);
|
||||
controller.progressview.removeProgress("detect_sensors");
|
||||
toast.error("Autodetect failed: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
getConfig(): PlantControllerConfig {
|
||||
return {
|
||||
hardware: controller.hardwareView.getConfig(),
|
||||
@@ -405,6 +435,12 @@ export class Controller {
|
||||
}
|
||||
|
||||
setConfig(current: PlantControllerConfig) {
|
||||
// Show Detect/Test button only for V4 HAL
|
||||
if (current.hardware && (current.hardware as any).board === "V4") {
|
||||
this.detectBtn.style.display = "inline-block";
|
||||
} else {
|
||||
this.detectBtn.style.display = "none";
|
||||
}
|
||||
this.tankView.setConfig(current.tank);
|
||||
this.networkView.setConfig(current.network);
|
||||
this.nightLampView.setConfig(current.night_lamp);
|
||||
@@ -500,6 +536,7 @@ export class Controller {
|
||||
readonly solarView: SolarView;
|
||||
readonly fileview: FileView;
|
||||
readonly logView: LogView
|
||||
readonly detectBtn: HTMLButtonElement
|
||||
|
||||
constructor() {
|
||||
this.timeView = new TimeView(this)
|
||||
@@ -515,6 +552,8 @@ export class Controller {
|
||||
this.fileview = new FileView(this)
|
||||
this.logView = new LogView(this)
|
||||
this.hardwareView = new HardwareConfigView(this)
|
||||
this.detectBtn = document.getElementById("detect_sensors") as HTMLButtonElement
|
||||
this.detectBtn.onclick = () => { controller.detectSensors(); }
|
||||
this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement
|
||||
this.rebootBtn.onclick = () => {
|
||||
controller.reboot();
|
||||
|
Reference in New Issue
Block a user