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();
|
||||
|
@@ -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 = "*"
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user