use crate::hal::TIME_ACCESS; use crate::vec; use alloc::string::ToString; use alloc::vec::Vec; use bytemuck::{AnyBitPattern, Pod, Zeroable}; use deranged::RangedU8; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::mutex::Mutex; use esp_hal::Persistable; use log::{info, warn}; use serde::Serialize; use strum_macros::IntoStaticStr; use unit_enum::UnitEnum; const LOG_ARRAY_SIZE: u8 = 220; const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1; #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LOG_ARRAY: LogArray = LogArray { buffer: [LogEntryInner { timestamp: 0, message_id: 0, a: 0, b: 0, txt_short: [0; TXT_SHORT_LENGTH], txt_long: [0; TXT_LONG_LENGTH], }; LOG_ARRAY_SIZE as usize], head: 0, }; // this is the only reference that is created for LOG_ARRAY and the only way to access it #[allow(static_mut_refs)] pub static LOG_ACCESS: Mutex = unsafe { Mutex::new(&mut LOG_ARRAY) }; const TXT_SHORT_LENGTH: usize = 8; const TXT_LONG_LENGTH: usize = 32; #[derive(Debug, Clone, Copy, AnyBitPattern)] #[repr(C)] pub struct LogArray { buffer: [LogEntryInner; LOG_ARRAY_SIZE as usize], head: u8, } unsafe impl Persistable for LogArray {} unsafe impl Zeroable for LogEntryInner {} unsafe impl Pod for LogEntryInner {} #[derive(Debug, Clone, Copy)] struct LogEntryInner { pub timestamp: u64, pub message_id: u16, pub a: u32, pub b: u32, pub txt_short: [u8; TXT_SHORT_LENGTH], pub txt_long: [u8; TXT_LONG_LENGTH], } #[derive(Serialize)] pub struct LogEntry { pub timestamp: u64, pub message_id: u16, pub a: u32, pub b: u32, pub txt_short: alloc::string::String, pub txt_long: alloc::string::String, } impl From for LogEntry { fn from(value: LogEntryInner) -> Self { LogEntry { timestamp: value.timestamp, message_id: value.message_id, a: value.a, b: value.b, txt_short: alloc::string::String::from_utf8_lossy_owned(value.txt_short.to_vec()), txt_long: alloc::string::String::from_utf8_lossy_owned(value.txt_long.to_vec()), } } } pub async fn log( message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str, ) { LOG_ACCESS .lock() .await .log(message_key, number_a, number_b, txt_short, txt_long) .await } impl LogArray { pub fn get(&mut self) -> Vec { let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> = RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap()); let mut rv: Vec = Vec::new(); let mut index = head.wrapping_sub(1); for _ in 0..self.buffer.len() { let entry = self.buffer[index.get() as usize]; if (entry.message_id as usize) != LogMessage::Empty.ordinal() { rv.push(entry.into()); } index = index.wrapping_sub(1); } rv } pub async fn log( &mut self, message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str, ) { let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> = RangedU8::new(self.head).unwrap_or(RangedU8::new(0).unwrap()); let mut txt_short_stack: heapless::String = heapless::String::new(); let mut txt_long_stack: heapless::String = heapless::String::new(); limit_length(txt_short, &mut txt_short_stack); limit_length(txt_long, &mut txt_long_stack); let time = { let guard = TIME_ACCESS.get().await.lock().await; guard.current_time_us() } / 1000; let ordinal = message_key.ordinal() as u16; let template: &str = message_key.into(); let mut template_string = template.to_string(); template_string = template_string.replace("${number_a}", number_a.to_string().as_str()); template_string = template_string.replace("${number_b}", number_b.to_string().as_str()); template_string = template_string.replace("${txt_long}", txt_long); template_string = template_string.replace("${txt_short}", txt_short); info!("{template_string}"); let to_modify = &mut self.buffer[head.get() as usize]; to_modify.timestamp = time; to_modify.message_id = ordinal; to_modify.a = number_a; to_modify.b = number_b; to_modify .txt_short .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); self.head = head.get(); } } fn limit_length(input: &str, target: &mut heapless::String) { for char in input.chars() { match target.push(char) { Ok(_) => {} //continue adding chars Err(_) => { //clear space for two asci chars info!("pushing char {char} to limit {LIMIT} current value {target} input {input}"); while target.len() + 2 >= LIMIT { target.pop(); } //add .. to shortened strings match target.push('.') { Ok(_) => {} Err(_) => { warn!( "Error pushin . to limit {LIMIT} current value {target} input {input}" ) } } match target.push('.') { Ok(_) => {} Err(_) => { warn!( "Error pushin . to limit {LIMIT} current value {target} input {input}" ) } } return; } } } while target.len() < LIMIT { match target.push(' ') { Ok(_) => {} Err(_) => { warn!("Error pushing space to limit {LIMIT} current value {target} input {input}") } } } } #[derive(IntoStaticStr, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, UnitEnum)] pub enum LogMessage { #[strum(serialize = "")] Empty, #[strum( serialize = "Reset due to ${txt_long} requires rtc clear ${number_a} and force config mode ${number_b}" )] ResetReason, #[strum(serialize = "Current restart to conf mode ${number_a}")] RestartToConfig, #[strum(serialize = "Current low voltage detection is ${number_a}")] LowVoltage, #[strum(serialize = "Error communicating with battery!! ${txt_long}")] BatteryCommunicationError, #[strum(serialize = "Tank water level cricial! Refill tank!")] TankWaterLevelLow, #[strum(serialize = "Tank sensor hardware error: ${txt_long}")] TankSensorBoardError, #[strum(serialize = "Tank sensor not present, raw voltage measured = ${number_a} mV")] TankSensorMissing, #[strum( serialize = "Tank sensor value out of range, min = ${number_a}%, max = ${number_b}%, value = ${text_short}%" )] TankSensorValueRangeError, #[strum( serialize = "raw measure unscaled ${number_a} hz ${number_b}, plant ${txt_short} sensor ${txt_long}" )] RawMeasure, #[strum(serialize = "IP info: ${txt_long}")] WifiInfo, #[strum(serialize = "Plant:${txt_short} a:${number_a} b:${number_b}")] TestSensor, #[strum(serialize = "Stay alive topic is ${txt_long}")] StayAlive, #[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")] MqttInfo, #[strum(serialize = "Received stay alive with value ${number_a}")] MqttStayAliveRec, #[strum(serialize = "Unknown topic recieved ${txt_long}")] UnknownTopic, #[strum(serialize = "Partition state is ${txt_long}")] PartitionState, #[strum(serialize = "Mounted Filesystem free ${number_a} total ${number_b} use ${txt_short}")] FilesystemMount, #[strum( serialize = "Mounting Filesystem, this will format the first time and needs quite some time!" )] MountingFilesystem, #[strum(serialize = "Year inplausible, force config mode")] YearInplausibleForceConfig, #[strum(serialize = "Going to config mode, due to request from prior run")] ConfigModeSoftwareOverride, #[strum(serialize = "Going to config mode, due to request via config mode button")] ConfigModeButtonOverride, #[strum(serialize = "Going to normal mode")] NormalRun, #[strum(serialize = "Missing normal config, entering config mode ${txt_long}")] ConfigModeMissingConfig, #[strum(serialize = "startup state wifi ${number_a} sntp ${number_b} mqtt ${txt_short}")] StartupInfo, #[strum( serialize = "Trying to pump for ${number_b}s with pump ${number_a} now dryrun: ${txt_short}" )] PumpPlant, #[strum(serialize = "Enable main power dryrun: ${number_a}")] EnableMain, #[strum( serialize = "Pumped multiple times, but plant is still to try attempt: ${number_a} limit :: ${number_b} plant: ${txt_short}" )] ConsecutivePumpCountLimit, #[strum( serialize = "Pump Overcurrent error, pump: ${number_a} tripped overcurrent ${number_b} limit was ${txt_short} @s ${txt_long}" )] PumpOverCurrent, #[strum( serialize = "Pump Open loop error, pump: ${number_a} is low, ${number_b} limit was ${txt_short} @s ${txt_long}" )] PumpOpenLoopCurrent, #[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")] PumpMissingSensorCurrent, } #[derive(Serialize)] pub struct MessageTranslation { msg_type: LogMessage, message: &'static str, } impl From<&LogMessage> for MessageTranslation { fn from(value: &LogMessage) -> Self { Self { msg_type: value.clone(), message: value.into(), } } } impl LogMessage { pub fn to_log_localisation_config() -> Vec { Vec::from_iter((0..LogMessage::len()).map(|i| { let msg_type = LogMessage::from_ordinal(i).unwrap(); (&msg_type).into() })) } }