improved sensor canbus code

This commit is contained in:
2025-12-06 04:01:16 +01:00
parent 6ffbf710d3
commit ca2fd8a5e1
5 changed files with 97 additions and 58 deletions

View File

@@ -66,6 +66,7 @@ dependencies = [
"embedded-can",
"heapless",
"log",
"nb 1.1.0",
"panic-halt",
"qingke",
"qingke-rt",

View File

@@ -28,6 +28,7 @@ embassy-usb = { version = "0.3.0" }
embassy-futures = { version = "0.1.0" }
embassy-sync = { version = "0.6.0" }
embedded-can = "0.4.1"
nb = "1.1"
embedded-alloc = { version = "0.6.0", default-features = false, features = ["llff"] }
# This is okay because we should automatically use whatever ch32-hal uses

View File

@@ -26,7 +26,6 @@ use {ch32_hal as hal, panic_halt as _};
use embedded_alloc::LlffHeap as Heap;
use embedded_can::nb::Can as nb_can;
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
@@ -61,9 +60,50 @@ async fn main(spawner: Spawner) {
// Build driver and USB stack using 'static buffers
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
let mut probe_gnd = Flex::new(p.PB1);
probe_gnd.set_as_input(Pull::None);
// Create GPIO for 555 Q output (PB0)
let q_out = Output::new(p.PB0, Level::Low, Speed::Low);
// Built-in LED on PB2 mirrors Q state
let mut info = Output::new(p.PB2, Level::Low, Speed::Low);
// Read configuration switches on PB3..PB7 at startup with floating detection
// PB3: Sensor A/B selector (Low=A, High=B)
// PB4..PB7: address bits (1,2,4,8)
let mut pb3 = Flex::new(p.PB3);
let mut pb4 = Flex::new(p.PB4);
let mut pb5 = Flex::new(p.PB5);
let mut pb6 = Flex::new(p.PB6);
let mut pb7 = Flex::new(p.PB7);
// Validate all config pins; if any is floating, stay in an error loop until fixed
// Try read PB3..PB7
let res_pb3 = detect_stable_pin(&mut pb3).await;
let res_pb4 = detect_stable_pin(&mut pb4).await;
let res_pb5 = detect_stable_pin(&mut pb5).await;
let res_pb6 = detect_stable_pin(&mut pb6).await;
let res_pb7 = detect_stable_pin(&mut pb7).await;
let slot = if res_pb3.unwrap_or(false) { SensorSlot::B } else { SensorSlot::A };
let mut addr: u8 = 0;
if res_pb4.unwrap_or(false) { addr |= 1; }
if res_pb5.unwrap_or(false) { addr |= 2; }
if res_pb6.unwrap_or(false) { addr |= 4; }
if res_pb7.unwrap_or(false) { addr |= 8; }
let moisture_id = plant_id(MOISTURE_DATA_OFFSET, slot, addr as u16);
let identify_id = plant_id(IDENTIFY_CMD_OFFSET, slot, addr as u16);
let invalid_config = res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none();
let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
config.manufacturer = Some("Embassy");
config.product = Some("USB-serial example");
config.manufacturer = Some("Can Sensor v0.2");
let msg = mk_static!(heapless::String<128>, heapless::String::new());;
let _ = core::fmt::Write::write_fmt(msg, format_args!("Sensor {:?} plant {}", slot, addr));
config.product = Some(msg.as_str());
config.serial_number = Some("12345678");
config.max_power = 100;
config.max_packet_size_0 = 64;
@@ -91,29 +131,9 @@ async fn main(spawner: Spawner) {
// Build USB device
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
// Create GPIO for 555 Q output (PB0)
let q_out = Output::new(p.PB0, Level::Low, Speed::Low);
// Built-in LED on PB2 mirrors Q state
let mut info = Output::new(p.PB2, Level::Low, Speed::Low);
// Read configuration switches on PB3..PB7 at startup with floating detection
// PB3: Sensor A/B selector (Low=A, High=B)
// PB4..PB7: address bits (1,2,4,8)
let mut pb3 = Flex::new(p.PB3);
let mut pb4 = Flex::new(p.PB4);
let mut pb5 = Flex::new(p.PB5);
let mut pb6 = Flex::new(p.PB6);
let mut pb7 = Flex::new(p.PB7);
// Validate all config pins; if any is floating, stay in an error loop until fixed
// Try read PB3..PB7
let res_pb3 = detect_stable_pin(&mut pb3).await;
let res_pb4 = detect_stable_pin(&mut pb4).await;
let res_pb5 = detect_stable_pin(&mut pb5).await;
let res_pb6 = detect_stable_pin(&mut pb6).await;
let res_pb7 = detect_stable_pin(&mut pb7).await;
if res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none() {
if invalid_config {
// At least one floating: report and blink code for the first one found.
let mut msg: heapless::String<128> = heapless::String::new();
let code = if res_pb4.is_none() { 1 } else if res_pb5.is_none() { 2 } else if res_pb6.is_none() { 3 } else if res_pb7.is_none() { 4 } else { 5 }; // PB3 -> 5
@@ -122,16 +142,10 @@ async fn main(spawner: Spawner) {
log(msg);
blink_error(&mut info, code).await;
};
let slot = if res_pb3.unwrap() { SensorSlot::B } else { SensorSlot::A };
let mut addr: u8 = 0;
if res_pb4.unwrap() { addr |= 1; }
if res_pb5.unwrap() { addr |= 2; }
if res_pb6.unwrap() { addr |= 4; }
if res_pb7.unwrap() { addr |= 8; }
// Log startup configuration and derived CAN IDs
let moisture_id = plant_id(MOISTURE_DATA_OFFSET, slot, addr as u16);
let identify_id = plant_id(IDENTIFY_CMD_OFFSET, slot, addr as u16);
{
let mut msg: heapless::String<128> = heapless::String::new();
let slot_chr = match slot { SensorSlot::A => 'a', SensorSlot::B => 'b' };
@@ -161,7 +175,7 @@ async fn main(spawner: Spawner) {
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, info, adc, ain, can, StandardId::new(moisture_id).unwrap(), StandardId::new(identify_id).unwrap())).unwrap();
spawner.spawn(worker(probe_gnd, q_out, info, adc, ain, can, StandardId::new(moisture_id).unwrap(), StandardId::new(identify_id).unwrap())).unwrap();
// Prevent main from exiting
core::future::pending::<()>().await;
@@ -195,6 +209,7 @@ async fn blink_error(mut info_led: &mut Output<'static>, code: u8) -> !{
#[task]
async fn worker(
mut probe_gnd: Flex<'static>,
mut q: Output<'static>,
mut info: Output<'static>,
mut adc: Adc<'static, hal::peripherals::ADC1>,
@@ -215,8 +230,8 @@ async fn worker(
.unwrap()
.set(identify_id.into(), Default::default());
can.add_filter(filter);
//can.add_filter(CanFilter::accept_all());
//can.add_filter(filter);
can.add_filter(CanFilter::accept_all());
loop {
// Count rising edges of Q in a 100 ms window
@@ -224,6 +239,8 @@ async fn worker(
let mut pulses: u32 = 0;
let mut last_q = q_high;
probe_gnd.set_as_output(Speed::Low);
probe_gnd.set_low();
while Instant::now()
.checked_duration_since(start)
.unwrap_or(Duration::from_millis(0))
@@ -258,6 +275,7 @@ async fn worker(
// Yield to allow USB and other tasks to run
yield_now().await;
}
probe_gnd.set_as_input(Pull::None);
// Compute frequency from 100 ms window
let freq_hz = pulses; // pulses per 0.1s => Hz
@@ -270,7 +288,7 @@ async fn worker(
);
log(msg);
let mut moisture = CanFrame::new(moisture_id, &[freq_hz as u8]).unwrap();
let mut moisture = CanFrame::new(moisture_id, &(freq_hz as u16).to_be_bytes()).unwrap();
match can.transmit(&mut moisture) {
Ok(..) => {
let mut msg: heapless::String<128> = heapless::String::new();
@@ -278,6 +296,12 @@ async fn worker(
log(msg);
}
Err(err) => {
for _ in 0..3 {
info.set_high();
Timer::after_millis(100).await;
info.set_low();
Timer::after_millis(100).await;
}
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(&mut msg, "err {:?}", err);
log(msg);
@@ -285,6 +309,15 @@ async fn worker(
}
loop {
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(
&mut msg,
"Check identity addr received: {:#x} \r\n",
identify_id.as_raw()
);
log(msg);
yield_now().await;
match can.receive() {
Ok(frame) => match frame.id() {
@@ -307,7 +340,8 @@ async fn worker(
}
Id::Extended(_) => {}
},
_ => {
Err(err) => {
break;
}
}

View File

@@ -10,12 +10,12 @@ use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use canapi::SensorSlot;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer, WithTimeout};
use embassy_time::{Duration, Instant, Timer, WithTimeout};
use embedded_can::{Frame, Id};
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::{Async, Blocking};
use log::{error, info, warn};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
@@ -129,10 +129,12 @@ impl SensorImpl {
// Power on CAN transceiver and start controller
can_power.set_high();
let config = twai_config.take().expect("twai config not set");
info!("convert can");
let mut as_async = config.into_async().start();
// Give CAN some time to stabilize
Timer::after_millis(10).await;
info!("Sending info messages now");
// Send a few test messages per potential sensor node
for plant in 0..crate::hal::PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
@@ -145,7 +147,7 @@ impl SensorImpl {
let can_buffer = [0_u8; 0];
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
// Try a few times; we intentionally ignore rx here and rely on stub logic
let resu = as_async.transmit_async(&frame).await;
let resu = as_async.transmit_async(&frame).with_timeout(Duration::from_millis(1000)).await;
match resu {
Ok(_) => {
info!("Sent test message to plant {plant} sensor {sensor:?}");
@@ -164,7 +166,7 @@ impl SensorImpl {
let mut moistures = Moistures::default();
let _ = Self::wait_for_can_measurements(&mut as_async, &mut moistures)
.with_timeout(Duration::from_millis(5000))
.with_timeout(Duration::from_millis(1000))
.await;
let config = as_async.stop().into_blocking();
@@ -200,15 +202,18 @@ impl SensorImpl {
let plant = msg.1 as usize;
let sensor = msg.2;
let data = can_frame.data();
match sensor {
SensorSlot::A => {
moistures.sensor_a_hz[plant] = data[0] as f32;
}
SensorSlot::B => {
moistures.sensor_b_hz[plant] = data[0] as f32;
if data.len() == 2 {
let frequency = u16::from_be_bytes([data[0], data[1]]);
match sensor {
SensorSlot::A => {
moistures.sensor_a_hz[plant] = frequency as f32;
}
SensorSlot::B => {
moistures.sensor_b_hz[plant] = frequency as f32;
}
}
}
}
}
}
@@ -218,6 +223,13 @@ impl SensorImpl {
}
},
Err(err) => {
match err {
EspTwaiError::BusOff => {
bail!("Bus offline")
}
EspTwaiError::NonCompliantDlc(_) => {}
EspTwaiError::EmbeddedHAL(_) => {}
}
error!("Error receiving CAN message: {err:?}");
}
}

View File

@@ -707,7 +707,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -732,7 +731,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -959,7 +957,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -2277,7 +2274,6 @@
"integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/html-minifier-terser": "^6.0.0",
"html-minifier-terser": "^6.0.2",
@@ -3384,7 +3380,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -4343,8 +4338,7 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/type-is": {
"version": "1.6.18",
@@ -4366,7 +4360,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4515,7 +4508,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -4565,7 +4557,6 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",