From 7f3910bcd05242d183f70acba280b96c54c0bbc8 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Tue, 7 Oct 2025 21:50:33 +0200 Subject: [PATCH] sensor sweep tester --- rust/src/hal/can_api.rs | 11 +-- rust/src/hal/mod.rs | 8 +- rust/src/hal/v4_hal.rs | 22 +++++- rust/src/hal/v4_sensor.rs | 125 +++++++++++++++++++++++++++----- rust/src/webserver/mod.rs | 3 +- rust/src/webserver/post_json.rs | 6 ++ rust/src_webpack/src/api.ts | 9 +++ rust/src_webpack/src/main.html | 1 + rust/src_webpack/src/main.ts | 39 ++++++++++ rust_can_sensor/Cargo.toml | 1 + rust_can_sensor/README.md | 12 --- rust_can_sensor/src/main.rs | 59 ++++++++++++--- 12 files changed, 242 insertions(+), 54 deletions(-) diff --git a/rust/src/hal/can_api.rs b/rust/src/hal/can_api.rs index 29ab5dd..045652c 100644 --- a/rust/src/hal/can_api.rs +++ b/rust/src/hal/can_api.rs @@ -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, -} diff --git a/rust/src/hal/mod.rs b/rust/src/hal/mod.rs index caa7de4..f6c2d0c 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -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>>, > = 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; async fn get_mptt_current(&mut self) -> Result; + // Return JSON string with autodetected sensors per plant. Default: not supported. + async fn detect_sensors(&mut self) -> Result { + 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); diff --git a/rust/src/hal/v4_hal.rs b/rust/src/hal/v4_hal.rs index 7647426..2159b55 100644 --- a/rust/src/hal/v4_hal.rs +++ b/rust/src/hal/v4_hal.rs @@ -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 { self.charger.get_mppt_current() } + + async fn detect_sensors(&mut self) -> Result { + // 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) + } } diff --git a/rust/src/hal/v4_sensor.rs b/rust/src/hal/v4_sensor.rs index ef65c65..b265cb0 100644 --- a/rust/src/hal/v4_sensor.rs +++ b/rust/src/hal/v4_sensor.rs @@ -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 { - 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 for can_api::Sensor { - fn from(value: Sensor) -> Self { - match value { - Sensor::A => can_api::Sensor::A, - Sensor::B => can_api::Sensor::B, - } - } -} diff --git a/rust/src/webserver/mod.rs b/rust/src/webserver/mod.rs index 2d5c16f..e50c991 100644 --- a/rust/src/webserver/mod.rs +++ b/rust/src/webserver/mod.rs @@ -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); diff --git a/rust/src/webserver/post_json.rs b/rust/src/webserver/post_json.rs index 17659ea..1686468 100644 --- a/rust/src/webserver/post_json.rs +++ b/rust/src/webserver/post_json.rs @@ -50,6 +50,12 @@ pub(crate) async fn board_test() -> FatResult> { Ok(None) } +pub(crate) async fn detect_sensors() -> FatResult> { + 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( request: &mut Connection<'_, T, N>, ) -> FatResult> diff --git a/rust/src_webpack/src/api.ts b/rust/src_webpack/src/api.ts index 915cdfe..7d0e09e 100644 --- a/rust/src_webpack/src/api.ts +++ b/rust/src_webpack/src/api.ts @@ -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, diff --git a/rust/src_webpack/src/main.html b/rust/src_webpack/src/main.html index c02a887..e821bfd 100644 --- a/rust/src_webpack/src/main.html +++ b/rust/src_webpack/src/main.html @@ -163,6 +163,7 @@

Plants:

+
diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index 3d90cc2..e8a4f41 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -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(); diff --git a/rust_can_sensor/Cargo.toml b/rust_can_sensor/Cargo.toml index 3a58a8c..bc8594c 100644 --- a/rust_can_sensor/Cargo.toml +++ b/rust_can_sensor/Cargo.toml @@ -26,6 +26,7 @@ embassy-usb = { version = "0.3.0" } embassy-futures = { version = "0.1.0" } embassy-sync = { version = "0.6.0" } embedded-can = "0.4.1" +embedded-alloc = { version = "0.6.0", default-features = false, features = ["llff"] } # This is okay because we should automatically use whatever ch32-hal uses qingke-rt = "*" diff --git a/rust_can_sensor/README.md b/rust_can_sensor/README.md index c70108c..2ae1eeb 100644 --- a/rust_can_sensor/README.md +++ b/rust_can_sensor/README.md @@ -28,12 +28,6 @@ If you need to map a label to code, use the same letter+number as in the silkscr cargo build --release ``` -## USB CDC Console (optional) - -This project includes an optional software USB CDC-ACM device stack using embassy-usb. It runs on the CH32V203’s USB device peripheral but implements the protocol fully in software (no built-in USB class firmware is required). - -How to enable: -- Build with the `usb-cdc` feature: `cargo build --release --features usb-cdc` - Wire the MCU’s USB pins to a USB connector: - D+ (PA12) - D− (PA11) @@ -46,12 +40,6 @@ Example: - macOS: `screen /dev/tty.usbmodemXXXX 115200` - Windows: Use PuTTY on the shown COM port. -Notes: -- The firmware currently implements an echo console: bytes you type are echoed back. You can extend it to print logs or interact with your application. -- If you don’t see a device, ensure D+ (PA12) and D− (PA11) are connected and the cable supports data. - -## Flash - You can flash the built ELF using wchisp (WCH ISP tool): ``` sh diff --git a/rust_can_sensor/src/main.rs b/rust_can_sensor/src/main.rs index ecc4883..103c151 100644 --- a/rust_can_sensor/src/main.rs +++ b/rust_can_sensor/src/main.rs @@ -1,6 +1,6 @@ #![no_std] #![no_main] - +extern crate alloc; use crate::hal::peripherals::CAN1; use core::fmt::Write as _; @@ -8,7 +8,7 @@ use ch32_hal::gpio::{Level, Output, Speed}; use ch32_hal::adc::{Adc, SampleTime, ADC_MAX}; use ch32_hal::can; use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode}; -use ch32_hal::mode::{Blocking, Mode}; +use ch32_hal::mode::{Blocking}; use ch32_hal::peripherals::USBD; // use ch32_hal::delay::Delay; use embassy_executor::{Spawner, task}; @@ -18,10 +18,10 @@ use embassy_futures::yield_now; use hal::usbd::{Driver}; use hal::{bind_interrupts}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::channel::{Channel, TrySendError}; -use embassy_time::{Timer, Instant, Duration, Ticker}; +use embassy_sync::channel::{Channel}; +use embassy_time::{Instant, Duration}; +use embedded_can::blocking::Can as bcan; use embedded_can::StandardId; -use heapless::String; use {ch32_hal as hal, panic_halt as _}; macro_rules! mk_static { @@ -37,10 +37,22 @@ bind_interrupts!(struct Irqs { USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler; }); + +use embedded_alloc::LlffHeap as Heap; + +#[global_allocator] +static HEAP: Heap = Heap::empty(); + + static LOG_CH: Channel, 8> = Channel::new(); #[embassy_executor::main(entry = "qingke_rt::entry")] async fn main(spawner: Spawner) { + unsafe { + static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap, adjust as needed + HEAP.init(HEAP_SPACE.as_ptr() as usize, HEAP_SPACE.len()); + } + let p = hal::init(hal::Config { rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI, ..Default::default() @@ -105,13 +117,11 @@ async fn main(spawner: Spawner) { can.add_filter(CanFilter::accept_all()); - // Spawn independent tasks using 'static references - unsafe { - spawner.spawn(usb_task(usb)).unwrap(); - spawner.spawn(usb_writer(class)).unwrap(); - // move Q output, LED, ADC and analog input into worker task - spawner.spawn(worker(q_out, led, adc, ain, can)).unwrap(); - } + spawner.spawn(usb_task(usb)).unwrap(); + spawner.spawn(usb_writer(class)).unwrap(); + // move Q output, LED, ADC and analog input into worker task + spawner.spawn(worker(q_out, led, adc, ain, can)).unwrap(); + // Prevent main from exiting @@ -182,6 +192,31 @@ async fn worker( pulses, freq_hz ); log(msg); + + let address = StandardId::new(0x580 | 0x42).unwrap(); + let moisture = CanFrame::new(address, &[freq_hz as u8]).unwrap(); + match bcan::transmit(&mut can, &moisture) { + Ok(..) => { + let mut msg: heapless::String<128> = heapless::String::new(); + let _ = write!( + &mut msg, + "Send to canbus" + ); + log(msg); + } + Err(err) => { + + + + let mut msg: heapless::String<128> = heapless::String::new(); + let _ = write!( + &mut msg, + "err {}" + ,err + ); + log(msg); + } + } } }