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};
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,
}

View File

@@ -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);

View File

@@ -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)
}
}

View File

@@ -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,
}
}
}

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::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);

View File

@@ -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>>

View File

@@ -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,

View File

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

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 {
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();

View File

@@ -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 = "*"

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
```
## 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:
- 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 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):
``` sh

View File

@@ -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<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();
#[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);
}
}
}
}