5 Commits

27 changed files with 842 additions and 1191 deletions

View File

@@ -23,8 +23,6 @@ target = "riscv32imac-unknown-none-elf"
CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland" CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland"
CARGO_WORKSPACE_DIR = { value = "", relative = true } CARGO_WORKSPACE_DIR = { value = "", relative = true }
ESP_LOG = "info" ESP_LOG = "info"
PATH = { value = "../../../bin:/usr/bin:/usr/local/bin", force = true, relative = true }
[unstable] [unstable]

View File

@@ -109,13 +109,12 @@ pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0" } ina219 = { version = "0.2.0" }
# Storage and filesystem # Storage and filesystem
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] } embedded-savegame = { version = "0.3.0" }
littlefs2-core = "0.1.2"
# Serialization / codecs # Serialization / codecs
serde = { version = "1.0.228", features = ["derive", "alloc"], default-features = false } serde = { version = "1.0.228", features = ["derive", "alloc"], default-features = false }
serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] } serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] }
bincode = { version = "2.0.1", default-features = false, features = ["derive"] } bincode = { version = "2.0.1", default-features = false, features = ["derive", "alloc"] }
# Time and time zones # Time and time zones
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] } chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
@@ -136,6 +135,7 @@ measurements = "0.11.1"
# Project-specific # Project-specific
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] } mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
no-panic = "0.1.36"
[patch.crates-io] [patch.crates-io]
mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' } mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }

View File

@@ -0,0 +1,2 @@
# This file is used for clippy configuration.
# It shouldn't contain the deny attributes, which belong to the crate root.

View File

@@ -1,17 +1,17 @@
use crate::hal::PLANT_COUNT; use crate::hal::PLANT_COUNT;
use crate::plant_state::PlantWateringMode; use crate::plant_state::PlantWateringMode;
use alloc::string::String; use alloc::string::{String, ToString};
use core::str::FromStr; use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
#[serde(default)] #[serde(default)]
pub struct NetworkConfig { pub struct NetworkConfig {
pub ap_ssid: heapless::String<32>, pub ap_ssid: String,
pub ssid: Option<heapless::String<32>>, pub ssid: Option<String>,
pub password: Option<heapless::String<64>>, pub password: Option<String>,
pub mqtt_url: Option<String>, pub mqtt_url: Option<String>,
pub base_topic: Option<heapless::String<64>>, pub base_topic: Option<String>,
pub mqtt_user: Option<String>, pub mqtt_user: Option<String>,
pub mqtt_password: Option<String>, pub mqtt_password: Option<String>,
pub max_wait: u32, pub max_wait: u32,
@@ -19,7 +19,7 @@ pub struct NetworkConfig {
impl Default for NetworkConfig { impl Default for NetworkConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
ap_ssid: heapless::String::from_str("PlantCtrl Init").unwrap(), ap_ssid: "PlantCtrl Init".to_string(),
ssid: None, ssid: None,
password: None, password: None,
mqtt_url: None, mqtt_url: None,
@@ -31,7 +31,7 @@ impl Default for NetworkConfig {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
#[serde(default)] #[serde(default)]
pub struct NightLampConfig { pub struct NightLampConfig {
pub enabled: bool, pub enabled: bool,
@@ -54,7 +54,7 @@ impl Default for NightLampConfig {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
#[serde(default)] #[serde(default)]
pub struct TankConfig { pub struct TankConfig {
pub tank_sensor_enabled: bool, pub tank_sensor_enabled: bool,
@@ -79,26 +79,26 @@ impl Default for TankConfig {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
pub enum BatteryBoardVersion { pub enum BatteryBoardVersion {
#[default] #[default]
Disabled, Disabled,
WchI2cSlave, WchI2cSlave,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
pub enum BoardVersion { pub enum BoardVersion {
Initial, Initial,
#[default] #[default]
V4, V4,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
pub struct BoardHardware { pub struct BoardHardware {
pub board: BoardVersion, pub board: BoardVersion,
pub battery: BatteryBoardVersion, pub battery: BatteryBoardVersion,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
#[serde(default)] #[serde(default)]
pub struct PlantControllerConfig { pub struct PlantControllerConfig {
pub hardware: BoardHardware, pub hardware: BoardHardware,
@@ -109,7 +109,7 @@ pub struct PlantControllerConfig {
pub timezone: Option<String>, pub timezone: Option<String>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
#[serde(default)] #[serde(default)]
pub struct PlantConfig { pub struct PlantConfig {
pub mode: PlantWateringMode, pub mode: PlantWateringMode,

View File

@@ -1,5 +1,7 @@
use alloc::format; use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use chrono::format::ParseErrorKind;
use chrono_tz::ParseError;
use core::convert::Infallible; use core::convert::Infallible;
use core::fmt; use core::fmt;
use core::fmt::Debug; use core::fmt::Debug;
@@ -14,28 +16,18 @@ use esp_hal::twai::EspTwaiError;
use esp_radio::wifi::WifiError; use esp_radio::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError}; use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use lib_bms_protocol::BmsProtocolError; use lib_bms_protocol::BmsProtocolError;
use littlefs2_core::PathError;
use onewire::Error; use onewire::Error;
use pca9535::ExpanderError; use pca9535::ExpanderError;
//All error superconstruct //All error superconstruct
#[derive(Debug)] #[derive(Debug)]
pub enum FatError { pub enum FatError {
BMSError {
error: String,
},
OneWireError { OneWireError {
error: Error<Infallible>, error: Error<Infallible>,
}, },
String { String {
error: String, error: String,
}, },
LittleFSError {
error: littlefs2_core::Error,
},
PathError {
error: PathError,
},
TryLockError { TryLockError {
error: TryLockError, error: TryLockError,
}, },
@@ -86,8 +78,6 @@ impl fmt::Display for FatError {
} }
FatError::OneWireError { error } => write!(f, "OneWireError {error:?}"), FatError::OneWireError { error } => write!(f, "OneWireError {error:?}"),
FatError::String { error } => write!(f, "{error}"), FatError::String { error } => write!(f, "{error}"),
FatError::LittleFSError { error } => write!(f, "LittleFSError {error:?}"),
FatError::PathError { error } => write!(f, "PathError {error:?}"),
FatError::TryLockError { error } => write!(f, "TryLockError {error:?}"), FatError::TryLockError { error } => write!(f, "TryLockError {error:?}"),
FatError::WifiError { error } => write!(f, "WifiError {error:?}"), FatError::WifiError { error } => write!(f, "WifiError {error:?}"),
FatError::SerdeError { error } => write!(f, "SerdeError {error:?}"), FatError::SerdeError { error } => write!(f, "SerdeError {error:?}"),
@@ -106,7 +96,6 @@ impl fmt::Display for FatError {
write!(f, "CanBusError {error:?}") write!(f, "CanBusError {error:?}")
} }
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"), FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
FatError::BMSError { error } => write!(f, "BMSError, {error}"),
FatError::OTAError => { FatError::OTAError => {
write!(f, "OTA missing partition") write!(f, "OTA missing partition")
} }
@@ -149,23 +138,28 @@ 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 }
} }
} }
impl From<littlefs2_core::Error> for FatError {
fn from(value: littlefs2_core::Error) -> Self {
FatError::LittleFSError { error: value }
}
}
impl From<PathError> for FatError {
fn from(value: PathError) -> Self {
FatError::PathError { error: value }
}
}
impl From<TryLockError> for FatError { impl From<TryLockError> for FatError {
fn from(value: TryLockError) -> Self { fn from(value: TryLockError) -> Self {
FatError::TryLockError { error: value } FatError::TryLockError { error: value }
@@ -283,7 +277,7 @@ impl<E: fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for FatError
impl From<Infallible> for FatError { impl From<Infallible> for FatError {
fn from(value: Infallible) -> Self { fn from(value: Infallible) -> Self {
panic!("Infallible error: {:?}", value) match value {}
} }
} }
@@ -336,3 +330,27 @@ impl From<BmsProtocolError> for FatError {
} }
} }
} }
impl From<ParseError> for FatError {
fn from(value: ParseError) -> Self {
FatError::String {
error: format!("Parsing error: {value:?}"),
}
}
}
impl From<ParseErrorKind> for FatError {
fn from(value: ParseErrorKind) -> Self {
FatError::String {
error: format!("Parsing error: {value:?}"),
}
}
}
impl From<chrono::format::ParseError> for FatError {
fn from(value: chrono::format::ParseError) -> Self {
FatError::String {
error: format!("Parsing error: {value:?}"),
}
}
}

View File

@@ -1,24 +1,22 @@
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::savegame_manager::SavegameManager;
use crate::log::{LogMessage, LOG_ACCESS}; use crate::hal::PLANT_COUNT;
use crate::log::{log, LogMessage};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::shared_flash::MutexFlashStorage; use crate::hal::shared_flash::MutexFlashStorage;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::{format, string::String, vec, vec::Vec}; use alloc::{format, string::String, vec, vec::Vec};
use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::net::{IpAddr, Ipv4Addr, SocketAddr};
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::udp::UdpSocket;
use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4}; use embassy_net::{DhcpConfig, 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;
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer, WithTimeout}; use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage}; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
@@ -39,8 +37,6 @@ use esp_radio::wifi::{
AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig, AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig,
ScanTypeConfig, WifiController, WifiDevice, WifiStaState, ScanTypeConfig, WifiController, WifiDevice, WifiStaState,
}; };
use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{error, info, warn}; use log::{error, info, warn};
use mcutie::{ use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable, Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
@@ -60,7 +56,6 @@ static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0; static mut RESTART_TO_CONF: i8 = 0;
const CONFIG_FILE: &str = "config.json";
const NTP_SERVER: &str = "pool.ntp.org"; const NTP_SERVER: &str = "pool.ntp.org";
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false); static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
@@ -68,19 +63,6 @@ static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false); pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new(); static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new();
#[derive(Serialize, Debug)]
pub struct FileInfo {
filename: String,
size: usize,
}
#[derive(Serialize, Debug)]
pub struct FileList {
total: usize,
used: usize,
files: Vec<FileInfo>,
}
#[derive(Copy, Clone, Default)] #[derive(Copy, Clone, Default)]
struct Timestamp { struct Timestamp {
stamp: DateTime<Utc>, stamp: DateTime<Utc>,
@@ -117,7 +99,7 @@ impl NtpTimestampGenerator for Timestamp {
} }
pub struct Esp<'a> { pub struct Esp<'a> {
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>, pub savegame: SavegameManager,
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<WifiDevice<'static>>,
@@ -130,6 +112,8 @@ pub struct Esp<'a> {
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>, pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub uart0: Uart<'a, Blocking>, pub uart0: Uart<'a, Blocking>,
pub rtc: Rtc<'a>,
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>, pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>, pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
pub current: AppPartitionSubType, pub current: AppPartitionSubType,
@@ -155,6 +139,14 @@ 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>> { pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
let mut buf = [0u8; 1]; let mut buf = [0u8; 1];
let mut line = String::new(); let mut line = String::new();
@@ -185,69 +177,6 @@ impl Esp<'_> {
} }
} }
} }
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
access.remove(&file)?;
Ok(())
}
pub(crate) async fn write_file(
&mut self,
filename: String,
offset: u32,
buf: &[u8],
) -> Result<(), FatError> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
access.open_file_with_options_and_then(
|options| options.read(true).write(true).create(true),
&file,
|file| {
file.seek(SeekFrom::Start(offset))?;
file.write(buf)?;
Ok(())
},
)?;
Ok(())
}
pub async fn get_size(&mut self, filename: String) -> FatResult<usize> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
let data = access.metadata(&file)?;
Ok(data.len())
}
pub(crate) async fn get_file(
&mut self,
filename: String,
chunk: u32,
) -> FatResult<([u8; 512], usize)> {
use littlefs2::io::Error as lfs2Error;
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
let mut buf = [0_u8; 512];
let mut read = 0;
let offset = chunk * buf.len() as u32;
access.open_file_with_options_and_then(
|options| options.read(true),
&file,
|file| {
let length = file.len()? as u32;
if length == 0 {
Err(lfs2Error::IO)
} else if length > offset {
file.seek(SeekFrom::Start(offset))?;
read = file.read(&mut buf)?;
Ok(())
} else {
//exactly at end, do nothing
Ok(())
}
},
)?;
Ok((buf, read))
}
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> { pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
let _ = check_erase(self.ota_target, offset, offset + 4096); let _ = check_erase(self.ota_target, offset, offset + 4096);
@@ -314,17 +243,19 @@ 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_addrs = stack let ntp_addrs = stack
.dns_query(NTP_SERVER, DnsQueryType::A) .dns_query(NTP_SERVER, DnsQueryType::A)
.await; .await
if ntp_addrs.is_err() { .context("Failed to resolve DNS")?;
bail!("Failed to resolve DNS");
if ntp_addrs.is_empty() {
bail!("No IP addresses found for NTP server");
} }
let ntp = ntp_addrs.unwrap()[0]; let ntp = ntp_addrs[0];
info!("NTP server: {ntp:?}"); info!("NTP server: {ntp:?}");
let mut counter = 0; let mut counter = 0;
@@ -416,9 +347,11 @@ impl Esp<'_> {
Err(_) => "PlantCtrl Emergency Mode".to_string(), Err(_) => "PlantCtrl Emergency Mode".to_string(),
}; };
let device = self.interface_ap.take().unwrap(); let device = self
let gw_ip_addr_str = "192.168.71.1"; .interface_ap
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); .take()
.context("AP interface already taken")?;
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),
@@ -447,7 +380,7 @@ impl Esp<'_> {
println!("start net task"); println!("start net task");
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)).ok();
println!("run dhcp"); println!("run dhcp");
spawner.spawn(run_dhcp(*stack, gw_ip_addr_str)).ok(); spawner.spawn(run_dhcp(*stack, gw_ip_addr)).ok();
loop { loop {
if stack.is_link_up() { if stack.is_link_up() {
@@ -458,7 +391,7 @@ 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:?}"));
@@ -472,18 +405,17 @@ impl Esp<'_> {
spawner: Spawner, spawner: Spawner,
) -> FatResult<Stack<'static>> { ) -> FatResult<Stack<'static>> {
esp_radio::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(),
@@ -491,7 +423,10 @@ impl Esp<'_> {
}; };
let max_wait = network_config.max_wait; let max_wait = network_config.max_wait;
let device = self.interface_sta.take().unwrap(); let device = self
.interface_sta
.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;
@@ -517,71 +452,66 @@ impl Esp<'_> {
spawner.spawn(net_task(runner)).ok(); spawner.spawn(net_task(runner)).ok();
self.controller.lock().await.start_async().await?; self.controller.lock().await.start_async().await?;
let timeout = { let res = async {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
loop { loop {
let state = esp_radio::wifi::sta_state(); let state = esp_radio::wifi::sta_state();
if state == WifiStaState::Started { if state == WifiStaState::Started {
self.controller.lock().await.connect()?; self.controller.lock().await.connect()?;
break; break;
} }
if {
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; Timer::after(Duration::from_millis(500)).await;
} }
let timeout = { Ok::<(), FatError>(())
let guard = TIME_ACCESS.get().await.lock().await; }
guard.current_time_us() .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
} + max_wait as u64 * 1000; .await;
if res.is_err() {
bail!("Timeout waiting for wifi sta ready")
}
let res = async {
loop { loop {
let state = esp_radio::wifi::sta_state(); let state = esp_radio::wifi::sta_state();
if state == WifiStaState::Connected { if state == WifiStaState::Connected {
break; break;
} }
if { Timer::after(Duration::from_millis(500)).await;
let guard = TIME_ACCESS.get().await.lock().await; }
guard.current_time_us() Ok::<(), FatError>(())
} > timeout }
{ .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await;
if res.is_err() {
bail!("Timeout waiting for wifi sta connected") bail!("Timeout waiting for wifi sta connected")
} }
let res = async {
while !stack.is_link_up() {
Timer::after(Duration::from_millis(500)).await; Timer::after(Duration::from_millis(500)).await;
} }
let timeout = { Ok::<(), FatError>(())
let guard = TIME_ACCESS.get().await.lock().await; }
guard.current_time_us() .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
} + max_wait as u64 * 1000; .await;
while !stack.is_link_up() {
if { if res.is_err() {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi link up") bail!("Timeout waiting for wifi link up")
} }
Timer::after(Duration::from_millis(500)).await;
} let res = async {
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
while !stack.is_config_up() { while !stack.is_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 Timer::after(Duration::from_millis(100)).await
} }
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await;
if res.is_err() {
bail!("Timeout waiting for wifi config up")
}
info!("Connected WIFI, dhcp: {:?}", stack.config_v4()); info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(*stack) Ok(*stack)
@@ -590,15 +520,14 @@ impl Esp<'_> {
pub fn deep_sleep( pub fn deep_sleep(
&mut self, &mut self,
duration_in_ms: u64, duration_in_ms: u64,
mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>,
) -> ! { ) -> ! {
// 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 {
info!("Marking OTA image as valid"); info!("Marking OTA image as valid");
self.ota if let Err(err) = self.ota.set_current_ota_state(Valid) {
.set_current_ota_state(Valid) error!("Could not set image to valid: {:?}", err);
.expect("Could not set image to valid"); }
} }
} else { } else {
info!("No OTA image to mark as valid"); info!("No OTA image to mark as valid");
@@ -611,52 +540,50 @@ impl Esp<'_> {
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]);
} }
} }
/// Load the most recently saved config from flash.
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> { pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
let cfg = PathBuf::try_from(CONFIG_FILE)?; match self.savegame.load_latest()? {
let config_exist = self.fs.lock().await.exists(&cfg); None => bail!("No config stored"),
if !config_exist { Some(data) => {
bail!("No config file stored")
}
let data = self.fs.lock().await.read::<4096>(&cfg)?;
let config: PlantControllerConfig = serde_json::from_slice(&data)?; let config: PlantControllerConfig = serde_json::from_slice(&data)?;
Ok(config) Ok(config)
} }
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> { }
let filesystem = self.fs.lock().await; }
let cfg = PathBuf::try_from(CONFIG_FILE)?;
filesystem.write(&cfg, &config)?; /// Load a config from a specific save slot.
pub(crate) async fn load_config_slot(
&mut self,
idx: usize,
) -> FatResult<String> {
match self.savegame.load_slot(idx)? {
None => bail!("Slot {idx} is empty or invalid"),
Some(data) => {
Ok(String::from_utf8_lossy(&*data).to_string())
}
}
}
/// Persist a JSON config blob to the next wear-leveling slot.
pub(crate) async fn save_config(&mut self, mut config: Vec<u8>) -> FatResult<()> {
self.savegame.save(config.as_mut_slice())?;
Ok(()) Ok(())
} }
pub(crate) async fn list_files(&self) -> FatResult<FileList> {
let path = PathBuf::new();
let fs = self.fs.lock().await; /// Delete a specific save slot by erasing it on flash.
let free_size = fs.available_space()?; pub(crate) async fn delete_save_slot(&mut self, idx: usize) -> FatResult<()> {
let total_size = fs.total_space(); self.savegame.delete_slot(idx)
let mut result = FileList {
total: total_size,
used: total_size - free_size,
files: Vec::new(),
};
fs.read_dir_and_then(&path, |dir| {
for entry in dir {
let e = entry?;
if e.file_type() == FileType::File {
result.files.push(FileInfo {
filename: e.path().to_string(),
size: e.metadata().len(),
});
} }
}
Ok(()) /// Return metadata about all valid save slots.
})?; pub(crate) async fn list_saves(
Ok(result) &mut self,
) -> FatResult<alloc::vec::Vec<crate::hal::savegame_manager::SaveInfo>> {
self.savegame.list_saves()
} }
pub(crate) async fn init_rtc_deepsleep_memory( pub(crate) async fn init_rtc_deepsleep_memory(
@@ -680,21 +607,8 @@ impl Esp<'_> {
if to_config_mode { if to_config_mode {
RESTART_TO_CONF = 1; RESTART_TO_CONF = 1;
} }
LOG_ACCESS log(LogMessage::RestartToConfig, RESTART_TO_CONF as u32, 0, "", "").await;
.lock() log(
.await
.log(
LogMessage::RestartToConfig,
RESTART_TO_CONF as u32,
0,
"",
"",
)
.await;
LOG_ACCESS
.lock()
.await
.log(
LogMessage::LowVoltage, LogMessage::LowVoltage,
LOW_VOLTAGE_DETECTED as u32, LOW_VOLTAGE_DETECTED as u32,
0, 0,
@@ -749,11 +663,11 @@ impl Esp<'_> {
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");
} }
@@ -780,50 +694,43 @@ impl Esp<'_> {
))?; ))?;
spawner.spawn(mqtt_runner(task))?; spawner.spawn(mqtt_runner(task))?;
LOG_ACCESS log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic).await;
.lock()
.await
.log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
.await;
LOG_ACCESS log(LogMessage::MqttInfo, 0, 0, "", mqtt_url).await;
.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;
guard.current_time_us()
} + mqtt_timeout as u64 * 1000;
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) { 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; Timer::after(Duration::from_millis(100)).await;
} }
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
Topic::General(round_trip_topic.clone()) if res.is_err() {
bail!("Timeout waiting MQTT connect event")
}
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;
guard.current_time_us()
} + mqtt_timeout as u64 * 1000;
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) { while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
let cur = TIME_ACCESS.get().await.lock().await.current_time_us(); Timer::after(Duration::from_millis(100)).await;
if cur > timeout { }
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
if res.is_err() {
//ensure we do not further try to publish //ensure we do not further try to publish
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed); MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip") bail!("Timeout waiting MQTT roundtrip")
} }
Timer::after(Duration::from_millis(100)).await;
}
Ok(()) Ok(())
} }
@@ -929,18 +836,10 @@ async fn mqtt_incoming_task(
true => 1, true => 1,
false => 0, false => 0,
}; };
LOG_ACCESS log(LogMessage::MqttStayAliveRec, a, 0, "", "").await;
.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).await;
.lock()
.await
.log(LogMessage::UnknownTopic, 0, 0, "", &topic)
.await;
} }
} }
}, },
@@ -961,8 +860,8 @@ async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
} }
#[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},
@@ -971,21 +870,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(

View File

@@ -1,88 +0,0 @@
use crate::hal::shared_flash::MutexFlashStorage;
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use littlefs2::consts::U4096 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::io::Error as lfs2Error;
use littlefs2::io::Result as lfs2Result;
use log::error;
pub struct LittleFs2Filesystem {
pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
}
impl lfs2Storage for LittleFs2Filesystem {
const READ_SIZE: usize = 4096;
const WRITE_SIZE: usize = 4096;
const BLOCK_SIZE: usize = 4096; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1000 * 1000 / 4096; //8Mb in 4k blocks + a little space for stupid calculation errors
const BLOCK_CYCLES: isize = 100;
type CACHE_SIZE = lfsCache;
type LOOKAHEAD_SIZE = lfsLookahead;
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
let read_size: usize = Self::READ_SIZE;
if off % 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) {
Ok(..) => Ok(buf.len()),
Err(err) => {
error!("Littlefs2Filesystem read error: {err:?}");
Err(lfs2Error::IO)
}
}
}
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE;
if off % 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) {
Ok(..) => Ok(data.len()),
Err(err) => {
error!("Littlefs2Filesystem write error: {err:?}");
Err(lfs2Error::IO)
}
}
}
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE;
if off % block_size != 0 {
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
return Err(lfs2Error::IO);
}
if len % block_size != 0 {
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
return Err(lfs2Error::IO);
}
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,14 +3,14 @@ use lib_bms_protocol::BmsReadable;
pub(crate) mod battery; pub(crate) mod battery;
// mod can_api; // replaced by external canapi crate // mod can_api; // replaced by external canapi crate
pub mod esp; pub mod esp;
mod little_fs2storage_adapter; pub(crate) mod savegame_manager;
pub(crate) mod rtc; pub(crate) mod rtc;
mod shared_flash; mod shared_flash;
mod v4_hal; mod v4_hal;
mod water; mod water;
use crate::alloc::string::ToString; use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use crate::hal::rtc::{BackupHeader, DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals; use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1; use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::GPIO0; use esp_hal::peripherals::GPIO0;
@@ -44,8 +44,8 @@ use crate::{
battery::{BatteryInteraction, NoBatteryMonitor}, battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp, esp::Esp,
}, },
log::log,
log::LogMessage, log::LogMessage,
BOARD_ACCESS,
}; };
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::format; use alloc::format;
@@ -75,9 +75,8 @@ use measurements::{Current, Voltage};
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::WCHI2CSlave; use crate::hal::battery::WCHI2CSlave;
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::savegame_manager::SavegameManager;
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock; use embassy_sync::once_lock::OnceLock;
use embedded_storage::nor_flash::RmwNorFlashStorage; use embedded_storage::nor_flash::RmwNorFlashStorage;
@@ -99,15 +98,11 @@ use esp_hal::uart::Uart;
use esp_hal::Blocking; use esp_hal::Blocking;
use esp_radio::{init, Controller}; use esp_radio::{init, Controller};
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; use log::{info, warn};
use littlefs2::object_safe::DynStorage;
use log::{error, info, warn};
use portable_atomic::AtomicBool; use portable_atomic::AtomicBool;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared_flash::MutexFlashStorage; use shared_flash::MutexFlashStorage;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
//Only support for 8 right now! //Only support for 8 right now!
pub const PLANT_COUNT: usize = 8; pub const PLANT_COUNT: usize = 8;
@@ -147,6 +142,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) -> !;
@@ -164,6 +161,10 @@ pub trait BoardInteraction<'a> {
async fn get_mptt_current(&mut self) -> FatResult<Current>; async fn get_mptt_current(&mut self) -> FatResult<Current>;
async fn can_power(&mut self, state: bool) -> FatResult<()>; async fn can_power(&mut self, state: bool) -> FatResult<()>;
async fn backup_config(&mut self, config: &PlantControllerConfig) -> FatResult<()>;
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig>;
async fn backup_info(&mut self) -> FatResult<BackupHeader>;
// Return JSON string with autodetected sensors per plant. Default: not supported. // Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self, _request: Detection) -> FatResult<Detection> { async fn detect_sensors(&mut self, _request: Detection) -> FatResult<Detection> {
bail!("Autodetection is only available on v4 HAL with CAN bus"); bail!("Autodetection is only available on v4 HAL with CAN bus");
@@ -244,12 +245,7 @@ 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 rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR);
TIME_ACCESS
.init(Mutex::new(rtc))
.map_err(|_| FatError::String {
error: "Init error rct".to_string(),
})?;
let timg0 = TimerGroup::new(peripherals.TIMG0); let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
@@ -266,12 +262,17 @@ impl PlantHal {
let rng = Rng::new(); let rng = Rng::new();
let esp_wifi_ctrl = &*mk_static!( let esp_wifi_ctrl = &*mk_static!(
Controller<'static>, Controller<'static>,
init().expect("Could not init wifi controller") init().map_err(|e| FatError::String {
error: format!("Could not init wifi controller: {:?}", e)
})?
); );
let (controller, interfaces) = let (controller, interfaces) =
esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default()) esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default()).map_err(
.expect("Could not init wifi"); |e| FatError::String {
error: format!("Could not init wifi: {:?}", e),
},
)?;
let pcnt_module = Pcnt::new(peripherals.PCNT); let pcnt_module = Pcnt::new(peripherals.PCNT);
@@ -325,7 +326,7 @@ impl PlantHal {
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::Ota, DataPartitionSubType::Ota,
))? ))?
.expect("No OTA data partition found") .context("No OTA data partition found")?
); );
let ota_data = mk_static!( let ota_data = mk_static!(
@@ -370,32 +371,16 @@ impl PlantHal {
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::LittleFs, DataPartitionSubType::LittleFs,
))? ))?
.expect("Data partition with littlefs not found"); .context("Storage data partition not found")?;
let data_partition = mk_static!(PartitionEntry, data_partition); let data_partition = mk_static!(PartitionEntry, data_partition);
let data = mk_static!( let data = mk_static!(
FlashRegion<MutexFlashStorage>, FlashRegion<'static, MutexFlashStorage>,
data_partition.as_embedded_storage(flash_storage_3) data_partition.as_embedded_storage(flash_storage_3)
); );
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() {
info!("Littlefs2 filesystem is mountable");
} else {
match lfs2filesystem.format() {
Ok(..) => {
info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
error!("Littlefs2 filesystem could not be formatted: {err:?}");
}
}
}
#[allow(clippy::arc_with_non_send_sync)] let savegame = SavegameManager::new(data);
let fs = Arc::new(Mutex::new( info!("Savegame storage initialized ({} slots × {} KB)", savegame_manager::SAVEGAME_SLOT_COUNT, savegame_manager::SAVEGAME_SLOT_SIZE / 1024);
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
));
let uart0 = let uart0 =
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String { Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
@@ -405,7 +390,7 @@ impl PlantHal {
let ap = interfaces.ap; let ap = interfaces.ap;
let sta = interfaces.sta; let sta = interfaces.sta;
let mut esp = Esp { let mut esp = Esp {
fs, savegame,
rng, rng,
controller: Arc::new(Mutex::new(controller)), controller: Arc::new(Mutex::new(controller)),
interface_sta: Some(sta), interface_sta: Some(sta),
@@ -418,6 +403,7 @@ impl PlantHal {
slot0_state: state_0, slot0_state: state_0,
slot1_state: state_1, slot1_state: state_1,
uart0, uart0,
rtc: rtc_peripheral,
}; };
//init,reset rtc memory depending on cause //init,reset rtc memory depending on cause
@@ -453,10 +439,7 @@ impl PlantHal {
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu", SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
}, },
}; };
LOG_ACCESS log(
.lock()
.await
.log(
LogMessage::ResetReason, LogMessage::ResetReason,
init_rtc_store as u32, init_rtc_store as u32,
to_config_mode as u32, to_config_mode as u32,
@@ -488,7 +471,9 @@ 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).map_err(|_| FatError::String {
error: "Could not init i2c driver".to_string(),
})?;
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);
@@ -555,10 +540,7 @@ impl PlantHal {
HAL { board_hal } HAL { board_hal }
} }
Err(err) => { Err(err) => {
LOG_ACCESS log(
.lock()
.await
.log(
LogMessage::ConfigModeMissingConfig, LogMessage::ConfigModeMissingConfig,
0, 0,
0, 0,
@@ -648,27 +630,6 @@ pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSub
Ok(next) Ok(next)
} }
pub async fn esp_time() -> DateTime<Utc> {
let guard = TIME_ACCESS.get().await.lock().await;
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
}
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
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)] #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
pub struct Moistures { pub struct Moistures {
pub sensor_a_hz: [Option<f32>; PLANT_COUNT], pub sensor_a_hz: [Option<f32>; PLANT_COUNT],

View File

@@ -1,8 +1,7 @@
use crate::fat_error::FatResult;
use crate::hal::Box; use crate::hal::Box;
use crate::fat_error::FatResult;
use async_trait::async_trait; use async_trait::async_trait;
use bincode::config::Configuration; use bincode::{Decode, Encode};
use bincode::{config, Decode, Encode};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ds323x::ic::DS3231; use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface; use ds323x::interface::I2cInterface;
@@ -19,24 +18,21 @@ use esp_hal::Blocking;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC); pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
const CONFIG: Configuration = config::standard(); pub const EEPROM_PAGE: usize = 32;
// //
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait RTCModuleInteraction { pub trait RTCModuleInteraction {
async fn get_backup_info(&mut self) -> FatResult<BackupHeader>;
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>;
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()>;
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()>;
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>>; async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>>;
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>; async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>;
fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()>;
fn read(&mut self, offset:u32, data: &mut [u8]) -> FatResult<()>;
} }
//
const BACKUP_HEADER_MAX_SIZE: usize = 64;
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)] #[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
pub struct BackupHeader { pub struct BackupHeader {
pub timestamp: i64, pub timestamp: i64,
crc16: u16, pub(crate) crc16: u16,
pub size: u16, pub size: u16,
} }
// //
@@ -46,7 +42,7 @@ pub struct DS3231Module {
DS3231, DS3231,
>, >,
pub(crate) storage: eeprom24x::Storage< pub storage: eeprom24x::Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>, I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
B32, B32,
TwoBytes, TwoBytes,
@@ -57,67 +53,6 @@ pub struct DS3231Module {
#[async_trait(?Send)] #[async_trait(?Send)]
impl RTCModuleInteraction for DS3231Module { impl RTCModuleInteraction for DS3231Module {
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage.read(0, &mut header_page_buffer)?;
let (header, len): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
log::info!("Raw header is {header_page_buffer:?} with size {len}");
Ok(header)
}
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage.read(0, &mut header_page_buffer)?;
let (header, _header_size): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
let mut buf = [0_u8; 32];
let offset = chunk * buf.len() + BACKUP_HEADER_MAX_SIZE;
let end: usize = header.size as usize + BACKUP_HEADER_MAX_SIZE;
let current_end = offset + buf.len();
let chunk_size = if current_end > end {
end - offset
} else {
buf.len()
};
if chunk_size == 0 {
Ok((buf, 0, header.crc16))
} else {
self.storage.read(offset as u32, &mut buf)?;
//&buf[..chunk_size];
Ok((buf, chunk_size, header.crc16))
}
}
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> {
//skip header and write after
self.storage
.write((BACKUP_HEADER_MAX_SIZE + offset) as u32, bytes)?;
Ok(())
}
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
let time = self.get_rtc_time().await?.timestamp_millis();
let header = BackupHeader {
crc16: crc,
timestamp: time,
size: length as u16,
};
let config = config::standard();
let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?;
log::info!("Raw header is {header_page_buffer:?} with size {encoded}");
self.storage.write(0, &header_page_buffer)?;
Ok(())
}
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> { async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> {
Ok(self.rtc.datetime()?.and_utc()) Ok(self.rtc.datetime()?.and_utc())
} }
@@ -126,4 +61,14 @@ impl RTCModuleInteraction for DS3231Module {
let naive_time = time.naive_utc(); let naive_time = time.naive_utc();
Ok(self.rtc.set_datetime(&naive_time)?) Ok(self.rtc.set_datetime(&naive_time)?)
} }
fn write(&mut self, offset: u32, data: &[u8]) -> FatResult<()> {
self.storage.write(offset, data)?;
Ok(())
}
fn read(&mut self, offset:u32, data: &mut [u8]) -> FatResult<()> {
self.storage.read(offset, data)?;
Ok(())
}
} }

View File

@@ -0,0 +1,150 @@
use alloc::vec::Vec;
use embedded_savegame::storage::{Flash, Storage};
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::{FlashRegion, Error as PartitionError};
use serde::Serialize;
use crate::fat_error::{FatError, FatResult};
use crate::hal::shared_flash::MutexFlashStorage;
/// Size of each save slot in bytes (16 KB).
pub const SAVEGAME_SLOT_SIZE: usize = 16384;
/// Number of slots in the 8 MB storage partition.
pub const SAVEGAME_SLOT_COUNT: usize = 8 * 1024 * 1024 / SAVEGAME_SLOT_SIZE; // 512
/// Metadata about a single existing save slot, returned by [`SavegameManager::list_saves`].
#[derive(Serialize, Debug, Clone)]
pub struct SaveInfo {
pub idx: usize,
pub len: u32,
}
// ── Flash adapter ──────────────────────────────────────────────────────────────
/// Newtype wrapper around a [`PartitionError`] so we can implement the
/// [`core::fmt::Debug`] bound required by [`embedded_savegame::storage::Flash`].
#[derive(Debug)]
pub struct SavegameFlashError(#[allow(dead_code)] PartitionError);
/// Adapts a `&mut FlashRegion<'static, MutexFlashStorage>` to the
/// [`embedded_savegame::storage::Flash`] trait.
///
/// `erase(addr)` erases exactly one slot (`SAVEGAME_SLOT_SIZE` bytes) starting
/// at `addr`, which is what embedded-savegame expects for NOR flash.
pub struct SavegameFlashAdapter<'a> {
region: &'a mut FlashRegion<'static, MutexFlashStorage>,
}
impl Flash for SavegameFlashAdapter<'_> {
type Error = SavegameFlashError;
fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Self::Error> {
ReadNorFlash::read(self.region, addr, buf).map_err(SavegameFlashError)
}
fn write(&mut self, addr: u32, data: &mut [u8]) -> Result<(), Self::Error> {
NorFlash::write(self.region, addr, data).map_err(SavegameFlashError)
}
/// Erase one full slot at `addr`.
/// embedded-savegame calls this before writing to a slot, so we erase
/// the entire `SAVEGAME_SLOT_SIZE` bytes so subsequent writes land on
/// pre-erased (0xFF) pages.
fn erase(&mut self, addr: u32) -> Result<(), Self::Error> {
let end = addr + SAVEGAME_SLOT_SIZE as u32;
NorFlash::erase(self.region, addr, end).map_err(SavegameFlashError)
}
}
impl From<SavegameFlashError> for FatError {
fn from(e: SavegameFlashError) -> Self {
FatError::String {
error: alloc::format!("Savegame flash error: {:?}", e),
}
}
}
// ── SavegameManager ────────────────────────────────────────────────────────────
/// High-level save-game manager that stores JSON config blobs on the storage
/// partition using [`embedded_savegame`] for wear leveling and power-fail safety.
pub struct SavegameManager {
region: &'static mut FlashRegion<'static, MutexFlashStorage>,
}
impl SavegameManager {
pub fn new(region: &'static mut FlashRegion<'static, MutexFlashStorage>) -> Self {
Self { region }
}
/// Build a short-lived [`Storage`] that borrows our flash region.
fn storage(
&mut self,
) -> Storage<SavegameFlashAdapter<'_>, SAVEGAME_SLOT_SIZE, SAVEGAME_SLOT_COUNT> {
Storage::new(SavegameFlashAdapter {
region: &mut *self.region,
})
}
/// Persist `data` (JSON bytes) to the next available slot.
///
/// `scan()` advances the internal wear-leveling pointer to the latest valid
/// slot before `append()` writes to the next free one.
pub fn save(&mut self, data: &mut [u8]) -> FatResult<()> {
let mut st = self.storage();
st.scan()?;
st.append(data)?;
Ok(())
}
/// Load the most recently saved data. Returns `None` if no valid save exists.
pub fn load_latest(&mut self) -> FatResult<Option<Vec<u8>>> {
let mut st = self.storage();
let slot = st.scan()?;
match slot {
None => Ok(None),
Some(slot) => {
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
match st.read(slot.idx, &mut buf)? {
None => Ok(None),
Some(data) => Ok(Some(data.to_vec())),
}
}
}
}
/// Load a specific save by slot index. Returns `None` if the slot is
/// empty or contains an invalid checksum.
pub fn load_slot(&mut self, idx: usize) -> FatResult<Option<Vec<u8>>> {
let mut st = self.storage();
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
match st.read(idx, &mut buf)? {
None => Ok(None),
Some(data) => Ok(Some(data.to_vec())),
}
}
/// Erase a specific slot by index, effectively deleting it.
pub fn delete_slot(&mut self, idx: usize) -> FatResult<()> {
let mut st = self.storage();
st.erase(idx).map_err(Into::into)
}
/// Iterate all slots and return metadata for every slot that contains a
/// valid save, using the Storage read API to avoid assuming internal slot structure.
pub fn list_saves(&mut self) -> FatResult<Vec<SaveInfo>> {
let mut saves = Vec::new();
let mut st = self.storage();
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
for idx in 0..SAVEGAME_SLOT_COUNT {
if let Some(data) = st.read(idx, &mut buf)? {
saves.push(SaveInfo {
idx,
len: data.len() as u32,
});
}
}
Ok(saves)
}
}

View File

@@ -3,18 +3,20 @@ use crate::config::PlantControllerConfig;
use crate::fat_error::{ContextExt, FatError, FatResult}; use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::BatteryInteraction; 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::{BackupHeader, RTCModuleInteraction, EEPROM_PAGE, X25};
use crate::hal::water::TankSensor; use crate::hal::water::TankSensor;
use crate::hal::{ use crate::hal::{
BoardInteraction, Detection, FreePeripherals, Moistures, Sensor, I2C_DRIVER, PLANT_COUNT, BoardInteraction, Detection, FreePeripherals, Moistures, Sensor, I2C_DRIVER, PLANT_COUNT,
TIME_ACCESS,
}; };
use crate::log::{LogMessage, LOG_ACCESS}; use crate::log::{log, LogMessage};
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 bincode::config;
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET}; use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use canapi::SensorSlot; use canapi::SensorSlot;
use chrono::{DateTime, FixedOffset, Utc};
use core::cmp::min;
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::{Duration, Timer, WithTimeout}; use embassy_time::{Duration, Timer, WithTimeout};
@@ -30,8 +32,12 @@ use ina219::SyncIna219;
use log::{error, info, warn}; use log::{error, info, warn};
use measurements::Resistance; use measurements::Resistance;
use measurements::{Current, Voltage}; use measurements::{Current, Voltage};
// use no_panic::no_panic;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface}; use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
pub const BACKUP_HEADER_MAX_SIZE: usize = 64;
const CONFIG: config::Configuration = config::standard();
const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64; const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::Custom(twai::TimingConfig { const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::Custom(twai::TimingConfig {
baud_rate_prescaler: 200, // 40MHz / 200 * 2 = 100 on C6, 100 * 20 = 2000 divisor, 40MHz / 2000 = 20kHz baud_rate_prescaler: 200, // 40MHz / 200 * 2 = 100 on C6, 100 * 20 = 2000 divisor, 40MHz / 2000 = 20kHz
@@ -294,6 +300,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)
} }
@@ -301,8 +317,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 {
@@ -334,19 +349,12 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
bail!("pump current sensor not available"); bail!("pump current sensor not available");
} }
Some(pump_ina) => { Some(pump_ina) => {
let v = pump_ina let raw = pump_ina.shunt_voltage()?;
.shunt_voltage()
.map_err(|e| FatError::String {
error: alloc::format!("{e:?}"),
})
.map(|v| {
let shunt_voltage = let shunt_voltage =
Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64); Voltage::from_microvolts(raw.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64); let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms(); let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current) Ok(Current::from_amperes(current))
})?;
Ok(v)
} }
} }
} }
@@ -364,7 +372,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> { async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
self.can_power.set_high(); self.can_power.set_high();
Timer::after_millis(500).await; Timer::after_millis(500).await;
let config = self.twai_config.take().expect("twai config not set"); let config = self.twai_config.take().context("twai config not set")?;
let mut twai = config.into_async().start(); let mut twai = config.into_async().start();
if twai.is_bus_off() { if twai.is_bus_off() {
@@ -394,7 +402,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> { async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
self.can_power.set_high(); self.can_power.set_high();
Timer::after_millis(500).await; Timer::after_millis(500).await;
let config = self.twai_config.take().expect("twai config not set"); let config = self.twai_config.take().context("twai config not set")?;
let mut twai = config.into_async().start(); let mut twai = config.into_async().start();
if twai.is_bus_off() { if twai.is_bus_off() {
@@ -508,10 +516,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
for plant in 0..PLANT_COUNT { for plant in 0..PLANT_COUNT {
let a = moisture.sensor_a_hz[plant].unwrap_or(0.0) as u32; let a = moisture.sensor_a_hz[plant].unwrap_or(0.0) as u32;
let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32; let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32;
LOG_ACCESS log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "")
.lock()
.await
.log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "")
.await; .await;
} }
Timer::after_millis(10).await; Timer::after_millis(10).await;
@@ -538,6 +543,74 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
} }
Ok(()) Ok(())
} }
async fn backup_config(&mut self, controller_config: &PlantControllerConfig) -> FatResult<()> {
let mut buffer: [u8; 4096 - BACKUP_HEADER_MAX_SIZE] = [0; 4096 - BACKUP_HEADER_MAX_SIZE];
let length = bincode::encode_into_slice(controller_config, &mut buffer, CONFIG)?;
info!("Writing backup config of size {}", length);
let mut checksum = X25.digest();
checksum.update(&buffer[..length]);
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
let time = self.rtc_module.get_rtc_time().await?.timestamp_millis();
let header = BackupHeader {
crc16: checksum.finalize(),
timestamp: time,
size: length as u16,
};
info!("Header is {:?}", header);
bincode::encode_into_slice(&header, &mut header_page_buffer, CONFIG)?;
info!("Header is serialized");
self.get_rtc_module().write(0, &header_page_buffer)?;
info!("Header written");
let mut to_write = length;
let mut chunk: usize = 0;
while to_write > 0 {
self.progress(chunk as u32).await;
let start = chunk * EEPROM_PAGE;
let end = start + min(EEPROM_PAGE, to_write);
let part = &buffer[start..end];
info!(
"Writing chunk {} of size {} to offset {}",
chunk,
part.len(),
start
);
to_write -= part.len();
chunk += 1;
self.get_rtc_module()
.write((BACKUP_HEADER_MAX_SIZE + chunk * EEPROM_PAGE) as u32, part)?;
}
info!("Backup complete");
self.clear_progress().await;
Ok(())
}
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig> {
let info = self.backup_info().await?;
let mut store = alloc::vec![0_u8; info.size as usize];
self.rtc_module
.read(BACKUP_HEADER_MAX_SIZE as u32, store.as_mut_slice())?;
let mut checksum = X25.digest();
checksum.update(&store[..]);
let crc = checksum.finalize();
if crc != info.crc16 {
bail!("CRC mismatch in backup data")
}
let (decoded, _) = bincode::decode_from_slice(&store[..], CONFIG)?;
Ok(decoded)
}
async fn backup_info(&mut self) -> FatResult<BackupHeader> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.get_rtc_module().read(0, &mut header_page_buffer)?;
info!("Read header page");
let info: Result<(BackupHeader, usize), bincode::error::DecodeError> =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG);
info!("decoding header: {:?}", info);
let (header, _) = info.context("Could not read backup header")?;
Ok(header)
}
} }
async fn wait_for_can_measurements( async fn wait_for_can_measurements(

View File

@@ -33,7 +33,8 @@ impl<'a> TankSensor<'a> {
one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None)); one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None));
let mut adc1_config = AdcConfig::new(); let mut adc1_config = AdcConfig::new();
let tank_pin = adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB); let tank_pin =
adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
let tank_channel = Adc::new(adc1, adc1_config); let tank_channel = Adc::new(adc1, adc1_config);
let one_wire_bus = OneWire::new(one_wire_pin, false); let one_wire_bus = OneWire::new(one_wire_pin, false);
@@ -141,7 +142,12 @@ impl<'a> TankSensor<'a> {
let value = self.tank_channel.read_oneshot(&mut self.tank_pin); let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
//force yield //force yield
Timer::after_millis(10).await; Timer::after_millis(10).await;
*sample = value.unwrap(); match value {
Ok(v) => *sample = v,
Err(e) => {
bail!("ADC Hardware error: {:?}", e);
}
};
} }
self.tank_power.set_low(); self.tank_power.set_low();

View File

@@ -1,10 +1,11 @@
use crate::hal::TIME_ACCESS; use crate::BOARD_ACCESS;
use crate::vec; use crate::vec;
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, warn}; use log::{info, warn};
@@ -32,6 +33,34 @@ static mut LOG_ARRAY: LogArray = LogArray {
pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> = pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
unsafe { Mutex::new(&mut LOG_ARRAY) }; unsafe { Mutex::new(&mut LOG_ARRAY) };
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;
@@ -87,17 +116,27 @@ pub async fn log(
txt_short: &str, txt_short: &str,
txt_long: &str, txt_long: &str,
) { ) {
LOG_ACCESS let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
.lock() let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
.await
.log(message_key, number_a, number_b, txt_short, txt_long) limit_length(txt_short, &mut txt_short_stack);
.await limit_length(txt_long, &mut txt_long_stack);
LOG_CHANNEL
.send(LogRequest {
message_key,
number_a,
number_b,
txt_short: txt_short_stack,
txt_long: txt_long_stack,
})
.await;
} }
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);
@@ -120,17 +159,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;
@@ -148,12 +181,8 @@ impl LogArray {
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();
} }
@@ -281,6 +310,8 @@ pub enum LogMessage {
PumpMissingSensorCurrent, PumpMissingSensorCurrent,
#[strum(serialize = "MPPT Current sensor could not be reached")] #[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError, MPPTError,
#[strum(serialize = "Parsing error reading message")]
UnknownMessage,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -301,7 +332,7 @@ impl From<&LogMessage> for MessageTranslation {
impl LogMessage { impl LogMessage {
pub fn 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

@@ -8,6 +8,7 @@
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer." holding buffers for the duration of a data transfer."
)] )]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
//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!();
@@ -17,8 +18,7 @@ use crate::config::{NetworkConfig, PlantConfig, PlantControllerConfig};
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::PROGRESS_ACTIVE; use crate::hal::PROGRESS_ACTIVE;
use crate::hal::{esp_time, TIME_ACCESS}; use crate::log::log;
use crate::log::{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::{
@@ -39,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::{Duration, Instant, Timer, WithTimeout}; 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};
@@ -167,26 +167,21 @@ 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.get_esp().rtc.set_current_time_us(value.timestamp_micros() as u64);
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
} }
}; };
//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, "", "").await;
.lock()
.await
.log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "")
.await;
} }
info!("cur is {cur}"); info!("cur is {cur}");
match update_charge_indicator(&mut board).await { match update_charge_indicator(&mut board).await {
@@ -199,11 +194,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
} }
} }
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, "", "").await;
.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;
@@ -215,11 +206,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, "", "").await;
.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;
@@ -247,7 +234,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
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))?; let _ = http_server(reboot_now.clone(), stack);
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await;
} }
@@ -297,14 +284,13 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
if let NetworkMode::Wifi { ref ip_address, .. } = network_mode { if let NetworkMode::Wifi { ref ip_address, .. } = network_mode {
publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await; publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await;
publish_battery_state(&mut board).await; publish_battery_state(&mut board).await.unwrap_or_else(|e| {
error!("Error publishing battery state {e}");
});
let _ = publish_mppt_state(&mut board).await; let _ = publish_mppt_state(&mut board).await;
} }
LOG_ACCESS log(
.lock()
.await
.log(
LogMessage::StartupInfo, LogMessage::StartupInfo,
matches!(network_mode, NetworkMode::Wifi { .. }) as u32, matches!(network_mode, NetworkMode::Wifi { .. }) as u32,
matches!( matches!(
@@ -326,17 +312,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
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();
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()).await; wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
} else { } else {
LOG_ACCESS log(LogMessage::NormalRun, 0, 0, "", "").await;
.lock()
.await
.log(LogMessage::NormalRun, 0, 0, "", "")
.await;
} }
let dry_run = false; let dry_run = MQTT_STAY_ALIVE.load(Ordering::Relaxed);
let tank_state = determine_tank_state(&mut board).await; let tank_state = determine_tank_state(&mut board).await;
@@ -345,23 +332,10 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
match err { match err {
TankError::SensorDisabled => { /* unreachable */ } TankError::SensorDisabled => { /* unreachable */ }
TankError::SensorMissing(raw_value_mv) => { TankError::SensorMissing(raw_value_mv) => {
LOG_ACCESS log(LogMessage::TankSensorMissing, raw_value_mv as u32, 0, "", "").await
.lock()
.await
.log(
LogMessage::TankSensorMissing,
raw_value_mv as u32,
0,
"",
"",
)
.await
} }
TankError::SensorValueError { value, min, max } => { TankError::SensorValueError { value, min, max } => {
LOG_ACCESS log(
.lock()
.await
.log(
LogMessage::TankSensorValueRangeError, LogMessage::TankSensorValueRangeError,
min as u32, min as u32,
max as u32, max as u32,
@@ -371,11 +345,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.await .await
} }
TankError::BoardError(err) => { TankError::BoardError(err) => {
LOG_ACCESS log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()).await
.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
@@ -384,11 +354,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, "", "").await;
.lock()
.await
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
.await;
board.board_hal.general_fault(true).await; board.board_hal.general_fault(true).await;
} }
} }
@@ -406,7 +372,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
} }
info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.)); info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.));
publish_tank_state(&mut board, &tank_state, water_temp).await; publish_tank_state(&mut board, &tank_state, water_temp)
.await
.unwrap_or_else(|e| {
error!("Error publishing tank state {e}");
});
let moisture = board.board_hal.measure_moisture_hz().await?; let moisture = board.board_hal.measure_moisture_hz().await?;
@@ -421,7 +391,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
PlantState::read_hardware_state(moisture, 7, &mut board).await, PlantState::read_hardware_state(moisture, 7, &mut board).await,
]; ];
publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; publish_plant_states(&mut board, &timezone_time.clone(), &plantstate)
.await
.unwrap_or_else(|e| {
error!("Error publishing plant states {e}");
});
let pump_required = plantstate let pump_required = plantstate
.iter() .iter()
@@ -429,11 +403,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time)) .any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
&& !water_frozen; && !water_frozen;
if pump_required { if pump_required {
LOG_ACCESS log(LogMessage::EnableMain, dry_run as u32, 0, "", "").await;
.lock()
.await
.log(LogMessage::EnableMain, dry_run as u32, 0, "", "")
.await;
for (plant_id, (state, plant_config)) in plantstate for (plant_id, (state, plant_config)) in plantstate
.iter() .iter()
.zip(&board.board_hal.get_config().plants.clone()) .zip(&board.board_hal.get_config().plants.clone())
@@ -473,12 +443,13 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
board.board_hal.get_esp().last_pump_time(plant_id); board.board_hal.get_esp().last_pump_time(plant_id);
//state.active = true; //state.active = true;
pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await; pump_info(&mut board, plant_id, true, pump_ineffective, 0, 0, 0, false).await;
let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await?; let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await?;
//stop pump regardless of prior result//todo refactor to inner? //stop pump regardless of prior result//todo refactor to inner?
board.board_hal.pump(plant_id, false).await?; board.board_hal.pump(plant_id, false).await?;
pump_info( pump_info(
&mut board,
plant_id, plant_id,
false, false,
pump_ineffective, pump_ineffective,
@@ -625,14 +596,18 @@ 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()); if let Some(s) = stack.take() {
spawner.spawn(http_server(reboot_now.clone(), s))?;
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await; wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await;
} else {
bail!("Network Stack missing, hard abort");
}
} else { } else {
//TODO wait for all mqtt publishes? //TODO wait for all mqtt publishes?
Timer::after_millis(5000).await; Timer::after_millis(5000).await;
board.board_hal.get_esp().set_restart_to_conf(false); board.board_hal.get_esp().set_restart_to_conf(false);
board let _ = board
.board_hal .board_hal
.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) .deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64)
.await; .await;
@@ -801,30 +776,29 @@ async fn publish_tank_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
tank_state: &TankState, tank_state: &TankState,
water_temp: FatResult<f32>, water_temp: FatResult<f32>,
) { ) -> FatResult<()> {
let state = serde_json::to_string( let state = serde_json::to_string(
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
) )?;
.unwrap();
board board
.board_hal .board_hal
.get_esp() .get_esp()
.mqtt_publish("/water", &state) .mqtt_publish("/water", &state)
.await; .await;
Ok(())
} }
async fn publish_plant_states( async fn publish_plant_states(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
timezone_time: &DateTime<Tz>, timezone_time: &DateTime<Tz>,
plantstate: &[PlantState; 8], plantstate: &[PlantState; 8],
) { ) -> FatResult<()> {
for (plant_id, (plant_state, plant_conf)) in plantstate for (plant_id, (plant_state, plant_conf)) in plantstate
.iter() .iter()
.zip(&board.board_hal.get_config().plants.clone()) .zip(&board.board_hal.get_config().plants.clone())
.enumerate() .enumerate()
{ {
let state = let state = serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time))?;
serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap();
let plant_topic = format!("/plant{}", plant_id + 1); let plant_topic = format!("/plant{}", plant_id + 1);
let _ = board let _ = board
.board_hal .board_hal
@@ -832,6 +806,7 @@ async fn publish_plant_states(
.mqtt_publish(&plant_topic, &state) .mqtt_publish(&plant_topic, &state)
.await; .await;
} }
Ok(())
} }
async fn publish_firmware_info( async fn publish_firmware_info(
@@ -907,13 +882,9 @@ async fn try_connect_wifi_sntp_mqtt(
let ip = match stack.config_v4() { let ip = match stack.config_v4() {
Some(config) => config.address.address().to_string(), Some(config) => config.address.address().to_string(),
None => { None => match stack.config_v6() {
match stack.config_v6() {
Some(config) => config.address.address().to_string(), Some(config) => config.address.address().to_string(),
None => { None => String::from("No IP"),
String::from("No IP")
}
}
}, },
}; };
NetworkMode::Wifi { NetworkMode::Wifi {
@@ -931,6 +902,7 @@ async fn try_connect_wifi_sntp_mqtt(
} }
async fn pump_info( async fn pump_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
plant_id: usize, plant_id: usize,
pump_active: bool, pump_active: bool,
pump_ineffective: bool, pump_ineffective: bool,
@@ -950,15 +922,7 @@ async fn pump_info(
match serde_json::to_string(&pump_info) { match serde_json::to_string(&pump_info) {
Ok(state) => { Ok(state) => {
BOARD_ACCESS board.board_hal.get_esp().mqtt_publish(&pump_topic, &state).await;
.get()
.await
.lock()
.await
.board_hal
.get_esp()
.mqtt_publish(&pump_topic, &state)
.await;
} }
Err(err) => { Err(err) => {
warn!("Error publishing pump state {err}"); warn!("Error publishing pump state {err}");
@@ -987,11 +951,11 @@ async fn publish_mppt_state(
async fn publish_battery_state( async fn publish_battery_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
) -> () { ) -> FatResult<()> {
let state = board.board_hal.get_battery_monitor().get_state().await; let state = board.board_hal.get_battery_monitor().get_state().await;
let value = match state { let value = match state {
Ok(state) => { Ok(state) => {
let json = serde_json::to_string(&state).unwrap().to_owned(); let json = serde_json::to_string(&state)?.to_owned();
json.to_owned() json.to_owned()
} }
Err(_) => "error".to_owned(), Err(_) => "error".to_owned(),
@@ -1003,6 +967,7 @@ async fn publish_battery_state(
.mqtt_publish("/battery", &value) .mqtt_publish("/battery", &value)
.await; .await;
} }
Ok(())
} }
async fn wait_infinity( async fn wait_infinity(
@@ -1049,8 +1014,7 @@ async fn wait_infinity(
exit_hold_blink = !exit_hold_blink; exit_hold_blink = !exit_hold_blink;
let progress = core::cmp::min(elapsed, exit_hold_duration); let progress = core::cmp::min(elapsed, exit_hold_duration);
let lit = ((progress.as_millis() as u64 * 8) let lit = ((progress.as_millis() * 8) / exit_hold_duration.as_millis())
/ exit_hold_duration.as_millis() as u64)
.saturating_add(1) .saturating_add(1)
.min(8) as usize; .min(8) as usize;
@@ -1200,10 +1164,13 @@ async fn handle_serial_config(
} }
} }
use embassy_time::WithTimeout;
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
#[esp_rtos::main] #[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();
spawner.must_spawn(crate::log::log_task());
//force init here! //force init here!
match BOARD_ACCESS.init( match BOARD_ACCESS.init(
PlantHal::create() PlantHal::create()

View File

@@ -1,3 +1,4 @@
use bincode::{Decode, Encode};
use crate::hal::Moistures; use crate::hal::Moistures;
use crate::{config::PlantConfig, hal::HAL, in_time_range}; use crate::{config::PlantConfig, hal::HAL, in_time_range};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
@@ -70,7 +71,7 @@ impl PumpState {
} }
} }
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Encode, Decode)]
pub enum PlantWateringMode { pub enum PlantWateringMode {
Off, Off,
TargetMoisture, TargetMoisture,

View File

@@ -158,12 +158,11 @@ pub async fn determine_tank_state(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) -> TankState { ) -> TankState {
if board.board_hal.get_config().tank.tank_sensor_enabled { if board.board_hal.get_config().tank.tank_sensor_enabled {
match board match board.board_hal.get_tank_sensor() {
.board_hal Ok(sensor) => match sensor.tank_sensor_voltage().await {
.get_tank_sensor() Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv),
.map(|f| f.tank_sensor_voltage()) Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
{ },
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()),
Err(err) => TankState::Error(TankError::BoardError(err.to_string())), Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
} }
} else { } else {

View File

@@ -1,8 +1,7 @@
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::hal::rtc::X25; use crate::webserver::read_up_to_bytes_from_request;
use crate::BOARD_ACCESS; use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned; use alloc::borrow::ToOwned;
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;
@@ -21,48 +20,9 @@ pub(crate) async fn get_backup_config<T, const N: usize>(
where where
T: Read + Write, T: Read + Write,
{ {
// First pass: verify checksum without sending data
let mut checksum = X25.digest();
let mut chunk = 0_usize;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await; let backup = board.board_hal.read_backup().await?;
let (buf, len, expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?;
// Update checksum with the actual data bytes of this chunk
checksum.update(&buf[..len]);
let is_last = len == 0 || len < buf.len();
if is_last {
let actual_crc = checksum.finalize();
if actual_crc != expected_crc {
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
409,
Some(
format!("Checksum mismatch expected {expected_crc} got {actual_crc}")
.as_str(),
),
&[],
)
.await?;
return Ok(Some(409));
}
break;
}
chunk += 1;
}
// Second pass: stream data // Second pass: stream data
conn.initiate_response( conn.initiate_response(
200, 200,
@@ -75,35 +35,8 @@ where
) )
.await?; .await?;
let mut chunk = 0_usize; conn.write_all(serde_json::to_string(&backup)?.as_bytes())
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
let (buf, len, _expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?; .await?;
if len == 0 {
break;
}
conn.write_all(&buf[..len]).await?;
if len < buf.len() {
break;
}
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Ok(Some(200)) Ok(Some(200))
} }
@@ -113,49 +46,12 @@ pub(crate) async fn backup_config<T, const N: usize>(
where where
T: Read + Write, T: Read + Write,
{ {
let mut offset = 0_usize; let input = read_up_to_bytes_from_request(conn, Some(4096)).await?;
let mut buf = [0_u8; 32]; info!("Read input with length {}", input.len());
let mut checksum = X25.digest();
let mut counter = 0;
loop {
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("backup finished");
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(counter).await; let config_to_backup = serde_json::from_slice(&input)?;
info!("Parsed send config to object");
counter += 1; board.board_hal.backup_config(&config_to_backup).await?;
board
.board_hal
.get_rtc_module()
.backup_config(offset, &buf[0..to_write])
.await?;
checksum.update(&buf[0..to_write]);
}
offset += to_write;
}
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_rtc_module()
.backup_config_finalize(checksum.finalize(), offset)
.await?;
board.board_hal.clear_progress().await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Ok(Some("saved".to_owned())) Ok(Some("saved".to_owned()))
} }
@@ -166,10 +62,12 @@ where
T: Read + Write, T: Read + Write,
{ {
let mut board = BOARD_ACCESS.get().await.lock().await; let mut board = BOARD_ACCESS.get().await.lock().await;
let header = board.board_hal.get_rtc_module().get_backup_info().await; let info = board.board_hal.backup_info().await;
let json = match header {
let json = match info {
Ok(h) => { Ok(h) => {
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap(); info!("Got backup info: {:?}", h);
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap_or_default();
let wbh = WebBackupHeader { let wbh = WebBackupHeader {
timestamp: timestamp.to_rfc3339(), timestamp: timestamp.to_rfc3339(),
size: h.size, size: h.size,
@@ -177,6 +75,7 @@ where
serde_json::to_string(&wbh)? serde_json::to_string(&wbh)?
} }
Err(err) => { Err(err) => {
info!("Error getting backup info: {:?}", err);
let wbh = WebBackupHeader { let wbh = WebBackupHeader {
timestamp: err.to_string(), timestamp: err.to_string(),
size: 0, size: 0,

View File

@@ -1,160 +0,0 @@
use crate::fat_error::{FatError, FatResult};
use crate::webserver::read_up_to_bytes_from_request;
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use edge_http::io::server::Connection;
use edge_http::Method;
use edge_nal::io::{Read, Write};
use log::info;
pub(crate) async fn list_files<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let result = board.board_hal.get_esp().list_files().await?;
let file_list_json = serde_json::to_string(&result)?;
Ok(Some(file_list_json))
}
pub(crate) async fn file_operations<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
method: Method,
path: &&str,
prefix: &&str,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
let filename = &path[prefix.len()..];
info!("file request for {filename} with method {method}");
Ok(match method {
Method::Delete => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await?;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
Method::Get => {
let disposition = format!("attachment; filename=\"{filename}\"");
let size = {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.get_size(filename.to_owned())
.await?
};
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "application/octet-stream"),
("Content-Disposition", disposition.as_str()),
("Content-Length", &format!("{size}")),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk).await;
let read_chunk = board
.board_hal
.get_esp()
.get_file(filename.to_owned(), chunk)
.await?;
let length = read_chunk.1;
if length == 0 {
info!("file request for {filename} finished");
break;
}
let data = &read_chunk.0[0..length];
conn.write_all(data).await?;
if length < read_chunk.0.len() {
info!("file request for {filename} finished");
break;
}
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Some(200)
}
Method::Post => {
{
let mut board = BOARD_ACCESS.get().await.lock().await;
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
let _ = board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await;
}
let mut offset = 0_usize;
let mut chunk = 0;
loop {
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
if buf.is_empty() {
info!("file request for {filename} finished");
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
board
.board_hal
.get_esp()
.write_file(filename.to_owned(), offset as u32, &buf)
.await?;
}
offset += buf.len();
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
_ => None,
})
}

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;
@@ -114,12 +114,39 @@ pub(crate) async fn get_version_web<T, const N: usize>(
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?)) Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
} }
/// Return the current in-memory config, or — when `saveidx` is `Some(idx)` —
/// the JSON stored in save slot `idx`.
pub(crate) async fn get_config<T, const N: usize>( pub(crate) async fn get_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>, _request: &mut Connection<'_, T, N>,
saveidx: Option<usize>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let json = match saveidx {
None => serde_json::to_string(board.board_hal.get_config())?,
Some(idx) => {
board.board_hal.get_esp().load_config_slot(idx).await?
}
};
Ok(Some(json))
}
/// Return a JSON array describing every valid save slot on flash.
pub(crate) async fn list_saves<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> 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 json = serde_json::to_string(&board.board_hal.get_config())?; let saves = board.board_hal.get_esp().list_saves().await?;
Ok(Some(json)) Ok(Some(serde_json::to_string(&saves)?))
}
/// Erase (delete) a single save slot by index.
pub(crate) async fn delete_save<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
idx: usize,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().delete_save_slot(idx).await?;
Ok(None)
} }
pub(crate) async fn get_battery_state<T, const N: usize>( pub(crate) async fn get_battery_state<T, const N: usize>(
@@ -147,7 +174,7 @@ pub(crate) async fn get_time<T, const N: usize>(
}, },
}; };
let native = esp_time().await.with_timezone(&tz).to_rfc3339(); 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(),

View File

@@ -1,7 +1,6 @@
//offer ota and config mode //offer ota and config mode
mod backup_manager; mod backup_manager;
mod file_manager;
mod get_json; mod get_json;
mod get_log; mod get_log;
mod get_static; mod get_static;
@@ -10,10 +9,9 @@ mod post_json;
use crate::fat_error::{FatError, FatResult}; use crate::fat_error::{FatError, FatResult};
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config}; use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
use crate::webserver::file_manager::{file_operations, list_files};
use crate::webserver::get_json::{ use crate::webserver::get_json::{
get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state, get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state,
get_time, get_timezones, get_version_web, tank_info, delete_save, get_time, get_timezones, get_version_web, list_saves, tank_info,
}; };
use crate::webserver::get_log::get_log; use crate::webserver::get_log::get_log;
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index}; use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
@@ -60,10 +58,7 @@ impl Handler for HTTPRequestRouter {
let method = headers.method; let method = headers.method;
let path = headers.path; let path = headers.path;
let prefix = "/file?filename="; let status = if path == "/ota" {
let status = if path.starts_with(prefix) {
file_operations(conn, method, &path, &prefix).await?
} else if path == "/ota" {
ota_operations(conn, method).await.map_err(|e| { ota_operations(conn, method).await.map_err(|e| {
error!("Error handling ota: {e}"); error!("Error handling ota: {e}");
e e
@@ -82,13 +77,20 @@ impl Handler for HTTPRequestRouter {
"/time" => Some(get_time(conn).await), "/time" => Some(get_time(conn).await),
"/battery" => Some(get_battery_state(conn).await), "/battery" => Some(get_battery_state(conn).await),
"/solar" => Some(get_solar_state(conn).await), "/solar" => Some(get_solar_state(conn).await),
"/get_config" => Some(get_config(conn).await),
"/files" => Some(list_files(conn).await),
"/log_localization" => Some(get_log_localization_config(conn).await), "/log_localization" => Some(get_log_localization_config(conn).await),
"/tank" => Some(tank_info(conn).await), "/tank" => Some(tank_info(conn).await),
"/backup_info" => Some(backup_info(conn).await), "/backup_info" => Some(backup_info(conn).await),
"/timezones" => Some(get_timezones().await), "/timezones" => Some(get_timezones().await),
"/moisture" => Some(get_live_moisture(conn).await), "/moisture" => Some(get_live_moisture(conn).await),
"/list_saves" => Some(list_saves(conn).await),
// /get_config accepts an optional ?saveidx=N query parameter
p if p == "/get_config" || p.starts_with("/get_config?") => {
let saveidx: Option<usize> = p
.find("saveidx=")
.and_then(|pos| p[pos + 8..].split('&').next())
.and_then(|s| s.parse().ok());
Some(get_config(conn, saveidx).await)
}
_ => None, _ => None,
}; };
match json { match json {
@@ -127,7 +129,26 @@ impl Handler for HTTPRequestRouter {
Some(json) => Some(handle_json(conn, json).await?), Some(json) => Some(handle_json(conn, json).await?),
} }
} }
Method::Options | Method::Delete | Method::Head | Method::Put => None, Method::Delete => {
// DELETE /delete_save?idx=N
let json = if path == "/delete_save" || path.starts_with("/delete_save?") {
let idx: Option<usize> = path
.find("idx=")
.and_then(|pos| path[pos + 4..].split('&').next())
.and_then(|s| s.parse().ok());
match idx {
Some(idx) => Some(delete_save(conn, idx).await),
None => Some(Err(FatError::String { error: "missing idx parameter".into() })),
}
} else {
None
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
}
Method::Options | Method::Head | Method::Put => None,
_ => None, _ => None,
} }
}; };
@@ -181,6 +202,7 @@ where
} }
#[embassy_executor::task] #[embassy_executor::task]
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) { pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
let buffer: TcpBuffers<2, 1024, 1024> = TcpBuffers::new(); let buffer: TcpBuffers<2, 1024, 1024> = TcpBuffers::new();
let tcp = Tcp::new(stack, &buffer); let tcp = Tcp::new(stack, &buffer);

View File

@@ -1,6 +1,6 @@
use crate::config::PlantControllerConfig; use crate::config::PlantControllerConfig;
use crate::fat_error::FatResult; use crate::fat_error::FatResult;
use crate::hal::{esp_set_time, Detection}; use crate::hal::Detection;
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::string::{String, ToString}; use alloc::string::{String, ToString};
@@ -108,8 +108,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)
} }

View File

@@ -34,6 +34,11 @@ export interface NetworkConfig {
max_wait: number max_wait: number
} }
export interface SaveInfo {
idx: number,
len: number,
}
export interface FileList { export interface FileList {
total: number, total: number,
used: number, used: number,

View File

@@ -29,41 +29,7 @@
} }
</style> </style>
<div class="subtitle">Files:</div> <div class="subtitle">Save Slots:</div>
<div class="flexcontainer">
<div class="filekey">Total Size</div>
<div id="filetotalsize" class="filevalue"></div>
</div>
<div class="flexcontainer">
<div class="filekey">Used Size</div>
<div id="fileusedsize" class="filevalue"></div>
</div>
<div class="flexcontainer">
<div class="filekey">Free Size</div>
<div id="filefreesize" class="filevalue"></div>
</div>
<br>
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
<div class="subtitle" >Upload:</div>
</div>
<div class="flexcontainer" style="border-left-style: double; border-right-style: double;">
<div class="flexcontainer">
<div class="filekey">
File:
</div>
<input id="fileuploadfile" class="filevalue" type="file">
</div>
<div class="flexcontainer">
<div class="filekey">
Name:
</div>
<input id="fileuploadname" class="filevalue" type="text">
</div>
</div>
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-bottom-style: double;">
<button id="fileuploadbtn" class="subtitle">Upload</button>
</div>
<br> <br>
<div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;"> <div class="flexcontainer" style="border-left-style: double; border-right-style: double; border-top-style: double;">
<div class="subtitle">List:</div> <div class="subtitle">List:</div>

View File

@@ -1,96 +1,49 @@
import {Controller} from "./main"; import {Controller} from "./main";
import {FileInfo, FileList} from "./api"; import {SaveInfo} from "./api";
const regex = /[^a-zA-Z0-9_.]/g;
function sanitize(str:string){
return str.replaceAll(regex, '_')
}
export class FileView { export class FileView {
readonly fileListView: HTMLElement; readonly fileListView: HTMLElement;
readonly controller: Controller; readonly controller: Controller;
readonly filefreesize: HTMLElement;
readonly filetotalsize: HTMLElement;
readonly fileusedsize: HTMLElement;
constructor(controller: Controller) { constructor(controller: Controller) {
(document.getElementById("fileview") as HTMLElement).innerHTML = require('./fileview.html') as string; (document.getElementById("fileview") as HTMLElement).innerHTML = require('./fileview.html') as string;
this.fileListView = document.getElementById("fileList") as HTMLElement this.fileListView = document.getElementById("fileList") as HTMLElement;
this.filefreesize = document.getElementById("filefreesize") as HTMLElement
this.filetotalsize = document.getElementById("filetotalsize") as HTMLElement
this.fileusedsize = document.getElementById("fileusedsize") as HTMLElement
let fileuploadfile = document.getElementById("fileuploadfile") as HTMLInputElement
let fileuploadname = document.getElementById("fileuploadname") as HTMLInputElement
let fileuploadbtn = document.getElementById("fileuploadbtn") as HTMLInputElement
fileuploadfile.onchange = () => {
const selectedFile = fileuploadfile.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return
}
fileuploadname.value = sanitize(selectedFile.name)
};
fileuploadname.onchange = () => {
let input = fileuploadname.value
let clean = sanitize(fileuploadname.value)
if (input != clean){
fileuploadname.value = clean
}
}
fileuploadbtn.onclick = () => {
const selectedFile = fileuploadfile.files?.[0];
if (selectedFile == null) {
//TODO error dialog here
return
}
controller.uploadFile(selectedFile, selectedFile.name)
}
this.controller = controller; this.controller = controller;
} }
setFileList(fileList: FileList, public_url: string) { setSaveList(saves: SaveInfo[], public_url: string) {
this.filetotalsize.innerText = Math.floor(fileList.total / 1024) + "kB" // Sort newest first (highest index = most recently written slot)
this.fileusedsize.innerText = Math.ceil(fileList.used / 1024) + "kB" const sorted = saves.slice().sort((a, b) => b.idx - a.idx);
this.filefreesize.innerText = Math.ceil((fileList.total - fileList.used) / 1024) + "kB"
//fast clear this.fileListView.textContent = "";
this.fileListView.textContent = "" for (let i = 0; i < sorted.length; i++) {
for (let i = 0; i < fileList.files.length; i++) { new SaveEntry(this.controller, i, sorted[i], this.fileListView, public_url);
let file = fileList.files[i]
new FileEntry(this.controller, i, file, this.fileListView, public_url);
} }
} }
} }
class FileEntry { class SaveEntry {
view: HTMLElement; view: HTMLElement;
constructor(controller: Controller, fileid: number, fileinfo: FileInfo, parent: HTMLElement, public_url: string) { constructor(controller: Controller, fileid: number, saveinfo: SaveInfo, parent: HTMLElement, public_url: string) {
this.view = document.createElement("div") as HTMLElement this.view = document.createElement("div") as HTMLElement;
parent.appendChild(this.view) parent.appendChild(this.view);
this.view.classList.add("fileentryouter") this.view.classList.add("fileentryouter");
const template = require('./fileviewentry.html') as string; const template = require('./fileviewentry.html') as string;
this.view.innerHTML = template.replaceAll("${fileid}", String(fileid)) this.view.innerHTML = template.replaceAll("${fileid}", String(fileid));
let name = document.getElementById("file_" + fileid + "_name") as HTMLElement; let name = document.getElementById("file_" + fileid + "_name") as HTMLElement;
let size = document.getElementById("file_" + fileid + "_size") as HTMLElement; let size = document.getElementById("file_" + fileid + "_size") as HTMLElement;
let deleteBtn = document.getElementById("file_" + fileid + "_delete") as HTMLButtonElement; let deleteBtn = document.getElementById("file_" + fileid + "_delete") as HTMLButtonElement;
deleteBtn.onclick = () => { deleteBtn.onclick = () => {
controller.deleteFile(fileinfo.filename); controller.deleteSlot(saveinfo.idx);
} };
let downloadBtn = document.getElementById("file_" + fileid + "_download") as HTMLAnchorElement; let downloadBtn = document.getElementById("file_" + fileid + "_download") as HTMLAnchorElement;
downloadBtn.href = public_url + "/file?filename=" + fileinfo.filename downloadBtn.href = public_url + "/get_config?saveidx=" + saveinfo.idx;
downloadBtn.download = fileinfo.filename downloadBtn.download = "config_slot_" + saveinfo.idx + ".json";
name.innerText = fileinfo.filename; name.innerText = "Slot " + saveinfo.idx;
size.innerText = fileinfo.size.toString() size.innerText = saveinfo.len + " bytes";
} }
} }

View File

@@ -1,5 +1,5 @@
<div class="flexcontainer"> <div class="flexcontainer">
<div id="file_${fileid}_name" class="filetitle">Name</div> <div id="file_${fileid}_name" class="filetitle">Slot</div>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
@@ -9,3 +9,4 @@
<button id="file_${fileid}_delete" class="filevalue">Delete</button> <button id="file_${fileid}_delete" class="filevalue">Delete</button>
</div> </div>

View File

@@ -29,7 +29,7 @@ import {
SetTime, SSIDList, TankInfo, SetTime, SSIDList, TankInfo,
TestPump, TestPump,
VersionInfo, VersionInfo,
FileList, SolarState, PumpTestResult, Detection, CanPower SaveInfo, SolarState, PumpTestResult, Detection, CanPower
} from "./api"; } from "./api";
import {SolarView} from "./solarview"; import {SolarView} from "./solarview";
import {toast} from "./toast"; import {toast} from "./toast";
@@ -93,65 +93,36 @@ export class Controller {
} }
} }
async updateFileList(): Promise<void> { async updateSaveList(): Promise<void> {
try { try {
const response = await fetch(PUBLIC_URL + "/files"); const response = await fetch(PUBLIC_URL + "/list_saves");
const json = await response.json(); const json = await response.json();
const filelist = json as FileList; const saves = json as SaveInfo[];
controller.fileview.setFileList(filelist, PUBLIC_URL); controller.fileview.setSaveList(saves, PUBLIC_URL);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
uploadFile(file: File, name: string) { deleteSlot(idx: number) {
let current = 0; controller.progressview.addIndeterminate("slot_delete", "Deleting slot " + idx);
let max = 100;
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
const ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", event => { ajax.open("DELETE", PUBLIC_URL + "/delete_save?idx=" + idx);
current = event.loaded / 1000;
max = event.total / 1000;
controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")")
}, false);
ajax.addEventListener("load", () => {
controller.progressview.removeProgress("file_upload")
controller.updateFileList()
}, false);
ajax.addEventListener("error", () => {
alert("Error upload")
controller.progressview.removeProgress("file_upload")
controller.updateFileList()
}, false);
ajax.addEventListener("abort", () => {
alert("abort upload")
controller.progressview.removeProgress("file_upload")
controller.updateFileList()
}, false);
ajax.open("POST", PUBLIC_URL + "/file?filename=" + name);
ajax.send(file);
}
deleteFile(name: string) {
controller.progressview.addIndeterminate("file_delete", "Deleting " + name);
const ajax = new XMLHttpRequest();
ajax.open("DELETE", PUBLIC_URL + "/file?filename=" + name);
ajax.send(); ajax.send();
ajax.addEventListener("error", () => { ajax.addEventListener("error", () => {
controller.progressview.removeProgress("file_delete") controller.progressview.removeProgress("slot_delete");
alert("Error delete") alert("Error deleting slot");
controller.updateFileList() controller.updateSaveList();
}, false); }, false);
ajax.addEventListener("abort", () => { ajax.addEventListener("abort", () => {
controller.progressview.removeProgress("file_delete") controller.progressview.removeProgress("slot_delete");
alert("Error upload") alert("Aborted deleting slot");
controller.updateFileList() controller.updateSaveList();
}, false); }, false);
ajax.addEventListener("load", () => { ajax.addEventListener("load", () => {
controller.progressview.removeProgress("file_delete") controller.progressview.removeProgress("slot_delete");
controller.updateFileList() controller.updateSaveList();
}, false); }, false);
controller.updateFileList()
} }
async updateRTCData(): Promise<void> { async updateRTCData(): Promise<void> {
@@ -668,7 +639,7 @@ const tasks = [
{task: controller.updateSolarData, displayString: "Updating Solar Data"}, {task: controller.updateSolarData, displayString: "Updating Solar Data"},
{task: controller.downloadConfig, displayString: "Downloading Configuration"}, {task: controller.downloadConfig, displayString: "Downloading Configuration"},
{task: controller.version, displayString: "Fetching Version Information"}, {task: controller.version, displayString: "Fetching Version Information"},
{task: controller.updateFileList, displayString: "Updating File List"}, {task: controller.updateSaveList, displayString: "Updating Save Slots"},
{task: controller.getBackupInfo, displayString: "Fetching Backup Information"}, {task: controller.getBackupInfo, displayString: "Fetching Backup Information"},
{task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config"}, {task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config"},
{task: controller.loadTankInfo, displayString: "Loading Tank Information"}, {task: controller.loadTankInfo, displayString: "Loading Tank Information"},