get most stuff working again, by upgrading to newer esp-hal version

this involved adding a lot of code from the develop branch step by step
there are still some bugs, but at least i can get into the web interface
and configure stuff again \o/ … measuring and pumping is working as well
This commit is contained in:
2026-05-04 23:46:27 +02:00
parent ecb7707357
commit db401aac55
23 changed files with 2029 additions and 1292 deletions

1048
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,77 +52,41 @@ partition_table = "partitions.csv"
[dependencies] [dependencies]
#ESP stuff #ESP stuff
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] } log = "0.4.28"
esp-hal = { version = "=1.0.0-rc.0", features = [ esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6", "log-04"] }
"esp32c6", esp-hal = { version = "1.1.0", features = ["esp32c6", "log-04"] }
"log-04", esp-rtos = { version = "0.3.0", features = ["esp32c6", "embassy", "esp-radio"] }
"unstable", esp-backtrace = { version = "0.19.0", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
"rt" esp-println = { version = "0.17.0", features = ["esp32c6", "log-04", "auto"] }
] } esp-storage = { version = "0.9.0", features = ["esp32c6"] }
log = "0.4.27" esp-radio = { version = "0.18.0", features = ["esp32c6", "log-04", "wifi", "unstable"] }
esp-alloc = { version = "0.10.0", features = ["esp32c6", "internal-heap-stats"] }
embassy-net = { version = "0.7.1", default-features = false, features = [ # Async runtime (Embassy core)
"dhcpv4", embassy-executor = { version = "0.10.0", features = ["log", "nightly"] }
"log", embassy-time = { version = "0.5.1", features = ["log"], default-features = false }
"medium-ip", embassy-sync = { version = "0.8.0", features = ["log"] }
"medium-ethernet",
"tcp",
"udp",
"proto-ipv4",
"dns"
] }
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
esp-alloc = "0.8.0"
esp-backtrace = { version = "0.17.0", features = [
"esp32c6",
"exception-handler",
"panic-handler",
"println",
"colors",
"custom-halt"
] }
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
# for more networking protocol support see https://crates.io/crates/edge-net
embassy-executor = { version = "0.7.0", features = [
"log",
"task-arena-size-64",
"nightly"
] }
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] }
esp-storage = { version = "0.7.0", features = ["esp32c6"] }
esp-wifi = { version = "0.15.0", features = [ # Networking and protocol stacks
"builtin-scheduler", embassy-net = { version = "0.8.0", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp", "proto-ipv4", "dns", "proto-ipv6"] }
"esp-alloc", sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
"esp32c6", edge-dhcp = "0.7.0"
"log-04", edge-nal = "0.6.0"
"smoltcp", edge-nal-embassy = "0.8.1"
"wifi", edge-http = { version = "0.7.0", features = ["log"] }
] }
smoltcp = { version = "0.12.0", default-features = false, features = [ esp32c6 = { version = "0.23.2" }
"alloc",
"log", # Hardware abstraction traits and HAL adapters
"medium-ethernet",
"multicast",
"proto-dhcpv4",
"proto-ipv6",
"proto-dns",
"proto-ipv4",
"socket-dns",
"socket-icmp",
"socket-raw",
"socket-tcp",
"socket-udp",
] }
#static_cell = "2.1.1"
embedded-hal = "1.0.0" embedded-hal = "1.0.0"
embedded-hal-bus = { version = "0.3.0" } embedded-storage = "0.3.1"
embassy-embedded-hal = "0.6.0"
nb = "1.1.0"
#Hardware additional driver #Hardware additional driver
#bq34z100 = { version = "0.3.0", default-features = false } #bq34z100 = { version = "0.3.0", default-features = false }
lib-bms-protocol = { git = "https://gitea.wlandt.de/judge/ch32-bms.git", default-features = false }
onewire = "0.4.0" onewire = "0.4.0"
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] } #strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
ds323x = "0.6.0" ds323x = "0.6.0"
@@ -139,23 +103,15 @@ strum_macros = "0.27.0"
unit-enum = "1.4.1" unit-enum = "1.4.1"
pca9535 = { version = "2.0.0" } pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0" } ina219 = { version = "0.2.0" }
embedded-storage = "=0.3.1"
portable-atomic = "1.11.1" portable-atomic = "1.11.1"
embassy-sync = { version = "0.7.2", features = ["log"] }
async-trait = "0.1.89" async-trait = "0.1.89"
bq34z100 = { version = "0.4.0", default-features = false } bq34z100 = { version = "0.4.0", default-features = false }
edge-dhcp = "0.6.0"
edge-nal = "0.5.0"
edge-nal-embassy = "0.6.0"
static_cell = "2.1.1" static_cell = "2.1.1"
edge-http = { version = "0.6.1", features = ["log"] }
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] } littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.1" littlefs2-core = "0.1.1"
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] } bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
deranged = "0.5.3" deranged = "0.5.3"
embassy-embedded-hal = "0.5.0"
bincode = { version = "2.0.1", default-features = false, features = ["derive"] } bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
sntpc = { version = "0.6.0", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
option-lock = { version = "0.3.1", default-features = false } option-lock = { version = "0.3.1", default-features = false }
measurements = "0.11.1" measurements = "0.11.1"
heapless = { version = "0.7.17", features = ["serde"] } heapless = { version = "0.7.17", features = ["serde"] }

BIN
rust/bootloader.bin Normal file

Binary file not shown.

View File

@@ -8,7 +8,7 @@ use embassy_executor::SpawnError;
use embassy_sync::mutex::TryLockError; use embassy_sync::mutex::TryLockError;
use esp_hal::i2c::master::ConfigError; use esp_hal::i2c::master::ConfigError;
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit}; use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
use esp_wifi::wifi::WifiError; use esp_radio::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError}; use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use littlefs2_core::PathError; use littlefs2_core::PathError;
use onewire::Error; use onewire::Error;
@@ -45,6 +45,7 @@ pub enum FatError {
SpawnError { SpawnError {
error: SpawnError, error: SpawnError,
}, },
OTAError,
PartitionError { PartitionError {
error: esp_bootloader_esp_idf::partitions::Error, error: esp_bootloader_esp_idf::partitions::Error,
}, },
@@ -60,6 +61,9 @@ pub enum FatError {
ExpanderError { ExpanderError {
error: String, error: String,
}, },
SNTPError {
error: sntpc::Error,
},
} }
pub type FatResult<T> = Result<T, FatError>; pub type FatResult<T> = Result<T, FatError>;
@@ -88,6 +92,10 @@ impl fmt::Display for FatError {
FatError::DS323 { error } => write!(f, "DS323 {:?}", error), FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error), FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error), FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
FatError::OTAError => {
write!(f, "OTA missing partition")
}
} }
} }
} }
@@ -127,6 +135,24 @@ impl<T> ContextExt<T> for Option<T> {
} }
} }
impl<T, E> ContextExt<T> for Result<T, E>
where
E: fmt::Debug,
{
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>,
{
match self {
Ok(value) => Ok(value),
Err(err) => Err(FatError::String {
error: format!("{}: {:?}", context.as_ref(), err),
}),
}
}
}
impl From<Error<Infallible>> for FatError { impl From<Error<Infallible>> for FatError {
fn from(error: Error<Infallible>) -> Self { fn from(error: Error<Infallible>) -> Self {
FatError::OneWireError { error } FatError::OneWireError { error }
@@ -168,6 +194,12 @@ impl From<SpawnError> for FatError {
} }
} }
impl From<sntpc::Error> for FatError {
fn from(value: sntpc::Error) -> Self {
FatError::SNTPError { error: value }
}
}
impl From<esp_bootloader_esp_idf::partitions::Error> for FatError { impl From<esp_bootloader_esp_idf::partitions::Error> for FatError {
fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self { fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self {
FatError::PartitionError { error: value } FatError::PartitionError { error: value }
@@ -279,3 +311,11 @@ impl From<InvalidHighLimit> for FatError {
} }
} }
} }
impl From<chrono::format::ParseError> for FatError {
fn from(value: chrono::format::ParseError) -> Self {
FatError::String {
error: format!("Parsing error: {value:?}"),
}
}
}

View File

@@ -1,8 +1,11 @@
use crate::bail; use crate::bail;
use crate::config::{NetworkConfig, PlantControllerConfig}; use crate::config::{NetworkConfig, PlantControllerConfig};
use crate::hal::{PLANT_COUNT, TIME_ACCESS}; use crate::hal::PLANT_COUNT;
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{log, LogMessage};
use alloc::vec;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use esp_hal::Blocking;
use esp_hal::uart::Uart;
use serde::Serialize; use serde::Serialize;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
@@ -14,15 +17,17 @@ use core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::str::FromStr; use core::str::FromStr;
use core::sync::atomic::Ordering; use core::sync::atomic::Ordering;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_net::udp::UdpSocket; use embassy_net::dns::DnsQueryType;
use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::ReadNorFlash; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
use esp_hal::gpio::{Input, RtcPinWithResistors}; use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng; use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{ use esp_hal::rtc_cntl::{
@@ -31,31 +36,34 @@ use esp_hal::rtc_cntl::{
}; };
use esp_hal::system::software_reset; use esp_hal::system::software_reset;
use esp_println::println; use esp_println::println;
use esp_radio::wifi::ap::{AccessPointConfig, AccessPointInfo};
use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{AuthenticationMethod, Config, Interface, WifiController};
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use esp_wifi::wifi::{
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState,
};
use littlefs2::fs::Filesystem; use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom}; use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{info, warn}; use log::{info, warn, error};
use mcutie::{ use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable, Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
QoS, Topic, QoS, Topic,
}; };
use portable_atomic::AtomicBool; use portable_atomic::AtomicBool;
use smoltcp::socket::udp::PacketMetadata; use sntpc::{NtpContext, NtpTimestampGenerator, NtpUdpSocket, get_time};
use smoltcp::wire::DnsQueryType;
use sntpc::{get_time, NtpContext, NtpTimestampGenerator};
#[esp_hal::ram(rtc_fast, persistent)] use super::shared_flash::MutexFlashStorage;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOW_VOLTAGE_DETECTED: i8 = 0; static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0; static mut RESTART_TO_CONF: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1;
const CONFIG_FILE: &str = "config.json"; const CONFIG_FILE: &str = "config.json";
const NTP_SERVER: &str = "pool.ntp.org"; const NTP_SERVER: &str = "pool.ntp.org";
@@ -112,22 +120,61 @@ impl NtpTimestampGenerator for Timestamp {
self.stamp.timestamp_subsec_micros() self.stamp.timestamp_subsec_micros()
} }
} }
struct EmbassyNtpSocket<'a, 'b> {
socket: &'a UdpSocket<'b>,
}
impl<'a, 'b> EmbassyNtpSocket<'a, 'b> {
fn new(socket: &'a UdpSocket<'b>) -> Self {
Self { socket }
}
}
impl NtpUdpSocket for EmbassyNtpSocket<'_, '_> {
async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> sntpc::Result<usize> {
self.socket
.send_to(buf, addr)
.await
.map_err(|_| sntpc::Error::Network)?;
Ok(buf.len())
}
async fn recv_from(&self, buf: &mut [u8]) -> sntpc::Result<(usize, SocketAddr)> {
let (len, metadata) = self
.socket
.recv_from(buf)
.await
.map_err(|_| sntpc::Error::Network)?;
let addr = match metadata.endpoint.addr {
IpAddress::Ipv4(ip) => IpAddr::V4(ip),
IpAddress::Ipv6(ip) => IpAddr::V6(ip),
};
Ok((len, SocketAddr::new(addr, metadata.endpoint.port)))
}
}
pub struct Esp<'a> { pub struct Esp<'a> {
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>, pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
pub rng: Rng, pub rng: Rng,
//first starter (ap or sta will take these) //first starter (ap or sta will take these)
pub interface_sta: Option<WifiDevice<'static>>, pub interface_sta: Option<Interface<'static>>,
pub interface_ap: Option<WifiDevice<'static>>, pub interface_ap: Option<Interface<'static>>,
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>, pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
pub boot_button: Input<'a>, pub boot_button: Input<'a>,
// RTC-capable GPIO used as external wake source (store the raw peripheral) // RTC-capable GPIO used as external wake source (store the raw peripheral)
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>, pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub uart0: Uart<'a, Blocking>,
pub ota: Ota<'static, FlashStorage>, pub rtc: Rtc<'a>,
pub ota_next: &'static mut FlashRegion<'static, FlashStorage>,
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
pub current: AppPartitionSubType,
pub slot0_state: OtaImageState,
pub slot1_state: OtaImageState,
} }
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core // SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
@@ -148,6 +195,47 @@ macro_rules! mk_static {
} }
impl Esp<'_> { impl Esp<'_> {
pub fn get_time(&self) -> DateTime<Utc> {
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
.unwrap_or(DateTime::UNIX_EPOCH)
}
pub fn set_time(&mut self, time: DateTime<Utc>) {
self.rtc.set_current_time_us(time.timestamp_micros() as u64);
}
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
let mut buf = [0u8; 1];
let mut line = String::new();
loop {
match self.uart0.read_buffered(&mut buf) {
Ok(read) => {
if read == 0 {
return Ok(None);
}
let c = buf[0] as char;
if c == '\n' {
return Ok(Some(line));
}
line.push(c);
}
Err(error) => {
if line.is_empty() {
return Ok(None);
} else {
error!("Error reading serial line: {error:?}");
// If we already have some data, we should probably wait a bit or just return what we have?
// But the protocol expects a full line or message.
// For simplicity in config mode, we can block here or just return None if nothing is there yet.
// However, if we started receiving, we should probably finish or timeout.
continue;
}
}
}
}
}
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> { pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
let file = PathBuf::try_from(filename.as_str())?; let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await; let access = self.fs.lock().await;
@@ -212,26 +300,47 @@ impl Esp<'_> {
Ok((buf, read)) Ok((buf, read))
} }
pub(crate) fn get_ota_slot(&mut self) -> String { pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
match self.ota.current_slot() { let _ = check_erase(self.ota_target, offset, offset + 4096);
Ok(slot) => { info!("erasing and writing block 0x{offset:x}");
format!("{:?}", slot) self.ota_target.erase(offset, offset + 4096)?;
}
Err(err) => { let mut temp = vec![0; buf.len()];
format!("{:?}", err) let read_back = temp.as_mut_slice();
} //change to nor flash, align writes!
self.ota_target.write(offset, buf)?;
self.ota_target.read(offset, read_back)?;
if buf != read_back {
info!("Expected {buf:?} but got {read_back:?}");
bail!(
"Flash error, read back does not match write buffer at offset {:x}",
offset
)
} }
Ok(())
} }
pub(crate) fn get_ota_state(&mut self) -> String { pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
match self.ota.current_ota_state() { let current = self.ota.current_app_partition()?;
Ok(state) => { if self.ota.current_ota_state()? != Valid {
format!("{:?}", state) info!("Validating current slot {current:?} as it was able to ota");
} self.ota.set_current_ota_state(Valid)?;
Err(err) => {
format!("{:?}", err)
}
} }
let next = match current {
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Invalid current slot {current:?} for ota");
}
};
self.ota.set_current_app_partition(next)?;
info!("switched slot");
self.ota.set_current_ota_state(OtaImageState::New)?;
info!("switched state for new partition");
let state_new = self.ota.current_ota_state()?;
info!("state on new partition now {state_new:?}");
self.set_restart_to_conf(true);
Ok(())
} }
// let current = ota.current_slot()?; // let current = ota.current_slot()?;
@@ -264,31 +373,38 @@ impl Esp<'_> {
&mut tx_meta, &mut tx_meta,
&mut tx_buffer, &mut tx_buffer,
); );
socket.bind(123).unwrap(); socket.bind(123).context("Could not bind UDP socket")?;
let context = NtpContext::new(Timestamp::default()); let context = NtpContext::new(Timestamp::default());
let ntp_socket = EmbassyNtpSocket::new(&socket);
let ntp_addrs = stack let ntp_addrs = stack
.dns_query(NTP_SERVER, DnsQueryType::A) .dns_query(NTP_SERVER, DnsQueryType::A)
.await .await
.expect("Failed to resolve DNS"); .context("Failed to resolve DNS")?;
if ntp_addrs.is_empty() { if ntp_addrs.is_empty() {
bail!("Failed to resolve DNS"); bail!("No IP addresses found for NTP server");
} }
let ntp = ntp_addrs[0];
info!("NTP server: {ntp:?}");
let mut counter = 0; let mut counter = 0;
loop { loop {
let addr: IpAddr = ntp_addrs[0].into(); let addr: IpAddr = ntp.into();
let result = get_time(SocketAddr::from((addr, 123)), &socket, context).await; let timeout = get_time(SocketAddr::from((addr, 123)), &ntp_socket, context)
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
.await;
match result { match timeout {
Ok(time) => { Ok(result) => {
info!("Time: {:?}", time); let time = result?;
info!("Time: {time:?}");
return DateTime::from_timestamp(time.seconds as i64, 0) return DateTime::from_timestamp(time.seconds as i64, 0)
.context("Could not convert Sntp result"); .context("Could not convert Sntp result");
} }
Err(e) => { Err(err) => {
warn!("Error: {:?}", e); warn!("sntp timeout, retry: {err:?}");
counter += 1; counter += 1;
if counter > 10 { if counter > 10 {
bail!("Failed to get time from NTP server"); bail!("Failed to get time from NTP server");
@@ -299,27 +415,15 @@ impl Esp<'_> {
} }
} }
pub async fn flash_ota(&mut self) -> FatResult<()> {
let capacity = self.ota_next.capacity();
bail!("not implemented")
}
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> { pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
info!("start wifi scan"); info!("start wifi scan");
let mut lock = self.controller.try_lock()?; let mut lock = self.controller.try_lock()?;
info!("start wifi scan lock"); info!("start wifi scan lock");
let scan_config = ScanConfig { let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
ssid: None, min: esp_hal::time::Duration::from_millis(0),
bssid: None, max: esp_hal::time::Duration::from_millis(0),
channel: None, });
show_hidden: false, let rv = lock.scan_async(&scan_config).await?;
scan_type: ScanTypeConfig::Active {
min: Default::default(),
max: Default::default(),
},
};
let rv = lock.scan_with_config_async(scan_config).await?;
info!("end wifi scan lock"); info!("end wifi scan lock");
Ok(rv) Ok(rv)
} }
@@ -368,17 +472,17 @@ impl Esp<'_> {
} }
} }
pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> { pub(crate) async fn wifi_ap(&mut self, spawner: Spawner) -> FatResult<Stack<'static>> {
let ssid = match self.load_config().await { let ssid = match self.load_config().await {
Ok(config) => config.network.ap_ssid.as_str().to_string(), Ok(config) => config.network.ap_ssid.as_str().to_string(),
Err(_) => "PlantCtrl Emergency Mode".to_string(), Err(_) => "PlantCtrl Emergency Mode".to_string(),
}; };
let spawner = Spawner::for_current_executor().await; let device = self
.interface_ap
let device = self.interface_ap.take().unwrap(); .take()
let gw_ip_addr_str = "192.168.71.1"; .context("AP interface already taken")?;
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); let gw_ip_addr = Ipv4Addr::new(192, 168, 71, 1);
let config = embassy_net::Config::ipv4_static(StaticConfigV4 { let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
address: Ipv4Cidr::new(gw_ip_addr, 24), address: Ipv4Cidr::new(gw_ip_addr, 24),
@@ -398,22 +502,14 @@ impl Esp<'_> {
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);
let client_config = Configuration::AccessPoint(AccessPointConfiguration { let client_config =
ssid: ssid.clone(), Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
..Default::default() self.controller.lock().await.set_config(&client_config)?;
});
self.controller
.lock()
.await
.set_configuration(&client_config)?;
println!("start new");
self.controller.lock().await.start()?;
println!("start net task"); println!("start net task");
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)?);
println!("run dhcp"); println!("run dhcp");
spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok(); spawner.spawn(run_dhcp(*stack, gw_ip_addr)?);
loop { loop {
if stack.is_link_up() { if stack.is_link_up() {
@@ -424,31 +520,31 @@ impl Esp<'_> {
while !stack.is_config_up() { while !stack.is_config_up() {
Timer::after(Duration::from_millis(100)).await Timer::after(Duration::from_millis(100)).await
} }
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr_str}/"); println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr}/");
stack stack
.config_v4() .config_v4()
.inspect(|c| println!("ipv4 config: {c:?}")); .inspect(|c| println!("ipv4 config: {c:?}"));
Ok(stack.clone()) Ok(*stack)
} }
pub(crate) async fn wifi( pub(crate) async fn wifi(
&mut self, &mut self,
network_config: &NetworkConfig, network_config: &NetworkConfig,
spawner: Spawner,
) -> FatResult<Stack<'static>> { ) -> FatResult<Stack<'static>> {
esp_wifi::wifi_set_log_verbose(); esp_radio::wifi_set_log_verbose();
let ssid = network_config.ssid.clone(); let ssid = match &network_config.ssid {
match &ssid {
Some(ssid) => { Some(ssid) => {
if ssid.is_empty() { if ssid.is_empty() {
bail!("Wifi ssid was empty") bail!("Wifi ssid was empty")
} }
ssid.to_string()
} }
None => { None => {
bail!("Wifi ssid was empty") bail!("Wifi ssid was empty")
} }
} };
let ssid = ssid.unwrap().to_string();
info!("attempting to connect wifi {ssid}"); info!("attempting to connect wifi {ssid}");
let password = match network_config.password { let password = match network_config.password {
Some(ref password) => password.to_string(), Some(ref password) => password.to_string(),
@@ -456,9 +552,10 @@ impl Esp<'_> {
}; };
let max_wait = network_config.max_wait; let max_wait = network_config.max_wait;
let spawner = Spawner::for_current_executor().await; let device = self
.interface_sta
let device = self.interface_sta.take().unwrap(); .take()
.context("STA interface already taken")?;
let config = embassy_net::Config::dhcpv4(DhcpConfig::default()); let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64; let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
@@ -472,122 +569,80 @@ impl Esp<'_> {
); );
let stack = mk_static!(Stack, stack); let stack = mk_static!(Stack, stack);
let client_config = Configuration::Client(ClientConfiguration { let auth_method = if password.is_empty() {
ssid, AuthenticationMethod::None
bssid: None, } else {
auth_method: AuthMethod::WPA2Personal, //FIXME read from config, fill via scan AuthenticationMethod::Wpa2Personal
password, };
channel: None, let client_config = StationConfig::default()
}); .with_ssid(ssid)
.with_auth_method(auth_method)
.with_password(password);
self.controller self.controller
.lock() .lock()
.await .await
.set_configuration(&client_config)?; .set_config(&Config::Station(client_config))?;
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)?);
self.controller.lock().await.start_async().await?; self.controller
.lock()
.await
.connect_async()
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await
.context("Timeout waiting for wifi sta connected")??;
let timeout = { let res = async {
let guard = TIME_ACCESS.get().await.lock().await; while !stack.is_link_up() {
guard.current_time_us() Timer::after(Duration::from_millis(500)).await;
} + max_wait as u64 * 1000;
loop {
let state = esp_wifi::wifi::sta_state();
match state {
WifiState::StaStarted => {
self.controller.lock().await.connect()?;
break;
}
_ => {}
} }
if { Ok::<(), FatError>(())
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi sta ready")
}
Timer::after(Duration::from_millis(500)).await;
} }
let timeout = { .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
let guard = TIME_ACCESS.get().await.lock().await; .await;
guard.current_time_us()
} + max_wait as u64 * 1000; if res.is_err() {
loop { bail!("Timeout waiting for wifi link up")
let state = esp_wifi::wifi::sta_state();
match state {
WifiState::StaConnected => {
break;
}
_ => {}
}
if {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi sta connected")
}
Timer::after(Duration::from_millis(500)).await;
} }
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await; let res = async {
guard.current_time_us() while !stack.is_config_up() {
} + max_wait as u64 * 1000; Timer::after(Duration::from_millis(100)).await
while !stack.is_link_up() {
if {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi link up")
} }
Timer::after(Duration::from_millis(500)).await; Ok::<(), FatError>(())
} }
let timeout = { .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
let guard = TIME_ACCESS.get().await.lock().await; .await;
guard.current_time_us()
} + max_wait as u64 * 1000; if res.is_err() {
while !stack.is_config_up() { bail!("Timeout waiting for wifi config up")
if {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi config up")
}
Timer::after(Duration::from_millis(100)).await
} }
info!("Connected WIFI, dhcp: {:?}", stack.config_v4()); info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(stack.clone()) Ok(*stack)
} }
pub fn deep_sleep( pub fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
&mut self,
duration_in_ms: u64,
mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>,
) -> ! {
// Configure and enter deep sleep using esp-hal. Also keep prior behavior where
// duration_in_ms == 0 triggers an immediate reset.
// Mark the current OTA image as valid if we reached here while in pending verify. // Mark the current OTA image as valid if we reached here while in pending verify.
if let Ok(cur) = self.ota.current_ota_state() { if let Ok(cur) = self.ota.current_ota_state() {
if cur == OtaImageState::PendingVerify { if cur == OtaImageState::PendingVerify {
self.ota info!("Marking OTA image as valid");
.set_current_ota_state(OtaImageState::Valid) if let Err(err) = self.ota.set_current_ota_state(Valid) {
.expect("Could not set image to valid"); error!("Could not set image to valid: {:?}", err);
}
} }
} else {
info!("No OTA image to mark as valid");
} }
if duration_in_ms == 0 { if duration_in_ms == 0 {
software_reset(); software_reset();
} else { } else {
///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms)); let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000));
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] = let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
[(&mut self.wake_gpio1, WakeupLevel::Low)]; [(&mut self.wake_gpio1, WakeupLevel::Low)];
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins); let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
rtc.sleep_deep(&[&timer, &ext1]); self.rtc.sleep_deep(&[&timer, &ext1]);
} }
} }
@@ -646,47 +701,36 @@ impl Esp<'_> {
} else { } else {
RESTART_TO_CONF = 0; RESTART_TO_CONF = 0;
} }
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
}; };
} else { } else {
unsafe { unsafe {
if to_config_mode { if to_config_mode {
RESTART_TO_CONF = 1; RESTART_TO_CONF = 1;
} }
LOG_ACCESS log(
.lock() LogMessage::RestartToConfig,
.await RESTART_TO_CONF as u32,
.log( 0,
LogMessage::RestartToConfig, "",
RESTART_TO_CONF as u32, "",
0, );
"", log(
"", LogMessage::LowVoltage,
) LOW_VOLTAGE_DETECTED as u32,
.await; 0,
LOG_ACCESS "",
.lock() "",
.await );
.log( // is executed before main, no other code will alter these values during printing
LogMessage::LowVoltage, #[allow(static_mut_refs)]
LOW_VOLTAGE_DETECTED as u32, for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() {
0, info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}");
"",
"",
)
.await;
for i in 0..PLANT_COUNT {
log::info!(
"LAST_WATERING_TIMESTAMP[{}] = UTC {}",
i,
LAST_WATERING_TIMESTAMP[i]
);
} }
for i in 0..PLANT_COUNT { // is executed before main, no other code will alter these values during printing
log::info!( #[allow(static_mut_refs)]
"CONSECUTIVE_WATERING_PLANT[{}] = {}", for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
i, info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
CONSECUTIVE_WATERING_PLANT[i]
);
} }
} }
} }
@@ -696,6 +740,7 @@ impl Esp<'_> {
&mut self, &mut self,
network_config: &'static NetworkConfig, network_config: &'static NetworkConfig,
stack: Stack<'static>, stack: Stack<'static>,
spawner: Spawner,
) -> FatResult<()> { ) -> FatResult<()> {
let base_topic = network_config let base_topic = network_config
.base_topic .base_topic
@@ -718,17 +763,17 @@ impl Esp<'_> {
bail!("Mqtt url was empty") bail!("Mqtt url was empty")
} }
let last_will_topic = format!("{}/state", base_topic); let last_will_topic = format!("{base_topic}/state");
let round_trip_topic = format!("{}/internal/roundtrip", base_topic); let round_trip_topic = format!("{base_topic}/internal/roundtrip");
let stay_alive_topic = format!("{}/stay_alive", base_topic); let stay_alive_topic = format!("{base_topic}/stay_alive");
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> = let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
McutieBuilder::new(stack, "plant ctrl", mqtt_url); McutieBuilder::new(stack, "plant ctrl", mqtt_url);
if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() { if let (Some(mqtt_user), Some(mqtt_password)) = (
builder = builder.with_authentication( network_config.mqtt_user.as_ref(),
network_config.mqtt_user.as_ref().unwrap().as_str(), network_config.mqtt_password.as_ref(),
network_config.mqtt_password.as_ref().unwrap().as_str(), ) {
); builder = builder.with_authentication(mqtt_user, mqtt_password);
info!("With authentification"); info!("With authentification");
} }
@@ -748,57 +793,51 @@ impl Esp<'_> {
let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16; let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
let (receiver, task) = builder.build(keep_alive); let (receiver, task) = builder.build(keep_alive);
let spawner = Spawner::for_current_executor().await;
spawner.spawn(mqtt_incoming_task( spawner.spawn(mqtt_incoming_task(
receiver, receiver,
round_trip_topic.clone(), round_trip_topic.clone(),
stay_alive_topic.clone(), stay_alive_topic.clone(),
))?; )?);
spawner.spawn(mqtt_runner(task))?; spawner.spawn(mqtt_runner(task)?);
LOG_ACCESS log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
.lock()
.await
.log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
.await;
LOG_ACCESS log(LogMessage::MqttInfo, 0, 0, "", mqtt_url);
.lock()
.await
.log(LogMessage::MqttInfo, 0, 0, "", mqtt_url)
.await;
let mqtt_timeout = 15000; let mqtt_timeout = 15000;
let timeout = { let res = async {
let guard = TIME_ACCESS.get().await.lock().await; while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
guard.current_time_us() crate::hal::PlantHal::feed_watchdog();
} + mqtt_timeout as u64 * 1000; Timer::after(Duration::from_millis(100)).await;
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
if cur > timeout {
bail!("Timeout waiting MQTT connect event")
} }
Timer::after(Duration::from_millis(100)).await; Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
if res.is_err() {
bail!("Timeout waiting MQTT connect event")
} }
Topic::General(round_trip_topic.clone()) let _ = Topic::General(round_trip_topic.clone())
.with_display("online_text") .with_display("online_text")
.publish() .publish()
.await .await;
.unwrap();
let timeout = { let res = async {
let guard = TIME_ACCESS.get().await.lock().await; while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
guard.current_time_us() crate::hal::PlantHal::feed_watchdog();
} + mqtt_timeout as u64 * 1000; Timer::after(Duration::from_millis(100)).await;
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
if cur > timeout {
//ensure we do not further try to publish
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip")
} }
Timer::after(Duration::from_millis(100)).await; Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
if res.is_err() {
//ensure we do not further try to publish
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip")
} }
Ok(()) Ok(())
} }
@@ -864,8 +903,7 @@ impl Esp<'_> {
Ok(()) => {} Ok(()) => {}
Err(err) => { Err(err) => {
info!( info!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}", "Error during mqtt send on topic {subtopic} with message {message:#?} error is {err:?}"
subtopic, message, err
); );
} }
}; };
@@ -893,8 +931,8 @@ async fn mqtt_incoming_task(
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed); MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
} }
MqttMessage::Publish(topic, payload) => match topic { MqttMessage::Publish(topic, payload) => match topic {
Topic::DeviceType(type_topic) => {} Topic::DeviceType(_type_topic) => {}
Topic::Device(device_topic) => {} Topic::Device(_device_topic) => {}
Topic::General(topic) => { Topic::General(topic) => {
let subtopic = topic.as_str(); let subtopic = topic.as_str();
@@ -907,18 +945,10 @@ async fn mqtt_incoming_task(
true => 1, true => 1,
false => 0, false => 0,
}; };
LOG_ACCESS log(LogMessage::MqttStayAliveRec, a, 0, "", "");
.lock()
.await
.log(LogMessage::MqttStayAliveRec, a, 0, "", "")
.await;
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed); MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
} else { } else {
LOG_ACCESS log(LogMessage::UnknownTopic, 0, 0, "", &topic);
.lock()
.await
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic)
.await;
} }
} }
}, },
@@ -931,13 +961,13 @@ async fn mqtt_incoming_task(
} }
#[embassy_executor::task(pool_size = 2)] #[embassy_executor::task(pool_size = 2)]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) { async fn net_task(mut runner: Runner<'static, Interface<'static>>) {
runner.run().await; runner.run().await;
} }
#[embassy_executor::task] #[embassy_executor::task]
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) { async fn run_dhcp(stack: Stack<'static>, ip: Ipv4Addr) {
use core::net::{Ipv4Addr, SocketAddrV4}; use core::net::SocketAddrV4;
use edge_dhcp::{ use edge_dhcp::{
io::{self, DEFAULT_SERVER_PORT}, io::{self, DEFAULT_SERVER_PORT},
@@ -946,21 +976,25 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
use edge_nal::UdpBind; use edge_nal::UdpBind;
use edge_nal_embassy::{Udp, UdpBuffers}; use edge_nal_embassy::{Udp, UdpBuffers};
let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip");
let mut buf = [0u8; 1500]; let mut buf = [0u8; 1500];
let mut gw_buf = [Ipv4Addr::UNSPECIFIED]; let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new(); let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
let unbound_socket = Udp::new(stack, &buffers); let unbound_socket = Udp::new(stack, &buffers);
let mut bound_socket = unbound_socket let mut bound_socket = match unbound_socket
.bind(SocketAddr::V4(SocketAddrV4::new( .bind(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED, Ipv4Addr::UNSPECIFIED,
DEFAULT_SERVER_PORT, DEFAULT_SERVER_PORT,
))) )))
.await .await
.unwrap(); {
Ok(s) => s,
Err(e) => {
error!("dhcp task failed to bind socket: {:?}", e);
return;
}
};
loop { loop {
_ = io::server::run( _ = io::server::run(
@@ -970,7 +1004,7 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
&mut buf, &mut buf,
) )
.await .await
.inspect_err(|e| log::warn!("DHCP server error: {e:?}")); .inspect_err(|e| warn!("DHCP server error: {e:?}"));
Timer::after(Duration::from_millis(500)).await; Timer::after(Duration::from_millis(500)).await;
} }
} }

View File

@@ -3,14 +3,14 @@ use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::Esp; use crate::hal::esp::Esp;
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction}; use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS}; use crate::hal::{BoardInteraction, FreePeripherals, Sensor};
use crate::{ use crate::{
bail, bail,
config::PlantControllerConfig, config::PlantControllerConfig,
hal::battery::{BatteryInteraction, NoBatteryMonitor}, hal::battery::{BatteryInteraction, NoBatteryMonitor},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, FixedOffset, Utc};
use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::gpio::{Level, Output, OutputConfig};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
@@ -90,13 +90,22 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
&mut self.rtc &mut self.rtc
} }
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> { async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
bail!("Please configure board revision") bail!("Please configure board revision")
} }
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { 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);
self.esp.deep_sleep(duration_in_ms, rtc);
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
false false

View File

@@ -1,7 +1,7 @@
use embedded_storage::{ReadStorage, Storage}; use crate::hal::shared_flash::MutexFlashStorage;
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage; use littlefs2::consts::U4096 as lfsCache;
use littlefs2::consts::U512 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead; use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage; use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::io::Error as lfs2Error; use littlefs2::io::Error as lfs2Error;
@@ -9,26 +9,32 @@ use littlefs2::io::Result as lfs2Result;
use log::error; use log::error;
pub struct LittleFs2Filesystem { pub struct LittleFs2Filesystem {
pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>, pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
} }
impl lfs2Storage for LittleFs2Filesystem { impl lfs2Storage for LittleFs2Filesystem {
const READ_SIZE: usize = 256; const READ_SIZE: usize = 4096;
const WRITE_SIZE: usize = 512; const WRITE_SIZE: usize = 4096;
const BLOCK_SIZE: usize = 512; //usually optimal for flash access const BLOCK_SIZE: usize = 4096; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks const BLOCK_COUNT: usize = 8 * 1000 * 1000 / 4096; //8Mb in 4k blocks + a little space for stupid calculation errors
const BLOCK_CYCLES: isize = 100; const BLOCK_CYCLES: isize = 100;
type CACHE_SIZE = lfsCache; type CACHE_SIZE = lfsCache;
type LOOKAHEAD_SIZE = lfsLookahead; type LOOKAHEAD_SIZE = lfsLookahead;
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> { fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
let read_size: usize = Self::READ_SIZE; let read_size: usize = Self::READ_SIZE;
assert_eq!(off % read_size, 0); if off % read_size != 0 {
assert_eq!(buf.len() % read_size, 0); error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {read_size}");
return Err(lfs2Error::IO);
}
if buf.len() % read_size != 0 {
error!("Littlefs2Filesystem read error: length not aligned to read size length: {} read_size: {}", buf.len(), read_size);
return Err(lfs2Error::IO);
}
match self.storage.read(off as u32, buf) { match self.storage.read(off as u32, buf) {
Ok(..) => Ok(buf.len()), Ok(..) => Ok(buf.len()),
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem read error: {:?}", err); error!("Littlefs2Filesystem read error: {err:?}");
Err(lfs2Error::IO) Err(lfs2Error::IO)
} }
} }
@@ -36,12 +42,18 @@ impl lfs2Storage for LittleFs2Filesystem {
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> { fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE; let write_size: usize = Self::WRITE_SIZE;
assert_eq!(off % write_size, 0); if off % write_size != 0 {
assert_eq!(data.len() % write_size, 0); error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {write_size}");
return Err(lfs2Error::IO);
}
if data.len() % write_size != 0 {
error!("Littlefs2Filesystem write error: length not aligned to write size length: {} write_size: {}", data.len(), write_size);
return Err(lfs2Error::IO);
}
match self.storage.write(off as u32, data) { match self.storage.write(off as u32, data) {
Ok(..) => Ok(data.len()), Ok(..) => Ok(data.len()),
Err(err) => { Err(err) => {
error!("Littlefs2Filesystem write error: {:?}", err); error!("Littlefs2Filesystem write error: {err:?}");
Err(lfs2Error::IO) Err(lfs2Error::IO)
} }
} }
@@ -49,15 +61,28 @@ impl lfs2Storage for LittleFs2Filesystem {
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> { fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE; let block_size: usize = Self::BLOCK_SIZE;
debug_assert!(off % block_size == 0); if off % block_size != 0 {
debug_assert!(len % block_size == 0); error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
//match self.storage.erase(off as u32, len as u32) { return Err(lfs2Error::IO);
//anyhow::Result::Ok(..) => lfs2Result::Ok(len), }
//Err(err) => { if len % block_size != 0 {
//error!("Littlefs2Filesystem erase error: {:?}", err); error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
//Err(lfs2Error::IO) return Err(lfs2Error::IO);
// } }
//}
lfs2Result::Ok(len) match check_erase(self.storage, off as u32, (off + len) as u32) {
Ok(_) => {}
Err(err) => {
error!("Littlefs2Filesystem check erase error: {err:?}");
return Err(lfs2Error::IO);
}
}
match self.storage.erase(off as u32, (off + len) as u32) {
Ok(..) => Ok(len),
Err(err) => {
error!("Littlefs2Filesystem erase error: {err:?}");
Err(lfs2Error::IO)
}
}
} }
} }

View File

@@ -3,6 +3,7 @@ pub mod esp;
mod initial_hal; mod initial_hal;
mod little_fs2storage_adapter; mod little_fs2storage_adapter;
pub(crate) mod rtc; pub(crate) mod rtc;
mod shared_flash;
mod v3_hal; mod v3_hal;
mod v3_shift_register; mod v3_shift_register;
mod v4_hal; mod v4_hal;
@@ -11,6 +12,7 @@ mod water;
use crate::alloc::string::ToString; use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::peripherals::Peripherals; use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1; use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::APB_SARADC; use esp_hal::peripherals::APB_SARADC;
@@ -43,6 +45,7 @@ use esp_hal::peripherals::GPIO7;
use esp_hal::peripherals::GPIO8; use esp_hal::peripherals::GPIO8;
use esp_hal::peripherals::PCNT; use esp_hal::peripherals::PCNT;
use esp_hal::peripherals::TWAI0; use esp_hal::peripherals::TWAI0;
use portable_atomic::AtomicBool;
use crate::{ use crate::{
bail, bail,
@@ -71,23 +74,29 @@ use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
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_sync::blocking_mutex::CriticalSectionMutex; use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::RmwNorFlashStorage;
use embedded_storage::ReadStorage;
use esp_bootloader_esp_idf::partitions::{ use esp_bootloader_esp_idf::partitions::{
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable,
PartitionType,
}; };
use esp_hal::clock::CpuClock; use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull}; use esp_hal::gpio::{Input, InputConfig, Pull};
use esp_hal::uart::{Config as UartConfig, Uart};
use esp_storage::FlashStorage;
use lib_bms_protocol::{BmsReadable, ProtocolVersion};
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1}; use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS; use crate::log::log;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use esp_alloc as _; use esp_alloc as _;
use esp_backtrace as _; use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::Slot; use esp_bootloader_esp_idf::ota::{OtaImageState, Ota};
use esp_hal::delay::Delay; use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c}; use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit; use esp_hal::pcnt::unit::Unit;
@@ -96,19 +105,25 @@ use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{Rtc, SocResetReason}; use esp_hal::rtc_cntl::{Rtc, SocResetReason};
use esp_hal::system::reset_reason; use esp_hal::system::reset_reason;
use esp_hal::time::Rate; use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup; use esp_hal::timer::timg::{TimerGroup, Wdt};
use esp_hal::Blocking; use esp_hal::Blocking;
use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage; use littlefs2::object_safe::DynStorage;
use log::{info, warn}; use log::{error, info, warn};
use shared_flash::MutexFlashStorage;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new(); pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
//Only support for 8 right now! //Only support for 8 right now!
pub const PLANT_COUNT: usize = 8; pub const PLANT_COUNT: usize = 8;
pub static WATCHDOG: OnceLock<
embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<Wdt<esp_hal::peripherals::TIMG0>>,
>,
> = OnceLock::new();
const TANK_MULTI_SAMPLE: usize = 11; const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: OnceLock< pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>, embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
@@ -126,6 +141,70 @@ pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>, pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
} }
fn ota_state(
slot: AppPartitionSubType,
ota_data: &mut FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> OtaImageState {
// Read and log OTA states for both slots before constructing Ota
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
let mut slot_buf = [0u8; 32];
if slot == AppPartitionSubType::Ota0 {
let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf);
} else {
let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf);
}
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
}
fn get_current_slot(
pt: &PartitionTable,
ota: &mut Ota<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> Result<AppPartitionSubType, FatError> {
let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?;
let booted_type = booted.partition_type();
let booted_ota_type = match booted_type {
PartitionType::App(subtype) => subtype,
_ => {
bail!("Booted partition is not an app partition");
}
};
let expected_partition = ota.current_app_partition()?;
if expected_partition == booted_ota_type {
info!("Booted partition matches expected partition");
} else {
info!("Booted partition does not match expected partition, fixing ota entry");
ota.set_current_app_partition(booted_ota_type)?;
}
let fixed = ota.current_app_partition()?;
let state = ota.current_ota_state();
info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}");
if fixed != booted_ota_type {
bail!(
"Could not fix ota entry, booted partition is still not correct: {:?} != {:?}",
booted_ota_type,
fixed
);
}
Ok(booted_ota_type)
}
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
let next = match current {
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Current slot is not ota0 or ota1");
}
};
Ok(next)
}
#[async_trait] #[async_trait]
pub trait BoardInteraction<'a> { pub trait BoardInteraction<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>; fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
@@ -133,6 +212,8 @@ pub trait BoardInteraction<'a> {
fn get_config(&mut self) -> &PlantControllerConfig; fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>; fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>; fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
async fn get_time(&mut self) -> DateTime<Utc>;
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()>;
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>; async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
async fn deep_sleep(&mut self, duration_in_ms: u64) -> !; async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
@@ -194,13 +275,7 @@ pub struct FreePeripherals<'a> {
pub gpio21: GPIO21<'a>, pub gpio21: GPIO21<'a>,
pub gpio22: GPIO22<'a>, pub gpio22: GPIO22<'a>,
pub gpio23: GPIO23<'a>, pub gpio23: GPIO23<'a>,
pub gpio24: GPIO24<'a>,
pub gpio25: GPIO25<'a>,
pub gpio26: GPIO26<'a>,
pub gpio27: GPIO27<'a>, pub gpio27: GPIO27<'a>,
pub gpio28: GPIO28<'a>,
pub gpio29: GPIO29<'a>,
pub gpio30: GPIO30<'a>,
pub twai: TWAI0<'a>, pub twai: TWAI0<'a>,
pub pcnt0: Unit<'a, 0>, pub pcnt0: Unit<'a, 0>,
pub pcnt1: Unit<'a, 1>, pub pcnt1: Unit<'a, 1>,
@@ -224,14 +299,12 @@ impl PlantHal {
esp_alloc::heap_allocator!(size: 64 * 1024); esp_alloc::heap_allocator!(size: 64 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000); esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let rtc: Rtc = Rtc::new(peripherals.LPWR); let mut rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR);
TIME_ACCESS rtc_peripheral.rwdt.disable();
.init(Mutex::new(rtc))
.map_err(|_| FatError::String {
error: "Init error rct".to_string(),
})?;
let systimer = SystemTimer::new(peripherals.SYSTIMER); let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
let boot_button = Input::new( let boot_button = Input::new(
peripherals.GPIO9, peripherals.GPIO9,
@@ -241,29 +314,13 @@ impl PlantHal {
// Reserve GPIO1 for deep sleep wake (configured just before entering sleep) // Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
let wake_gpio1 = peripherals.GPIO1; let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new(peripherals.RNG); let rng = Rng::new();
let timg0 = TimerGroup::new(peripherals.TIMG0); let (controller, interfaces) = esp_radio::wifi::new(peripherals.WIFI, Default::default())
let esp_wifi_ctrl = &*mk_static!( .expect("Could not init wifi");
EspWifiController<'static>,
init(timg0.timer0, rng.clone()).expect("Could not init wifi controller")
);
let (controller, interfaces) =
esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).expect("Could not init wifi");
use esp_hal::timer::systimer::SystemTimer;
esp_hal_embassy::init(systimer.alarm0);
//let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
//
let pcnt_module = Pcnt::new(peripherals.PCNT); let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals { let free_pins = FreePeripherals {
// can: peripherals.can,
// adc1: peripherals.adc1,
// pcnt0: peripherals.pcnt0,
// pcnt1: peripherals.pcnt1,
gpio0: peripherals.GPIO0, gpio0: peripherals.GPIO0,
gpio2: peripherals.GPIO2, gpio2: peripherals.GPIO2,
gpio3: peripherals.GPIO3, gpio3: peripherals.GPIO3,
@@ -284,13 +341,7 @@ impl PlantHal {
gpio21: peripherals.GPIO21, gpio21: peripherals.GPIO21,
gpio22: peripherals.GPIO22, gpio22: peripherals.GPIO22,
gpio23: peripherals.GPIO23, gpio23: peripherals.GPIO23,
gpio24: peripherals.GPIO24,
gpio25: peripherals.GPIO25,
gpio26: peripherals.GPIO26,
gpio27: peripherals.GPIO27, gpio27: peripherals.GPIO27,
gpio28: peripherals.GPIO28,
gpio29: peripherals.GPIO29,
gpio30: peripherals.GPIO30,
twai: peripherals.TWAI0, twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0, pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1, pcnt1: pcnt_module.unit1,
@@ -301,14 +352,19 @@ impl PlantHal {
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN], [u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN] [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
); );
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
// List all partitions - this is just FYI let bullshit = MutexFlashStorage {
for i in 0..pt.len() { inner: Arc::new(CriticalSectionMutex::new(RefCell::new(FlashStorage::new(
info!("{:?}", pt.get_partition(i)); peripherals.FLASH,
} )))),
};
let flash_storage = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_2 = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_3 = mk_static!(MutexFlashStorage, bullshit.clone());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?;
let ota_data = mk_static!( let ota_data = mk_static!(
PartitionEntry, PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
@@ -317,34 +373,39 @@ impl PlantHal {
.expect("No OTA data partition found") .expect("No OTA data partition found")
); );
let ota_data = mk_static!( let mut ota_data = ota_data.as_embedded_storage(mk_static!(
FlashRegion<FlashStorage>, RmwNorFlashStorage<&mut MutexFlashStorage>,
ota_data.as_embedded_storage(storage_ota) RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
); ));
let mut ota = esp_bootloader_esp_idf::ota::Ota::new(ota_data)?; let state_0 = ota_state(AppPartitionSubType::Ota0, &mut ota_data);
let state_1 = ota_state(AppPartitionSubType::Ota1, &mut ota_data);
let mut ota = Ota::new(ota_data, 2)?;
let running = get_current_slot(&pt, &mut ota)?;
let target = next_partition(running)?;
let ota_partition = match ota.current_slot()? { info!("Currently running OTA slot: {running:?}");
Slot::None => { info!("Updates will be stored in OTA slot: {target:?}");
panic!("No OTA slot active?"); info!("Slot0 state: {state_0:?}");
info!("Slot1 state: {state_1:?}");
//get current_state and next_state here!
let ota_target = match target {
AppPartitionSubType::Ota0 => pt
.find_partition(PartitionType::App(AppPartitionSubType::Ota0))?
.context("Partition table invalid no ota0")?,
AppPartitionSubType::Ota1 => pt
.find_partition(PartitionType::App(AppPartitionSubType::Ota1))?
.context("Partition table invalid no ota1")?,
_ => {
bail!("Invalid target partition");
} }
Slot::Slot0 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0,
))?
.expect("No OTA slot0 found"),
Slot::Slot1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.expect("No OTA slot1 found"),
}; };
let ota_next = mk_static!(PartitionEntry, ota_partition); let ota_target = mk_static!(PartitionEntry, ota_target);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new()); let ota_target = mk_static!(
let ota_next = mk_static!( FlashRegion<MutexFlashStorage>,
FlashRegion<FlashStorage>, ota_target.as_embedded_storage(flash_storage)
ota_next.as_embedded_storage(storage_ota)
); );
let data_partition = pt let data_partition = pt
@@ -354,32 +415,38 @@ impl PlantHal {
.expect("Data partition with littlefs not found"); .expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition); let data_partition = mk_static!(PartitionEntry, data_partition);
let storage_data = mk_static!(FlashStorage, FlashStorage::new());
let data = mk_static!( let data = mk_static!(
FlashRegion<FlashStorage>, FlashRegion<MutexFlashStorage>,
data_partition.as_embedded_storage(storage_data) data_partition.as_embedded_storage(flash_storage_3)
); );
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data }); let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate()); let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() { if lfs2filesystem.is_mountable() {
log::info!("Littlefs2 filesystem is mountable"); info!("Littlefs2 filesystem is mountable");
} else { } else {
match lfs2filesystem.format() { match lfs2filesystem.format() {
Result::Ok(..) => { Ok(..) => {
log::info!("Littlefs2 filesystem is formatted"); info!("Littlefs2 filesystem is formatted");
} }
Err(err) => { Err(err) => {
bail!("Littlefs2 filesystem could not be formatted: {:?}", err); error!("Littlefs2 filesystem could not be formatted: {err:?}");
} }
} }
} }
#[allow(clippy::arc_with_non_send_sync)]
let fs = Arc::new(Mutex::new( let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"), lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
)); ));
let ap = interfaces.ap;
let sta = interfaces.sta; let uart0 =
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
error: "Uart creation failed".to_string(),
})?;
let ap = interfaces.access_point;
let sta = interfaces.station;
let mut esp = Esp { let mut esp = Esp {
fs, fs,
rng, rng,
@@ -389,7 +456,12 @@ impl PlantHal {
boot_button, boot_button,
wake_gpio1, wake_gpio1,
ota, ota,
ota_next, ota_target,
current: running,
slot0_state: state_0,
slot1_state: state_1,
uart0,
rtc: rtc_peripheral,
}; };
//init,reset rtc memory depending on cause //init,reset rtc memory depending on cause
@@ -425,24 +497,21 @@ impl PlantHal {
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu", SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
}, },
}; };
LOG_ACCESS log(
.lock() LogMessage::ResetReason,
.await init_rtc_store as u32,
.log( to_config_mode as u32,
LogMessage::ResetReason, "",
init_rtc_store as u32, &format!("{reasons:?}"),
to_config_mode as u32, );
"",
&format!("{reasons:?}"),
)
.await;
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode) esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await; .await;
let config = esp.load_config().await; let config = esp.load_config().await;
log::info!("Init rtc driver"); info!("Init rtc driver");
let sda = peripherals.GPIO20; let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19; let scl = peripherals.GPIO19;
@@ -460,26 +529,30 @@ impl PlantHal {
RefCell<I2c<Blocking>>, RefCell<I2c<Blocking>>,
> = CriticalSectionMutex::new(RefCell::new(i2c)); > = CriticalSectionMutex::new(RefCell::new(i2c));
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver"); I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await; let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(&i2c_bus); let rtc_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(&i2c_bus); let mut bms_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(i2c_bus);
let mut rtc: Ds323x< let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>, I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
DS3231, DS3231,
> = Ds323x::new_ds3231(rtc_device); > = Ds323x::new_ds3231(rtc_device);
info!("Init rtc eeprom driver"); info!("Init rtc eeprom driver");
let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true)); let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true));
let rtc_time = rtc.datetime(); let rtc_time = rtc.datetime();
match rtc_time { match rtc_time {
Ok(tt) => { Ok(tt) => {
log::info!("Rtc Module reports time at UTC {}", tt); info!("Rtc Module reports time at UTC {tt}");
} }
Err(err) => { Err(err) => {
log::info!("Rtc Module could not be read {:?}", err); info!("Rtc Module could not be read {err:?}");
} }
} }
@@ -494,40 +567,26 @@ impl PlantHal {
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>; Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
let hal = match config { let hal = match config {
Result::Ok(config) => { Ok(config) => {
let battery_interaction: Box<dyn BatteryInteraction + Send> = let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery { match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}), BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => {
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut battery_driver = Bq34z100g1Driver {
i2c: battery_device,
delay: Delay::new(),
flash_block_data: [0; 32],
};
let status = print_battery_bq34z100(&mut battery_driver);
match status {
Ok(_) => {}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::BatteryCommunicationError,
0u32,
0,
"",
&format!("{err:?})"),
)
.await;
}
}
Box::new(BQ34Z100G1 { battery_driver })
}
BatteryBoardVersion::WchI2cSlave => { BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible let version = ProtocolVersion::read_from_i2c(&mut bms_device);
Box::new(NoBatteryMonitor {}) let version_val = match version {
Ok(v) => unsafe { core::mem::transmute::<ProtocolVersion, u32>(v) },
Err(_) => 0,
};
if version_val == 1 {
//Box::new(WCHI2CSlave { i2c: bms_device })
// todo fix the type above
Box::new(NoBatteryMonitor {})
} else {
//todo should be an error variant instead?
Box::new(NoBatteryMonitor {})
}
} }
BatteryBoardVersion::BQ34Z100G1 => Box::new(NoBatteryMonitor {}),
}; };
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board { let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
@@ -546,17 +605,13 @@ impl PlantHal {
HAL { board_hal } HAL { board_hal }
} }
Err(err) => { Err(err) => {
LOG_ACCESS log(
.lock() LogMessage::ConfigModeMissingConfig,
.await 0,
.log( 0,
LogMessage::ConfigModeMissingConfig, "",
0, &err.to_string(),
0, );
"",
&err.to_string(),
)
.await;
HAL { HAL {
board_hal: initial_hal::create_initial_board( board_hal: initial_hal::create_initial_board(
free_pins, free_pins,
@@ -569,25 +624,13 @@ impl PlantHal {
Ok(Mutex::new(hal)) Ok(Mutex::new(hal))
} }
}
pub async fn esp_time() -> DateTime<Utc> { /// Feed the watchdog timer to prevent system reset
let guard = TIME_ACCESS.get().await.lock().await; pub fn feed_watchdog() {
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap() if let Some(wdt_mutex) = WATCHDOG.try_get() {
} wdt_mutex.lock(|cell| {
cell.borrow_mut().feed();
pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> { });
{ }
let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(time.timestamp_micros() as u64);
} }
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.get_rtc_module()
.set_rtc_time(&time.to_utc())
.await
} }

View File

@@ -0,0 +1,65 @@
use alloc::sync::Arc;
use core::cell::RefCell;
use core::ops::{Deref, DerefMut};
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
use embedded_storage::ReadStorage;
use esp_storage::{FlashStorage, FlashStorageError};
use log::info;
#[derive(Clone)]
pub struct MutexFlashStorage {
pub(crate) inner: Arc<CriticalSectionMutex<RefCell<FlashStorage<'static>>>>,
}
impl ReadStorage for MutexFlashStorage {
type Error = FlashStorageError;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> {
self.inner
.lock(|f| ReadStorage::read(f.borrow_mut().deref_mut(), offset, bytes))
}
fn capacity(&self) -> usize {
self.inner
.lock(|f| ReadStorage::capacity(f.borrow().deref()))
}
}
impl embedded_storage::Storage for MutexFlashStorage {
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
NorFlash::write(self, offset, bytes)
}
}
impl ErrorType for MutexFlashStorage {
type Error = FlashStorageError;
}
impl ReadNorFlash for MutexFlashStorage {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
ReadStorage::read(self, offset, bytes)
}
fn capacity(&self) -> usize {
ReadStorage::capacity(self)
}
}
impl NorFlash for MutexFlashStorage {
const WRITE_SIZE: usize = 1;
const ERASE_SIZE: usize = 4096;
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
info!("Erasing flash from 0x{:x} to 0x{:x}", from, to);
self.inner
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
self.inner
.lock(|f| NorFlash::write(f.borrow_mut().deref_mut(), offset, bytes))
}
}

View File

@@ -1,11 +1,11 @@
use crate::bail; use crate::bail;
use crate::fat_error::FatError; use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::{hold_disable, hold_enable}; use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40; use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS}; use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT};
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{log, LogMessage, LOG_ACCESS};
use crate::{ use crate::{
config::PlantControllerConfig, config::PlantControllerConfig,
hal::{battery::BatteryInteraction, esp::Esp}, hal::{battery::BatteryInteraction, esp::Esp},
@@ -14,6 +14,7 @@ use alloc::boxed::Box;
use alloc::format; use alloc::format;
use alloc::string::ToString; use alloc::string::ToString;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_time::Timer; use embassy_time::Timer;
@@ -195,6 +196,17 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> { fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module &mut self.rtc_module
} }
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> { async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
let shift_register = self.shift_register.lock().await; let shift_register = self.shift_register.lock().await;
if charging { if charging {
@@ -207,8 +219,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low(); let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low();
let guard = TIME_ACCESS.get().await.lock().await; self.esp.deep_sleep(duration_in_ms)
self.esp.deep_sleep(duration_in_ms, guard)
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {
@@ -362,17 +373,13 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Timer::after_millis(10).await; Timer::after_millis(10).await;
let unscaled = self.signal_counter.value(); let unscaled = self.signal_counter.value();
let hz = unscaled as f32 * factor; let hz = unscaled as f32 * factor;
LOG_ACCESS log(
.lock() LogMessage::RawMeasure,
.await unscaled as u32,
.log( hz as u32,
LogMessage::RawMeasure, &plant.to_string(),
unscaled as u32, &format!("{sensor:?}"),
hz as u32, );
&plant.to_string(),
&format!("{sensor:?}"),
)
.await;
results[repeat] = hz; results[repeat] = hz;
} }
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
@@ -425,11 +432,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Ok(b) => b as u32, Ok(b) => b as u32,
Err(_) => u32::MAX, Err(_) => u32::MAX,
}; };
LOG_ACCESS log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "");
.lock()
.await
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
.await;
} }
Timer::after_millis(10).await; Timer::after_millis(10).await;
Ok(()) Ok(())

View File

@@ -3,10 +3,11 @@ use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::{hold_disable, hold_enable, Esp}; use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction; use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS}; use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::string::ToString; use alloc::string::ToString;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
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::Timer; use embassy_time::Timer;
@@ -326,6 +327,16 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
&mut self.rtc_module &mut self.rtc_module
} }
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> { async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
self.charger.set_charge_indicator(charging) self.charger.set_charge_indicator(charging)
} }
@@ -333,8 +344,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! { async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low(); self.awake.set_low();
self.charger.power_save(); self.charger.power_save();
let rtc = TIME_ACCESS.get().await.lock().await; self.esp.deep_sleep(duration_in_ms);
self.esp.deep_sleep(duration_in_ms, rtc);
} }
fn is_day(&self) -> bool { fn is_day(&self) -> bool {

View File

@@ -2,22 +2,25 @@ use crate::bail;
use crate::fat_error::FatError; use crate::fat_error::FatError;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE}; use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use embassy_time::Timer; use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation}; use esp_hal::analog::adc::{Adc, AdcCalLine, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay; use esp_hal::delay::Delay;
use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull}; use esp_hal::gpio::{DriveMode, Flex, Input, InputConfig, Output, OutputConfig, Pull};
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::unit::Unit; use esp_hal::pcnt::unit::Unit;
use esp_hal::peripherals::GPIO5; use esp_hal::peripherals::GPIO5;
use esp_hal::Blocking; use esp_hal::Async;
use esp_println::println; use esp_println::println;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20}; use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
unsafe impl Send for TankSensor<'_> {}
pub struct TankSensor<'a> { pub struct TankSensor<'a> {
one_wire_bus: OneWire<Flex<'a>>, one_wire_bus: OneWire<Flex<'a>>,
tank_channel: Adc<'a, ADC1<'a>, Blocking>, tank_channel: Adc<'a, ADC1<'a>, Async>,
tank_power: Output<'a>, tank_power: Output<'a>,
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>, tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>, AdcCalLine<ADC1<'a>>>,
// flow_counter: PcntDriver<'a>, flow_counter: Unit<'a, 1>,
// delay: Delay,
} }
impl<'a> TankSensor<'a> { impl<'a> TankSensor<'a> {
@@ -29,62 +32,55 @@ impl<'a> TankSensor<'a> {
flow_sensor: Input, flow_sensor: Input,
pcnt1: Unit<'a, 1>, pcnt1: Unit<'a, 1>,
) -> Result<TankSensor<'a>, FatError> { ) -> Result<TankSensor<'a>, FatError> {
one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None)); one_wire_pin.apply_output_config(
&OutputConfig::default()
.with_drive_mode(DriveMode::OpenDrain)
.with_pull(Pull::None),
);
one_wire_pin.apply_input_config(&InputConfig::default().with_pull(Pull::None));
one_wire_pin.set_high();
one_wire_pin.set_input_enable(true);
one_wire_pin.set_output_enable(true);
let mut adc1_config = AdcConfig::new(); let mut adc1_config = AdcConfig::new();
let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB); let tank_pin =
let tank_channel = Adc::new(adc1, adc1_config); adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
let tank_channel = Adc::new(adc1, adc1_config).into_async();
let one_wire_bus = OneWire::new(one_wire_pin, false); let one_wire_bus = OneWire::new(one_wire_pin, false);
// pcnt1.set_high_limit(Some(i16::MAX))?;
// let mut flow_counter = PcntDriver::new(
// pcnt1, let ch0 = &pcnt1.channel0;
// Some(flow_sensor_pin), ch0.set_edge_signal(flow_sensor.peripheral_input());
// Option::<AnyInputPin>::None, ch0.set_input_mode(Hold, Increment);
// Option::<AnyInputPin>::None, ch0.set_ctrl_mode(Keep, Keep);
// Option::<AnyInputPin>::None, pcnt1.listen();
// )?;
//
// flow_counter.channel_config(
// PcntChannel::Channel1,
// PinIndex::Pin0,
// PinIndex::Pin1,
// &PcntChannelConfig {
// lctrl_mode: PcntControlMode::Keep,
// hctrl_mode: PcntControlMode::Keep,
// pos_mode: PcntCountMode::Increment,
// neg_mode: PcntCountMode::Hold,
// counter_h_lim: i16::MAX,
// counter_l_lim: 0,
// },
// )?;
//
Ok(TankSensor { Ok(TankSensor {
one_wire_bus, one_wire_bus,
tank_channel, tank_channel,
tank_power, tank_power,
tank_pin, // flow_counter, tank_pin,
// delay: Default::default(), flow_counter: pcnt1,
}) })
} }
pub fn reset_flow_meter(&mut self) { pub fn reset_flow_meter(&mut self) {
// self.flow_counter.counter_pause().unwrap(); self.flow_counter.pause();
// self.flow_counter.counter_clear().unwrap(); self.flow_counter.clear();
} }
pub fn start_flow_meter(&mut self) { pub fn start_flow_meter(&mut self) {
//self.flow_counter.counter_resume().unwrap(); self.flow_counter.resume();
} }
pub fn get_flow_meter_value(&mut self) -> i16 { pub fn get_flow_meter_value(&mut self) -> i16 {
//self.flow_counter.get_counter_value().unwrap() self.flow_counter.value()
5_i16
} }
pub fn stop_flow_meter(&mut self) -> i16 { pub fn stop_flow_meter(&mut self) -> i16 {
//self.flow_counter.counter_pause().unwrap(); self.flow_counter.pause();
self.get_flow_meter_value() self.get_flow_meter_value()
} }
@@ -92,15 +88,33 @@ impl<'a> TankSensor<'a> {
//multisample should be moved to water_temperature_c //multisample should be moved to water_temperature_c
let mut attempt = 1; let mut attempt = 1;
let mut delay = Delay::new(); let mut delay = Delay::new();
self.one_wire_bus.reset(&mut delay)?;
let presence = self.one_wire_bus.reset(&mut delay)?;
println!("OneWire: reset presence pulse = {}", presence);
if !presence {
println!("OneWire: no device responded to reset — check pull-up resistor and wiring");
}
let mut search = DeviceSearch::new(); let mut search = DeviceSearch::new();
let mut water_temp_sensor: Option<Device> = None; let mut water_temp_sensor: Option<Device> = None;
let mut devices_found = 0u8;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? { while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
devices_found += 1;
println!(
"OneWire: found device #{} family=0x{:02X} addr={:02X?}",
devices_found, device.address[0], device.address
);
if device.address[0] == ds18b20::FAMILY_CODE { if device.address[0] == ds18b20::FAMILY_CODE {
water_temp_sensor = Some(device); water_temp_sensor = Some(device);
break; break;
} else {
println!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE);
} }
} }
if devices_found == 0 {
println!("OneWire: search found zero devices on the bus");
}
match water_temp_sensor { match water_temp_sensor {
Some(device) => { Some(device) => {
println!("Found one wire device: {:?}", device); println!("Found one wire device: {:?}", device);
@@ -152,17 +166,15 @@ impl<'a> TankSensor<'a> {
Timer::after_millis(100).await; Timer::after_millis(100).await;
let mut store = [0_u16; TANK_MULTI_SAMPLE]; let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE { for sample in store.iter_mut() {
let value = self.tank_channel.read_oneshot(&mut self.tank_pin); *sample = self.tank_channel.read_oneshot(&mut self.tank_pin).await;
//force yield //force yield between successful samples
Timer::after_millis(10).await; Timer::after_millis(10).await;
store[multisample] = value.unwrap();
} }
self.tank_power.set_low(); self.tank_power.set_low();
store.sort(); store.sort();
//TODO probably wrong? check! let median_mv = store[TANK_MULTI_SAMPLE / 2] as f32;
let median_mv = store[6] as f32 * 3300_f32 / 4096_f32; Ok(median_mv / 1000.0)
Ok(median_mv)
} }
} }

108
rust/src/log/interceptor.rs Normal file
View File

@@ -0,0 +1,108 @@
use alloc::string::String;
use alloc::vec::Vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
use log::{LevelFilter, Log, Metadata, Record};
const MAX_LIVE_LOG_ENTRIES: usize = 64;
struct LiveLogBuffer {
entries: Vec<(u64, String)>,
next_seq: u64,
}
impl LiveLogBuffer {
const fn new() -> Self {
Self {
entries: Vec::new(),
next_seq: 0,
}
}
fn push(&mut self, text: String) {
if self.entries.len() >= MAX_LIVE_LOG_ENTRIES {
self.entries.remove(0);
}
self.entries.push((self.next_seq, text));
self.next_seq += 1;
}
fn get_after(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
let next_seq = self.next_seq;
match after {
None => (self.entries.clone(), false, next_seq),
Some(after_seq) => {
let result: Vec<_> = self.entries
.iter()
.filter(|(seq, _)| *seq > after_seq)
.cloned()
.collect();
// Dropped if there are entries that should exist (seq > after_seq) but
// the oldest retained entry has a higher seq than after_seq + 1.
let dropped = if next_seq > after_seq.saturating_add(1) {
if let Some((oldest_seq, _)) = self.entries.first() {
*oldest_seq > after_seq.saturating_add(1)
} else {
// Buffer empty but entries were written — all dropped
true
}
} else {
false
};
(result, dropped, next_seq)
}
}
}
}
pub struct InterceptorLogger {
live_log: BlockingMutex<CriticalSectionRawMutex, core::cell::RefCell<LiveLogBuffer>>,
}
impl InterceptorLogger {
pub const fn new() -> Self {
Self {
live_log: BlockingMutex::new(core::cell::RefCell::new(LiveLogBuffer::new())),
}
}
/// Returns (entries_after, dropped, next_seq).
/// Pass `after = None` to retrieve the entire current buffer.
/// Pass `after = Some(seq)` to retrieve only entries with seq > that value.
pub fn get_live_logs(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
self.live_log.lock(|buf| buf.borrow().get_after(after))
}
pub fn init(&'static self) {
match log::set_logger(self).map(|()| log::set_max_level(LevelFilter::Info)) {
Ok(()) => {}
Err(_e) => {
esp_println::println!("ERROR: Logger already set");
}
}
}
}
impl Log for InterceptorLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let message = alloc::format!("{}: {}", record.level(), record.args());
// Print to serial
esp_println::println!("{}", message);
// Store in live log ring buffer
self.live_log.lock(|buf| {
buf.borrow_mut().push(message);
});
}
}
fn flush(&self) {}
}

View File

@@ -1,20 +1,21 @@
use crate::hal::TIME_ACCESS;
use crate::vec; use crate::vec;
use crate::BOARD_ACCESS;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::vec::Vec; use alloc::vec::Vec;
use bytemuck::{AnyBitPattern, Pod, Zeroable}; use bytemuck::{AnyBitPattern, Pod, Zeroable};
use deranged::RangedU8; use deranged::RangedU8;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use esp_hal::Persistable; use esp_hal::Persistable;
use log::info; use log::{info, warn};
use serde::Serialize; use serde::Serialize;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use unit_enum::UnitEnum; use unit_enum::UnitEnum;
const LOG_ARRAY_SIZE: u8 = 220; const LOG_ARRAY_SIZE: u8 = 220;
const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1; const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1;
#[esp_hal::ram(rtc_fast, persistent)] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOG_ARRAY: LogArray = LogArray { static mut LOG_ARRAY: LogArray = LogArray {
buffer: [LogEntryInner { buffer: [LogEntryInner {
timestamp: 0, timestamp: 0,
@@ -26,8 +27,45 @@ static mut LOG_ARRAY: LogArray = LogArray {
}; LOG_ARRAY_SIZE as usize], }; LOG_ARRAY_SIZE as usize],
head: 0, head: 0,
}; };
// this is the only reference created for LOG_ARRAY and the only way to access it
#[allow(static_mut_refs)]
pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> = pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
unsafe { Mutex::new(&mut *&raw mut LOG_ARRAY) }; unsafe { Mutex::new(&mut LOG_ARRAY) };
mod interceptor;
pub use interceptor::InterceptorLogger;
pub static INTERCEPTOR: InterceptorLogger = InterceptorLogger::new();
pub struct LogRequest {
pub message_key: LogMessage,
pub number_a: u32,
pub number_b: u32,
pub txt_short: heapless::String<TXT_SHORT_LENGTH>,
pub txt_long: heapless::String<TXT_LONG_LENGTH>,
}
static LOG_CHANNEL: Channel<CriticalSectionRawMutex, LogRequest, 16> = Channel::new();
#[embassy_executor::task]
pub async fn log_task() {
loop {
let request = LOG_CHANNEL.receive().await;
LOG_ACCESS
.lock()
.await
.log(
request.message_key,
request.number_a,
request.number_b,
request.txt_short.as_str(),
request.txt_long.as_str(),
)
.await;
}
}
const TXT_SHORT_LENGTH: usize = 8; const TXT_SHORT_LENGTH: usize = 8;
const TXT_LONG_LENGTH: usize = 32; const TXT_LONG_LENGTH: usize = 32;
@@ -77,10 +115,31 @@ impl From<LogEntryInner> for LogEntry {
} }
} }
pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str) {
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack);
match LOG_CHANNEL.try_send(LogRequest {
message_key,
number_a,
number_b,
txt_short: txt_short_stack,
txt_long: txt_long_stack,
}) {
Ok(_) => {}
Err(_) => {
warn!("Log channel full, dropping log entry");
}
}
}
impl LogArray { impl LogArray {
pub fn get(&mut self) -> Vec<LogEntry> { pub fn get(&mut self) -> Vec<LogEntry> {
let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> = let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap()); RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
let mut rv: Vec<LogEntry> = Vec::new(); let mut rv: Vec<LogEntry> = Vec::new();
let mut index = head.wrapping_sub(1); let mut index = head.wrapping_sub(1);
@@ -103,17 +162,11 @@ impl LogArray {
txt_long: &str, txt_long: &str,
) { ) {
let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> = let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap()); RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack);
let time = { let time = {
let guard = TIME_ACCESS.get().await.lock().await; let mut guard = BOARD_ACCESS.get().await.lock().await;
guard.current_time_us() guard.board_hal.get_esp().rtc.current_time_us()
} / 1000; } / 1000;
let ordinal = message_key.ordinal() as u16; let ordinal = message_key.ordinal() as u16;
@@ -124,19 +177,15 @@ impl LogArray {
template_string = template_string.replace("${txt_long}", txt_long); template_string = template_string.replace("${txt_long}", txt_long);
template_string = template_string.replace("${txt_short}", txt_short); template_string = template_string.replace("${txt_short}", txt_short);
info!("{}", template_string); info!("{template_string}");
let to_modify = &mut self.buffer[head.get() as usize]; let to_modify = &mut self.buffer[head.get() as usize];
to_modify.timestamp = time; to_modify.timestamp = time;
to_modify.message_id = ordinal; to_modify.message_id = ordinal;
to_modify.a = number_a; to_modify.a = number_a;
to_modify.b = number_b; to_modify.b = number_b;
to_modify to_modify.txt_short.clone_from_slice(txt_short.as_bytes());
.txt_short to_modify.txt_long.clone_from_slice(txt_long.as_bytes());
.clone_from_slice(&txt_short_stack.as_bytes());
to_modify
.txt_long
.clone_from_slice(&txt_long_stack.as_bytes());
head = head.wrapping_add(1); head = head.wrapping_add(1);
self.head = head.get(); self.head = head.get();
} }
@@ -148,18 +197,37 @@ fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<L
Ok(_) => {} //continue adding chars Ok(_) => {} //continue adding chars
Err(_) => { Err(_) => {
//clear space for two asci chars //clear space for two asci chars
info!("pushing char {char} to limit {LIMIT} current value {target} input {input}");
while target.len() + 2 >= LIMIT { while target.len() + 2 >= LIMIT {
target.pop().unwrap(); target.pop();
} }
//add .. to shortened strings //add .. to shortened strings
target.push('.').unwrap(); match target.push('.') {
target.push('.').unwrap(); Ok(_) => {}
return; Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
match target.push('.') {
Ok(_) => {}
Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
} }
} }
} }
while target.len() < LIMIT { while target.len() < LIMIT {
target.push(' ').unwrap(); match target.push(' ') {
Ok(_) => {}
Err(_) => {
warn!("Error pushing space to limit {LIMIT} current value {target} input {input}")
}
}
} }
} }
@@ -243,6 +311,20 @@ pub enum LogMessage {
PumpOpenLoopCurrent, PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")] #[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent, PumpMissingSensorCurrent,
#[strum(
serialize = "Fertilizer applied for ${number_a}s on plant ${number_b} (last application ${txt_short} minutes ago)"
)]
FertilizerApplied,
#[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError,
#[strum(
serialize = "Trace: a: ${number_a} b: ${number_b} txt_s ${txt_short} long ${txt_long}"
)]
Trace,
#[strum(serialize = "Parsing error reading message")]
UnknownMessage,
#[strum(serialize = "Going to deep sleep for ${number_a} minutes")]
DeepSleep,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -261,9 +343,9 @@ impl From<&LogMessage> for MessageTranslation {
} }
impl LogMessage { impl LogMessage {
pub fn to_log_localisation_config() -> Vec<MessageTranslation> { pub fn log_localisation_config() -> Vec<MessageTranslation> {
Vec::from_iter((0..LogMessage::len()).map(|i| { Vec::from_iter((0..LogMessage::len()).map(|i| {
let msg_type = LogMessage::from_ordinal(i).unwrap(); let msg_type = LogMessage::from_ordinal(i).unwrap_or(LogMessage::UnknownMessage);
(&msg_type).into() (&msg_type).into()
})) }))
} }

View File

@@ -11,20 +11,22 @@
//TODO insert version here and read it in other parts, also read this for the ota webview //TODO insert version here and read it in other parts, also read this for the ota webview
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
use alloc::vec::Vec;
use config::PlantControllerConfig;
use esp_backtrace as _; use esp_backtrace as _;
use hal::PROGRESS_ACTIVE;
use crate::config::{NetworkConfig, PlantConfig}; use crate::config::{NetworkConfig, PlantConfig};
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::esp::MQTT_STAY_ALIVE; use crate::hal::esp::MQTT_STAY_ALIVE;
use crate::hal::{esp_time, TIME_ACCESS}; use crate::log::log;
use crate::log::LOG_ACCESS;
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH}; use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
use crate::webserver::http_server; use crate::webserver::http_server;
use crate::{ use crate::{
config::BoardVersion::INITIAL, config::BoardVersion::INITIAL,
hal::{PlantHal, HAL, PLANT_COUNT}, hal::{PlantHal, HAL, PLANT_COUNT},
}; };
use ::log::{info, warn}; use ::log::{info, warn, error};
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::sync::Arc; use alloc::sync::Arc;
@@ -37,7 +39,7 @@ use embassy_net::Stack;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embassy_time::Timer; use embassy_time::{Duration, Instant, Timer};
use esp_hal::rom::ets_delay_us; use esp_hal::rom::ets_delay_us;
use esp_hal::system::software_reset; use esp_hal::system::software_reset;
use esp_println::{logger, println}; use esp_println::{logger, println};
@@ -165,35 +167,28 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let cur = match board.board_hal.get_rtc_module().get_rtc_time().await { let cur = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(value) => { Ok(value) => {
{ {
let guard = TIME_ACCESS.get().await.lock().await; board.board_hal.set_time(&value.fixed_offset()).await;
guard.set_current_time_us(value.timestamp_micros() as u64);
} }
value value
} }
Err(err) => { Err(err) => {
info!("rtc module error: {:?}", err); info!("rtc module error: {:?}", err);
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
esp_time().await board.board_hal.get_time().await
} }
}; };
info!("Step 1");
//check if we know the time current > 2020 (plausibility checks, this code is newer than 2020) //check if we know the time current > 2020 (plausibility checks, this code is newer than 2020)
if cur.year() < 2020 { if cur.year() < 2020 {
to_config = true; to_config = true;
LOG_ACCESS log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "");
.lock()
.await
.log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "")
.await;
} }
info!("cur is {}", cur); info!("cur is {}", cur);
update_charge_indicator(&mut board).await; update_charge_indicator(&mut board).await;
if board.board_hal.get_esp().get_restart_to_conf() { if board.board_hal.get_esp().get_restart_to_conf() {
LOG_ACCESS log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "");
.lock()
.await
.log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "")
.await;
for _i in 0..2 { for _i in 0..2 {
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
Timer::after_millis(100).await; Timer::after_millis(100).await;
@@ -205,11 +200,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
board.board_hal.get_esp().set_restart_to_conf(false); board.board_hal.get_esp().set_restart_to_conf(false);
} else if board.board_hal.get_esp().mode_override_pressed() { } else if board.board_hal.get_esp().mode_override_pressed() {
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
LOG_ACCESS log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "");
.lock()
.await
.log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "")
.await;
for _i in 0..5 { for _i in 0..5 {
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
Timer::after_millis(100).await; Timer::after_millis(100).await;
@@ -232,18 +223,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
{ {
info!("No wifi configured, starting initial config mode"); info!("No wifi configured, starting initial config mode");
let stack = board.board_hal.get_esp().wifi_ap().await?; let stack = board.board_hal.get_esp().wifi_ap(spawner).await?;
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver"); println!("starting webserver");
spawner.spawn(http_server(reboot_now.clone(), stack))?; spawner.spawn(http_server(reboot_now.clone(), stack)?);
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MissingConfig, reboot_now.clone(), UTC).await;
} }
let mut stack: OptionLock<Stack> = OptionLock::empty(); let mut stack: OptionLock<Stack> = OptionLock::empty();
let network_mode = if board.board_hal.get_config().network.ssid.is_some() { let network_mode = if board.board_hal.get_config().network.ssid.is_some() {
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await try_connect_wifi_sntp_mqtt(&mut board, &mut stack, spawner).await
} else { } else {
info!("No wifi configured"); info!("No wifi configured");
//the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings; //the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
@@ -256,7 +247,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let res = { let res = {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
esp.wifi_ap().await esp.wifi_ap(spawner).await
}; };
match res { match res {
Ok(ap_stack) => { Ok(ap_stack) => {
@@ -291,39 +282,36 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let _ = publish_mppt_state(&mut board).await; let _ = publish_mppt_state(&mut board).await;
} }
LOG_ACCESS log(
.lock() LogMessage::StartupInfo,
.await matches!(network_mode, NetworkMode::WIFI { .. }) as u32,
.log( matches!(
LogMessage::StartupInfo, network_mode,
matches!(network_mode, NetworkMode::WIFI { .. }) as u32, NetworkMode::WIFI {
matches!( sntp: SntpMode::SYNC { .. },
network_mode, ..
NetworkMode::WIFI { }
sntp: SntpMode::SYNC { .. }, ) as u32,
.. matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. })
} .to_string()
) as u32, .as_str(),
matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. }) "",
.to_string() );
.as_str(),
"",
)
.await;
if to_config { if to_config {
//check if client or ap mode and init Wi-Fi //check if client or ap mode and init Wi-Fi
info!("executing config mode override"); info!("executing config mode override");
//config upload will trigger reboot! //config upload will trigger reboot!
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
spawner.spawn(http_server(reboot_now.clone(), stack.take().unwrap()))?; let stack_val = stack.take();
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await; if let Some(s) = stack_val {
spawner.spawn(http_server(reboot_now.clone(), s)?);
} else {
bail!("Network stack missing, hard abort")
}
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone(), UTC).await;
} else { } else {
LOG_ACCESS log(LogMessage::NormalRun, 0, 0, "", "");
.lock()
.await
.log(LogMessage::NormalRun, 0, 0, "", "")
.await;
} }
let _dry_run = false; let _dry_run = false;
@@ -334,38 +322,22 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) { if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
match err { match err {
TankError::SensorDisabled => { /* unreachable */ } TankError::SensorDisabled => { /* unreachable */ }
TankError::SensorMissing(raw_value_mv) => { TankError::SensorMissing(raw_value_mv) => log(
LOG_ACCESS LogMessage::TankSensorMissing,
.lock() raw_value_mv as u32,
.await 0,
.log( "",
LogMessage::TankSensorMissing, "",
raw_value_mv as u32, ),
0, TankError::SensorValueError { value, min, max } => log(
"", LogMessage::TankSensorValueRangeError,
"", min as u32,
) max as u32,
.await &format!("{value}"),
} "",
TankError::SensorValueError { value, min, max } => { ),
LOG_ACCESS
.lock()
.await
.log(
LogMessage::TankSensorValueRangeError,
min as u32,
max as u32,
&format!("{}", value),
"",
)
.await
}
TankError::BoardError(err) => { TankError::BoardError(err) => {
LOG_ACCESS log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
.lock()
.await
.log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
.await
} }
} }
// disabled cannot trigger this because of wrapping if is_enabled // disabled cannot trigger this because of wrapping if is_enabled
@@ -374,11 +346,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.warn_level(&board.board_hal.get_config().tank) .warn_level(&board.board_hal.get_config().tank)
.is_ok_and(|warn| warn) .is_ok_and(|warn| warn)
{ {
LOG_ACCESS log(LogMessage::TankWaterLevelLow, 0, 0, "", "");
.lock()
.await
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
.await;
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
} }
} }
@@ -620,7 +588,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
if stay_alive { if stay_alive {
let reboot_now = Arc::new(AtomicBool::new(false)); let reboot_now = Arc::new(AtomicBool::new(false));
let _webserver = http_server(reboot_now.clone(), stack.take().unwrap()); let _webserver = http_server(reboot_now.clone(), stack.take().unwrap());
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MqttConfig, reboot_now.clone(), UTC).await;
} else { } else {
//TODO wait for all mqtt publishes? //TODO wait for all mqtt publishes?
Timer::after_millis(5000).await; Timer::after_millis(5000).await;
@@ -671,17 +639,13 @@ pub async fn do_secure_pump(
let high_current = current_ma > plant_config.max_pump_current_ma; let high_current = current_ma > plant_config.max_pump_current_ma;
if high_current { if high_current {
if first_error { if first_error {
LOG_ACCESS log(
.lock() LogMessage::PumpOverCurrent,
.await plant_id as u32 + 1,
.log( current_ma as u32,
LogMessage::PumpOverCurrent, plant_config.max_pump_current_ma.to_string().as_str(),
plant_id as u32 + 1, step.to_string().as_str(),
current_ma as u32, );
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?; board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
@@ -694,17 +658,13 @@ pub async fn do_secure_pump(
let low_current = current_ma < plant_config.min_pump_current_ma; let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current { if low_current {
if first_error { if first_error {
LOG_ACCESS log(
.lock() LogMessage::PumpOpenLoopCurrent,
.await plant_id as u32 + 1,
.log( current_ma as u32,
LogMessage::PumpOpenLoopCurrent, plant_config.min_pump_current_ma.to_string().as_str(),
plant_id as u32 + 1, step.to_string().as_str(),
current_ma as u32, );
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?; board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
@@ -718,17 +678,13 @@ pub async fn do_secure_pump(
Err(err) => { Err(err) => {
if !plant_config.ignore_current_error { if !plant_config.ignore_current_error {
info!("Error getting pump current: {}", err); info!("Error getting pump current: {}", err);
LOG_ACCESS log(
.lock() LogMessage::PumpMissingSensorCurrent,
.await plant_id as u32,
.log( 0,
LogMessage::PumpMissingSensorCurrent, "",
plant_id as u32, "",
0, );
"",
"",
)
.await;
error = true; error = true;
break; break;
} else { } else {
@@ -761,25 +717,15 @@ pub async fn do_secure_pump(
async fn update_charge_indicator( async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) { ) -> FatResult<()> {
//FIXME add config and code to allow power supply mode, in this case this is a nop
//we have mppt controller, ask it for charging current //we have mppt controller, ask it for charging current
if let Ok(current) = board.board_hal.get_mptt_current().await { let current = board.board_hal.get_mptt_current().await?;
let _ = board board
.board_hal
.set_charge_indicator(current.as_milliamperes() > 20_f64);
}
//fallback to battery controller and ask it instead
else if let Ok(charging) = board
.board_hal .board_hal
.get_battery_monitor() .set_charge_indicator(current.as_milliamperes() > 20_f64)
.average_current_milli_ampere() .await?;
.await Ok(())
{
let _ = board.board_hal.set_charge_indicator(charging > 20);
} else {
//who knows
let _ = board.board_hal.set_charge_indicator(false);
}
} }
async fn publish_tank_state( async fn publish_tank_state(
@@ -818,25 +764,16 @@ async fn publish_plant_states(
async fn publish_firmware_info( async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
version: VersionInfo, version: VersionInfo,
ip_address: &String, ip_address: &str,
timezone_time: &String, timezone_time: &str,
) { ) {
let esp = board.board_hal.get_esp(); let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await; esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp esp.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
.mqtt_publish("/firmware/githash", &version.git_hash)
.await; .await;
let _ = esp esp.mqtt_publish("/firmware/last_online", timezone_time)
.mqtt_publish("/firmware/buildtime", &version.build_time)
.await; .await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time); esp.mqtt_publish("/state", "online").await;
let state = esp.get_ota_state();
let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
let slot = esp.get_ota_slot();
let _ = esp
.mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
.await;
let _ = esp.mqtt_publish("/state", "online").await;
} }
macro_rules! mk_static { macro_rules! mk_static {
($t:ty,$val:expr) => {{ ($t:ty,$val:expr) => {{
@@ -849,25 +786,25 @@ macro_rules! mk_static {
async fn try_connect_wifi_sntp_mqtt( async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
stack_store: &mut OptionLock<Stack<'static>>, stack_store: &mut OptionLock<Stack<'static>>,
spawner: Spawner,
) -> NetworkMode { ) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone(); let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await { match board.board_hal.get_esp().wifi(nw_conf, spawner).await {
Ok(stack) => { Ok(stack) => {
stack_store.replace(stack); stack_store.replace(stack);
let sntp_mode: SntpMode = match board let sntp_mode: SntpMode = match board.board_hal.get_esp().sntp(1000 * 10, stack).await {
.board_hal
.get_esp()
.sntp(1000 * 10, stack.clone())
.await
{
Ok(new_time) => { Ok(new_time) => {
info!("Using time from sntp {}", new_time.to_rfc3339()); info!("Using time from sntp {}", new_time.to_rfc3339());
let _ = board.board_hal.get_rtc_module().set_rtc_time(&new_time); let _ = board
.board_hal
.get_rtc_module()
.set_rtc_time(&new_time)
.await;
SntpMode::SYNC { current: new_time } SntpMode::SYNC { current: new_time }
} }
Err(err) => { Err(err) => {
warn!("sntp error: {}", err); warn!("sntp error: {err}");
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
SntpMode::OFFLINE SntpMode::OFFLINE
} }
@@ -876,27 +813,40 @@ async fn try_connect_wifi_sntp_mqtt(
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() { let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
let nw_config = board.board_hal.get_config().network.clone(); let nw_config = board.board_hal.get_config().network.clone();
let nw_config = mk_static!(NetworkConfig, nw_config); let nw_config = mk_static!(NetworkConfig, nw_config);
match board.board_hal.get_esp().mqtt(nw_config, stack).await { match board
.board_hal
.get_esp()
.mqtt(nw_config, stack, spawner)
.await
{
Ok(_) => { Ok(_) => {
info!("Mqtt connection ready"); info!("Mqtt connection ready");
true true
} }
Err(err) => { Err(err) => {
warn!("Could not connect mqtt due to {}", err); warn!("Could not connect mqtt due to {err}");
false false
} }
} }
} else { } else {
false false
}; };
let ip = match stack.config_v4() {
Some(config) => config.address.address().to_string(),
None => match stack.config_v6() {
Some(config) => config.address.address().to_string(),
None => String::from("No IP"),
},
};
NetworkMode::WIFI { NetworkMode::WIFI {
sntp: sntp_mode, sntp: sntp_mode,
mqtt: mqtt_connected, mqtt: mqtt_connected,
ip_address: stack.hardware_address().to_string(), ip_address: ip,
} }
} }
Err(err) => { Err(err) => {
info!("Offline mode due to {}", err); info!("Offline mode due to {err}");
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
NetworkMode::OFFLINE NetworkMode::OFFLINE
} }
@@ -985,6 +935,7 @@ async fn wait_infinity(
board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
wait_type: WaitType, wait_type: WaitType,
reboot_now: Arc<AtomicBool>, reboot_now: Arc<AtomicBool>,
timezone: Tz,
) -> ! { ) -> ! {
//since we force to have the lock when entering, we can release it to ensure the caller does not forget to dispose of it //since we force to have the lock when entering, we can release it to ensure the caller does not forget to dispose of it
drop(board); drop(board);
@@ -992,55 +943,155 @@ async fn wait_infinity(
let delay = wait_type.blink_pattern(); let delay = wait_type.blink_pattern();
let mut led_count = 8; let mut led_count = 8;
let mut pattern_step = 0; let mut pattern_step = 0;
let serial_config_receive = AtomicBool::new(false);
let mut suppress_further_mppt_error = false;
let mut last_mqtt_update: Option<Instant> = None;
// Long-press exit (for webserver config modes): hold boot button for 5 seconds.
let mut exit_hold_started: Option<Instant> = None;
let exit_hold_duration = Duration::from_secs(5);
let mut exit_hold_blink = false;
loop { loop {
// While in config webserver mode, allow exiting via long-press.
if matches!(wait_type, WaitType::MissingConfig | WaitType::ConfigButton) {
let mut board = BOARD_ACCESS.get().await.lock().await;
let pressed = board.board_hal.get_esp().mode_override_pressed();
match (pressed, exit_hold_started) {
(true, None) => {
exit_hold_started = Some(Instant::now());
PROGRESS_ACTIVE.store(true, Ordering::Relaxed);
}
(false, Some(_)) => {
exit_hold_started = None;
// Clear any interim hold display.
board.board_hal.clear_progress().await;
}
_ => {}
}
if let Some(started) = exit_hold_started {
let elapsed = Instant::now() - started;
// Visible countdown: fill LEDs progressively during the hold.
// Also toggle general fault LED to match the "enter config" visibility.
exit_hold_blink = !exit_hold_blink;
let progress = core::cmp::min(elapsed, exit_hold_duration);
let lit = ((progress.as_millis() * 8) / exit_hold_duration.as_millis())
.saturating_add(1)
.min(8) as usize;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < lit).await;
}
board.board_hal.general_fault(exit_hold_blink).await;
if elapsed >= exit_hold_duration {
info!("Exiting config mode due to 5s button hold");
board.board_hal.get_esp().set_restart_to_conf(false);
// ensure clean http answer / visible confirmation
Timer::after_millis(500).await;
board.board_hal.deep_sleep(0).await;
}
// Short tick while holding so the pattern updates smoothly.
drop(board);
Timer::after_millis(100).await;
continue;
}
// Release lock and continue with normal wait blinking.
drop(board);
}
{ {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
update_charge_indicator(&mut board).await; match update_charge_indicator(&mut board).await {
Ok(_) => {}
Err(error) => {
if !suppress_further_mppt_error {
error!("Error updating charge indicator: {error}");
suppress_further_mppt_error = true;
}
}
};
match wait_type { match handle_serial_config(&mut board, &serial_config_receive, &reboot_now).await {
WaitType::MissingConfig => { Ok(_) => {}
// Keep existing behavior: circular filling pattern Err(e) => {
led_count %= 8; error!("Error handling serial config: {e}");
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count);
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0);
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step);
}
} }
} }
board.board_hal.general_fault(true).await;
// MQTT updates in config mode
let now = Instant::now();
if last_mqtt_update.is_none()
|| now.duration_since(last_mqtt_update.unwrap_or(Instant::from_secs(0)))
>= Duration::from_secs(60)
{
let cur = board.board_hal.get_time().await;
let timezone_time = cur.with_timezone(&timezone);
let esp = board.board_hal.get_esp();
esp.mqtt_publish("/state", "config").await;
esp.mqtt_publish("/firmware/last_online", &timezone_time.to_rfc3339())
.await;
last_mqtt_update = Some(now);
}
// Skip default blink code when a progress display is active
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count).await;
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0).await;
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step).await;
}
}
}
board.board_hal.general_fault(true).await;
}
} }
Timer::after_millis(delay).await; Timer::after_millis(delay).await;
{ {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.general_fault(false).await;
// Clear all LEDs // Skip clearing LEDs when progress is active to avoid interrupting the progress display
for i in 0..8 { if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
let _ = board.board_hal.fault(i, false); board.board_hal.general_fault(false).await;
// Clear all LEDs
for i in 0..8 {
let _ = board.board_hal.fault(i, false).await;
}
} }
} }
Timer::after_millis(delay).await; Timer::after_millis(delay).await;
hal::PlantHal::feed_watchdog();
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) { if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
reboot_now.store(true, Ordering::Relaxed); reboot_now.store(true, Ordering::Relaxed);
} }
if reboot_now.load(Ordering::Relaxed) { if reboot_now.load(Ordering::Relaxed) {
info!("Rebooting now");
//ensure clean http answer //ensure clean http answer
Timer::after_millis(500).await; Timer::after_millis(500).await;
BOARD_ACCESS BOARD_ACCESS
@@ -1055,7 +1106,47 @@ async fn wait_infinity(
} }
} }
#[esp_hal_embassy::main] async fn handle_serial_config(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>,
serial_config_receive: &AtomicBool,
reboot_now: &AtomicBool,
) -> FatResult<()> {
match board.board_hal.get_esp().read_serial_line().await {
Ok(serial_line) => match serial_line {
None => Ok(()),
Some(line) => {
if serial_config_receive.load(Ordering::Relaxed) {
let ll = line.as_str();
let config: PlantControllerConfig = serde_json::from_str(ll)?;
board
.board_hal
.get_esp()
.save_config(Vec::from(ll.as_bytes()))
.await?;
board.board_hal.set_config(config);
serial_config_receive.store(false, Ordering::Relaxed);
info!("Config received, rebooting");
board.board_hal.get_esp().set_restart_to_conf(false);
reboot_now.store(true, Ordering::Relaxed);
Ok(())
} else {
if line == "automation:streamconfig" {
serial_config_receive.store(true, Ordering::Relaxed);
info!("streamconfig:recieving");
}
Ok(())
}
}
},
Err(_) => {
error!("Error reading serial line");
Ok(())
}
}
}
use embassy_time::WithTimeout;
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! { async fn main(spawner: Spawner) -> ! {
// intialize embassy // intialize embassy
logger::init_logger_from_env(); logger::init_logger_from_env();
@@ -1099,11 +1190,17 @@ async fn get_version(
let hash = &env!("VERGEN_GIT_SHA")[0..8]; let hash = &env!("VERGEN_GIT_SHA")[0..8];
let board = board.board_hal.get_esp(); let board = board.board_hal.get_esp();
let ota_slot = board.get_ota_slot(); let heap = esp_alloc::HEAP.stats();
VersionInfo { VersionInfo {
git_hash: branch + "@" + hash, git_hash: branch + "@" + hash,
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(), build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
partition: ota_slot, current: format!("{:?}", board.current),
slot0_state: format!("{:?}", board.slot0_state),
slot1_state: format!("{:?}", board.slot1_state),
heap_total: heap.size,
heap_used: heap.current_usage,
heap_free: heap.size.saturating_sub(heap.current_usage),
heap_max_used: heap.max_usage,
} }
} }
@@ -1111,5 +1208,11 @@ async fn get_version(
struct VersionInfo { struct VersionInfo {
git_hash: String, git_hash: String,
build_time: String, build_time: String,
partition: String, current: String,
slot0_state: String,
slot1_state: String,
heap_total: usize,
heap_used: usize,
heap_free: usize,
heap_max_used: usize,
} }

View File

@@ -14,7 +14,7 @@ defmt = []
log = ["dep:log"] log = ["dep:log"]
[dependencies] [dependencies]
embassy-net = { version = "0.7.1", default-features = false, features = ["tcp", "dns", "proto-ipv4", "proto-ipv6", "medium-ethernet"] } embassy-net = { version = "0.8.0", default-features = false, features = ["tcp", "dns", "proto-ipv4", "proto-ipv6", "medium-ethernet"] }
embassy-sync = { version = "0.8.0", default-features = false } embassy-sync = { version = "0.8.0", default-features = false }
embassy-time = { version = "0.5.1", default-features = false } embassy-time = { version = "0.5.1", default-features = false }
embassy-futures = { version = "0.1.2", default-features = false } embassy-futures = { version = "0.1.2", default-features = false }

View File

@@ -6,7 +6,7 @@ use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use chrono::DateTime; use chrono::DateTime;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -5,7 +5,7 @@ use alloc::format;
use alloc::string::String; use alloc::string::String;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use edge_http::Method; use edge_http::Method;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info; use log::info;
pub(crate) async fn list_files<T, const N: usize>( pub(crate) async fn list_files<T, const N: usize>(

View File

@@ -1,5 +1,5 @@
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::hal::{esp_time, PLANT_COUNT}; use crate::hal::PLANT_COUNT;
use crate::log::LogMessage; use crate::log::LogMessage;
use crate::plant_state::{MoistureSensorState, PlantState}; use crate::plant_state::{MoistureSensorState, PlantState};
use crate::tank::determine_tank_state; use crate::tank::determine_tank_state;
@@ -10,7 +10,8 @@ use alloc::vec::Vec;
use chrono_tz::Tz; use chrono_tz::Tz;
use core::str::FromStr; use core::str::FromStr;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use log::info;
use serde::Serialize; use serde::Serialize;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@@ -139,13 +140,29 @@ pub(crate) async fn get_time<T, const N: usize>(
) -> FatResult<Option<String>> { ) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
let conf = board.board_hal.get_config(); let conf = board.board_hal.get_config();
let tz = Tz::from_str(conf.timezone.as_ref().unwrap_or(&"Europe/Berlin".to_string()).as_str()).unwrap();
let native = esp_time().await.with_timezone(&tz).to_rfc3339(); let tz: Tz = match conf.timezone.as_ref() {
None => Tz::UTC,
Some(tz_string) => match Tz::from_str(tz_string) {
Ok(tz) => tz,
Err(err) => {
info!("failed parsing timezone {err}");
Tz::UTC
}
},
};
let native = board
.board_hal
.get_time()
.await
.with_timezone(&tz)
.to_rfc3339();
let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await { let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(time) => time.with_timezone(&tz).to_rfc3339(), Ok(time) => time.with_timezone(&tz).to_rfc3339(),
Err(err) => { Err(err) => {
format!("Error getting time: {}", err) format!("Error getting time: {err}")
} }
}; };
@@ -162,6 +179,6 @@ pub(crate) async fn get_log_localization_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>, _request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> { ) -> FatResult<Option<String>> {
Ok(Some(serde_json::to_string( Ok(Some(serde_json::to_string(
&LogMessage::to_log_localisation_config(), &LogMessage::log_localisation_config(),
)?)) )?))
} }

View File

@@ -1,7 +1,7 @@
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::log::LOG_ACCESS; use crate::log::LOG_ACCESS;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
pub(crate) async fn get_log<T, const N: usize>( pub(crate) async fn get_log<T, const N: usize>(
conn: &mut Connection<'_, T, N>, conn: &mut Connection<'_, T, N>,

View File

@@ -1,6 +1,6 @@
use crate::fat_error::FatError; use crate::fat_error::FatError;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
pub(crate) async fn serve_favicon<T, const N: usize>( pub(crate) async fn serve_favicon<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>, conn: &mut Connection<'_, T, { N }>,

View File

@@ -31,10 +31,10 @@ use core::sync::atomic::{AtomicBool, Ordering};
use edge_http::io::server::{Connection, Handler, Server}; use edge_http::io::server::{Connection, Handler, Server};
use edge_http::Method; use edge_http::Method;
use edge_nal::TcpBind; use edge_nal::TcpBind;
use edge_nal::io::{Read, Write};
use edge_nal_embassy::{Tcp, TcpBuffers}; use edge_nal_embassy::{Tcp, TcpBuffers};
use embassy_net::Stack; use embassy_net::Stack;
use embassy_time::Instant; use embassy_time::Instant;
use embedded_io_async::{Read, Write};
use log::info; use log::info;
// fn ota( // fn ota(
@@ -228,34 +228,6 @@ pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
info!("Webserver started and waiting for connections"); info!("Webserver started and waiting for connections");
//TODO https if mbed_esp lands //TODO https if mbed_esp lands
// server
// .fn_handler("/ota", Method::Post, |request| {
// handle_error_to500(request, ota)
// })
// .unwrap();
// server
// .fn_handler("/ota", Method::Options, |request| {
// cors_response(request, 200, "")
// })
// .unwrap();
// let reboot_now_for_reboot = reboot_now.clone();
// server
// .fn_handler("/reboot", Method::Post, move |_| {
// BOARD_ACCESS
// .lock()
// .unwrap()
// .board_hal
// .get_esp()
// .set_restart_to_conf(true);
// reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed);
// anyhow::Ok(())
// })
// .unwrap();
//
// unsafe { vTaskDelay(1) };
//
// server
} }
async fn handle_json<'a, T, const N: usize>( async fn handle_json<'a, T, const N: usize>(
@@ -264,7 +236,7 @@ async fn handle_json<'a, T, const N: usize>(
) -> FatResult<u32> ) -> FatResult<u32>
where where
T: Read + Write, T: Read + Write,
<T as embedded_io_async::ErrorType>::Error: Debug, <T as edge_nal::io::ErrorType>::Error: Debug,
{ {
match chain { match chain {
Ok(answer) => match answer { Ok(answer) => match answer {

View File

@@ -1,13 +1,14 @@
use crate::config::PlantControllerConfig; use crate::config::PlantControllerConfig;
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::esp_set_time;
use crate::webserver::read_up_to_bytes_from_request; use crate::webserver::read_up_to_bytes_from_request;
use crate::{do_secure_pump, BOARD_ACCESS}; use crate::{do_secure_pump, BOARD_ACCESS};
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use chrono::DateTime; use chrono::DateTime;
use edge_http::io::server::Connection; use edge_http::io::server::Connection;
use embedded_io_async::{Read, Write}; use edge_nal::io::{Read, Write};
use esp_radio::wifi::ap::AccessPointInfo;
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -35,10 +36,10 @@ pub(crate) async fn wifi_scan<T, const N: usize>(
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
info!("start wifi scan"); info!("start wifi scan");
let mut ssids: Vec<String> = Vec::new(); let mut ssids: Vec<String> = Vec::new();
let scan_result = board.board_hal.get_esp().wifi_scan().await?; let scan_result: Vec<AccessPointInfo> = board.board_hal.get_esp().wifi_scan().await?;
scan_result scan_result
.iter() .iter()
.for_each(|s| ssids.push(s.ssid.to_string())); .for_each(|s| ssids.push(s.ssid.as_str().to_owned()));
let ssid_json = serde_json::to_string(&SSIDList { ssids })?; let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
info!("Sending ssid list {}", &ssid_json); info!("Sending ssid list {}", &ssid_json);
Ok(Some(ssid_json)) Ok(Some(ssid_json))
@@ -89,8 +90,9 @@ where
{ {
let actual_data = read_up_to_bytes_from_request(request, None).await?; let actual_data = read_up_to_bytes_from_request(request, None).await?;
let time: SetTime = serde_json::from_slice(&actual_data)?; let time: SetTime = serde_json::from_slice(&actual_data)?;
let parsed = DateTime::parse_from_rfc3339(time.time).unwrap(); let parsed = DateTime::parse_from_rfc3339(time.time)?;
esp_set_time(parsed).await?; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.set_time(&parsed).await?;
Ok(None) Ok(None)
} }