This commit is contained in:
2025-10-15 02:14:49 +02:00
parent 45e948636b
commit f6f8829cf5
355 changed files with 3283784 additions and 4 deletions

View File

@@ -0,0 +1,258 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use async_trait::async_trait;
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c;
use esp_hal::Blocking;
use measurements::Temperature;
use serde::Serialize;
#[async_trait(?Send)]
pub trait BatteryInteraction {
async fn state_charge_percent(&mut self) -> FatResult<f32>;
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn voltage_milli_volt(&mut self) -> FatResult<u16>;
async fn average_current_milli_ampere(&mut self) -> FatResult<i16>;
async fn cycle_count(&mut self) -> FatResult<u16>;
async fn state_health_percent(&mut self) -> FatResult<u16>;
async fn bat_temperature(&mut self) -> FatResult<u16>;
async fn get_battery_state(&mut self) -> FatResult<BatteryState>;
}
#[derive(Debug, Serialize)]
pub struct BatteryInfo {
pub voltage_milli_volt: u16,
pub average_current_milli_ampere: i16,
pub cycle_count: u16,
pub design_milli_ampere_hour: u16,
pub remaining_milli_ampere_hour: u16,
pub state_of_charge: f32,
pub state_of_health: u16,
pub temperature: u16,
}
#[derive(Debug, Serialize)]
pub enum BatteryState {
Unknown,
Info(BatteryInfo),
}
/// If no battery monitor is installed this implementation will be used
pub struct NoBatteryMonitor {}
#[async_trait(?Send)]
impl BatteryInteraction for NoBatteryMonitor {
async fn state_charge_percent(&mut self) -> FatResult<f32> {
// No monitor configured: assume full battery for lightstate logic
Ok(100.0)
}
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
Err(FatError::NoBatteryMonitor)
}
async fn cycle_count(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn state_health_percent(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn bat_temperature(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Unknown)
}
}
//TODO implement this battery monitor kind once controller is complete
#[allow(dead_code)]
pub struct WchI2cSlave {}
pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>;
pub struct BQ34Z100G1 {
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
}
#[async_trait(?Send)]
impl BatteryInteraction for BQ34Z100G1 {
async fn state_charge_percent(&mut self) -> FatResult<f32> {
self.battery_driver
.state_of_charge()
.map(|v| v as f32)
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.remaining_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.full_charge_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.design_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
self.battery_driver.voltage().map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
self.battery_driver
.average_current()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn cycle_count(&mut self) -> FatResult<u16> {
self.battery_driver
.cycle_count()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn state_health_percent(&mut self) -> FatResult<u16> {
self.battery_driver
.state_of_health()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn bat_temperature(&mut self) -> FatResult<u16> {
self.battery_driver
.temperature()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Info(BatteryInfo {
voltage_milli_volt: self.voltage_milli_volt().await?,
average_current_milli_ampere: self.average_current_milli_ampere().await?,
cycle_count: self.cycle_count().await?,
design_milli_ampere_hour: self.design_milli_ampere_hour().await?,
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?,
state_of_charge: self.state_charge_percent().await?,
state_of_health: self.state_health_percent().await?,
temperature: self.bat_temperature().await?,
}))
}
}
pub fn print_battery_bq34z100(
battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>,
) -> FatResult<()> {
log::info!("Try communicating with battery");
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
log::info!("Firmware {:?}", e);
0
});
log::info!("fw version is {}", fwversion);
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
log::info!("Design capacity {:?}", e);
0
});
log::info!("Design Capacity {}", design_capacity);
if design_capacity == 1000 {
log::info!("Still stock configuring battery, readouts are likely to be wrong!");
}
let flags = battery_driver.get_flags_decoded().unwrap_or(Flags {
fast_charge_allowed: false,
full_chage: false,
charging_not_allowed: false,
charge_inhibit: false,
bat_low: false,
bat_high: false,
over_temp_discharge: false,
over_temp_charge: false,
discharge: false,
state_of_charge_f: false,
state_of_charge_1: false,
cf: false,
ocv_taken: false,
});
log::info!("Flags {:?}", flags);
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
log::info!("Chemid {:?}", e);
0
});
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
log::info!("Bat Temp {:?}", e);
0
});
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
let voltage = battery_driver.voltage().unwrap_or_else(|e| {
log::info!("Bat volt {:?}", e);
0
});
let current = battery_driver.current().unwrap_or_else(|e| {
log::info!("Bat current {:?}", e);
0
});
let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
log::info!("Bat Soc {:?}", e);
0
});
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
log::info!("Bat Charge Volt {:?}", e);
0
});
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
log::info!("Bat Charge Current {:?}", e);
0
});
log::info!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
let _ = battery_driver.unsealed();
let _ = battery_driver.it_enable();
Ok(())
}

View File

@@ -0,0 +1,13 @@
use crate::hal::Sensor;
use bincode::{Decode, Encode};
pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000;
#[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct AutoDetectRequest {}
#[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct ResponseMoisture {
pub plant: u8,
pub sensor: Sensor,
pub hz: u32,
}

View File

@@ -0,0 +1,994 @@
use crate::bail;
use crate::config::{NetworkConfig, PlantControllerConfig};
use crate::hal::{PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS};
use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::{format, string::String, vec, vec::Vec};
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::str::FromStr;
use core::sync::atomic::Ordering;
use embassy_executor::Spawner;
use embassy_net::udp::UdpSocket;
use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{
sleep::{TimerWakeupSource, WakeupLevel},
Rtc,
};
use esp_hal::system::software_reset;
use esp_println::println;
use esp_storage::FlashStorage;
use esp_wifi::wifi::{
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState,
};
use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{info, warn};
use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
QoS, Topic,
};
use portable_atomic::AtomicBool;
use smoltcp::socket::udp::PacketMetadata;
use smoltcp::wire::DnsQueryType;
use sntpc::{get_time, NtpContext, NtpTimestampGenerator};
#[esp_hal::ram(rtc_fast, persistent)]
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)]
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(rtc_fast, persistent)]
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(rtc_fast, persistent)]
static mut RESTART_TO_CONF: i8 = 0;
const CONFIG_FILE: &str = "config.json";
const NTP_SERVER: &str = "pool.ntp.org";
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
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)]
struct Timestamp {
stamp: DateTime<Utc>,
}
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
extern "C" {
fn gpio_pad_hold(gpio_num: u32);
fn gpio_pad_unhold(gpio_num: u32);
}
#[inline(always)]
pub fn hold_enable(gpio_num: u8) {
unsafe { gpio_pad_hold(gpio_num as u32) }
}
#[inline(always)]
pub fn hold_disable(gpio_num: u8) {
unsafe { gpio_pad_unhold(gpio_num as u32) }
}
impl NtpTimestampGenerator for Timestamp {
fn init(&mut self) {
self.stamp = DateTime::default();
}
fn timestamp_sec(&self) -> u64 {
self.stamp.timestamp() as u64
}
fn timestamp_subsec_micros(&self) -> u32 {
self.stamp.timestamp_subsec_micros()
}
}
pub struct Esp<'a> {
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
pub rng: Rng,
//first starter (ap or sta will take these)
pub interface_sta: Option<WifiDevice<'static>>,
pub interface_ap: Option<WifiDevice<'static>>,
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
pub boot_button: Input<'a>,
// RTC-capable GPIO used as external wake source (store the raw peripheral)
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub ota: Ota<'static, FlashStorage>,
pub ota_target: &'static mut FlashRegion<'static, FlashStorage>,
pub current: Slot,
pub slot0_state: OtaImageState,
pub slot1_state: OtaImageState,
}
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
// cooperative tasks with Embassy. All interior mutability of non-Send peripherals is gated
// behind &mut self or embassy_sync Mutex with CriticalSectionRawMutex, which does not rely on
// thread scheduling. Therefore it is sound to mark Esp as Send to satisfy trait object bounds
// (e.g., Box<dyn BoardInteraction + Send>). If you add fields that are accessed from multiple
// CPU cores/threads, reconsider this.
unsafe impl Send for Esp<'_> {}
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
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> {
let _ = check_erase(self.ota_target, offset, offset + 4096);
self.ota_target.erase(offset, offset + 4096)?;
let mut temp = vec![0; buf.len()];
let read_back = temp.as_mut_slice();
//change to nor flash, align writes!
self.ota_target.write(offset, buf)?;
self.ota_target.read(offset, read_back)?;
if buf != read_back {
info!("Expected {:?} but got {:?}", buf, read_back);
bail!(
"Flash error, read back does not match write buffer at offset {:x}",
offset
)
}
Ok(())
}
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
let current = self.ota.current_slot()?;
if self.ota.current_ota_state()? != OtaImageState::Valid {
info!(
"Validating current slot {:?} as it was able to ota",
current
);
self.ota.set_current_ota_state(Valid)?;
}
self.ota.set_current_slot(current.next())?;
info!("switched slot");
self.ota.set_current_ota_state(OtaImageState::New)?;
info!("switched state for new partition");
let state_new = self.ota.current_ota_state()?;
info!("state on new partition now {:?}", state_new);
//determine nextslot crc
self.set_restart_to_conf(true);
Ok(())
}
pub(crate) fn mode_override_pressed(&mut self) -> bool {
self.boot_button.is_low()
}
pub(crate) async fn sntp(
&mut self,
_max_wait_ms: u32,
stack: Stack<'_>,
) -> FatResult<DateTime<Utc>> {
println!("start sntp");
let mut rx_meta = [PacketMetadata::EMPTY; 16];
let mut rx_buffer = [0; 4096];
let mut tx_meta = [PacketMetadata::EMPTY; 16];
let mut tx_buffer = [0; 4096];
let mut socket = UdpSocket::new(
stack,
&mut rx_meta,
&mut rx_buffer,
&mut tx_meta,
&mut tx_buffer,
);
socket.bind(123).unwrap();
let context = NtpContext::new(Timestamp::default());
let ntp_addrs = stack
.dns_query(NTP_SERVER, DnsQueryType::A)
.await
.expect("Failed to resolve DNS");
if ntp_addrs.is_empty() {
bail!("Failed to resolve DNS");
}
info!("NTP server: {:?}", ntp_addrs);
let mut counter = 0;
loop {
let addr: IpAddr = ntp_addrs[0].into();
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
.await;
match timeout {
Ok(result) => {
let time = result?;
info!("Time: {:?}", time);
return DateTime::from_timestamp(time.seconds as i64, 0)
.context("Could not convert Sntp result");
}
Err(err) => {
warn!("sntp timeout, retry: {:?}", err);
counter += 1;
if counter > 10 {
bail!("Failed to get time from NTP server");
}
Timer::after(Duration::from_millis(100)).await;
}
}
}
}
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
info!("start wifi scan");
let mut lock = self.controller.try_lock()?;
info!("start wifi scan lock");
let scan_config = ScanConfig {
ssid: None,
bssid: None,
channel: None,
show_hidden: false,
scan_type: ScanTypeConfig::Active {
min: Default::default(),
max: Default::default(),
},
};
let rv = lock.scan_with_config_async(scan_config).await?;
info!("end wifi scan lock");
Ok(rv)
}
pub(crate) fn last_pump_time(&self, plant: usize) -> Option<DateTime<Utc>> {
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
DateTime::from_timestamp_millis(ts)
}
pub(crate) fn store_last_pump_time(&mut self, plant: usize, time: DateTime<Utc>) {
unsafe {
LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis();
}
}
pub(crate) fn set_low_voltage_in_cycle(&mut self) {
unsafe {
LOW_VOLTAGE_DETECTED = 1;
}
}
pub(crate) fn clear_low_voltage_in_cycle(&mut self) {
unsafe {
LOW_VOLTAGE_DETECTED = 0;
}
}
pub(crate) fn low_voltage_in_cycle(&mut self) -> bool {
unsafe { LOW_VOLTAGE_DETECTED == 1 }
}
pub(crate) fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) {
unsafe {
CONSECUTIVE_WATERING_PLANT[plant] = count;
}
}
pub(crate) fn consecutive_pump_count(&mut self, plant: usize) -> u32 {
unsafe { CONSECUTIVE_WATERING_PLANT[plant] }
}
pub(crate) fn get_restart_to_conf(&mut self) -> bool {
unsafe { RESTART_TO_CONF == 1 }
}
pub(crate) fn set_restart_to_conf(&mut self, to_conf: bool) {
unsafe {
if to_conf {
RESTART_TO_CONF = 1;
} else {
RESTART_TO_CONF = 0;
}
}
}
pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> {
let ssid = match self.load_config().await {
Ok(config) => config.network.ap_ssid.as_str().to_string(),
Err(_) => "PlantCtrl Emergency Mode".to_string(),
};
let spawner = Spawner::for_current_executor().await;
let device = self.interface_ap.take().unwrap();
let gw_ip_addr_str = "192.168.71.1";
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
address: Ipv4Cidr::new(gw_ip_addr, 24),
gateway: Some(gw_ip_addr),
dns_servers: Default::default(),
});
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
println!("init secondary stack");
// Init network stack
let (stack, runner) = embassy_net::new(
device,
config,
mk_static!(StackResources<4>, StackResources::<4>::new()),
seed,
);
let stack = mk_static!(Stack, stack);
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
ssid: ssid.clone(),
..Default::default()
});
self.controller
.lock()
.await
.set_configuration(&client_config)?;
println!("start new");
self.controller.lock().await.start()?;
println!("start net task");
spawner.spawn(net_task(runner)).ok();
println!("run dhcp");
spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok();
loop {
if stack.is_link_up() {
break;
}
Timer::after(Duration::from_millis(500)).await;
}
while !stack.is_config_up() {
Timer::after(Duration::from_millis(100)).await
}
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr_str}/");
stack
.config_v4()
.inspect(|c| println!("ipv4 config: {c:?}"));
Ok(stack.clone())
}
pub(crate) async fn wifi(
&mut self,
network_config: &NetworkConfig,
) -> FatResult<Stack<'static>> {
esp_wifi::wifi_set_log_verbose();
let ssid = network_config.ssid.clone();
match &ssid {
Some(ssid) => {
if ssid.is_empty() {
bail!("Wifi ssid was empty")
}
}
None => {
bail!("Wifi ssid was empty")
}
}
let ssid = ssid.unwrap().to_string();
info!("attempting to connect wifi {ssid}");
let password = match network_config.password {
Some(ref password) => password.to_string(),
None => "".to_string(),
};
let max_wait = network_config.max_wait;
let spawner = Spawner::for_current_executor().await;
let device = self.interface_sta.take().unwrap();
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
// Init network stack
let (stack, runner) = embassy_net::new(
device,
config,
mk_static!(StackResources<8>, StackResources::<8>::new()),
seed,
);
let stack = mk_static!(Stack, stack);
let client_config = Configuration::Client(ClientConfiguration {
ssid,
bssid: None,
auth_method: AuthMethod::WPA2Personal, //FIXME read from config, fill via scan
password,
channel: None,
});
self.controller
.lock()
.await
.set_configuration(&client_config)?;
spawner.spawn(net_task(runner)).ok();
self.controller.lock().await.start_async().await?;
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
loop {
let state = esp_wifi::wifi::sta_state();
match state {
WifiState::StaStarted => {
self.controller.lock().await.connect()?;
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;
}
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
loop {
let state = esp_wifi::wifi::sta_state();
match state {
WifiState::StaConnected => {
break;
}
_ => {}
}
if {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi sta connected")
}
Timer::after(Duration::from_millis(500)).await;
}
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
while !stack.is_link_up() {
if {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} > timeout
{
bail!("Timeout waiting for wifi link up")
}
Timer::after(Duration::from_millis(500)).await;
}
let timeout = {
let guard = TIME_ACCESS.get().await.lock().await;
guard.current_time_us()
} + max_wait as u64 * 1000;
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
}
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(stack.clone())
}
pub fn deep_sleep(
&mut self,
duration_in_ms: u64,
mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>,
) -> ! {
// Configure and enter deep sleep using esp-hal. Also keep prior behavior where
// duration_in_ms == 0 triggers an immediate reset.
// Mark the current OTA image as valid if we reached here while in pending verify.
if let Ok(cur) = self.ota.current_ota_state() {
if cur == OtaImageState::PendingVerify {
self.ota
.set_current_ota_state(OtaImageState::Valid)
.expect("Could not set image to valid");
}
}
if duration_in_ms == 0 {
software_reset();
} else {
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
[(&mut self.wake_gpio1, WakeupLevel::Low)];
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
rtc.sleep_deep(&[&timer, &ext1]);
}
}
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
let cfg = PathBuf::try_from(CONFIG_FILE)?;
let config_exist = self.fs.lock().await.exists(&cfg);
if !config_exist {
bail!("No config file stored")
}
let data = self.fs.lock().await.read::<4096>(&cfg)?;
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
return 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)?;
Ok(())
}
pub(crate) async fn list_files(&self) -> FatResult<FileList> {
let path = PathBuf::new();
let fs = self.fs.lock().await;
let free_size = fs.available_space()?;
let total_size = fs.total_space();
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(())
})?;
Ok(result)
}
pub(crate) async fn init_rtc_deepsleep_memory(
&self,
init_rtc_store: bool,
to_config_mode: bool,
) {
if init_rtc_store {
unsafe {
LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT];
CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT];
LOW_VOLTAGE_DETECTED = 0;
if to_config_mode {
RESTART_TO_CONF = 1
} else {
RESTART_TO_CONF = 0;
}
};
} else {
unsafe {
if to_config_mode {
RESTART_TO_CONF = 1;
}
LOG_ACCESS
.lock()
.await
.log(
LogMessage::RestartToConfig,
RESTART_TO_CONF as u32,
0,
"",
"",
)
.await;
LOG_ACCESS
.lock()
.await
.log(
LogMessage::LowVoltage,
LOW_VOLTAGE_DETECTED as u32,
0,
"",
"",
)
.await;
for i in 0..PLANT_COUNT {
log::info!(
"LAST_WATERING_TIMESTAMP[{}] = UTC {}",
i,
LAST_WATERING_TIMESTAMP[i]
);
}
for i in 0..PLANT_COUNT {
log::info!(
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
i,
CONSECUTIVE_WATERING_PLANT[i]
);
}
}
}
}
pub(crate) async fn mqtt(
&mut self,
network_config: &'static NetworkConfig,
stack: Stack<'static>,
) -> FatResult<()> {
let base_topic = network_config
.base_topic
.as_ref()
.context("missing base topic")?;
if base_topic.is_empty() {
bail!("Mqtt base_topic was empty")
}
MQTT_BASE_TOPIC
.init(base_topic.to_string())
.map_err(|_| FatError::String {
error: "Error setting basetopic".to_string(),
})?;
let mqtt_url = network_config
.mqtt_url
.as_ref()
.context("missing mqtt url")?;
if mqtt_url.is_empty() {
bail!("Mqtt url was empty")
}
let last_will_topic = format!("{}/state", base_topic);
let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
let stay_alive_topic = format!("{}/stay_alive", base_topic);
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
McutieBuilder::new(stack, "plant ctrl", mqtt_url);
if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() {
builder = builder.with_authentication(
network_config.mqtt_user.as_ref().unwrap().as_str(),
network_config.mqtt_password.as_ref().unwrap().as_str(),
);
info!("With authentification");
}
let lwt = Topic::General(last_will_topic);
let lwt = mk_static!(Topic<String>, lwt);
let lwt = lwt.with_display("lost").retain(true).qos(QoS::AtLeastOnce);
builder = builder.with_last_will(lwt);
//TODO make configurable
builder = builder.with_device_id("plantctrl");
let builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 2> = builder
.with_subscriptions([
Topic::General(round_trip_topic.clone()),
Topic::General(stay_alive_topic.clone()),
]);
let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
let (receiver, task) = builder.build(keep_alive);
let spawner = Spawner::for_current_executor().await;
spawner.spawn(mqtt_incoming_task(
receiver,
round_trip_topic.clone(),
stay_alive_topic.clone(),
))?;
spawner.spawn(mqtt_runner(task))?;
LOG_ACCESS
.lock()
.await
.log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
.await;
LOG_ACCESS
.lock()
.await
.log(LogMessage::MqttInfo, 0, 0, "", mqtt_url)
.await;
let mqtt_timeout = 15000;
let timeout = {
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) {
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;
}
Topic::General(round_trip_topic.clone())
.with_display("online_text")
.publish()
.await
.unwrap();
let timeout = {
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) {
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
if cur > timeout {
//ensure we do not further try to publish
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip")
}
Timer::after(Duration::from_millis(100)).await;
}
Ok(())
}
pub(crate) async fn mqtt_inner(&mut self, subtopic: &str, message: &str) -> FatResult<()> {
if !subtopic.starts_with("/") {
bail!("Subtopic without / at start {}", subtopic);
}
if subtopic.len() > 192 {
bail!("Subtopic exceeds 192 chars {}", subtopic);
}
let base_topic = MQTT_BASE_TOPIC
.try_get()
.context("missing base topic in static!")?;
let full_topic = format!("{base_topic}{subtopic}");
loop {
let result = Topic::General(full_topic.as_str())
.with_display(message)
.retain(true)
.publish()
.await;
match result {
Ok(()) => return Ok(()),
Err(err) => {
let retry = match err {
Error::IOError => false,
Error::TimedOut => true,
Error::TooLarge => false,
Error::PacketError => false,
Error::Invalid => false,
};
if !retry {
bail!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
&full_topic,
message,
err
);
}
info!(
"Retransmit for {} with message {:#?} error is {:?} retrying {}",
&full_topic, message, err, retry
);
Timer::after(Duration::from_millis(100)).await;
}
}
}
}
pub(crate) async fn mqtt_publish(&mut self, subtopic: &str, message: &str) {
let online = MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed);
if !online {
return;
}
let roundtrip_ok = MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed);
if !roundtrip_ok {
info!("MQTT roundtrip not received yet, dropping message");
return;
}
match self.mqtt_inner(subtopic, message).await {
Ok(()) => {}
Err(err) => {
info!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
subtopic, message, err
);
}
};
}
}
#[embassy_executor::task]
async fn mqtt_runner(
task: McutieTask<'static, String, PublishDisplay<'static, String, &'static str>, 2>,
) {
task.run().await;
}
#[embassy_executor::task]
async fn mqtt_incoming_task(
receiver: McutieReceiver,
round_trip_topic: String,
stay_alive_topic: String,
) {
loop {
let message = receiver.receive().await;
match message {
MqttMessage::Connected => {
info!("Mqtt connected");
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
}
MqttMessage::Publish(topic, payload) => match topic {
Topic::DeviceType(_type_topic) => {}
Topic::Device(_device_topic) => {}
Topic::General(topic) => {
let subtopic = topic.as_str();
if subtopic.eq(round_trip_topic.as_str()) {
MQTT_ROUND_TRIP_RECEIVED.store(true, Ordering::Relaxed);
} else if subtopic.eq(stay_alive_topic.as_str()) {
let value = payload.eq_ignore_ascii_case("true".as_ref())
|| payload.eq_ignore_ascii_case("1".as_ref());
let a = match value {
true => 1,
false => 0,
};
LOG_ACCESS
.lock()
.await
.log(LogMessage::MqttStayAliveRec, a, 0, "", "")
.await;
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
} else {
LOG_ACCESS
.lock()
.await
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic)
.await;
}
}
},
MqttMessage::Disconnected => {
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
info!("Mqtt disconnected");
}
MqttMessage::HomeAssistantOnline => {
info!("Home assistant is online");
}
}
}
}
#[embassy_executor::task(pool_size = 2)]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await;
}
#[embassy_executor::task]
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
use core::net::{Ipv4Addr, SocketAddrV4};
use edge_dhcp::{
io::{self, DEFAULT_SERVER_PORT},
server::{Server, ServerOptions},
};
use edge_nal::UdpBind;
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 gw_buf = [Ipv4Addr::UNSPECIFIED];
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
let unbound_socket = Udp::new(stack, &buffers);
let mut bound_socket = unbound_socket
.bind(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
DEFAULT_SERVER_PORT,
)))
.await
.unwrap();
loop {
_ = io::server::run(
&mut Server::<_, 64>::new_with_et(ip),
&ServerOptions::new(ip, Some(&mut gw_buf)),
&mut bound_socket,
&mut buf,
)
.await
.inspect_err(|e| log::warn!("DHCP server error: {e:?}"));
Timer::after(Duration::from_millis(500)).await;
}
}

View File

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

View File

@@ -0,0 +1,88 @@
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage;
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, FlashStorage>,
}
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: {} read_size: {}", off, 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: {} write_size: {}", off, 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: {} block_size: {}", off, block_size);
return lfs2Result::Err(lfs2Error::IO);
}
if len % block_size != 0 {
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {} block_size: {}", len, block_size);
return lfs2Result::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 lfs2Result::Err(lfs2Error::IO);
}
}
match self.storage.erase(off as u32, (off + len) as u32) {
Ok(..) => lfs2Result::Ok(len),
Err(err) => {
error!("Littlefs2Filesystem erase error: {:?}", err);
lfs2Result::Err(lfs2Error::IO)
}
}
}
}

View File

@@ -0,0 +1,715 @@
pub(crate) mod battery;
// mod can_api; // replaced by external canapi crate
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
pub(crate) mod rtc;
mod v3_hal;
mod v3_shift_register;
mod v4_hal;
pub(crate) mod v4_sensor;
mod water;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::GPIO0;
use esp_hal::peripherals::GPIO10;
use esp_hal::peripherals::GPIO11;
use esp_hal::peripherals::GPIO12;
use esp_hal::peripherals::GPIO13;
use esp_hal::peripherals::GPIO14;
use esp_hal::peripherals::GPIO15;
use esp_hal::peripherals::GPIO16;
use esp_hal::peripherals::GPIO17;
use esp_hal::peripherals::GPIO18;
use esp_hal::peripherals::GPIO2;
use esp_hal::peripherals::GPIO21;
use esp_hal::peripherals::GPIO22;
use esp_hal::peripherals::GPIO23;
use esp_hal::peripherals::GPIO24;
use esp_hal::peripherals::GPIO25;
use esp_hal::peripherals::GPIO26;
use esp_hal::peripherals::GPIO27;
use esp_hal::peripherals::GPIO28;
use esp_hal::peripherals::GPIO29;
use esp_hal::peripherals::GPIO3;
use esp_hal::peripherals::GPIO30;
use esp_hal::peripherals::GPIO4;
use esp_hal::peripherals::GPIO5;
use esp_hal::peripherals::GPIO6;
use esp_hal::peripherals::GPIO7;
use esp_hal::peripherals::GPIO8;
use esp_hal::peripherals::TWAI0;
use crate::{
bail,
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
hal::{
battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp,
},
log::LogMessage,
BOARD_ACCESS,
};
use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver;
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
use canapi::SensorSlot;
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use esp_bootloader_esp_idf::partitions::{
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
};
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull};
use measurements::{Current, Voltage};
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock;
use embedded_storage::nor_flash::ReadNorFlash;
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::ota::{Slot as ota_slot, Slot};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit;
use esp_hal::pcnt::Pcnt;
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
use esp_hal::system::reset_reason;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::Blocking;
use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage;
use log::{error, info, warn};
use portable_atomic::AtomicBool;
use serde::Serialize;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
//Only support for 8 right now!
pub const PLANT_COUNT: usize = 8;
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new();
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
pub enum Sensor {
A,
B,
}
impl Into<SensorSlot> for Sensor {
fn into(self) -> SensorSlot {
match self {
Sensor::A => SensorSlot::A,
Sensor::B => SensorSlot::B,
}
}
}
pub struct PlantHal {}
pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
}
#[async_trait(?Send)]
pub trait BoardInteraction<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
fn get_esp(&mut self) -> &mut Esp<'a>;
fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
fn is_day(&self) -> bool;
//should be multsampled
async fn light(&mut self, enable: bool) -> FatResult<()>;
async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()>;
async fn pump_current(&mut self, plant: usize) -> FatResult<Current>;
async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()>;
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError>;
async fn general_fault(&mut self, enable: bool);
async fn test(&mut self) -> FatResult<()>;
fn set_config(&mut self, config: PlantControllerConfig);
async fn get_mptt_voltage(&mut self) -> FatResult<Voltage>;
async fn get_mptt_current(&mut self) -> FatResult<Current>;
// Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
bail!("Autodetection is only available on v4 HAL with CAN bus");
}
async fn progress(&mut self, counter: u32) {
// Indicate progress is active to suppress default wait_infinity blinking
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
let current = counter % PLANT_COUNT as u32;
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, current == led as u32).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let even = counter % 2 == 0;
let _ = self.general_fault(even.into()).await;
}
async fn clear_progress(&mut self) {
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, false).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let _ = self.general_fault(false).await;
// Reset progress active flag so wait_infinity can resume blinking
crate::hal::PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
}
}
#[allow(dead_code)]
pub struct FreePeripherals<'a> {
pub gpio0: GPIO0<'a>,
pub gpio2: GPIO2<'a>,
pub gpio3: GPIO3<'a>,
pub gpio4: GPIO4<'a>,
pub gpio5: GPIO5<'a>,
pub gpio6: GPIO6<'a>,
pub gpio7: GPIO7<'a>,
pub gpio8: GPIO8<'a>,
// //config button here
pub gpio10: GPIO10<'a>,
pub gpio11: GPIO11<'a>,
pub gpio12: GPIO12<'a>,
pub gpio13: GPIO13<'a>,
pub gpio14: GPIO14<'a>,
pub gpio15: GPIO15<'a>,
pub gpio16: GPIO16<'a>,
pub gpio17: GPIO17<'a>,
pub gpio18: GPIO18<'a>,
// //i2c here
pub gpio21: GPIO21<'a>,
pub gpio22: GPIO22<'a>,
pub gpio23: GPIO23<'a>,
pub gpio24: GPIO24<'a>,
pub gpio25: GPIO25<'a>,
pub gpio26: GPIO26<'a>,
pub gpio27: GPIO27<'a>,
pub gpio28: GPIO28<'a>,
pub gpio29: GPIO29<'a>,
pub gpio30: GPIO30<'a>,
pub twai: TWAI0<'a>,
pub pcnt0: Unit<'a, 0>,
pub pcnt1: Unit<'a, 1>,
pub adc1: ADC1<'a>,
}
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
impl PlantHal {
pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 64 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let rtc: Rtc = Rtc::new(peripherals.LPWR);
TIME_ACCESS
.init(Mutex::new(rtc))
.map_err(|_| FatError::String {
error: "Init error rct".to_string(),
})?;
let systimer = SystemTimer::new(peripherals.SYSTIMER);
let boot_button = Input::new(
peripherals.GPIO9,
InputConfig::default().with_pull(Pull::None),
);
// Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new(peripherals.RNG);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let esp_wifi_ctrl = &*mk_static!(
EspWifiController<'static>,
init(timg0.timer0, rng.clone()).expect("Could not init wifi controller")
);
let (controller, interfaces) =
esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).expect("Could not init wifi");
use esp_hal::timer::systimer::SystemTimer;
esp_hal_embassy::init(systimer.alarm0);
//let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
//
let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals {
// can: peripherals.can,
// adc1: peripherals.adc1,
// pcnt0: peripherals.pcnt0,
// pcnt1: peripherals.pcnt1,
gpio0: peripherals.GPIO0,
gpio2: peripherals.GPIO2,
gpio3: peripherals.GPIO3,
gpio4: peripherals.GPIO4,
gpio5: peripherals.GPIO5,
gpio6: peripherals.GPIO6,
gpio7: peripherals.GPIO7,
gpio8: peripherals.GPIO8,
gpio10: peripherals.GPIO10,
gpio11: peripherals.GPIO11,
gpio12: peripherals.GPIO12,
gpio13: peripherals.GPIO13,
gpio14: peripherals.GPIO14,
gpio15: peripherals.GPIO15,
gpio16: peripherals.GPIO16,
gpio17: peripherals.GPIO17,
gpio18: peripherals.GPIO18,
gpio21: peripherals.GPIO21,
gpio22: peripherals.GPIO22,
gpio23: peripherals.GPIO23,
gpio24: peripherals.GPIO24,
gpio25: peripherals.GPIO25,
gpio26: peripherals.GPIO26,
gpio27: peripherals.GPIO27,
gpio28: peripherals.GPIO28,
gpio29: peripherals.GPIO29,
gpio30: peripherals.GPIO30,
twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1,
adc1: peripherals.ADC1,
};
let tablebuffer = mk_static!(
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
let ota_data = mk_static!(
PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::Ota,
))?
.expect("No OTA data partition found")
);
let ota_data = mk_static!(
FlashRegion<FlashStorage>,
ota_data.as_embedded_storage(storage_ota)
);
let state_0 = ota_state(ota_slot::Slot0, ota_data);
let state_1 = ota_state(ota_slot::Slot1, ota_data);
let mut ota = Ota::new(ota_data)?;
let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?;
let target = running.next();
info!("Currently running OTA slot: {:?}", running);
info!("Slot0 state: {:?}", state_0);
info!("Slot1 state: {:?}", state_1);
//obtain current_state and next_state here!
let ota_target = match target {
Slot::None => {
panic!("No OTA slot active?");
}
Slot::Slot0 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0,
))?
.context("Partition table invalid no ota0")?,
Slot::Slot1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.context("Partition table invalid no ota1")?,
};
let ota_target = mk_static!(PartitionEntry, ota_target);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let ota_target = mk_static!(
FlashRegion<FlashStorage>,
ota_target.as_embedded_storage(storage_ota)
);
let data_partition = pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::LittleFs,
))?
.expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition);
let storage_data = mk_static!(FlashStorage, FlashStorage::new());
let data = mk_static!(
FlashRegion<FlashStorage>,
data_partition.as_embedded_storage(storage_data)
);
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() {
log::info!("Littlefs2 filesystem is mountable");
} else {
match lfs2filesystem.format() {
Result::Ok(..) => {
log::info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
error!("Littlefs2 filesystem could not be formatted: {:?}", err);
}
}
}
let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
));
let ap = interfaces.ap;
let sta = interfaces.sta;
let mut esp = Esp {
fs,
rng,
controller: Arc::new(Mutex::new(controller)),
interface_sta: Some(sta),
interface_ap: Some(ap),
boot_button,
wake_gpio1,
ota,
ota_target,
current: running,
slot0_state: state_0,
slot1_state: state_1,
};
//init,reset rtc memory depending on cause
let mut init_rtc_store: bool = false;
let mut to_config_mode: bool = false;
let reasons = match reset_reason() {
None => "unknown",
Some(reason) => match reason {
SocResetReason::ChipPowerOn => "power on",
SocResetReason::CoreSDIO => "sdio reset",
SocResetReason::CoreMwdt0 => "Watchdog Main",
SocResetReason::CoreMwdt1 => "Watchdog 1",
SocResetReason::CoreRtcWdt => "Watchdog RTC",
SocResetReason::Cpu0Mwdt0 => "Watchdog MCpu0",
SocResetReason::Cpu0Sw => "software reset cpu0",
SocResetReason::SysRtcWdt => "Watchdog Sys rtc",
SocResetReason::Cpu0Mwdt1 => "cpu0 mwdt1",
SocResetReason::SysSuperWdt => "Watchdog Super",
SocResetReason::Cpu0RtcWdt => {
init_rtc_store = true;
"Watchdog RTC cpu0"
}
SocResetReason::CoreSw => "software reset",
SocResetReason::CoreDeepSleep => "deep sleep",
SocResetReason::SysBrownOut => "sys brown out",
SocResetReason::CoreEfuseCrc => "core efuse crc",
SocResetReason::CoreUsbUart => {
//TODO still required? or via button ignore? to_config_mode = true;
to_config_mode = true;
"core usb uart"
}
SocResetReason::CoreUsbJtag => "core usb jtag",
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
},
};
LOG_ACCESS
.lock()
.await
.log(
LogMessage::ResetReason,
init_rtc_store as u32,
to_config_mode as u32,
"",
&format!("{reasons:?}"),
)
.await;
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await;
let config = esp.load_config().await;
log::info!("Init rtc driver");
let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19;
let i2c = I2c::new(
peripherals.I2C0,
Config::default()
.with_frequency(Rate::from_hz(100))
.with_timeout(BusTimeout::Maximum),
)?
.with_scl(scl)
.with_sda(sda);
let i2c_bus: embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<I2c<Blocking>>,
> = CriticalSectionMutex::new(RefCell::new(i2c));
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(&i2c_bus);
let eeprom_device = I2cDevice::new(&i2c_bus);
let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
DS3231,
> = Ds323x::new_ds3231(rtc_device);
info!("Init rtc eeprom driver");
let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true));
let rtc_time = rtc.datetime();
match rtc_time {
Ok(tt) => {
log::info!("Rtc Module reports time at UTC {}", tt);
}
Err(err) => {
log::info!("Rtc Module could not be read {:?}", err);
}
}
let storage: Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<Blocking>>,
B32,
TwoBytes,
No,
Delay,
> = Storage::new(eeprom, Delay::new());
let rtc_module: Box<dyn RTCModuleInteraction + Send> =
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
let hal = match config {
Result::Ok(config) => {
let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => {
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut battery_driver = Bq34z100g1Driver {
i2c: battery_device,
delay: Delay::new(),
flash_block_data: [0; 32],
};
let status = print_battery_bq34z100(&mut battery_driver);
match status {
Ok(_) => {}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::BatteryCommunicationError,
0u32,
0,
"",
&format!("{err:?})"),
)
.await;
}
}
Box::new(BQ34Z100G1 { battery_driver })
}
BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible
Box::new(NoBatteryMonitor {})
}
};
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
BoardVersion::INITIAL => {
initial_hal::create_initial_board(free_pins, config, esp)?
}
BoardVersion::V3 => {
v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)?
}
BoardVersion::V4 => {
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?
}
};
HAL { board_hal }
}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::ConfigModeMissingConfig,
0,
0,
"",
&err.to_string(),
)
.await;
HAL {
board_hal: initial_hal::create_initial_board(
free_pins,
PlantControllerConfig::default(),
esp,
)?,
}
}
};
Ok(Mutex::new(hal))
}
}
fn ota_state(slot: ota_slot, ota_data: &mut FlashRegion<FlashStorage>) -> OtaImageState {
// Read and log OTA states for both slots before constructing Ota
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
if slot == ota_slot::None {
return OtaImageState::Undefined;
}
let mut slot_buf = [0u8; 32];
if slot == ota_slot::Slot0 {
let _ = ota_data.read(0x0000, &mut slot_buf);
} else {
let _ = ota_data.read(0x1000, &mut slot_buf);
}
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
let state0 = OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined);
state0
}
fn get_current_slot_and_fix_ota_data(
ota: &mut Ota<FlashStorage>,
state0: OtaImageState,
state1: OtaImageState,
) -> Result<ota_slot, FatError> {
let state = ota.current_ota_state().unwrap_or_default();
let swap = match state {
OtaImageState::Invalid => true,
OtaImageState::Aborted => true,
OtaImageState::Undefined => {
info!("Undefined image in current slot, bootloader wrong?");
false
}
_ => false,
};
let current = ota.current_slot()?;
if swap {
let other = match current {
ota_slot::Slot0 => state1,
ota_slot::Slot1 => state0,
_ => OtaImageState::Invalid,
};
match other {
OtaImageState::Invalid => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
OtaImageState::Aborted => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
_ => {}
}
info!(
"Current slot has state {:?} other state has {:?} swapping",
state, other
);
ota.set_current_slot(current.next())?;
//we actually booted other slot, than partition table assumes
return Ok(ota.current_slot()?);
};
Ok(current)
}
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)]
pub struct Moistures{
pub sensor_a_hz: [f32; PLANT_COUNT],
pub sensor_b_hz: [f32; PLANT_COUNT],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub struct DetectionResult {
plant: [DetectionSensorResult; crate::hal::PLANT_COUNT]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub struct DetectionSensorResult{
sensor_a: bool,
sensor_b: bool,
}

View File

@@ -0,0 +1,133 @@
use crate::hal::Box;
use crate::fat_error::FatResult;
use async_trait::async_trait;
use bincode::config::Configuration;
use bincode::{config, Decode, Encode};
use chrono::{DateTime, Utc};
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_storage::{ReadStorage, Storage};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c;
use esp_hal::Blocking;
use serde::{Deserialize, Serialize};
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
const CONFIG: Configuration = config::standard();
//
#[async_trait(?Send)]
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 set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>;
}
//
const BACKUP_HEADER_MAX_SIZE: usize = 64;
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
pub struct BackupHeader {
pub timestamp: i64,
crc16: u16,
pub size: u16,
}
//
pub struct DS3231Module {
pub(crate) rtc: Ds323x<
I2cInterface<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
DS3231,
>,
pub(crate) storage: eeprom24x::Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
B32,
TwoBytes,
No,
Delay,
>,
}
#[async_trait(?Send)]
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 {:?} with size {}", header_page_buffer, 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 {:?} with size {}",
header_page_buffer,
encoded
);
self.storage.write(0, &header_page_buffer)?;
Ok(())
}
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> {
Ok(self.rtc.datetime()?.and_utc())
}
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()> {
let naive_time = time.naive_utc();
Ok(self.rtc.set_datetime(&naive_time)?)
}
}

View File

@@ -0,0 +1,450 @@
use crate::bail;
use crate::fat_error::FatError;
use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, Sensor, PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS};
use crate::{
config::PlantControllerConfig,
hal::{battery::BatteryInteraction, esp::Esp},
};
use alloc::boxed::Box;
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::Timer;
use embedded_hal::digital::OutputPin as _;
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::unit::Unit;
use measurements::{Current, Voltage};
const PUMP8_BIT: usize = 0;
const PUMP1_BIT: usize = 1;
const PUMP2_BIT: usize = 2;
const PUMP3_BIT: usize = 3;
const PUMP4_BIT: usize = 4;
const PUMP5_BIT: usize = 5;
const PUMP6_BIT: usize = 6;
const PUMP7_BIT: usize = 7;
const MS_0: usize = 8;
const MS_4: usize = 9;
const MS_2: usize = 10;
const MS_3: usize = 11;
const MS_1: usize = 13;
const SENSOR_ON: usize = 12;
const SENSOR_A_1: u8 = 7;
const SENSOR_A_2: u8 = 6;
const SENSOR_A_3: u8 = 5;
const SENSOR_A_4: u8 = 4;
const SENSOR_A_5: u8 = 3;
const SENSOR_A_6: u8 = 2;
const SENSOR_A_7: u8 = 1;
const SENSOR_A_8: u8 = 0;
const SENSOR_B_1: u8 = 8;
const SENSOR_B_2: u8 = 9;
const SENSOR_B_3: u8 = 10;
const SENSOR_B_4: u8 = 11;
const SENSOR_B_5: u8 = 12;
const SENSOR_B_6: u8 = 13;
const SENSOR_B_7: u8 = 14;
const SENSOR_B_8: u8 = 15;
const CHARGING: usize = 14;
const AWAKE: usize = 15;
const FAULT_3: usize = 16;
const FAULT_8: usize = 17;
const FAULT_7: usize = 18;
const FAULT_6: usize = 19;
const FAULT_5: usize = 20;
const FAULT_4: usize = 21;
const FAULT_1: usize = 22;
const FAULT_2: usize = 23;
const REPEAT_MOIST_MEASURE: usize = 1;
pub struct V3<'a> {
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
esp: Esp<'a>,
shift_register:
Mutex<CriticalSectionRawMutex, ShiftRegister40<Output<'a>, Output<'a>, Output<'a>>>,
_shift_register_enable_invert: Output<'a>,
tank_sensor: TankSensor<'a>,
solar_is_day: Input<'a>,
light: Output<'a>,
main_pump: Output<'a>,
general_fault: Output<'a>,
pub signal_counter: Unit<'static, 0>,
}
pub(crate) fn create_v3(
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
log::info!("Start v3");
let clock = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
let latch = Output::new(peripherals.gpio3, Level::Low, OutputConfig::default());
let data = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
let shift_register = ShiftRegister40::new(clock, latch, data);
//disable all
for mut pin in shift_register.decompose() {
let _ = pin.set_low();
}
// Set always-on status bits
let _ = shift_register.decompose()[AWAKE].set_high();
let _ = shift_register.decompose()[CHARGING].set_high();
// Multiplexer defaults: ms0..ms3 low, ms4 high (disabled)
let _ = shift_register.decompose()[MS_0].set_low();
let _ = shift_register.decompose()[MS_1].set_low();
let _ = shift_register.decompose()[MS_2].set_low();
let _ = shift_register.decompose()[MS_3].set_low();
let _ = shift_register.decompose()[MS_4].set_high();
let one_wire_pin = Flex::new(peripherals.gpio18);
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let flow_sensor_pin = Input::new(
peripherals.gpio4,
InputConfig::default().with_pull(Pull::Up),
);
let tank_sensor = TankSensor::create(
one_wire_pin,
peripherals.adc1,
peripherals.gpio5,
tank_power_pin,
flow_sensor_pin,
peripherals.pcnt1,
)?;
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, OutputConfig::default());
let mut main_pump = Output::new(peripherals.gpio2, Level::Low, OutputConfig::default());
main_pump.set_low();
let mut general_fault = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
general_fault.set_low();
let mut shift_register_enable_invert =
Output::new(peripherals.gpio21, Level::Low, OutputConfig::default());
shift_register_enable_invert.set_low();
let signal_counter = peripherals.pcnt0;
signal_counter.set_high_limit(Some(i16::MAX))?;
let ch0 = &signal_counter.channel0;
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
ch0.set_edge_signal(edge_pin.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
signal_counter.listen();
Ok(Box::new(V3 {
config,
battery_monitor,
rtc_module,
esp,
shift_register: Mutex::new(shift_register),
_shift_register_enable_invert: shift_register_enable_invert,
tank_sensor,
solar_is_day,
light,
main_pump,
general_fault,
signal_counter,
}))
}
impl V3<'_> {
async fn inner_measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE {
self.signal_counter.pause();
self.signal_counter.clear();
//Disable all
{
let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?;
}
let sensor_channel = match sensor {
Sensor::A => match plant {
0 => SENSOR_A_1,
1 => SENSOR_A_2,
2 => SENSOR_A_3,
3 => SENSOR_A_4,
4 => SENSOR_A_5,
5 => SENSOR_A_6,
6 => SENSOR_A_7,
7 => SENSOR_A_8,
_ => bail!("Invalid plant id {}", plant),
},
Sensor::B => match plant {
0 => SENSOR_B_1,
1 => SENSOR_B_2,
2 => SENSOR_B_3,
3 => SENSOR_B_4,
4 => SENSOR_B_5,
5 => SENSOR_B_6,
6 => SENSOR_B_7,
7 => SENSOR_B_8,
_ => bail!("Invalid plant id {}", plant),
},
};
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
{
let shift_register = self.shift_register.lock().await;
let pin_0 = &mut shift_register.decompose()[MS_0];
let pin_1 = &mut shift_register.decompose()[MS_1];
let pin_2 = &mut shift_register.decompose()[MS_2];
let pin_3 = &mut shift_register.decompose()[MS_3];
if is_bit_set(0) {
pin_0.set_high()?;
} else {
pin_0.set_low()?;
}
if is_bit_set(1) {
pin_1.set_high()?;
} else {
pin_1.set_low()?;
}
if is_bit_set(2) {
pin_2.set_high()?;
} else {
pin_2.set_low()?;
}
if is_bit_set(3) {
pin_3.set_high()?;
} else {
pin_3.set_low()?;
}
shift_register.decompose()[MS_4].set_low()?;
shift_register.decompose()[SENSOR_ON].set_high()?;
}
let measurement = 100; //how long to measure and then extrapolate to hz
let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz
//give some time to stabilize
Timer::after_millis(10).await;
self.signal_counter.resume();
Timer::after_millis(measurement).await;
self.signal_counter.pause();
{
let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?;
shift_register.decompose()[SENSOR_ON].set_low()?;
}
Timer::after_millis(10).await;
let unscaled = self.signal_counter.value();
let hz = unscaled as f32 * factor;
LOG_ACCESS
.lock()
.await
.log(
LogMessage::RawMeasure,
unscaled as u32,
hz as u32,
&plant.to_string(),
&format!("{sensor:?}"),
)
.await;
results[repeat] = hz;
}
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
let mid = results.len() / 2;
let median = results[mid];
Ok(median)
}
}
#[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for V3<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Ok(&mut self.tank_sensor)
}
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
fn get_config(&mut self) -> &PlantControllerConfig {
&self.config
}
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
&mut self.battery_monitor
}
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module
}
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
let shift_register = self.shift_register.lock().await;
if charging {
let _ = shift_register.decompose()[CHARGING].set_high();
} else {
let _ = shift_register.decompose()[CHARGING].set_low();
}
Ok(())
}
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low();
let guard = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, guard)
}
fn is_day(&self) -> bool {
self.solar_is_day.is_high()
}
async fn light(&mut self, enable: bool) -> Result<(), FatError> {
hold_disable(10);
if enable {
self.light.set_high();
} else {
self.light.set_low();
}
hold_enable(10);
Ok(())
}
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
if enable {
self.main_pump.set_high();
}
let index = match plant {
0 => PUMP1_BIT,
1 => PUMP2_BIT,
2 => PUMP3_BIT,
3 => PUMP4_BIT,
4 => PUMP5_BIT,
5 => PUMP6_BIT,
6 => PUMP7_BIT,
7 => PUMP8_BIT,
_ => bail!("Invalid pump {plant}"),
};
let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
if !enable {
self.main_pump.set_low();
}
Ok(())
}
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Not implemented in v3")
}
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
let index = match plant {
0 => FAULT_1,
1 => FAULT_2,
2 => FAULT_3,
3 => FAULT_4,
4 => FAULT_5,
5 => FAULT_6,
6 => FAULT_7,
7 => FAULT_8,
_ => panic!("Invalid plant id {}", plant),
};
let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
Ok(())
}
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
let mut result = Moistures::default();
for plant in 0..PLANT_COUNT {
let a = self.inner_measure_moisture_hz(plant, Sensor::A).await;
let b = self.inner_measure_moisture_hz(plant, Sensor::B).await;
let aa = a.unwrap_or_else(|_| u32::MAX as f32);
let bb = b.unwrap_or_else(|_| u32::MAX as f32);
LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, aa as u32, bb as u32, &plant.to_string(), "")
.await;
result.sensor_a_hz[plant] = aa;
result.sensor_b_hz[plant] = bb;
}
Ok(result)
}
async fn general_fault(&mut self, enable: bool) {
hold_disable(6);
if enable {
self.general_fault.set_high();
} else {
self.general_fault.set_low();
}
hold_enable(6);
}
async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true).await;
Timer::after_millis(100).await;
self.general_fault(false).await;
Timer::after_millis(100).await;
self.light(true).await?;
Timer::after_millis(500).await;
self.light(false).await?;
Timer::after_millis(500).await;
for i in 0..PLANT_COUNT {
self.fault(i, true).await?;
Timer::after_millis(500).await;
self.fault(i, false).await?;
Timer::after_millis(500).await;
}
for i in 0..PLANT_COUNT {
self.pump(i, true).await?;
Timer::after_millis(100).await;
self.pump(i, false).await?;
Timer::after_millis(100).await;
}
self.measure_moisture_hz().await?;
Timer::after_millis(10).await;
Ok(())
}
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
}
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
bail!("Not implemented in v3")
}
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
bail!("Not implemented in v3")
}
}

View File

@@ -0,0 +1,154 @@
//! Serial-in parallel-out shift register
#![allow(warnings)]
use core::cell::RefCell;
use core::convert::Infallible;
use core::iter::Iterator;
use core::mem::{self, MaybeUninit};
use core::result::{Result, Result::Ok};
use embedded_hal::digital::OutputPin;
trait ShiftRegisterInternal: Send {
fn update(&self, index: usize, command: bool) -> Result<(), ()>;
}
/// Output pin of the shift register
pub struct ShiftRegisterPin<'a> {
shift_register: &'a dyn ShiftRegisterInternal,
index: usize,
}
impl<'a> ShiftRegisterPin<'a> {
fn new(shift_register: &'a dyn ShiftRegisterInternal, index: usize) -> Self {
ShiftRegisterPin {
shift_register,
index,
}
}
}
impl embedded_hal::digital::ErrorType for ShiftRegisterPin<'_> {
type Error = Infallible;
}
impl OutputPin for ShiftRegisterPin<'_> {
fn set_low(&mut self) -> Result<(), Infallible> {
self.shift_register.update(self.index, false).unwrap();
Ok(())
}
fn set_high(&mut self) -> Result<(), Infallible> {
self.shift_register.update(self.index, true).unwrap();
Ok(())
}
}
macro_rules! ShiftRegisterBuilder {
($name: ident, $size: expr) => {
/// Serial-in parallel-out shift register
pub struct $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
clock: RefCell<Pin1>,
latch: RefCell<Pin2>,
data: RefCell<Pin3>,
output_state: RefCell<[bool; $size]>,
}
impl<Pin1, Pin2, Pin3> ShiftRegisterInternal for $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
/// Sets the value of the shift register output at `index` to value `command`
fn update(&self, index: usize, command: bool) -> Result<(), ()> {
self.output_state.borrow_mut()[index] = command;
let output_state = self.output_state.borrow();
self.latch.borrow_mut().set_low().map_err(|_e| ())?;
for i in 1..=output_state.len() {
if output_state[output_state.len() - i] {
self.data.borrow_mut().set_high().map_err(|_e| ())?;
} else {
self.data.borrow_mut().set_low().map_err(|_e| ())?;
}
self.clock.borrow_mut().set_high().map_err(|_e| ())?;
self.clock.borrow_mut().set_low().map_err(|_e| ())?;
}
self.latch.borrow_mut().set_high().map_err(|_e| ())?;
Ok(())
}
}
impl<Pin1, Pin2, Pin3> $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
/// Creates a new SIPO shift register from clock, latch, and data output pins
pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self {
$name {
clock: RefCell::new(clock),
latch: RefCell::new(latch),
data: RefCell::new(data),
output_state: RefCell::new([false; $size]),
}
}
/// Get embedded-hal output pins to control the shift register outputs
pub fn decompose(&self) -> [ShiftRegisterPin<'_>; $size] {
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
// safe because the type we are claiming to have initialized here is a
// bunch of `MaybeUninit`s, which do not require initialization.
let mut pins: [MaybeUninit<ShiftRegisterPin>; $size] =
unsafe { MaybeUninit::uninit().assume_init() };
// Dropping a `MaybeUninit` does nothing, so if there is a panic during this loop,
// we have a memory leak, but there is no memory safety issue.
for (index, elem) in pins.iter_mut().enumerate() {
elem.write(ShiftRegisterPin::new(self, index));
}
// Everything is initialized. Transmute the array to the
// initialized type.
unsafe { mem::transmute::<_, [ShiftRegisterPin; $size]>(pins) }
}
/// Consume the shift register and return the original clock, latch, and data output pins
pub fn release(self) -> (Pin1, Pin2, Pin3) {
let Self {
clock,
latch,
data,
output_state: _,
} = self;
(clock.into_inner(), latch.into_inner(), data.into_inner())
}
}
};
}
ShiftRegisterBuilder!(ShiftRegister8, 8);
ShiftRegisterBuilder!(ShiftRegister16, 16);
ShiftRegisterBuilder!(ShiftRegister24, 24);
ShiftRegisterBuilder!(ShiftRegister32, 32);
ShiftRegisterBuilder!(ShiftRegister40, 40);
ShiftRegisterBuilder!(ShiftRegister48, 48);
ShiftRegisterBuilder!(ShiftRegister56, 56);
ShiftRegisterBuilder!(ShiftRegister64, 64);
ShiftRegisterBuilder!(ShiftRegister72, 72);
ShiftRegisterBuilder!(ShiftRegister80, 80);
ShiftRegisterBuilder!(ShiftRegister88, 88);
ShiftRegisterBuilder!(ShiftRegister96, 96);
ShiftRegisterBuilder!(ShiftRegister104, 104);
ShiftRegisterBuilder!(ShiftRegister112, 112);
ShiftRegisterBuilder!(ShiftRegister120, 120);
ShiftRegisterBuilder!(ShiftRegister128, 128);
/// 8 output serial-in parallel-out shift register
pub type ShiftRegister<Pin1, Pin2, Pin3> = ShiftRegister8<Pin1, Pin2, Pin3>;

View File

@@ -0,0 +1,458 @@
use crate::bail;
use crate::config::PlantControllerConfig;
use crate::fat_error::{FatError, FatResult};
use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, DetectionResult, FreePeripherals, Moistures, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::boxed::Box;
use alloc::string::ToString;
use async_trait::async_trait;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::twai::TwaiMode;
use esp_hal::{twai, Blocking};
use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode, Resolution};
use ina219::SyncIna219;
use measurements::Resistance;
use measurements::{Current, Voltage};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::B125K;
pub enum Charger<'a> {
SolarMpptV1 {
mppt_ina: SyncIna219<
I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>,
UnCalibrated,
>,
solar_is_day: Input<'a>,
charge_indicator: Output<'a>,
},
ErrorInit {},
}
impl<'a> Charger<'a> {
pub(crate) fn get_mppt_current(&mut self) -> FatResult<Current> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.shunt_voltage()?;
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(MPPT_CURRENT_SHUNT_OHMS);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Ok(Current::from_amperes(current))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
pub(crate) fn get_mptt_voltage(&mut self) -> FatResult<Voltage> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.bus_voltage()?;
Ok(Voltage::from_millivolts(v.voltage_mv() as f64))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
}
impl Charger<'_> {
pub(crate) fn power_save(&mut self) {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let _ = mppt_ina
.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Default::default(),
operating_mode: OperatingMode::PowerDown,
})
.map_err(|e| {
log::info!(
"Error setting ina mppt configuration during deep sleep preparation{:?}",
e
);
});
}
_ => {}
}
}
fn set_charge_indicator(&mut self, charging: bool) -> FatResult<()> {
match self {
Self::SolarMpptV1 {
charge_indicator, ..
} => {
charge_indicator.set_level(charging.into());
}
_ => {}
}
Ok(())
}
fn is_day(&self) -> bool {
match self {
Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.is_high(),
_ => true,
}
}
}
pub struct V4<'a> {
esp: Esp<'a>,
tank_sensor: TankSensor<'a>,
charger: Charger<'a>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
battery_monitor: Box<dyn BatteryInteraction + Send>,
config: PlantControllerConfig,
awake: Output<'a>,
light: Output<'a>,
general_fault: Output<'a>,
pump_expander: Pca9535Immediate<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
pump_ina: Option<
SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
>,
sensor: SensorImpl,
extra1: Output<'a>,
extra2: Output<'a>,
}
pub(crate) async fn create_v4(
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
log::info!("Start v4");
let mut awake = Output::new(peripherals.gpio21, Level::High, OutputConfig::default());
awake.set_high();
let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
general_fault.set_low();
let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
let one_wire_pin = Flex::new(peripherals.gpio18);
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let flow_sensor_pin = Input::new(
peripherals.gpio4,
InputConfig::default().with_pull(Pull::Up),
);
let tank_sensor = TankSensor::create(
one_wire_pin,
peripherals.adc1,
peripherals.gpio5,
tank_power_pin,
flow_sensor_pin,
peripherals.pcnt1,
)?;
let sensor_expander_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut sensor_expander = Pca9535Immediate::new(sensor_expander_device, 34);
let sensor = match sensor_expander.pin_into_output(GPIOBank::Bank0, 0) {
Ok(_) => {
log::info!("SensorExpander answered");
let signal_counter = peripherals.pcnt0;
signal_counter.set_high_limit(Some(i16::MAX))?;
let ch0 = &signal_counter.channel0;
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
ch0.set_edge_signal(edge_pin.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
signal_counter.listen();
for pin in 0..8 {
let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_into_output(GPIOBank::Bank1, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin);
}
SensorImpl::PulseCounter {
signal_counter,
sensor_expander,
}
}
Err(_) => {
log::info!("Can bus mode ");
let twai_config = Some(twai::TwaiConfiguration::new(
peripherals.twai,
peripherals.gpio2,
peripherals.gpio0,
TWAI_BAUDRATE,
TwaiMode::Normal,
));
let can_power = Output::new(peripherals.gpio22, Level::Low, OutputConfig::default());
//can bus version
SensorImpl::CanBus {
twai_config,
can_power,
}
}
};
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, Default::default());
let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default());
let pump_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut pump_expander = Pca9535Immediate::new(pump_device, 32);
for pin in 0..8 {
let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin);
let _ = pump_expander.pin_set_low(GPIOBank::Bank0, pin);
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
}
let mppt_current = I2cDevice::new(I2C_DRIVER.get().await);
let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) {
Ok(mut ina) => {
// Prefer higher averaging for more stable readings
let _ = ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Resolution::Avg128,
operating_mode: Default::default(),
});
Some(ina)
}
Err(err) => {
log::info!("Error creating mppt ina: {:?}", err);
None
}
};
let pump_current_dev = I2cDevice::new(I2C_DRIVER.get().await);
let pump_ina = match SyncIna219::new(pump_current_dev, Address::from_pins(Pin::Gnd, Pin::Sda)) {
Ok(ina) => Some(ina),
Err(err) => {
log::info!("Error creating pump ina: {:?}", err);
None
}
};
let charger = match mppt_ina {
Some(mut mppt_ina) => {
mppt_ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: ina219::configuration::Resolution::Avg128,
operating_mode: Default::default(),
})?;
Charger::SolarMpptV1 {
mppt_ina,
solar_is_day,
charge_indicator,
}
}
None => Charger::ErrorInit {},
};
let v = V4 {
rtc_module,
esp,
awake,
tank_sensor,
light,
general_fault,
pump_expander,
config,
battery_monitor,
pump_ina,
charger,
extra1,
extra2,
sensor,
};
Ok(Box::new(v))
}
#[async_trait(?Send)]
impl<'a> BoardInteraction<'a> for V4<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Ok(&mut self.tank_sensor)
}
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
fn get_config(&mut self) -> &PlantControllerConfig {
&self.config
}
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
&mut self.battery_monitor
}
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module
}
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
self.charger.set_charge_indicator(charging)
}
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low();
self.charger.power_save();
let rtc = TIME_ACCESS.get().await.lock().await;
self.esp.deep_sleep(duration_in_ms, rtc);
}
fn is_day(&self) -> bool {
self.charger.is_day()
}
async fn light(&mut self, enable: bool) -> Result<(), FatError> {
hold_disable(10);
self.light.set_level(enable.into());
hold_enable(10);
Ok(())
}
async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable {
self.pump_expander
.pin_set_high(GPIOBank::Bank0, plant as u8)?;
} else {
self.pump_expander
.pin_set_low(GPIOBank::Bank0, plant as u8)?;
}
Ok(())
}
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
// sensor is shared for all pumps, ignore plant id
match self.pump_ina.as_mut() {
None => {
bail!("pump current sensor not available");
}
Some(pump_ina) => {
let v = pump_ina
.shunt_voltage()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
.map(|v| {
let shunt_voltage =
Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current)
})?;
Ok(v)
}
}
}
async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable {
self.pump_expander
.pin_set_high(GPIOBank::Bank1, plant as u8)?;
} else {
self.pump_expander
.pin_set_low(GPIOBank::Bank1, plant as u8)?;
}
Ok(())
}
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
self.sensor.measure_moisture_hz().await
}
async fn general_fault(&mut self, enable: bool) {
hold_disable(23);
self.general_fault.set_level(enable.into());
hold_enable(23);
}
async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true).await;
Timer::after_millis(100).await;
self.general_fault(false).await;
Timer::after_millis(500).await;
self.extra1.set_high();
Timer::after_millis(500).await;
self.extra1.set_low();
Timer::after_millis(500).await;
self.extra2.set_high();
Timer::after_millis(500).await;
self.extra2.set_low();
Timer::after_millis(500).await;
self.light(true).await?;
Timer::after_millis(500).await;
self.light(false).await?;
Timer::after_millis(500).await;
for i in 0..PLANT_COUNT {
self.fault(i, true).await?;
Timer::after_millis(500).await;
self.fault(i, false).await?;
Timer::after_millis(500).await;
}
for i in 0..PLANT_COUNT {
self.pump(i, true).await?;
Timer::after_millis(100).await;
self.pump(i, false).await?;
Timer::after_millis(100).await;
}
let moisture = self.measure_moisture_hz().await?;
for plant in 0..PLANT_COUNT {
let a = moisture.sensor_a_hz[plant] as u32;
let b = moisture.sensor_b_hz[plant] as u32;
LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, a, b, &plant.to_string(), "")
.await;
}
Timer::after_millis(10).await;
Ok(())
}
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
}
async fn get_mptt_voltage(&mut self) -> FatResult<Voltage> {
self.charger.get_mptt_voltage()
}
async fn get_mptt_current(&mut self) -> FatResult<Current> {
self.charger.get_mppt_current()
}
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
self.sensor.autodetect().await
}
}

View File

@@ -0,0 +1,314 @@
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
use crate::bail;
use crate::fat_error::{ContextExt, FatError, FatResult};
use canapi::{SensorSlot};
use crate::hal::{DetectionResult, Moistures, Sensor};
use crate::hal::Box;
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use bincode::config;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Instant, Timer, WithTimeout};
use embedded_can::{Frame, Id};
use esp_hal::gpio::Output;
use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::unit::Unit;
use esp_hal::twai::{EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
use esp_hal::{Blocking};
use log::{error, info, warn};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const REPEAT_MOIST_MEASURE: usize = 10;
#[async_trait(?Send)]
pub trait SensorInteraction {
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures>;
}
const MS0: u8 = 1_u8;
const MS1: u8 = 0_u8;
const MS2: u8 = 3_u8;
const MS3: u8 = 4_u8;
const MS4: u8 = 2_u8;
const SENSOR_ON: u8 = 5_u8;
pub enum SensorImpl {
PulseCounter {
signal_counter: Unit<'static, 0>,
sensor_expander:
Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
},
CanBus {
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
can_power: Output<'static>,
},
}
#[async_trait(?Send)]
impl SensorInteraction for SensorImpl {
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
match self {
SensorImpl::PulseCounter {
signal_counter,
sensor_expander,
..
} => {
let mut result = Moistures::default();
for plant in 0..crate::hal::PLANT_COUNT{
result.sensor_a_hz[plant] = Self::inner_pulse(plant, Sensor::A, signal_counter, sensor_expander).await?;
info!("Sensor {} {:?}: {}", plant, Sensor::A, result.sensor_a_hz[plant]);
result.sensor_b_hz[plant] = Self::inner_pulse(plant, Sensor::B, signal_counter, sensor_expander).await?;
info!("Sensor {} {:?}: {}", plant, Sensor::B, result.sensor_b_hz[plant]);
}
Ok(result)
}
SensorImpl::CanBus {
twai_config,
can_power,
} => {
can_power.set_high();
let config = twai_config.take().expect("twai config not set");
let mut twai = config.start();
loop {
let rec = twai.receive();
match rec {
Ok(_) => {}
Err(err) => {
info!("Error receiving CAN message: {:?}", err);
break;
}
}
}
Timer::after_millis(10).await;
let can = Self::inner_can(&mut twai).await;
can_power.set_low();
let config = twai.stop();
twai_config.replace(config);
let value = can?;
Ok(value)
}
}
}
}
impl SensorImpl {
pub async fn autodetect(&mut self) -> FatResult<DetectionResult> {
match self {
SensorImpl::PulseCounter { .. } => {
bail!("Only CAN bus implementation supports autodetection")
}
SensorImpl::CanBus {
twai_config,
can_power,
} => {
// Power on CAN transceiver and start controller
can_power.set_high();
let config = twai_config.take().expect("twai config not set");
let mut as_async = config.into_async().start();
// Give CAN some time to stabilize
Timer::after_millis(10).await;
// Send a few test messages per potential sensor node
for plant in 0..crate::hal::PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
let target = StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, sensor.into(), plant as u16)).context(">> Could not create address for sensor! (plant: {}) <<")?;
let can_buffer = [0_u8; 0];
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
// Try a few times; we intentionally ignore rx here and rely on stub logic
let resu = as_async.transmit_async(&frame).await;
match resu {
Ok(_) => {
info!(
"Sent test message to plant {} sensor {:?}",
plant, sensor
);
}
Err(err) => {
info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err);
}
}
} else {
info!("Error building CAN frame");
}
}
}
let mut result = DetectionResult::default();
loop {
match as_async.receive_async().with_deadline(Instant::from_millis(100)).await {
Ok(or) => {
match or {
Ok(can_frame) => {
match can_frame.id() {
Id::Standard(id) => {
let rawid = id.as_raw();
match classify(rawid) {
None => {}
Some(msg) => {
if msg.0 == MessageKind::MoistureData {
let plant = msg.1 as usize;
let sensor = msg.2;
match sensor {
SensorSlot::A => {
result.plant[plant].sensor_a = true;
}
SensorSlot::B => {
result.plant[plant].sensor_b = true;
}
}
}
}
}
}
Id::Extended(ext) => {
warn!("Received extended ID: {:?}", ext);
}
}
}
Err(err ) => {
error!("Error receiving CAN message: {:?}", err);
break;
}
}
info!("Received CAN message: {:?}", or);
}
Err(err) => {
error!("Timeout receiving CAN message: {:?}", err);
break;
}
}
}
let config = as_async.stop().into_blocking();
can_power.set_low();
twai_config.replace(config);
info!("Autodetection result: {:?}", result);
Ok(result)
}
}
}
pub async fn inner_pulse(plant: usize, sensor: Sensor, signal_counter: &mut Unit<'_, 0>, sensor_expander: &mut Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>) -> FatResult<f32> {
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE {
signal_counter.pause();
signal_counter.clear();
//Disable all
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
let sensor_channel = match sensor {
Sensor::A => plant as u32,
Sensor::B => (15 - plant) as u32,
};
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
if is_bit_set(0) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
}
if is_bit_set(1) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
}
if is_bit_set(2) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
}
if is_bit_set(3) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
}
sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?;
sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000f32 / measurement as f32;
//give some time to stabilize
Timer::after_millis(10).await;
signal_counter.resume();
Timer::after_millis(measurement).await;
signal_counter.pause();
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
Timer::after_millis(10).await;
let unscaled = 1337; //signal_counter.get_counter_value()? as i32;
let hz = unscaled as f32 * factor;
LOG_ACCESS
.lock()
.await
.log(
LogMessage::RawMeasure,
unscaled as u32,
hz as u32,
&plant.to_string(),
&format!("{sensor:?}"),
)
.await;
results[repeat] = hz;
}
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
let mid = results.len() / 2;
let median = results[mid];
Ok(median)
}
async fn inner_can(
twai: &mut Twai<'static, Blocking>,
) -> FatResult<Moistures> {
[0_u8; 8];
config::standard();
let timeout = Instant::now()
.checked_add(embassy_time::Duration::from_millis(100))
.context("Timeout")?;
loop {
let answer = twai.receive();
match answer {
Ok(answer) => {
info!("Received CAN message: {:?}", answer);
}
Err(error) => match error {
nb::Error::Other(error) => {
return Err(FatError::CanBusError { error });
}
nb::Error::WouldBlock => {
if Instant::now() > timeout {
bail!("Timeout waiting for CAN answer");
}
Timer::after_millis(10).await;
}
},
}
}
}
}

View File

@@ -0,0 +1,153 @@
use crate::bail;
use crate::fat_error::FatError;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay;
use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull};
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::unit::Unit;
use esp_hal::peripherals::GPIO5;
use esp_hal::Blocking;
use esp_println::println;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
pub struct TankSensor<'a> {
one_wire_bus: OneWire<Flex<'a>>,
tank_channel: Adc<'a, ADC1<'a>, Blocking>,
tank_power: Output<'a>,
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>,
flow_counter: Unit<'a, 1>,
}
impl<'a> TankSensor<'a> {
pub(crate) fn create(
mut one_wire_pin: Flex<'a>,
adc1: ADC1<'a>,
gpio5: GPIO5<'a>,
tank_power: Output<'a>,
flow_sensor: Input,
pcnt1: Unit<'a, 1>,
) -> Result<TankSensor<'a>, FatError> {
one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None));
let mut adc1_config = AdcConfig::new();
let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB);
let tank_channel = Adc::new(adc1, adc1_config);
let one_wire_bus = OneWire::new(one_wire_pin, false);
pcnt1.set_high_limit(Some(i16::MAX))?;
let ch0 = &pcnt1.channel0;
ch0.set_edge_signal(flow_sensor.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
pcnt1.listen();
Ok(TankSensor {
one_wire_bus,
tank_channel,
tank_power,
tank_pin,
flow_counter: pcnt1,
})
}
pub fn reset_flow_meter(&mut self) {
self.flow_counter.pause();
self.flow_counter.clear();
}
pub fn start_flow_meter(&mut self) {
self.flow_counter.resume();
}
pub fn get_flow_meter_value(&mut self) -> i16 {
self.flow_counter.value()
}
pub fn stop_flow_meter(&mut self) -> i16 {
self.flow_counter.pause();
self.get_flow_meter_value()
}
pub async fn water_temperature_c(&mut self) -> Result<f32, FatError> {
//multisample should be moved to water_temperature_c
let mut attempt = 1;
let mut delay = Delay::new();
self.one_wire_bus.reset(&mut delay)?;
let mut search = DeviceSearch::new();
let mut water_temp_sensor: Option<Device> = None;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
if device.address[0] == ds18b20::FAMILY_CODE {
water_temp_sensor = Some(device);
break;
}
}
match water_temp_sensor {
Some(device) => {
println!("Found one wire device: {:?}", device);
let mut water_temp_sensor = DS18B20::new(device)?;
let water_temp: Result<f32, FatError> = loop {
let temp = self
.single_temperature_c(&mut water_temp_sensor, &mut delay)
.await;
match &temp {
Ok(res) => {
println!("Water temp is {}", res);
break temp;
}
Err(err) => {
println!("Could not get water temp {} attempt {}", err, attempt)
}
}
if attempt == 5 {
break temp;
}
attempt += 1;
};
water_temp
}
None => {
bail!("Not found any one wire Ds18b20");
}
}
}
async fn single_temperature_c(
&mut self,
sensor: &mut DS18B20,
delay: &mut Delay,
) -> Result<f32, FatError> {
let resolution = sensor.measure_temperature(&mut self.one_wire_bus, delay)?;
Timer::after_millis(resolution.time_ms() as u64).await;
let temperature = sensor.read_temperature(&mut self.one_wire_bus, delay)? as f32;
if temperature == 85_f32 {
bail!("Ds18b20 dummy temperature returned");
}
Ok(temperature / 10_f32)
}
pub async fn tank_sensor_voltage(&mut self) -> Result<f32, FatError> {
self.tank_power.set_high();
//let stabilize
Timer::after_millis(100).await;
let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE {
let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
//force yield
Timer::after_millis(10).await;
store[multisample] = value.unwrap();
}
self.tank_power.set_low();
store.sort();
//TODO probably wrong? check!
let median_mv = store[6] as f32 * 3300_f32 / 4096_f32;
Ok(median_mv)
}
}