cleanups
This commit is contained in:
258
Software/MainBoard/rust/src/hal/battery.rs
Normal file
258
Software/MainBoard/rust/src/hal/battery.rs
Normal 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(())
|
||||
}
|
||||
13
Software/MainBoard/rust/src/hal/can_api.rs
Normal file
13
Software/MainBoard/rust/src/hal/can_api.rs
Normal 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,
|
||||
}
|
||||
994
Software/MainBoard/rust/src/hal/esp.rs
Normal file
994
Software/MainBoard/rust/src/hal/esp.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
144
Software/MainBoard/rust/src/hal/initial_hal.rs
Normal file
144
Software/MainBoard/rust/src/hal/initial_hal.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
88
Software/MainBoard/rust/src/hal/little_fs2storage_adapter.rs
Normal file
88
Software/MainBoard/rust/src/hal/little_fs2storage_adapter.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
715
Software/MainBoard/rust/src/hal/mod.rs
Normal file
715
Software/MainBoard/rust/src/hal/mod.rs
Normal 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,
|
||||
}
|
||||
133
Software/MainBoard/rust/src/hal/rtc.rs
Normal file
133
Software/MainBoard/rust/src/hal/rtc.rs
Normal 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)?)
|
||||
}
|
||||
}
|
||||
450
Software/MainBoard/rust/src/hal/v3_hal.rs
Normal file
450
Software/MainBoard/rust/src/hal/v3_hal.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
154
Software/MainBoard/rust/src/hal/v3_shift_register.rs
Normal file
154
Software/MainBoard/rust/src/hal/v3_shift_register.rs
Normal 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>;
|
||||
458
Software/MainBoard/rust/src/hal/v4_hal.rs
Normal file
458
Software/MainBoard/rust/src/hal/v4_hal.rs
Normal 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
|
||||
}
|
||||
}
|
||||
314
Software/MainBoard/rust/src/hal/v4_sensor.rs
Normal file
314
Software/MainBoard/rust/src/hal/v4_sensor.rs
Normal 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;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
Software/MainBoard/rust/src/hal/water.rs
Normal file
153
Software/MainBoard/rust/src/hal/water.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user