sensor sweep tester

This commit is contained in:
2025-10-07 21:50:33 +02:00
parent 712e8c8b8f
commit 7f3910bcd0
12 changed files with 242 additions and 54 deletions

View File

@@ -1,10 +1,9 @@
use crate::hal::Sensor;
use bincode::{Decode, Encode}; use bincode::{Decode, Encode};
pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000; pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000;
#[derive(Debug, Clone, Copy, Encode, Decode)] #[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct RequestMoisture { pub(crate) struct AutoDetectRequest {}
pub(crate) sensor: Sensor,
}
#[derive(Debug, Clone, Copy, Encode, Decode)] #[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct ResponseMoisture { pub(crate) struct ResponseMoisture {
@@ -12,9 +11,3 @@ pub(crate) struct ResponseMoisture {
pub sensor: Sensor, pub sensor: Sensor,
pub hz: u32, pub hz: u32,
} }
#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq, Eq)]
pub(crate) enum Sensor {
A,
B,
}

View File

@@ -56,6 +56,7 @@ use alloc::boxed::Box;
use alloc::format; use alloc::format;
use alloc::sync::Arc; use alloc::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
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;
@@ -117,7 +118,7 @@ pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>, embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new(); > = OnceLock::new();
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
pub enum Sensor { pub enum Sensor {
A, A,
B, B,
@@ -152,6 +153,11 @@ pub trait BoardInteraction<'a> {
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>; async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
async fn get_mptt_current(&mut self) -> Result<Current, 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) { async fn progress(&mut self, counter: u32) {
// Indicate progress is active to suppress default wait_infinity blinking // Indicate progress is active to suppress default wait_infinity blinking
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed); crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);

View File

@@ -201,8 +201,8 @@ pub(crate) async fn create_v4(
log::info!("Can bus mode "); log::info!("Can bus mode ");
let twai_config = Some(twai::TwaiConfiguration::new( let twai_config = Some(twai::TwaiConfiguration::new(
peripherals.twai, peripherals.twai,
peripherals.gpio0,
peripherals.gpio2, peripherals.gpio2,
peripherals.gpio0,
TWAI_BAUDRATE, TWAI_BAUDRATE,
TwaiMode::Normal, TwaiMode::Normal,
)); ));
@@ -458,4 +458,24 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn get_mptt_current(&mut self) -> Result<Current, FatError> { async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
self.charger.get_mppt_current() 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)
}
} }

View File

@@ -9,15 +9,17 @@ 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 bincode::error::DecodeError;
use can_api::RequestMoisture; use core::mem;
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}; use embassy_sync::mutex::Mutex;
use embassy_time::{Instant, Timer, WithTimeout};
use embedded_can::nb::Can;
use embedded_can::Frame; use embedded_can::Frame;
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::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
use esp_hal::Blocking; use esp_hal::Blocking;
use log::info; use log::info;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
@@ -140,9 +142,22 @@ impl SensorInteraction for SensorImpl {
let config = twai_config.take().expect("twai config not set"); let config = twai_config.take().expect("twai config not set");
let mut twai = config.start(); 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; Timer::after_millis(10).await;
let can = Self::inner_can(plant, sensor, &mut twai).await; let can = Self::inner_can(plant, sensor, &mut twai).await;
can_power.set_low(); can_power.set_low();
let config = twai.stop(); let config = twai.stop();
twai_config.replace(config); twai_config.replace(config);
@@ -154,21 +169,104 @@ impl SensorInteraction for SensorImpl {
} }
impl 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( async fn inner_can(
plant: usize, plant: usize,
sensor: Sensor, sensor: Sensor,
twai: &mut Twai<'static, Blocking>, twai: &mut Twai<'static, Blocking>,
) -> FatResult<f32> { ) -> FatResult<f32> {
let can_sensor: can_api::Sensor = sensor.into(); let can_sensor: Sensor = sensor.into();
let request = RequestMoisture { sensor: can_sensor }; //let request = RequestMoisture { sensor: can_sensor };
let mut can_buffer = [0_u8; 8]; let can_buffer = [0_u8; 8];
let config = config::standard(); 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) let address = StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16)
.context(">> Could not create address for sensor! (plant: {}) <<")?; .context(">> Could not create address for sensor! (plant: {}) <<")?;
let request = EspTwaiFrame::new(address, &can_buffer[0..encoded]) let request =
.context("Error building CAN frame")?; EspTwaiFrame::new(address, &can_buffer[0..8]).context("Error building CAN frame")?;
twai.transmit(&request)?; twai.transmit(&request)?;
let timeout = Instant::now() 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,
}
}
}

View File

@@ -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::get_static::{serve_bundle, serve_favicon, serve_index};
use crate::webserver::ota::ota_operations; use crate::webserver::ota::ota_operations;
use crate::webserver::post_json::{ 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 crate::{bail, BOARD_ACCESS};
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
@@ -151,6 +151,7 @@ impl Handler for HTTPRequestRouter {
"/pumptest" => Some(pump_test(conn).await), "/pumptest" => Some(pump_test(conn).await),
"/lamptest" => Some(night_lamp_test(conn).await), "/lamptest" => Some(night_lamp_test(conn).await),
"/boardtest" => Some(board_test().await), "/boardtest" => Some(board_test().await),
"/detect_sensors" => Some(detect_sensors().await),
"/reboot" => { "/reboot" => {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().set_restart_to_conf(true); board.board_hal.get_esp().set_restart_to_conf(true);

View File

@@ -50,6 +50,12 @@ pub(crate) async fn board_test() -> FatResult<Option<String>> {
Ok(None) 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>( pub(crate) async fn pump_test<T, const N: usize>(
request: &mut Connection<'_, T, N>, request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> ) -> FatResult<Option<String>>

View File

@@ -173,6 +173,15 @@ export interface BatteryState {
state_of_health: string state_of_health: string
} }
export interface DetectionPlant {
a: boolean,
b: boolean
}
export interface DetectionResult {
plants: DetectionPlant[]
}
export interface TankInfo { export interface TankInfo {
/// is there enough water in the tank /// is there enough water in the tank
enough_water: boolean, enough_water: boolean,

View File

@@ -163,6 +163,7 @@
<h3>Plants:</h3> <h3>Plants:</h3>
<button id="measure_moisture">Measure Moisture</button> <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 id="plants" class="plantlist"></div>
<div class="flexcontainer-rev"> <div class="flexcontainer-rev">

View File

@@ -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 { getConfig(): PlantControllerConfig {
return { return {
hardware: controller.hardwareView.getConfig(), hardware: controller.hardwareView.getConfig(),
@@ -405,6 +435,12 @@ export class Controller {
} }
setConfig(current: PlantControllerConfig) { 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.tankView.setConfig(current.tank);
this.networkView.setConfig(current.network); this.networkView.setConfig(current.network);
this.nightLampView.setConfig(current.night_lamp); this.nightLampView.setConfig(current.night_lamp);
@@ -500,6 +536,7 @@ export class Controller {
readonly solarView: SolarView; readonly solarView: SolarView;
readonly fileview: FileView; readonly fileview: FileView;
readonly logView: LogView readonly logView: LogView
readonly detectBtn: HTMLButtonElement
constructor() { constructor() {
this.timeView = new TimeView(this) this.timeView = new TimeView(this)
@@ -515,6 +552,8 @@ export class Controller {
this.fileview = new FileView(this) this.fileview = new FileView(this)
this.logView = new LogView(this) this.logView = new LogView(this)
this.hardwareView = new HardwareConfigView(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 = document.getElementById("reboot") as HTMLButtonElement
this.rebootBtn.onclick = () => { this.rebootBtn.onclick = () => {
controller.reboot(); controller.reboot();

View File

@@ -26,6 +26,7 @@ embassy-usb = { version = "0.3.0" }
embassy-futures = { version = "0.1.0" } embassy-futures = { version = "0.1.0" }
embassy-sync = { version = "0.6.0" } embassy-sync = { version = "0.6.0" }
embedded-can = "0.4.1" 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 # This is okay because we should automatically use whatever ch32-hal uses
qingke-rt = "*" qingke-rt = "*"

View File

@@ -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 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 CH32V203s 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 MCUs USB pins to a USB connector: - Wire the MCUs USB pins to a USB connector:
- D+ (PA12) - D+ (PA12)
- D (PA11) - D (PA11)
@@ -46,12 +40,6 @@ Example:
- macOS: `screen /dev/tty.usbmodemXXXX 115200` - macOS: `screen /dev/tty.usbmodemXXXX 115200`
- Windows: Use PuTTY on the shown COM port. - 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 dont 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): You can flash the built ELF using wchisp (WCH ISP tool):
``` sh ``` sh

View File

@@ -1,6 +1,6 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
extern crate alloc;
use crate::hal::peripherals::CAN1; use crate::hal::peripherals::CAN1;
use core::fmt::Write as _; 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::adc::{Adc, SampleTime, ADC_MAX};
use ch32_hal::can; use ch32_hal::can;
use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode}; 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::peripherals::USBD;
// use ch32_hal::delay::Delay; // use ch32_hal::delay::Delay;
use embassy_executor::{Spawner, task}; use embassy_executor::{Spawner, task};
@@ -18,10 +18,10 @@ use embassy_futures::yield_now;
use hal::usbd::{Driver}; use hal::usbd::{Driver};
use hal::{bind_interrupts}; use hal::{bind_interrupts};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::{Channel, TrySendError}; use embassy_sync::channel::{Channel};
use embassy_time::{Timer, Instant, Duration, Ticker}; use embassy_time::{Instant, Duration};
use embedded_can::blocking::Can as bcan;
use embedded_can::StandardId; use embedded_can::StandardId;
use heapless::String;
use {ch32_hal as hal, panic_halt as _}; use {ch32_hal as hal, panic_halt as _};
macro_rules! mk_static { macro_rules! mk_static {
@@ -37,10 +37,22 @@ bind_interrupts!(struct Irqs {
USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>; USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>;
}); });
use embedded_alloc::LlffHeap as Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new(); static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
#[embassy_executor::main(entry = "qingke_rt::entry")] #[embassy_executor::main(entry = "qingke_rt::entry")]
async fn main(spawner: Spawner) { 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 { let p = hal::init(hal::Config {
rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI, rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI,
..Default::default() ..Default::default()
@@ -105,13 +117,11 @@ async fn main(spawner: Spawner) {
can.add_filter(CanFilter::accept_all()); can.add_filter(CanFilter::accept_all());
// Spawn independent tasks using 'static references spawner.spawn(usb_task(usb)).unwrap();
unsafe { spawner.spawn(usb_writer(class)).unwrap();
spawner.spawn(usb_task(usb)).unwrap(); // move Q output, LED, ADC and analog input into worker task
spawner.spawn(usb_writer(class)).unwrap(); spawner.spawn(worker(q_out, led, adc, ain, can)).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 // Prevent main from exiting
@@ -182,6 +192,31 @@ async fn worker(
pulses, freq_hz pulses, freq_hz
); );
log(msg); 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);
}
}
} }
} }