get most stuff working again, by upgrading to newer esp-hal version
this involved adding a lot of code from the develop branch step by step there are still some bugs, but at least i can get into the web interface and configure stuff again \o/ … measuring and pumping is working as well
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
use crate::bail;
|
||||
use crate::config::{NetworkConfig, PlantControllerConfig};
|
||||
use crate::hal::{PLANT_COUNT, TIME_ACCESS};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use crate::hal::PLANT_COUNT;
|
||||
use crate::log::{log, LogMessage};
|
||||
use alloc::vec;
|
||||
use chrono::{DateTime, Utc};
|
||||
use esp_hal::Blocking;
|
||||
use esp_hal::uart::Uart;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
@@ -14,15 +17,17 @@ 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_net::dns::DnsQueryType;
|
||||
use embassy_net::udp::{PacketMetadata, UdpSocket};
|
||||
use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||
use embassy_sync::once_lock::OnceLock;
|
||||
use embassy_time::{Duration, Timer};
|
||||
use embedded_storage::nor_flash::ReadNorFlash;
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
|
||||
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
||||
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||
use esp_hal::rng::Rng;
|
||||
use esp_hal::rtc_cntl::{
|
||||
@@ -31,31 +36,34 @@ use esp_hal::rtc_cntl::{
|
||||
};
|
||||
use esp_hal::system::software_reset;
|
||||
use esp_println::println;
|
||||
use esp_radio::wifi::ap::{AccessPointConfig, AccessPointInfo};
|
||||
use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
|
||||
use esp_radio::wifi::sta::StationConfig;
|
||||
use esp_radio::wifi::{AuthenticationMethod, Config, Interface, WifiController};
|
||||
use esp_storage::FlashStorage;
|
||||
use esp_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 log::{info, warn, error};
|
||||
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};
|
||||
use sntpc::{NtpContext, NtpTimestampGenerator, NtpUdpSocket, get_time};
|
||||
|
||||
#[esp_hal::ram(rtc_fast, persistent)]
|
||||
use super::shared_flash::MutexFlashStorage;
|
||||
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||
#[esp_hal::ram(rtc_fast, persistent)]
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||
#[esp_hal::ram(rtc_fast, persistent)]
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
|
||||
#[esp_hal::ram(rtc_fast, persistent)]
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut RESTART_TO_CONF: i8 = 0;
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1;
|
||||
|
||||
|
||||
const CONFIG_FILE: &str = "config.json";
|
||||
const NTP_SERVER: &str = "pool.ntp.org";
|
||||
@@ -112,22 +120,61 @@ impl NtpTimestampGenerator for Timestamp {
|
||||
self.stamp.timestamp_subsec_micros()
|
||||
}
|
||||
}
|
||||
struct EmbassyNtpSocket<'a, 'b> {
|
||||
socket: &'a UdpSocket<'b>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> EmbassyNtpSocket<'a, 'b> {
|
||||
fn new(socket: &'a UdpSocket<'b>) -> Self {
|
||||
Self { socket }
|
||||
}
|
||||
}
|
||||
|
||||
impl NtpUdpSocket for EmbassyNtpSocket<'_, '_> {
|
||||
async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> sntpc::Result<usize> {
|
||||
self.socket
|
||||
.send_to(buf, addr)
|
||||
.await
|
||||
.map_err(|_| sntpc::Error::Network)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, buf: &mut [u8]) -> sntpc::Result<(usize, SocketAddr)> {
|
||||
let (len, metadata) = self
|
||||
.socket
|
||||
.recv_from(buf)
|
||||
.await
|
||||
.map_err(|_| sntpc::Error::Network)?;
|
||||
let addr = match metadata.endpoint.addr {
|
||||
IpAddress::Ipv4(ip) => IpAddr::V4(ip),
|
||||
IpAddress::Ipv6(ip) => IpAddr::V6(ip),
|
||||
};
|
||||
Ok((len, SocketAddr::new(addr, metadata.endpoint.port)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Esp<'a> {
|
||||
pub 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 interface_sta: Option<Interface<'static>>,
|
||||
pub interface_ap: Option<Interface<'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 uart0: Uart<'a, Blocking>,
|
||||
|
||||
pub ota: Ota<'static, FlashStorage>,
|
||||
pub ota_next: &'static mut FlashRegion<'static, FlashStorage>,
|
||||
pub rtc: Rtc<'a>,
|
||||
|
||||
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
|
||||
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
||||
pub current: AppPartitionSubType,
|
||||
pub slot0_state: OtaImageState,
|
||||
pub slot1_state: OtaImageState,
|
||||
}
|
||||
|
||||
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
|
||||
@@ -148,6 +195,47 @@ macro_rules! mk_static {
|
||||
}
|
||||
|
||||
impl Esp<'_> {
|
||||
pub fn get_time(&self) -> DateTime<Utc> {
|
||||
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
|
||||
.unwrap_or(DateTime::UNIX_EPOCH)
|
||||
}
|
||||
|
||||
pub fn set_time(&mut self, time: DateTime<Utc>) {
|
||||
self.rtc.set_current_time_us(time.timestamp_micros() as u64);
|
||||
}
|
||||
|
||||
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
|
||||
let mut buf = [0u8; 1];
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
match self.uart0.read_buffered(&mut buf) {
|
||||
Ok(read) => {
|
||||
if read == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let c = buf[0] as char;
|
||||
if c == '\n' {
|
||||
return Ok(Some(line));
|
||||
}
|
||||
line.push(c);
|
||||
}
|
||||
Err(error) => {
|
||||
if line.is_empty() {
|
||||
return Ok(None);
|
||||
} else {
|
||||
error!("Error reading serial line: {error:?}");
|
||||
// If we already have some data, we should probably wait a bit or just return what we have?
|
||||
// But the protocol expects a full line or message.
|
||||
// For simplicity in config mode, we can block here or just return None if nothing is there yet.
|
||||
// However, if we started receiving, we should probably finish or timeout.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
|
||||
let file = PathBuf::try_from(filename.as_str())?;
|
||||
let access = self.fs.lock().await;
|
||||
@@ -212,26 +300,47 @@ impl Esp<'_> {
|
||||
Ok((buf, read))
|
||||
}
|
||||
|
||||
pub(crate) fn get_ota_slot(&mut self) -> String {
|
||||
match self.ota.current_slot() {
|
||||
Ok(slot) => {
|
||||
format!("{:?}", slot)
|
||||
}
|
||||
Err(err) => {
|
||||
format!("{:?}", err)
|
||||
}
|
||||
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
||||
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
||||
info!("erasing and writing block 0x{offset:x}");
|
||||
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 {buf:?} but got {read_back:?}");
|
||||
bail!(
|
||||
"Flash error, read back does not match write buffer at offset {:x}",
|
||||
offset
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_ota_state(&mut self) -> String {
|
||||
match self.ota.current_ota_state() {
|
||||
Ok(state) => {
|
||||
format!("{:?}", state)
|
||||
}
|
||||
Err(err) => {
|
||||
format!("{:?}", err)
|
||||
}
|
||||
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
||||
let current = self.ota.current_app_partition()?;
|
||||
if self.ota.current_ota_state()? != Valid {
|
||||
info!("Validating current slot {current:?} as it was able to ota");
|
||||
self.ota.set_current_ota_state(Valid)?;
|
||||
}
|
||||
let next = match current {
|
||||
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
|
||||
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
|
||||
_ => {
|
||||
bail!("Invalid current slot {current:?} for ota");
|
||||
}
|
||||
};
|
||||
self.ota.set_current_app_partition(next)?;
|
||||
info!("switched slot");
|
||||
self.ota.set_current_ota_state(OtaImageState::New)?;
|
||||
info!("switched state for new partition");
|
||||
let state_new = self.ota.current_ota_state()?;
|
||||
info!("state on new partition now {state_new:?}");
|
||||
self.set_restart_to_conf(true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// let current = ota.current_slot()?;
|
||||
@@ -264,31 +373,38 @@ impl Esp<'_> {
|
||||
&mut tx_meta,
|
||||
&mut tx_buffer,
|
||||
);
|
||||
socket.bind(123).unwrap();
|
||||
socket.bind(123).context("Could not bind UDP socket")?;
|
||||
|
||||
let context = NtpContext::new(Timestamp::default());
|
||||
let ntp_socket = EmbassyNtpSocket::new(&socket);
|
||||
|
||||
let ntp_addrs = stack
|
||||
.dns_query(NTP_SERVER, DnsQueryType::A)
|
||||
.await
|
||||
.expect("Failed to resolve DNS");
|
||||
.context("Failed to resolve DNS")?;
|
||||
|
||||
if ntp_addrs.is_empty() {
|
||||
bail!("Failed to resolve DNS");
|
||||
bail!("No IP addresses found for NTP server");
|
||||
}
|
||||
let ntp = ntp_addrs[0];
|
||||
info!("NTP server: {ntp:?}");
|
||||
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
let addr: IpAddr = ntp_addrs[0].into();
|
||||
let result = get_time(SocketAddr::from((addr, 123)), &socket, context).await;
|
||||
let addr: IpAddr = ntp.into();
|
||||
let timeout = get_time(SocketAddr::from((addr, 123)), &ntp_socket, context)
|
||||
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(time) => {
|
||||
info!("Time: {:?}", time);
|
||||
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(e) => {
|
||||
warn!("Error: {:?}", e);
|
||||
Err(err) => {
|
||||
warn!("sntp timeout, retry: {err:?}");
|
||||
counter += 1;
|
||||
if counter > 10 {
|
||||
bail!("Failed to get time from NTP server");
|
||||
@@ -299,27 +415,15 @@ impl Esp<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn flash_ota(&mut self) -> FatResult<()> {
|
||||
let capacity = self.ota_next.capacity();
|
||||
|
||||
bail!("not implemented")
|
||||
}
|
||||
|
||||
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
|
||||
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?;
|
||||
let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
|
||||
min: esp_hal::time::Duration::from_millis(0),
|
||||
max: esp_hal::time::Duration::from_millis(0),
|
||||
});
|
||||
let rv = lock.scan_async(&scan_config).await?;
|
||||
info!("end wifi scan lock");
|
||||
Ok(rv)
|
||||
}
|
||||
@@ -368,17 +472,17 @@ impl Esp<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> {
|
||||
pub(crate) async fn wifi_ap(&mut self, spawner: Spawner) -> FatResult<Stack<'static>> {
|
||||
let ssid = match self.load_config().await {
|
||||
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 device = self
|
||||
.interface_ap
|
||||
.take()
|
||||
.context("AP interface already taken")?;
|
||||
let gw_ip_addr = Ipv4Addr::new(192, 168, 71, 1);
|
||||
|
||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
||||
@@ -398,22 +502,14 @@ impl Esp<'_> {
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
||||
ssid: ssid.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
let client_config =
|
||||
Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
self.controller.lock().await.set_config(&client_config)?;
|
||||
|
||||
self.controller
|
||||
.lock()
|
||||
.await
|
||||
.set_configuration(&client_config)?;
|
||||
|
||||
println!("start new");
|
||||
self.controller.lock().await.start()?;
|
||||
println!("start net task");
|
||||
spawner.spawn(net_task(runner)).ok();
|
||||
spawner.spawn(net_task(runner)?);
|
||||
println!("run dhcp");
|
||||
spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok();
|
||||
spawner.spawn(run_dhcp(*stack, gw_ip_addr)?);
|
||||
|
||||
loop {
|
||||
if stack.is_link_up() {
|
||||
@@ -424,31 +520,31 @@ impl Esp<'_> {
|
||||
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}/");
|
||||
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr}/");
|
||||
stack
|
||||
.config_v4()
|
||||
.inspect(|c| println!("ipv4 config: {c:?}"));
|
||||
|
||||
Ok(stack.clone())
|
||||
Ok(*stack)
|
||||
}
|
||||
|
||||
pub(crate) async fn wifi(
|
||||
&mut self,
|
||||
network_config: &NetworkConfig,
|
||||
spawner: Spawner,
|
||||
) -> FatResult<Stack<'static>> {
|
||||
esp_wifi::wifi_set_log_verbose();
|
||||
let ssid = network_config.ssid.clone();
|
||||
match &ssid {
|
||||
esp_radio::wifi_set_log_verbose();
|
||||
let ssid = match &network_config.ssid {
|
||||
Some(ssid) => {
|
||||
if ssid.is_empty() {
|
||||
bail!("Wifi ssid was empty")
|
||||
}
|
||||
ssid.to_string()
|
||||
}
|
||||
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(),
|
||||
@@ -456,9 +552,10 @@ impl Esp<'_> {
|
||||
};
|
||||
let max_wait = network_config.max_wait;
|
||||
|
||||
let spawner = Spawner::for_current_executor().await;
|
||||
|
||||
let device = self.interface_sta.take().unwrap();
|
||||
let device = self
|
||||
.interface_sta
|
||||
.take()
|
||||
.context("STA interface already taken")?;
|
||||
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
|
||||
|
||||
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
|
||||
@@ -472,122 +569,80 @@ impl Esp<'_> {
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config = Configuration::Client(ClientConfiguration {
|
||||
ssid,
|
||||
bssid: None,
|
||||
auth_method: AuthMethod::WPA2Personal, //FIXME read from config, fill via scan
|
||||
password,
|
||||
channel: None,
|
||||
});
|
||||
let auth_method = if password.is_empty() {
|
||||
AuthenticationMethod::None
|
||||
} else {
|
||||
AuthenticationMethod::Wpa2Personal
|
||||
};
|
||||
let client_config = StationConfig::default()
|
||||
.with_ssid(ssid)
|
||||
.with_auth_method(auth_method)
|
||||
.with_password(password);
|
||||
|
||||
self.controller
|
||||
.lock()
|
||||
.await
|
||||
.set_configuration(&client_config)?;
|
||||
spawner.spawn(net_task(runner)).ok();
|
||||
self.controller.lock().await.start_async().await?;
|
||||
.set_config(&Config::Station(client_config))?;
|
||||
spawner.spawn(net_task(runner)?);
|
||||
self.controller
|
||||
.lock()
|
||||
.await
|
||||
.connect_async()
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await
|
||||
.context("Timeout waiting for wifi sta connected")??;
|
||||
|
||||
let timeout = {
|
||||
let 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;
|
||||
}
|
||||
_ => {}
|
||||
let res = async {
|
||||
while !stack.is_link_up() {
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
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;
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
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;
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting for wifi link up")
|
||||
}
|
||||
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")
|
||||
|
||||
let res = async {
|
||||
while !stack.is_config_up() {
|
||||
Timer::after(Duration::from_millis(100)).await
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
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
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting for wifi config up")
|
||||
}
|
||||
|
||||
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
|
||||
Ok(stack.clone())
|
||||
Ok(*stack)
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
pub fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||
// 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");
|
||||
info!("Marking OTA image as valid");
|
||||
if let Err(err) = self.ota.set_current_ota_state(Valid) {
|
||||
error!("Could not set image to valid: {:?}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No OTA image to mark as valid");
|
||||
}
|
||||
|
||||
if duration_in_ms == 0 {
|
||||
software_reset();
|
||||
} else {
|
||||
///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
||||
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000));
|
||||
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]);
|
||||
self.rtc.sleep_deep(&[&timer, &ext1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,47 +701,36 @@ impl Esp<'_> {
|
||||
} else {
|
||||
RESTART_TO_CONF = 0;
|
||||
}
|
||||
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
|
||||
};
|
||||
} 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]
|
||||
);
|
||||
log(
|
||||
LogMessage::RestartToConfig,
|
||||
RESTART_TO_CONF as u32,
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
);
|
||||
log(
|
||||
LogMessage::LowVoltage,
|
||||
LOW_VOLTAGE_DETECTED as u32,
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
);
|
||||
// is executed before main, no other code will alter these values during printing
|
||||
#[allow(static_mut_refs)]
|
||||
for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() {
|
||||
info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}");
|
||||
}
|
||||
for i in 0..PLANT_COUNT {
|
||||
log::info!(
|
||||
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
|
||||
i,
|
||||
CONSECUTIVE_WATERING_PLANT[i]
|
||||
);
|
||||
// is executed before main, no other code will alter these values during printing
|
||||
#[allow(static_mut_refs)]
|
||||
for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
|
||||
info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -696,6 +740,7 @@ impl Esp<'_> {
|
||||
&mut self,
|
||||
network_config: &'static NetworkConfig,
|
||||
stack: Stack<'static>,
|
||||
spawner: Spawner,
|
||||
) -> FatResult<()> {
|
||||
let base_topic = network_config
|
||||
.base_topic
|
||||
@@ -718,17 +763,17 @@ impl Esp<'_> {
|
||||
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 last_will_topic = format!("{base_topic}/state");
|
||||
let round_trip_topic = format!("{base_topic}/internal/roundtrip");
|
||||
let stay_alive_topic = format!("{base_topic}/stay_alive");
|
||||
|
||||
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(),
|
||||
);
|
||||
if let (Some(mqtt_user), Some(mqtt_password)) = (
|
||||
network_config.mqtt_user.as_ref(),
|
||||
network_config.mqtt_password.as_ref(),
|
||||
) {
|
||||
builder = builder.with_authentication(mqtt_user, mqtt_password);
|
||||
info!("With authentification");
|
||||
}
|
||||
|
||||
@@ -748,57 +793,51 @@ impl Esp<'_> {
|
||||
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))?;
|
||||
)?);
|
||||
spawner.spawn(mqtt_runner(task)?);
|
||||
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
|
||||
.await;
|
||||
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
|
||||
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::MqttInfo, 0, 0, "", mqtt_url)
|
||||
.await;
|
||||
log(LogMessage::MqttInfo, 0, 0, "", mqtt_url);
|
||||
|
||||
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")
|
||||
let res = async {
|
||||
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
|
||||
crate::hal::PlantHal::feed_watchdog();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting MQTT connect event")
|
||||
}
|
||||
|
||||
Topic::General(round_trip_topic.clone())
|
||||
let _ = Topic::General(round_trip_topic.clone())
|
||||
.with_display("online_text")
|
||||
.publish()
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
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")
|
||||
let res = async {
|
||||
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
|
||||
crate::hal::PlantHal::feed_watchdog();
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
//ensure we do not further try to publish
|
||||
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
|
||||
bail!("Timeout waiting MQTT roundtrip")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -864,8 +903,7 @@ impl Esp<'_> {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
|
||||
subtopic, message, err
|
||||
"Error during mqtt send on topic {subtopic} with message {message:#?} error is {err:?}"
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -893,8 +931,8 @@ async fn mqtt_incoming_task(
|
||||
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
|
||||
}
|
||||
MqttMessage::Publish(topic, payload) => match topic {
|
||||
Topic::DeviceType(type_topic) => {}
|
||||
Topic::Device(device_topic) => {}
|
||||
Topic::DeviceType(_type_topic) => {}
|
||||
Topic::Device(_device_topic) => {}
|
||||
Topic::General(topic) => {
|
||||
let subtopic = topic.as_str();
|
||||
|
||||
@@ -907,18 +945,10 @@ async fn mqtt_incoming_task(
|
||||
true => 1,
|
||||
false => 0,
|
||||
};
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::MqttStayAliveRec, a, 0, "", "")
|
||||
.await;
|
||||
log(LogMessage::MqttStayAliveRec, a, 0, "", "");
|
||||
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
|
||||
} else {
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic)
|
||||
.await;
|
||||
log(LogMessage::UnknownTopic, 0, 0, "", &topic);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -931,13 +961,13 @@ async fn mqtt_incoming_task(
|
||||
}
|
||||
|
||||
#[embassy_executor::task(pool_size = 2)]
|
||||
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||
async fn net_task(mut runner: Runner<'static, Interface<'static>>) {
|
||||
runner.run().await;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||
async fn run_dhcp(stack: Stack<'static>, ip: Ipv4Addr) {
|
||||
use core::net::SocketAddrV4;
|
||||
|
||||
use edge_dhcp::{
|
||||
io::{self, DEFAULT_SERVER_PORT},
|
||||
@@ -946,21 +976,25 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||
use edge_nal::UdpBind;
|
||||
use edge_nal_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
|
||||
let mut bound_socket = match unbound_socket
|
||||
.bind(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::UNSPECIFIED,
|
||||
DEFAULT_SERVER_PORT,
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("dhcp task failed to bind socket: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
_ = io::server::run(
|
||||
@@ -970,7 +1004,7 @@ async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
||||
&mut buf,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| log::warn!("DHCP server error: {e:?}"));
|
||||
.inspect_err(|e| warn!("DHCP server error: {e:?}"));
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ 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, Sensor, TIME_ACCESS};
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor};
|
||||
use crate::{
|
||||
bail,
|
||||
config::PlantControllerConfig,
|
||||
hal::battery::{BatteryInteraction, NoBatteryMonitor},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
use measurements::{Current, Voltage};
|
||||
|
||||
@@ -90,13 +90,22 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
|
||||
&mut self.rtc
|
||||
}
|
||||
|
||||
async fn get_time(&mut self) -> DateTime<Utc> {
|
||||
self.esp.get_time()
|
||||
}
|
||||
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
|
||||
self.rtc.set_rtc_time(&time.to_utc()).await?;
|
||||
self.esp.set_time(time.to_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
|
||||
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);
|
||||
self.esp.deep_sleep(duration_in_ms);
|
||||
}
|
||||
fn is_day(&self) -> bool {
|
||||
false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use embedded_storage::{ReadStorage, Storage};
|
||||
use crate::hal::shared_flash::MutexFlashStorage;
|
||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
||||
use esp_storage::FlashStorage;
|
||||
use littlefs2::consts::U512 as lfsCache;
|
||||
use littlefs2::consts::U4096 as lfsCache;
|
||||
use littlefs2::consts::U512 as lfsLookahead;
|
||||
use littlefs2::driver::Storage as lfs2Storage;
|
||||
use littlefs2::io::Error as lfs2Error;
|
||||
@@ -9,26 +9,32 @@ use littlefs2::io::Result as lfs2Result;
|
||||
use log::error;
|
||||
|
||||
pub struct LittleFs2Filesystem {
|
||||
pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>,
|
||||
pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
||||
}
|
||||
|
||||
impl lfs2Storage for LittleFs2Filesystem {
|
||||
const READ_SIZE: usize = 256;
|
||||
const WRITE_SIZE: usize = 512;
|
||||
const BLOCK_SIZE: usize = 512; //usually optimal for flash access
|
||||
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks
|
||||
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;
|
||||
assert_eq!(off % read_size, 0);
|
||||
assert_eq!(buf.len() % read_size, 0);
|
||||
if off % read_size != 0 {
|
||||
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {read_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
if buf.len() % read_size != 0 {
|
||||
error!("Littlefs2Filesystem read error: length not aligned to read size length: {} read_size: {}", buf.len(), read_size);
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
match self.storage.read(off as u32, buf) {
|
||||
Ok(..) => Ok(buf.len()),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem read error: {:?}", err);
|
||||
error!("Littlefs2Filesystem read error: {err:?}");
|
||||
Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
@@ -36,12 +42,18 @@ impl lfs2Storage for LittleFs2Filesystem {
|
||||
|
||||
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
||||
let write_size: usize = Self::WRITE_SIZE;
|
||||
assert_eq!(off % write_size, 0);
|
||||
assert_eq!(data.len() % write_size, 0);
|
||||
if off % write_size != 0 {
|
||||
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {write_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
if data.len() % write_size != 0 {
|
||||
error!("Littlefs2Filesystem write error: length not aligned to write size length: {} write_size: {}", data.len(), write_size);
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
match self.storage.write(off as u32, data) {
|
||||
Ok(..) => Ok(data.len()),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem write error: {:?}", err);
|
||||
error!("Littlefs2Filesystem write error: {err:?}");
|
||||
Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
@@ -49,15 +61,28 @@ impl lfs2Storage for LittleFs2Filesystem {
|
||||
|
||||
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
||||
let block_size: usize = Self::BLOCK_SIZE;
|
||||
debug_assert!(off % block_size == 0);
|
||||
debug_assert!(len % block_size == 0);
|
||||
//match self.storage.erase(off as u32, len as u32) {
|
||||
//anyhow::Result::Ok(..) => lfs2Result::Ok(len),
|
||||
//Err(err) => {
|
||||
//error!("Littlefs2Filesystem erase error: {:?}", err);
|
||||
//Err(lfs2Error::IO)
|
||||
// }
|
||||
//}
|
||||
lfs2Result::Ok(len)
|
||||
if off % block_size != 0 {
|
||||
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
if len % block_size != 0 {
|
||||
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
|
||||
match check_erase(self.storage, off as u32, (off + len) as u32) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem check erase error: {err:?}");
|
||||
return Err(lfs2Error::IO);
|
||||
}
|
||||
}
|
||||
match self.storage.erase(off as u32, (off + len) as u32) {
|
||||
Ok(..) => Ok(len),
|
||||
Err(err) => {
|
||||
error!("Littlefs2Filesystem erase error: {err:?}");
|
||||
Err(lfs2Error::IO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod esp;
|
||||
mod initial_hal;
|
||||
mod little_fs2storage_adapter;
|
||||
pub(crate) mod rtc;
|
||||
mod shared_flash;
|
||||
mod v3_hal;
|
||||
mod v3_shift_register;
|
||||
mod v4_hal;
|
||||
@@ -11,6 +12,7 @@ mod water;
|
||||
|
||||
use crate::alloc::string::ToString;
|
||||
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
||||
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
||||
use esp_hal::peripherals::Peripherals;
|
||||
use esp_hal::peripherals::ADC1;
|
||||
use esp_hal::peripherals::APB_SARADC;
|
||||
@@ -43,6 +45,7 @@ use esp_hal::peripherals::GPIO7;
|
||||
use esp_hal::peripherals::GPIO8;
|
||||
use esp_hal::peripherals::PCNT;
|
||||
use esp_hal::peripherals::TWAI0;
|
||||
use portable_atomic::AtomicBool;
|
||||
|
||||
use crate::{
|
||||
bail,
|
||||
@@ -71,23 +74,29 @@ 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 embedded_storage::nor_flash::RmwNorFlashStorage;
|
||||
use embedded_storage::ReadStorage;
|
||||
use esp_bootloader_esp_idf::partitions::{
|
||||
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
|
||||
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable,
|
||||
PartitionType,
|
||||
};
|
||||
use esp_hal::clock::CpuClock;
|
||||
use esp_hal::gpio::{Input, InputConfig, Pull};
|
||||
use esp_hal::uart::{Config as UartConfig, Uart};
|
||||
use esp_storage::FlashStorage;
|
||||
use lib_bms_protocol::{BmsReadable, ProtocolVersion};
|
||||
use measurements::{Current, Voltage};
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
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 crate::log::log;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_sync::once_lock::OnceLock;
|
||||
use esp_alloc as _;
|
||||
use esp_backtrace as _;
|
||||
use esp_bootloader_esp_idf::ota::Slot;
|
||||
use esp_bootloader_esp_idf::ota::{OtaImageState, Ota};
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
@@ -96,19 +105,25 @@ use esp_hal::rng::Rng;
|
||||
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
|
||||
use esp_hal::system::reset_reason;
|
||||
use esp_hal::time::Rate;
|
||||
use esp_hal::timer::timg::TimerGroup;
|
||||
use esp_hal::timer::timg::{TimerGroup, Wdt};
|
||||
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::{info, warn};
|
||||
use log::{error, info, warn};
|
||||
use shared_flash::MutexFlashStorage;
|
||||
|
||||
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
||||
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
//Only support for 8 right now!
|
||||
pub const PLANT_COUNT: usize = 8;
|
||||
|
||||
pub static WATCHDOG: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<
|
||||
CriticalSectionRawMutex,
|
||||
RefCell<Wdt<esp_hal::peripherals::TIMG0>>,
|
||||
>,
|
||||
> = OnceLock::new();
|
||||
|
||||
const TANK_MULTI_SAMPLE: usize = 11;
|
||||
pub static I2C_DRIVER: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||
@@ -126,6 +141,70 @@ pub struct HAL<'a> {
|
||||
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
|
||||
}
|
||||
|
||||
fn ota_state(
|
||||
slot: AppPartitionSubType,
|
||||
ota_data: &mut FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
|
||||
) -> OtaImageState {
|
||||
// Read and log OTA states for both slots before constructing Ota
|
||||
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
|
||||
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
|
||||
let mut slot_buf = [0u8; 32];
|
||||
if slot == AppPartitionSubType::Ota0 {
|
||||
let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf);
|
||||
} else {
|
||||
let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf);
|
||||
}
|
||||
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
|
||||
|
||||
OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
|
||||
}
|
||||
fn get_current_slot(
|
||||
pt: &PartitionTable,
|
||||
ota: &mut Ota<RmwNorFlashStorage<&mut MutexFlashStorage>>,
|
||||
) -> Result<AppPartitionSubType, FatError> {
|
||||
let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?;
|
||||
let booted_type = booted.partition_type();
|
||||
let booted_ota_type = match booted_type {
|
||||
PartitionType::App(subtype) => subtype,
|
||||
_ => {
|
||||
bail!("Booted partition is not an app partition");
|
||||
}
|
||||
};
|
||||
|
||||
let expected_partition = ota.current_app_partition()?;
|
||||
if expected_partition == booted_ota_type {
|
||||
info!("Booted partition matches expected partition");
|
||||
} else {
|
||||
info!("Booted partition does not match expected partition, fixing ota entry");
|
||||
ota.set_current_app_partition(booted_ota_type)?;
|
||||
}
|
||||
|
||||
let fixed = ota.current_app_partition()?;
|
||||
let state = ota.current_ota_state();
|
||||
info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}");
|
||||
|
||||
if fixed != booted_ota_type {
|
||||
bail!(
|
||||
"Could not fix ota entry, booted partition is still not correct: {:?} != {:?}",
|
||||
booted_ota_type,
|
||||
fixed
|
||||
);
|
||||
}
|
||||
|
||||
Ok(booted_ota_type)
|
||||
}
|
||||
|
||||
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
|
||||
let next = match current {
|
||||
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
|
||||
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
|
||||
_ => {
|
||||
bail!("Current slot is not ota0 or ota1");
|
||||
}
|
||||
};
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BoardInteraction<'a> {
|
||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
|
||||
@@ -133,6 +212,8 @@ pub trait BoardInteraction<'a> {
|
||||
fn get_config(&mut self) -> &PlantControllerConfig;
|
||||
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
|
||||
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
|
||||
async fn get_time(&mut self) -> DateTime<Utc>;
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()>;
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
|
||||
|
||||
@@ -194,13 +275,7 @@ pub struct FreePeripherals<'a> {
|
||||
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>,
|
||||
@@ -224,14 +299,12 @@ impl PlantHal {
|
||||
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 mut rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR);
|
||||
rtc_peripheral.rwdt.disable();
|
||||
|
||||
let systimer = SystemTimer::new(peripherals.SYSTIMER);
|
||||
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
|
||||
|
||||
let boot_button = Input::new(
|
||||
peripherals.GPIO9,
|
||||
@@ -241,29 +314,13 @@ impl PlantHal {
|
||||
// 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 rng = Rng::new();
|
||||
let (controller, interfaces) = esp_radio::wifi::new(peripherals.WIFI, Default::default())
|
||||
.expect("Could not init wifi");
|
||||
|
||||
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,
|
||||
@@ -284,13 +341,7 @@ impl PlantHal {
|
||||
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,
|
||||
@@ -301,14 +352,19 @@ impl PlantHal {
|
||||
[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)?;
|
||||
|
||||
// List all partitions - this is just FYI
|
||||
for i in 0..pt.len() {
|
||||
info!("{:?}", pt.get_partition(i));
|
||||
}
|
||||
let bullshit = MutexFlashStorage {
|
||||
inner: Arc::new(CriticalSectionMutex::new(RefCell::new(FlashStorage::new(
|
||||
peripherals.FLASH,
|
||||
)))),
|
||||
};
|
||||
let flash_storage = mk_static!(MutexFlashStorage, bullshit.clone());
|
||||
let flash_storage_2 = mk_static!(MutexFlashStorage, bullshit.clone());
|
||||
let flash_storage_3 = mk_static!(MutexFlashStorage, bullshit.clone());
|
||||
|
||||
let pt =
|
||||
esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?;
|
||||
|
||||
let ota_data = mk_static!(
|
||||
PartitionEntry,
|
||||
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
||||
@@ -317,34 +373,39 @@ impl PlantHal {
|
||||
.expect("No OTA data partition found")
|
||||
);
|
||||
|
||||
let ota_data = mk_static!(
|
||||
FlashRegion<FlashStorage>,
|
||||
ota_data.as_embedded_storage(storage_ota)
|
||||
);
|
||||
let mut ota_data = ota_data.as_embedded_storage(mk_static!(
|
||||
RmwNorFlashStorage<&mut MutexFlashStorage>,
|
||||
RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
|
||||
));
|
||||
|
||||
let mut ota = esp_bootloader_esp_idf::ota::Ota::new(ota_data)?;
|
||||
let state_0 = ota_state(AppPartitionSubType::Ota0, &mut ota_data);
|
||||
let state_1 = ota_state(AppPartitionSubType::Ota1, &mut ota_data);
|
||||
let mut ota = Ota::new(ota_data, 2)?;
|
||||
let running = get_current_slot(&pt, &mut ota)?;
|
||||
let target = next_partition(running)?;
|
||||
|
||||
let ota_partition = match ota.current_slot()? {
|
||||
Slot::None => {
|
||||
panic!("No OTA slot active?");
|
||||
info!("Currently running OTA slot: {running:?}");
|
||||
info!("Updates will be stored in OTA slot: {target:?}");
|
||||
info!("Slot0 state: {state_0:?}");
|
||||
info!("Slot1 state: {state_1:?}");
|
||||
|
||||
//get current_state and next_state here!
|
||||
let ota_target = match target {
|
||||
AppPartitionSubType::Ota0 => pt
|
||||
.find_partition(PartitionType::App(AppPartitionSubType::Ota0))?
|
||||
.context("Partition table invalid no ota0")?,
|
||||
AppPartitionSubType::Ota1 => pt
|
||||
.find_partition(PartitionType::App(AppPartitionSubType::Ota1))?
|
||||
.context("Partition table invalid no ota1")?,
|
||||
_ => {
|
||||
bail!("Invalid target partition");
|
||||
}
|
||||
Slot::Slot0 => pt
|
||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||
AppPartitionSubType::Ota0,
|
||||
))?
|
||||
.expect("No OTA slot0 found"),
|
||||
Slot::Slot1 => pt
|
||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||
AppPartitionSubType::Ota1,
|
||||
))?
|
||||
.expect("No OTA slot1 found"),
|
||||
};
|
||||
|
||||
let ota_next = mk_static!(PartitionEntry, ota_partition);
|
||||
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
|
||||
let ota_next = mk_static!(
|
||||
FlashRegion<FlashStorage>,
|
||||
ota_next.as_embedded_storage(storage_ota)
|
||||
let ota_target = mk_static!(PartitionEntry, ota_target);
|
||||
let ota_target = mk_static!(
|
||||
FlashRegion<MutexFlashStorage>,
|
||||
ota_target.as_embedded_storage(flash_storage)
|
||||
);
|
||||
|
||||
let data_partition = pt
|
||||
@@ -354,32 +415,38 @@ impl PlantHal {
|
||||
.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)
|
||||
FlashRegion<MutexFlashStorage>,
|
||||
data_partition.as_embedded_storage(flash_storage_3)
|
||||
);
|
||||
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
|
||||
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
|
||||
if lfs2filesystem.is_mountable() {
|
||||
log::info!("Littlefs2 filesystem is mountable");
|
||||
info!("Littlefs2 filesystem is mountable");
|
||||
} else {
|
||||
match lfs2filesystem.format() {
|
||||
Result::Ok(..) => {
|
||||
log::info!("Littlefs2 filesystem is formatted");
|
||||
Ok(..) => {
|
||||
info!("Littlefs2 filesystem is formatted");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Littlefs2 filesystem could not be formatted: {:?}", err);
|
||||
error!("Littlefs2 filesystem could not be formatted: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let fs = Arc::new(Mutex::new(
|
||||
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
|
||||
));
|
||||
|
||||
let ap = interfaces.ap;
|
||||
let sta = interfaces.sta;
|
||||
|
||||
let uart0 =
|
||||
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
|
||||
error: "Uart creation failed".to_string(),
|
||||
})?;
|
||||
|
||||
let ap = interfaces.access_point;
|
||||
let sta = interfaces.station;
|
||||
let mut esp = Esp {
|
||||
fs,
|
||||
rng,
|
||||
@@ -389,7 +456,12 @@ impl PlantHal {
|
||||
boot_button,
|
||||
wake_gpio1,
|
||||
ota,
|
||||
ota_next,
|
||||
ota_target,
|
||||
current: running,
|
||||
slot0_state: state_0,
|
||||
slot1_state: state_1,
|
||||
uart0,
|
||||
rtc: rtc_peripheral,
|
||||
};
|
||||
|
||||
//init,reset rtc memory depending on cause
|
||||
@@ -425,24 +497,21 @@ impl PlantHal {
|
||||
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;
|
||||
log(
|
||||
LogMessage::ResetReason,
|
||||
init_rtc_store as u32,
|
||||
to_config_mode as u32,
|
||||
"",
|
||||
&format!("{reasons:?}"),
|
||||
);
|
||||
|
||||
|
||||
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
|
||||
.await;
|
||||
|
||||
let config = esp.load_config().await;
|
||||
|
||||
log::info!("Init rtc driver");
|
||||
info!("Init rtc driver");
|
||||
|
||||
let sda = peripherals.GPIO20;
|
||||
let scl = peripherals.GPIO19;
|
||||
@@ -460,26 +529,30 @@ impl PlantHal {
|
||||
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 rtc_device = I2cDevice::new(i2c_bus);
|
||||
let mut bms_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);
|
||||
info!("Rtc Module reports time at UTC {tt}");
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!("Rtc Module could not be read {:?}", err);
|
||||
info!("Rtc Module could not be read {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,40 +567,26 @@ impl PlantHal {
|
||||
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
|
||||
|
||||
let hal = match config {
|
||||
Result::Ok(config) => {
|
||||
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 version = ProtocolVersion::read_from_i2c(&mut bms_device);
|
||||
let version_val = match version {
|
||||
Ok(v) => unsafe { core::mem::transmute::<ProtocolVersion, u32>(v) },
|
||||
Err(_) => 0,
|
||||
};
|
||||
if version_val == 1 {
|
||||
//Box::new(WCHI2CSlave { i2c: bms_device })
|
||||
// todo fix the type above
|
||||
Box::new(NoBatteryMonitor {})
|
||||
} else {
|
||||
//todo should be an error variant instead?
|
||||
Box::new(NoBatteryMonitor {})
|
||||
}
|
||||
}
|
||||
BatteryBoardVersion::BQ34Z100G1 => Box::new(NoBatteryMonitor {}),
|
||||
};
|
||||
|
||||
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
|
||||
@@ -546,17 +605,13 @@ impl PlantHal {
|
||||
HAL { board_hal }
|
||||
}
|
||||
Err(err) => {
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(
|
||||
LogMessage::ConfigModeMissingConfig,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
&err.to_string(),
|
||||
)
|
||||
.await;
|
||||
log(
|
||||
LogMessage::ConfigModeMissingConfig,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
&err.to_string(),
|
||||
);
|
||||
HAL {
|
||||
board_hal: initial_hal::create_initial_board(
|
||||
free_pins,
|
||||
@@ -569,25 +624,13 @@ impl PlantHal {
|
||||
|
||||
Ok(Mutex::new(hal))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
/// Feed the watchdog timer to prevent system reset
|
||||
pub fn feed_watchdog() {
|
||||
if let Some(wdt_mutex) = WATCHDOG.try_get() {
|
||||
wdt_mutex.lock(|cell| {
|
||||
cell.borrow_mut().feed();
|
||||
});
|
||||
}
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.get_rtc_module()
|
||||
.set_rtc_time(&time.to_utc())
|
||||
.await
|
||||
}
|
||||
|
||||
65
rust/src/hal/shared_flash.rs
Normal file
65
rust/src/hal/shared_flash.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use alloc::sync::Arc;
|
||||
use core::cell::RefCell;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use embassy_sync::blocking_mutex::CriticalSectionMutex;
|
||||
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
|
||||
use embedded_storage::ReadStorage;
|
||||
use esp_storage::{FlashStorage, FlashStorageError};
|
||||
use log::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MutexFlashStorage {
|
||||
pub(crate) inner: Arc<CriticalSectionMutex<RefCell<FlashStorage<'static>>>>,
|
||||
}
|
||||
|
||||
impl ReadStorage for MutexFlashStorage {
|
||||
type Error = FlashStorageError;
|
||||
|
||||
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> {
|
||||
self.inner
|
||||
.lock(|f| ReadStorage::read(f.borrow_mut().deref_mut(), offset, bytes))
|
||||
}
|
||||
|
||||
fn capacity(&self) -> usize {
|
||||
self.inner
|
||||
.lock(|f| ReadStorage::capacity(f.borrow().deref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl embedded_storage::Storage for MutexFlashStorage {
|
||||
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
|
||||
NorFlash::write(self, offset, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorType for MutexFlashStorage {
|
||||
type Error = FlashStorageError;
|
||||
}
|
||||
|
||||
impl ReadNorFlash for MutexFlashStorage {
|
||||
const READ_SIZE: usize = 1;
|
||||
|
||||
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
|
||||
ReadStorage::read(self, offset, bytes)
|
||||
}
|
||||
|
||||
fn capacity(&self) -> usize {
|
||||
ReadStorage::capacity(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl NorFlash for MutexFlashStorage {
|
||||
const WRITE_SIZE: usize = 1;
|
||||
const ERASE_SIZE: usize = 4096;
|
||||
|
||||
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
|
||||
info!("Erasing flash from 0x{:x} to 0x{:x}", from, to);
|
||||
self.inner
|
||||
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
|
||||
}
|
||||
|
||||
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.lock(|f| NorFlash::write(f.borrow_mut().deref_mut(), offset, bytes))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::bail;
|
||||
use crate::fat_error::FatError;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::esp::{hold_disable, hold_enable};
|
||||
use crate::hal::rtc::RTCModuleInteraction;
|
||||
use crate::hal::v3_shift_register::ShiftRegister40;
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS};
|
||||
use crate::log::{LogMessage, LOG_ACCESS};
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT};
|
||||
use crate::log::{log, LogMessage, LOG_ACCESS};
|
||||
use crate::{
|
||||
config::PlantControllerConfig,
|
||||
hal::{battery::BatteryInteraction, esp::Esp},
|
||||
@@ -14,6 +14,7 @@ use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_time::Timer;
|
||||
@@ -195,6 +196,17 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
|
||||
&mut self.rtc_module
|
||||
}
|
||||
|
||||
async fn get_time(&mut self) -> DateTime<Utc> {
|
||||
self.esp.get_time()
|
||||
}
|
||||
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
|
||||
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
|
||||
self.esp.set_time(time.to_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
|
||||
let shift_register = self.shift_register.lock().await;
|
||||
if charging {
|
||||
@@ -207,8 +219,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||
|
||||
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)
|
||||
self.esp.deep_sleep(duration_in_ms)
|
||||
}
|
||||
|
||||
fn is_day(&self) -> bool {
|
||||
@@ -362,17 +373,13 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||
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;
|
||||
log(
|
||||
LogMessage::RawMeasure,
|
||||
unscaled as u32,
|
||||
hz as u32,
|
||||
&plant.to_string(),
|
||||
&format!("{sensor:?}"),
|
||||
);
|
||||
results[repeat] = hz;
|
||||
}
|
||||
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
||||
@@ -425,11 +432,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||
Ok(b) => b as u32,
|
||||
Err(_) => u32::MAX,
|
||||
};
|
||||
LOG_ACCESS
|
||||
.lock()
|
||||
.await
|
||||
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
|
||||
.await;
|
||||
log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "");
|
||||
}
|
||||
Timer::after_millis(10).await;
|
||||
Ok(())
|
||||
|
||||
@@ -3,10 +3,11 @@ use crate::hal::battery::BatteryInteraction;
|
||||
use crate::hal::esp::{hold_disable, hold_enable, Esp};
|
||||
use crate::hal::rtc::RTCModuleInteraction;
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS};
|
||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::Timer;
|
||||
@@ -326,6 +327,16 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
&mut self.rtc_module
|
||||
}
|
||||
|
||||
async fn get_time(&mut self) -> DateTime<Utc> {
|
||||
self.esp.get_time()
|
||||
}
|
||||
|
||||
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
|
||||
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
|
||||
self.esp.set_time(time.to_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
|
||||
self.charger.set_charge_indicator(charging)
|
||||
}
|
||||
@@ -333,8 +344,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
||||
self.awake.set_low();
|
||||
self.charger.power_save();
|
||||
let rtc = TIME_ACCESS.get().await.lock().await;
|
||||
self.esp.deep_sleep(duration_in_ms, rtc);
|
||||
self.esp.deep_sleep(duration_in_ms);
|
||||
}
|
||||
|
||||
fn is_day(&self) -> bool {
|
||||
|
||||
@@ -2,22 +2,25 @@ 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::analog::adc::{Adc, AdcCalLine, AdcConfig, AdcPin, Attenuation};
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull};
|
||||
use esp_hal::gpio::{DriveMode, Flex, Input, InputConfig, Output, OutputConfig, Pull};
|
||||
use esp_hal::pcnt::channel::CtrlMode::Keep;
|
||||
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
|
||||
use esp_hal::pcnt::unit::Unit;
|
||||
use esp_hal::peripherals::GPIO5;
|
||||
use esp_hal::Blocking;
|
||||
use esp_hal::Async;
|
||||
use esp_println::println;
|
||||
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
|
||||
|
||||
unsafe impl Send for TankSensor<'_> {}
|
||||
|
||||
pub struct TankSensor<'a> {
|
||||
one_wire_bus: OneWire<Flex<'a>>,
|
||||
tank_channel: Adc<'a, ADC1<'a>, Blocking>,
|
||||
tank_channel: Adc<'a, ADC1<'a>, Async>,
|
||||
tank_power: Output<'a>,
|
||||
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>,
|
||||
// flow_counter: PcntDriver<'a>,
|
||||
// delay: Delay,
|
||||
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>, AdcCalLine<ADC1<'a>>>,
|
||||
flow_counter: Unit<'a, 1>,
|
||||
}
|
||||
|
||||
impl<'a> TankSensor<'a> {
|
||||
@@ -29,62 +32,55 @@ impl<'a> TankSensor<'a> {
|
||||
flow_sensor: Input,
|
||||
pcnt1: Unit<'a, 1>,
|
||||
) -> Result<TankSensor<'a>, FatError> {
|
||||
one_wire_pin.apply_output_config(&OutputConfig::default().with_pull(Pull::None));
|
||||
one_wire_pin.apply_output_config(
|
||||
&OutputConfig::default()
|
||||
.with_drive_mode(DriveMode::OpenDrain)
|
||||
.with_pull(Pull::None),
|
||||
);
|
||||
one_wire_pin.apply_input_config(&InputConfig::default().with_pull(Pull::None));
|
||||
one_wire_pin.set_high();
|
||||
one_wire_pin.set_input_enable(true);
|
||||
one_wire_pin.set_output_enable(true);
|
||||
|
||||
let mut adc1_config = AdcConfig::new();
|
||||
let tank_pin = adc1_config.enable_pin(gpio5, Attenuation::_11dB);
|
||||
let tank_channel = Adc::new(adc1, adc1_config);
|
||||
let tank_pin =
|
||||
adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
|
||||
let tank_channel = Adc::new(adc1, adc1_config).into_async();
|
||||
|
||||
let one_wire_bus = OneWire::new(one_wire_pin, false);
|
||||
|
||||
//
|
||||
// let mut flow_counter = PcntDriver::new(
|
||||
// pcnt1,
|
||||
// Some(flow_sensor_pin),
|
||||
// Option::<AnyInputPin>::None,
|
||||
// Option::<AnyInputPin>::None,
|
||||
// Option::<AnyInputPin>::None,
|
||||
// )?;
|
||||
//
|
||||
// flow_counter.channel_config(
|
||||
// PcntChannel::Channel1,
|
||||
// PinIndex::Pin0,
|
||||
// PinIndex::Pin1,
|
||||
// &PcntChannelConfig {
|
||||
// lctrl_mode: PcntControlMode::Keep,
|
||||
// hctrl_mode: PcntControlMode::Keep,
|
||||
// pos_mode: PcntCountMode::Increment,
|
||||
// neg_mode: PcntCountMode::Hold,
|
||||
// counter_h_lim: i16::MAX,
|
||||
// counter_l_lim: 0,
|
||||
// },
|
||||
// )?;
|
||||
//
|
||||
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,
|
||||
// delay: Default::default(),
|
||||
tank_pin,
|
||||
flow_counter: pcnt1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset_flow_meter(&mut self) {
|
||||
// self.flow_counter.counter_pause().unwrap();
|
||||
// self.flow_counter.counter_clear().unwrap();
|
||||
self.flow_counter.pause();
|
||||
self.flow_counter.clear();
|
||||
}
|
||||
|
||||
pub fn start_flow_meter(&mut self) {
|
||||
//self.flow_counter.counter_resume().unwrap();
|
||||
self.flow_counter.resume();
|
||||
}
|
||||
|
||||
pub fn get_flow_meter_value(&mut self) -> i16 {
|
||||
//self.flow_counter.get_counter_value().unwrap()
|
||||
5_i16
|
||||
self.flow_counter.value()
|
||||
}
|
||||
|
||||
pub fn stop_flow_meter(&mut self) -> i16 {
|
||||
//self.flow_counter.counter_pause().unwrap();
|
||||
self.flow_counter.pause();
|
||||
self.get_flow_meter_value()
|
||||
}
|
||||
|
||||
@@ -92,15 +88,33 @@ impl<'a> TankSensor<'a> {
|
||||
//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 presence = self.one_wire_bus.reset(&mut delay)?;
|
||||
println!("OneWire: reset presence pulse = {}", presence);
|
||||
if !presence {
|
||||
println!("OneWire: no device responded to reset — check pull-up resistor and wiring");
|
||||
}
|
||||
|
||||
let mut search = DeviceSearch::new();
|
||||
let mut water_temp_sensor: Option<Device> = None;
|
||||
let mut devices_found = 0u8;
|
||||
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
|
||||
devices_found += 1;
|
||||
println!(
|
||||
"OneWire: found device #{} family=0x{:02X} addr={:02X?}",
|
||||
devices_found, device.address[0], device.address
|
||||
);
|
||||
if device.address[0] == ds18b20::FAMILY_CODE {
|
||||
water_temp_sensor = Some(device);
|
||||
break;
|
||||
} else {
|
||||
println!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE);
|
||||
}
|
||||
}
|
||||
if devices_found == 0 {
|
||||
println!("OneWire: search found zero devices on the bus");
|
||||
}
|
||||
|
||||
match water_temp_sensor {
|
||||
Some(device) => {
|
||||
println!("Found one wire device: {:?}", device);
|
||||
@@ -152,17 +166,15 @@ impl<'a> TankSensor<'a> {
|
||||
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
|
||||
for sample in store.iter_mut() {
|
||||
*sample = self.tank_channel.read_oneshot(&mut self.tank_pin).await;
|
||||
//force yield between successful samples
|
||||
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)
|
||||
let median_mv = store[TANK_MULTI_SAMPLE / 2] as f32;
|
||||
Ok(median_mv / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user