7 Commits

16 changed files with 19448 additions and 186286 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@
extern crate alloc;
use crate::hal::peripherals::CAN1;
use canapi::id::{plant_id, MessageKind, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
use canapi::id::{plant_id, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
use canapi::SensorSlot;
use ch32_hal::adc::{Adc, SampleTime, ADC_MAX};
use ch32_hal::can;
@@ -16,15 +16,15 @@ use embassy_executor::{task, Spawner};
use embassy_futures::yield_now;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_time::{Delay, Duration, Instant, Timer};
use embassy_time::{Duration, Instant, Timer};
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_usb::{Builder, UsbDevice};
use embedded_alloc::LlffHeap as Heap;
use embedded_can::nb::Can as nb_can;
use embedded_can::{Id, StandardId};
use hal::bind_interrupts;
use hal::usbd::Driver;
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) => {{
@@ -42,6 +42,8 @@ bind_interrupts!(struct Irqs {
#[global_allocator]
static HEAP: Heap = Heap::empty();
static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
static CAN_RX_CH: Channel<CriticalSectionRawMutex, CanFrame, 4> = Channel::new();
static CAN_TX_CH: Channel<CriticalSectionRawMutex, CanFrame, 4> = Channel::new();
#[embassy_executor::main(entry = "qingke_rt::entry")]
async fn main(spawner: Spawner) {
@@ -60,49 +62,78 @@ 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);
let mut probe_gnd = Flex::new(p.PA2);
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);
let q_out = Output::new(p.PA0, Level::Low, Speed::Low);
let info = Output::new(p.PA10, Level::Low, Speed::Low);
let warn = mk_static!(Output, Output::new(p.PA9, 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);
let mut sensor_ab_pin = Flex::new(p.PA3);
let mut sensor_address_bit_1_pin = Flex::new(p.PA4);
let mut sensor_address_bit_2_pin = Flex::new(p.PA5);
let mut sensor_address_bit_3_pin = Flex::new(p.PA6);
let mut sensor_address_bit_4_pin = Flex::new(p.PA7);
// 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 sensor_ab_config = detect_stable_pin(&mut sensor_ab_pin).await;
let sensor_address_bit_1_config = detect_stable_pin(&mut sensor_address_bit_1_pin).await;
let sensor_address_bit_2_config = detect_stable_pin(&mut sensor_address_bit_2_pin).await;
let sensor_address_bit_3_config = detect_stable_pin(&mut sensor_address_bit_3_pin).await;
let sensor_address_bit_4_config = detect_stable_pin(&mut sensor_address_bit_4_pin).await;
let slot = if res_pb3.unwrap_or(false) { SensorSlot::B } else { SensorSlot::A };
let slot = if sensor_ab_config.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; }
if sensor_address_bit_1_config.unwrap_or(false) {
addr |= 1;
}
if sensor_address_bit_2_config.unwrap_or(false) {
addr |= 2;
}
if sensor_address_bit_3_config.unwrap_or(false) {
addr |= 4;
}
if sensor_address_bit_4_config.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 standard_identify_id = StandardId::new(identify_id).unwrap();
let invalid_config = res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none();
//is any floating, or invalid addr (only 1-8 are valid)
let invalid_config = sensor_ab_config.is_none()
|| sensor_address_bit_1_config.is_none()
|| sensor_address_bit_2_config.is_none()
|| sensor_address_bit_3_config.is_none()
|| sensor_address_bit_4_config.is_none()
|| addr == 0
|| addr > 8;
let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
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));
let msg = mk_static!(heapless::String<128>, heapless::String::new());
if invalid_config {
let _ = core::fmt::Write::write_fmt(
msg,
format_args!(
"CFG err: {:?} {:?} {:?} {:?} {:?}",
to_info(sensor_ab_config), to_info(sensor_address_bit_1_config), to_info(sensor_address_bit_2_config), to_info(sensor_address_bit_3_config), to_info(sensor_address_bit_4_config)
),
);
} else {
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;
@@ -131,56 +162,117 @@ async fn main(spawner: Spawner) {
// Build USB device
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
spawner.spawn(usb_task(usb)).unwrap();
spawner.spawn(usb_writer(class)).unwrap();
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
let which = match code { 1 => "PB4", 2 => "PB5", 3 => "PB6", 4 => "PB7", _ => "PB3 (A/B)" };
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!("Config pin floating detected on {} -> blinking code {}. Fix jumpers.\r\n", which, code));
let code = if sensor_address_bit_1_config.is_none() {
1
} else if sensor_address_bit_2_config.is_none() {
2
} else if sensor_address_bit_3_config.is_none() {
3
} else if sensor_address_bit_4_config.is_none() {
4
} else if sensor_ab_config.is_none() {
5
} else {
6 // Invalid address (0 or > 8)
};
let which = match code {
1 => "PB4 (bit 1)",
2 => "PB5 (bit 2)",
3 => "PB6 (bit 3)",
4 => "PB7 (bit 4)",
5 => "PB3 (A/B)",
_ => "Address (0 or > 8)",
};
if code == 6 {
let _ = core::fmt::Write::write_fmt(
&mut msg,
format_args!(
"Invalid address {} (only 1-8 allowed) -> blinking code {}. Fix jumpers.\r\n",
addr, code
),
);
} else {
let _ = core::fmt::Write::write_fmt(
&mut msg,
format_args!(
"Config pin floating detected on {} -> blinking code {}. Fix jumpers.\r\n",
which, code
),
);
}
log(msg);
blink_error(&mut info, code).await;
};
spawner.spawn(blink_error(warn, code)).unwrap();
} else {
// Log startup configuration and derived CAN IDs
{
let mut msg: heapless::String<128> = heapless::String::new();
let slot_chr = match slot {
SensorSlot::A => 'a',
SensorSlot::B => 'b',
};
let _ = core::fmt::Write::write_fmt(
&mut msg,
format_args!(
"Startup: slot={} addr={} moisture_id=0x{:03X} identity_id=0x{:03X}\r\n",
slot_chr, addr, moisture_id, identify_id
),
);
log(msg);
}
// Log startup configuration and derived CAN IDs
// Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
let adc = Adc::new(p.ADC1, Default::default());
let ain = p.PA1;
let config = can::can::Config::default();
let can: Can<CAN1, NonBlocking> = Can::new_nb(
p.CAN1,
p.PB8,
p.PB9,
CanFifo::Fifo0,
CanMode::Normal,
125_000,
config,
)
.expect("Valid");
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
{
let mut msg: heapless::String<128> = heapless::String::new();
let slot_chr = match slot { SensorSlot::A => 'a', SensorSlot::B => 'b' };
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!(
"Startup: slot={} addr={} moisture_id=0x{:03X} identity_id=0x{:03X}\r\n",
slot_chr, addr, moisture_id, identify_id
));
log(msg);
can.add_filter(CanFilter::accept_all());
// let mut filter = CanFilter::new_id_list();
// filter.get(0).unwrap().set(Id::Standard(standard_identify_id), Default::default());
// can.add_filter(filter);
spawner.spawn(can_task(can, warn, standard_identify_id)).unwrap();
// move Q output, LED, ADC and analog input into worker task
spawner
.spawn(worker(
probe_gnd,
q_out,
info,
adc,
ain,
StandardId::new(moisture_id).unwrap(),
standard_identify_id,
))
.unwrap();
}
// Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
let adc = Adc::new(p.ADC1, Default::default());
let ain = p.PA1;
let config = can::can::Config::default();
let can: Can<CAN1, NonBlocking> = Can::new_nb(
p.CAN1,
p.PB8,
p.PB9,
CanFifo::Fifo0,
CanMode::Normal,
125_000,
config,
)
.expect("Valid");
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
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(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;
}
fn to_info(res: Option<bool>) -> i8 {
match res {
Some(true) => 1,
Some(false) => -1,
None => 0,
}
}
// Helper closure: detect stable pin by comparing readings under Pull::Down and Pull::Up
async fn detect_stable_pin(pin: &mut Flex<'static>) -> Option<bool> {
@@ -190,9 +282,14 @@ async fn detect_stable_pin(pin: &mut Flex<'static>) -> Option<bool> {
pin.set_as_input(Pull::Up);
Timer::after_millis(2).await;
let high_read = pin.is_high();
if low_read == high_read { Some(high_read) } else { None }
if low_read == high_read {
Some(high_read)
} else {
None
}
}
async fn blink_error(mut info_led: &mut Output<'static>, code: u8) -> !{
#[task]
async fn blink_error(info_led: &'static mut Output<'static>, code: u8) -> ! {
loop {
// code: 1-4 for PB4..PB7, 5 for PB3 (A/B)
for _ in 0..code {
@@ -206,6 +303,79 @@ async fn blink_error(mut info_led: &mut Output<'static>, code: u8) -> !{
}
}
#[task]
async fn can_task(
mut can: Can<'static, CAN1, NonBlocking>,
warn: &'static mut Output<'static>,
identify_id: StandardId,
) {
loop {
match can.receive() {
Ok(frame) => {
match frame.id() {
Id::Standard(s_frame) => {
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(
&mut msg,
"Received from canbus: {:?} ident is {:?} \r\n",
s_frame.as_raw(),
identify_id.as_raw()
);
log(msg);
if s_frame.as_raw() == identify_id.as_raw() {
CAN_RX_CH.send(frame).await;
}
}
Id::Extended(_) => {}
}
}
Err(nb::Error::WouldBlock) => {
// No frame available
}
Err(nb::Error::Other(err)) => {
for _ in 0..3 {
warn.set_high();
Timer::after_millis(100).await;
warn.set_low();
Timer::after_millis(100).await;
}
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(&mut msg, "rx err {:?}", err);
log(msg);
}
}
while let Ok(mut frame) = CAN_TX_CH.try_receive() {
match can.transmit(&mut frame) {
Ok(..) => {
}
Err(nb::Error::WouldBlock) => {
for _ in 0..2 {
warn.set_high();
Timer::after_millis(100).await;
warn.set_low();
Timer::after_millis(100).await;
}
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(&mut msg, "canbus out buffer full");
log(msg);
}
Err(nb::Error::Other(err)) => {
for _ in 0..3 {
warn.set_high();
Timer::after_millis(100).await;
warn.set_low();
Timer::after_millis(100).await;
}
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(&mut msg, "tx err {:?}", err);
log(msg);
}
}
}
yield_now().await;
}
}
#[task]
async fn worker(
@@ -214,25 +384,14 @@ async fn worker(
mut info: Output<'static>,
mut adc: Adc<'static, hal::peripherals::ADC1>,
mut ain: hal::peripherals::PA1,
mut can: Can<'static, CAN1, NonBlocking>,
moisture_id: StandardId,
identify_id: StandardId
identify_id: StandardId,
) {
// 555 emulation state: Q initially Low
let mut q_high = false;
let low_th: u16 = (ADC_MAX as u16) / 3; // ~1/3 Vref
let high_th: u16 = ((ADC_MAX as u32 * 2) / 3) as u16; // ~2/3 Vref
let mut filter = CanFilter::new_id_list();
filter
.get(0)
.unwrap()
.set(identify_id.into(), Default::default());
//can.add_filter(filter);
can.add_filter(CanFilter::accept_all());
loop {
// Count rising edges of Q in a 100 ms window
let start = Instant::now();
@@ -241,7 +400,7 @@ async fn worker(
probe_gnd.set_as_output(Speed::Low);
probe_gnd.set_low();
let probe_duration = Duration::from_millis(1000);
let probe_duration = Duration::from_millis(100);
while Instant::now()
.checked_duration_since(start)
.unwrap_or(Duration::from_millis(0))
@@ -278,74 +437,28 @@ async fn worker(
}
probe_gnd.set_as_input(Pull::None);
// Compute frequency from 100 ms window
let freq_hz = pulses; // pulses per 0.1s => Hz
let freq_hz: u32 = pulses * (1000 / probe_duration.as_millis()) as u32; // pulses per 0.1s => Hz
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(
&mut msg,
"555 window={}ms pulses={} freq={} Hz (A1->Q on PB0)\r\n", probe_duration.as_millis(),
pulses, freq_hz
"555 window={}ms pulses={} freq={} Hz (A1->Q on PB0) id={:?}\r\n",
probe_duration.as_millis(),
pulses,
freq_hz,
identify_id.as_raw()
);
log(msg);
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();
let _ = write!(&mut msg, "Send to canbus");
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);
}
}
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() {
Id::Standard(s_frame) => {
let mut msg: heapless::String<128> = heapless::String::new();
let _ = write!(
&mut msg,
"Received from canbus: {:?} ident is {:?} \r\n",
s_frame.as_raw(),
identify_id.as_raw()
);
log(msg);
if s_frame.as_raw() == identify_id.as_raw() {
for _ in 0..10 {
Timer::after_millis(250).await;
info.toggle();
}
info.set_low();
}
}
Id::Extended(_) => {}
},
Err(err) => {
break;
}
let moisture = CanFrame::new(moisture_id, &(freq_hz as u32).to_be_bytes()).unwrap();
CAN_TX_CH.send(moisture).await;
while let Ok(_frame) = CAN_RX_CH.try_receive() {
for _ in 0..10 {
Timer::after_millis(250).await;
info.toggle();
}
info.set_low();
}
}
}

View File

@@ -162,7 +162,7 @@ impl Esp<'_> {
loop {
match self.uart0.read_buffered(&mut buf) {
Ok(read) => {
if (read == 0) {
if read == 0 {
return Ok(None);
}
let c = buf[0] as char;

View File

@@ -1,143 +0,0 @@
use crate::alloc::boxed::Box;
use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::Esp;
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, TIME_ACCESS};
use crate::{
bail,
config::PlantControllerConfig,
hal::battery::{BatteryInteraction, NoBatteryMonitor},
};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use esp_hal::gpio::{Level, Output, OutputConfig};
use measurements::{Current, Voltage};
pub struct Initial<'a> {
pub(crate) general_fault: Output<'a>,
pub(crate) esp: Esp<'a>,
pub(crate) config: PlantControllerConfig,
pub(crate) battery: Box<dyn BatteryInteraction + Send>,
pub rtc: Box<dyn RTCModuleInteraction + Send>,
}
pub(crate) struct NoRTC {}
#[async_trait(?Send)]
impl RTCModuleInteraction for NoRTC {
async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
bail!("Please configure board revision")
}
async fn get_backup_config(&mut self, _chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
bail!("Please configure board revision")
}
async fn backup_config(&mut self, _offset: usize, _bytes: &[u8]) -> FatResult<()> {
bail!("Please configure board revision")
}
async fn backup_config_finalize(&mut self, _crc: u16, _length: usize) -> FatResult<()> {
bail!("Please configure board revision")
}
async fn get_rtc_time(&mut self) -> Result<DateTime<Utc>, FatError> {
bail!("Please configure board revision")
}
async fn set_rtc_time(&mut self, _time: &DateTime<Utc>) -> Result<(), FatError> {
bail!("Please configure board revision")
}
}
pub(crate) fn create_initial_board(
free_pins: FreePeripherals<'static>,
config: PlantControllerConfig,
esp: Esp<'static>,
) -> Result<Box<dyn BoardInteraction<'static> + Send>, FatError> {
log::info!("Start initial");
let general_fault = Output::new(free_pins.gpio23, Level::Low, OutputConfig::default());
let v = Initial {
general_fault,
config,
esp,
battery: Box::new(NoBatteryMonitor {}),
rtc: Box::new(NoRTC {}),
};
Ok(Box::new(v))
}
#[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for Initial<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
bail!("Please configure board revision")
}
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
fn get_config(&mut self) -> &PlantControllerConfig {
&self.config
}
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
&mut self.battery
}
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc
}
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
let rtc = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, rtc);
}
fn is_day(&self) -> bool {
false
}
async fn light(&mut self, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
async fn pump(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Please configure board revision")
}
async fn fault(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
bail!("Please configure board revision")
}
async fn general_fault(&mut self, enable: bool) {
self.general_fault.set_level(enable.into());
}
async fn test(&mut self) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
}
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
bail!("Please configure board revision")
}
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
bail!("Please configure board revision")
}
}

View File

@@ -3,7 +3,6 @@ use esp_hal::uart::{Config as UartConfig};
pub(crate) mod battery;
// mod can_api; // replaced by external canapi crate
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
pub(crate) mod rtc;
mod shared_flash;
@@ -40,7 +39,7 @@ use esp_hal::peripherals::TWAI0;
use crate::{
bail,
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
config::{BatteryBoardVersion, PlantControllerConfig},
hal::{
battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp,
@@ -50,7 +49,6 @@ use crate::{
};
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use async_trait::async_trait;
use bincode::{Decode, Encode};
@@ -162,6 +160,7 @@ pub trait BoardInteraction<'a> {
fn set_config(&mut self, config: PlantControllerConfig);
async fn get_mptt_voltage(&mut self) -> FatResult<Voltage>;
async fn get_mptt_current(&mut self) -> FatResult<Current>;
async fn can_power(&mut self, state: bool) -> FatResult<()>;
// Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
@@ -681,8 +680,8 @@ pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
pub struct Moistures {
pub sensor_a_hz: [f32; PLANT_COUNT],
pub sensor_b_hz: [f32; PLANT_COUNT],
pub sensor_a_hz: [Option<f32>; PLANT_COUNT],
pub sensor_b_hz: [Option<f32>; PLANT_COUNT],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]

View File

@@ -11,8 +11,7 @@ use crate::hal::{
};
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::string::{ToString};
use async_trait::async_trait;
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use canapi::SensorSlot;
@@ -22,9 +21,9 @@ use embassy_time::{Duration, Timer, WithTimeout};
use embedded_can::{Frame, Id};
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c;
use esp_hal::peripherals;
use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration, TwaiMode};
use esp_hal::{twai, Async, Blocking};
use esp_println::println;
use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode, Resolution};
@@ -130,15 +129,14 @@ pub struct V4<'a> {
pump_ina: Option<
SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
>,
twai_peripheral: Option<esp_hal::peripherals::TWAI0<'static>>,
twai_rx_pin: Option<esp_hal::peripherals::GPIO2<'static>>,
twai_tx_pin: Option<esp_hal::peripherals::GPIO0<'static>>,
can_power: Output<'static>,
extra1: Output<'a>,
extra2: Output<'a>,
twai_config: Option<TwaiConfiguration<'static, Blocking>>
}
pub(crate) async fn create_v4(
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
@@ -153,11 +151,13 @@ pub(crate) async fn create_v4(
let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
general_fault.set_low();
let twai_peripheral = Some(peripherals.twai);
let twai_rx_pin = Some(peripherals.gpio2);
let twai_tx_pin = Some(peripherals.gpio0);
let twai_config = Some(TwaiConfiguration::new(
peripherals.twai,
peripherals.gpio0,
peripherals.gpio2,
TWAI_BAUDRATE,
TwaiMode::Normal,
));
let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
@@ -257,36 +257,15 @@ pub(crate) async fn create_v4(
config,
battery_monitor,
pump_ina,
twai_peripheral,
twai_rx_pin,
twai_tx_pin,
charger,
extra1,
extra2,
can_power,
twai_config
};
Ok(Box::new(v))
}
impl<'a> V4<'a> {
fn teardown_twai(&mut self, old: TwaiConfiguration<Blocking>) {
drop(old);
// Re-acquire the peripheral and pins
let twai = unsafe { peripherals::TWAI0::steal() };
let rx_pin = unsafe { peripherals::GPIO2::steal() };
let tx_pin = unsafe { peripherals::GPIO0::steal() };
// Set pins to low to avoid parasitic powering
let mut rx = Input::new(rx_pin, InputConfig::default().with_pull(Pull::None));
let mut tx = Input::new(tx_pin, InputConfig::default().with_pull(Pull::None));
// Release the pins from Output back to raw pins and store everything
self.twai_peripheral = Some(twai);
self.twai_rx_pin = Some(unsafe { peripherals::GPIO2::steal() });
self.twai_tx_pin = Some(unsafe { peripherals::GPIO0::steal() });
self.can_power.set_low();
}
}
#[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for V4<'a> {
@@ -379,37 +358,78 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
}
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
self.can_power.set_high();
let twai_config = TwaiConfiguration::new(
self.twai_peripheral.take().unwrap(),
self.twai_rx_pin.take().unwrap(),
self.twai_tx_pin.take().unwrap(),
TWAI_BAUDRATE,
TwaiMode::Normal,
);
let mut twai = twai_config.into_async().start();
loop {
let rec = twai.receive();
match rec {
Ok(_) => {}
Err(err) => {
info!("Error receiving CAN message: {err:?}");
break;
}
}
}
let config = self.twai_config.take().expect("twai config not set");
let mut twai = config.into_async().start();
Timer::after_millis(10).await;
let mut moistures = Moistures::default();
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
.with_timeout(Duration::from_millis(2000))
.with_timeout(Duration::from_millis(5000))
.await;
self.teardown_twai(twai.stop().into_blocking());
let config = twai.stop().into_blocking();
self.twai_config.replace(config);
self.can_power.set_low();
Ok(moistures)
}
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
self.can_power.set_high();
let config = self.twai_config.take().expect("twai config not set");
let mut twai = config.into_async().start();
Timer::after_millis(1000).await;
info!("Sending info messages now");
// Send a few test messages per potential sensor node
for plant in 0..PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
let target =
StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, sensor.into(), (plant +1) as u16))
.context(">> Could not create address for sensor! (plant: {}) <<")?;
let can_buffer = [0_u8; 0];
info!("Sending test message to plant {} sensor {sensor:?} with id {}", plant +1, target.as_raw());
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 = twai
.transmit_async(&frame)
.with_timeout(Duration::from_millis(3000))
.await;
match resu {
Ok(_) => {
}
Err(err) => {
info!(
"Error sending test message to plant {} sensor {sensor:?}: {err:?}", plant +1
);
}
}
} else {
info!("Error building CAN frame");
}
}
}
let mut moistures = Moistures::default();
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
.with_timeout(Duration::from_millis(3000))
.await;
let config = twai.stop().into_blocking();
self.twai_config.replace(config);
self.can_power.set_low();
let result = moistures.into();
info!("Autodetection result: {result:?}");
Ok(result)
}
async fn general_fault(&mut self, enable: bool) {
hold_disable(23);
self.general_fault.set_level(enable.into());
@@ -447,12 +467,12 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
}
let moisture = self.measure_moisture_hz().await?;
for plant in 0..PLANT_COUNT {
let a = moisture.sensor_a_hz[plant] as u32;
let b = moisture.sensor_b_hz[plant] as u32;
let a = moisture.sensor_a_hz[plant].unwrap_or(0.0) as u32;
let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32;
LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, a, b, &plant.to_string(), "")
.log(LogMessage::TestSensor, a, b, &(plant+1).to_string(), "")
.await;
}
Timer::after_millis(10).await;
@@ -471,68 +491,17 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
self.charger.get_mppt_current()
}
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
// Power on CAN transceiver and start controller
self.can_power.set_high();
let twai_config = TwaiConfiguration::new(
self.twai_peripheral.take().unwrap(),
self.twai_rx_pin.take().unwrap(),
self.twai_tx_pin.take().unwrap(),
TWAI_BAUDRATE,
TwaiMode::Normal,
);
info!("convert can");
let mut as_async = twai_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..PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
let target =
StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, sensor.into(), plant as u16))
.context(">> Could not create address for sensor! (plant: {}) <<")?;
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)
.with_timeout(Duration::from_millis(1000))
.await;
match resu {
Ok(_) => {
info!("Sent test message to plant {plant} sensor {sensor:?}");
}
Err(err) => {
info!(
"Error sending test message to plant {plant} sensor {sensor:?}: {err:?}"
);
}
}
} else {
info!("Error building CAN frame");
}
}
async fn can_power(&mut self, state: bool) -> FatResult<()> {
if state && self.can_power.is_set_low() {
self.can_power.set_high();
} else {
self.can_power.set_low();
}
let mut moistures = Moistures::default();
let _ = wait_for_can_measurements(&mut as_async, &mut moistures)
.with_timeout(Duration::from_millis(1000))
.await;
let config = as_async.stop().into_blocking();
self.teardown_twai(config);
let result = moistures.into();
info!("Autodetection result: {result:?}");
Ok(result)
Ok(())
}
}
async fn wait_for_can_measurements(
as_async: &mut Twai<'_, Async>,
moistures: &mut Moistures,
@@ -554,16 +523,19 @@ async fn wait_for_can_measurements(
let plant = msg.1 as usize;
let sensor = msg.2;
let data = can_frame.data();
if data.len() == 2 {
let frequency = u16::from_be_bytes([data[0], data[1]]);
info!("Received moisture data: {:?}", data);
if let Ok(bytes) = data.try_into() {
let frequency = u32::from_be_bytes(bytes);
match sensor {
SensorSlot::A => {
moistures.sensor_a_hz[plant] = frequency as f32;
moistures.sensor_a_hz[plant-1] = Some(frequency as f32);
}
SensorSlot::B => {
moistures.sensor_b_hz[plant] = frequency as f32;
moistures.sensor_b_hz[plant-1] = Some(frequency as f32);
}
}
} else {
error!("Received moisture data with invalid length: {} (expected 4)", data.len());
}
}
}
@@ -591,10 +563,10 @@ impl From<Moistures> for DetectionResult {
fn from(value: Moistures) -> Self {
let mut result = DetectionResult::default();
for (plant, sensor) in value.sensor_a_hz.iter().enumerate() {
result.plant[plant].sensor_a = *sensor > 1.0_f32;
result.plant[plant].sensor_a = sensor.is_some();
}
for (plant, sensor) in value.sensor_b_hz.iter().enumerate() {
result.plant[plant].sensor_b = *sensor > 1.0_f32;
result.plant[plant].sensor_b = sensor.is_some();
}
result
}

View File

@@ -9,6 +9,7 @@ const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, thi
#[derive(Debug, PartialEq, Serialize)]
pub enum MoistureSensorError {
NoMessage,
ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 },
}
@@ -118,41 +119,56 @@ impl PlantState {
) -> Self {
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
let raw = moistures.sensor_a_hz[plant_id];
match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_min_frequency
.map(|a| a as f32),
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_max_frequency
.map(|b| b as f32),
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
match raw {
None => {
MoistureSensorState::SensorError(MoistureSensorError::NoMessage)
}
Some(raw) => {
match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_min_frequency
.map(|a| a as f32),
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_max_frequency
.map(|b| b as f32),
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
}
}
}
} else {
MoistureSensorState::Disabled
};
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
let raw = moistures.sensor_b_hz[plant_id];
match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_min_frequency
.map(|a| a as f32),
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_max_frequency
.map(|b| b as f32),
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
match raw {
None => {
MoistureSensorState::SensorError(MoistureSensorError::NoMessage)
}
Some(raw) => {
match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_min_frequency
.map(|a| a as f32),
board.board_hal.get_config().plants[plant_id]
.moisture_sensor_max_frequency
.map(|b| b as f32),
) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => MoistureSensorState::SensorError(err),
}
}
}
} else {
MoistureSensorState::Disabled

View File

@@ -18,9 +18,7 @@ use crate::webserver::get_json::{
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, detect_sensors, night_lamp_test, pump_test, set_config, wifi_scan, write_time,
};
use crate::webserver::post_json::{board_test, can_power, detect_sensors, night_lamp_test, pump_test, set_config, wifi_scan, write_time};
use crate::{bail, BOARD_ACCESS};
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
@@ -103,6 +101,7 @@ impl Handler for HTTPRequestRouter {
"/time" => Some(write_time(conn).await),
"/backup_config" => Some(backup_config(conn).await),
"/pumptest" => Some(pump_test(conn).await),
"/can_power" => Some(can_power(conn).await),
"/lamptest" => Some(night_lamp_test(conn).await),
"/boardtest" => Some(board_test().await),
"/detect_sensors" => Some(detect_sensors().await),

View File

@@ -29,6 +29,11 @@ pub struct TestPump {
pump: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CanPower {
state: bool,
}
pub(crate) async fn wifi_scan<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
@@ -117,3 +122,22 @@ where
board.board_hal.set_config(config);
Ok(Some("Ok".to_string()))
}
pub(crate) async fn can_power<T, const N: usize>(
request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let actual_data = read_up_to_bytes_from_request(request, None).await?;
let can_power_request: CanPower = serde_json::from_slice(&actual_data)?;
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.can_power(can_power_request.state).await?;
let enable = can_power_request.state;
info!(
"set can power to {enable}"
);
Ok(None)
}

View File

@@ -141,6 +141,10 @@ export interface TestPump {
pump: number
}
export interface CanPower {
state: boolean
}
export interface SetTime {
time: string
}

View File

@@ -164,6 +164,7 @@
<h3>Plants:</h3>
<button id="measure_moisture">Measure Moisture</button>
<button id="detect_sensors" style="display:none">Detect/Test Sensors</button>
<input id="can_power" type="checkbox">Power CAN</input>
<div id="plants" class="plantlist"></div>
<div class="flexcontainer-rev">

View File

@@ -29,7 +29,7 @@ import {
SetTime, SSIDList, TankInfo,
TestPump,
VersionInfo,
FileList, SolarState, PumpTestResult, DetectionResult
FileList, SolarState, PumpTestResult, DetectionResult, CanPower
} from "./api";
import {SolarView} from "./solarview";
import {toast} from "./toast";
@@ -527,6 +527,18 @@ export class Controller {
setTimeout(this.waitForReboot, 1000)
}
private setCanPower(checked: boolean) {
var body: CanPower = {
state : checked
}
var pretty = JSON.stringify(body, undefined, 1);
fetch(PUBLIC_URL + "/can_power", {
method: "POST",
body: pretty
})
}
initialConfig: PlantControllerConfig | null = null
readonly rebootBtn: HTMLButtonElement
readonly exitBtn: HTMLButtonElement
@@ -544,6 +556,7 @@ export class Controller {
readonly fileview: FileView;
readonly logView: LogView
readonly detectBtn: HTMLButtonElement
readonly can_power: HTMLInputElement;
constructor() {
this.timeView = new TimeView(this)
@@ -569,7 +582,13 @@ export class Controller {
this.exitBtn.onclick = () => {
controller.exit();
}
this.can_power = document.getElementById("can_power") as HTMLInputElement
this.can_power.onchange = () => {
controller.setCanPower(this.can_power.checked);
}
}
}
const controller = new Controller();

View File

@@ -227,47 +227,47 @@ export class PlantView {
let showTarget = plantConfig.mode === "TargetMoisture"
let showMin = plantConfig.mode === "MinMoisture"
if(this.showDisabled || plantConfig.sensor_a || plantConfig.sensor_b) {
console.log("Showing plant " + this.plantId);
this.plantDiv.style.display = "block";
} else {
console.log("Hiding plant " + this.plantId);
this.plantDiv.style.display = "none";
}
// if(this.showDisabled || plantConfig.sensor_a || plantConfig.sensor_b) {
// console.log("Showing plant " + this.plantId);
// this.plantDiv.style.display = "block";
// } else {
// console.log("Hiding plant " + this.plantId);
// this.plantDiv.style.display = "none";
// }
console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " +showTarget + " min " + showMin)
for (const element of Array.from(sensorOnly)) {
if (showSensor) {
element.classList.remove("plantHidden_" + this.plantId)
} else {
element.classList.add("plantHidden_" + this.plantId)
}
}
for (const element of Array.from(pumpOnly)) {
if (showPump) {
element.classList.remove("plantHidden_" + this.plantId)
} else {
element.classList.add("plantHidden_" + this.plantId)
}
}
for (const element of Array.from(targetOnly)) {
if (showTarget) {
element.classList.remove("plantHidden_" + this.plantId)
} else {
element.classList.add("plantHidden_" + this.plantId)
}
}
for (const element of Array.from(minOnly)) {
if (showMin) {
element.classList.remove("plantHidden_" + this.plantId)
} else {
element.classList.add("plantHidden_" + this.plantId)
}
}
// for (const element of Array.from(sensorOnly)) {
// if (showSensor) {
// element.classList.remove("plantHidden_" + this.plantId)
// } else {
// element.classList.add("plantHidden_" + this.plantId)
// }
// }
//
// for (const element of Array.from(pumpOnly)) {
// if (showPump) {
// element.classList.remove("plantHidden_" + this.plantId)
// } else {
// element.classList.add("plantHidden_" + this.plantId)
// }
// }
//
// for (const element of Array.from(targetOnly)) {
// if (showTarget) {
// element.classList.remove("plantHidden_" + this.plantId)
// } else {
// element.classList.add("plantHidden_" + this.plantId)
// }
// }
//
// for (const element of Array.from(minOnly)) {
// if (showMin) {
// element.classList.remove("plantHidden_" + this.plantId)
// } else {
// element.classList.add("plantHidden_" + this.plantId)
// }
// }
}
setTestResult(result: PumpTestResult) {
@@ -335,8 +335,9 @@ export class PlantView {
}
setDetectionResult(plantResult: DetectionPlant) {
console.log("setDetectionResult plantResult: " + plantResult.sensor_a + " " + plantResult.sensor_b)
var changed = false;
if (this.sensorAInstalled.checked != plantResult.sensor_b){
if (this.sensorAInstalled.checked != plantResult.sensor_a){
changed = true;
this.sensorAInstalled.checked = plantResult.sensor_a;
}