Compare commits
8 Commits
79daecf97d
...
016047ab23
| Author | SHA1 | Date | |
|---|---|---|---|
| 016047ab23 | |||
| eb276cfa68 | |||
| f1c85d1d74 | |||
| 097aff5360 | |||
| fc0e18da56 | |||
| 2e4eb283b5 | |||
| cc92c82ac9 | |||
| b8f01f0de9 |
62
AGENTS.md
Normal file
62
AGENTS.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Scope
|
||||
These instructions apply to the entire repository unless a deeper `AGENTS.md` overrides them.
|
||||
|
||||
## Repository Overview
|
||||
`PlantCtrl` is a mixed-discipline repository with embedded firmware, shared Rust crates, hardware design files, and a Hugo-based website.
|
||||
|
||||
Top-level layout:
|
||||
- `Software/MainBoard/rust`: main embedded Rust firmware for the controller board (`plant-ctrl2`).
|
||||
- `Software/CAN_Sensor`: embedded Rust firmware for the CAN sensor / BMS board.
|
||||
- `Software/Shared/canapi`: shared Rust crate used by firmware projects.
|
||||
- `Hardware`: PCB, case, and related hardware design assets.
|
||||
- `DataSheets`: reference material; treat as source data, not generated output.
|
||||
- `website`: Hugo site based on the Blowfish theme.
|
||||
- `bin`: helper scripts and local tooling, if present.
|
||||
|
||||
## Working Rules
|
||||
- Keep changes tightly scoped to the user request; this repo spans hardware, firmware, and website code.
|
||||
- Prefer fixing the underlying cause instead of applying cosmetic workarounds.
|
||||
- Preserve existing file structure and naming unless the user explicitly asks for restructuring.
|
||||
- Avoid mass formatting or opportunistic cleanup in KiCad files, lockfiles, generated assets, or vendored dependencies.
|
||||
- Do not edit dependency directories such as `website/themes` or `Software/MainBoard/rust/src_webpack/node_modules` unless the user explicitly asks for vendor changes.
|
||||
- When touching firmware code, keep resource usage and target constraints in mind; avoid unnecessary allocations or feature creep.
|
||||
|
||||
## Firmware Guidance
|
||||
- Shared protocol or serialization changes must be checked for impact across both `Software/MainBoard/rust` and `Software/CAN_Sensor`.
|
||||
- Prefer small, explicit changes in embedded code paths; do not introduce heavyweight abstractions without a clear payoff.
|
||||
- Keep `no_std`/embedded assumptions intact unless the code clearly opts into something else.
|
||||
- Be careful with feature flags, target-specific dependencies, and boot/runtime configuration in Cargo manifests.
|
||||
|
||||
## Hardware Guidance
|
||||
- Treat hardware directories as design artifacts, not generic text files.
|
||||
- Do not reorder, normalize, or bulk-edit PCB / CAD files unless the user specifically requests those changes.
|
||||
- If a software change depends on hardware assumptions, call that out clearly in the final handoff.
|
||||
|
||||
## Website Guidance
|
||||
- The site in `website` uses Hugo with the Blowfish theme.
|
||||
- Prefer editing site content, config, or custom assets over modifying vendored theme internals.
|
||||
- Keep frontend changes consistent with the existing site structure unless the user asks for a redesign.
|
||||
|
||||
## Validation
|
||||
Use the narrowest relevant check first.
|
||||
|
||||
Useful commands:
|
||||
- `cargo check --manifest-path Software/Shared/canapi/Cargo.toml`
|
||||
- `cargo check --manifest-path Software/CAN_Sensor/Cargo.toml`
|
||||
- `cargo check --manifest-path Software/MainBoard/rust/Cargo.toml`
|
||||
- `npm run dev` from `website` for local Hugo development if the environment has the required tools installed.
|
||||
|
||||
Validation notes:
|
||||
- Embedded firmware may require target-specific toolchains or hardware-adjacent tooling that is not always available.
|
||||
- If you cannot run a meaningful validation step, say so explicitly and describe the likely prerequisite.
|
||||
|
||||
## File Hygiene
|
||||
- Read large files in chunks.
|
||||
- Prefer targeted searches (`rg`, or `find` if unavailable) over broad scans.
|
||||
- Do not commit build outputs, generated binaries, or local IDE metadata unless the user explicitly requests it.
|
||||
|
||||
## Handoff Expectations
|
||||
- Summarize what changed, where it changed, and any validation performed.
|
||||
- Call out follow-up work when a change likely affects both firmware targets, hardware assumptions, or the website.
|
||||
BIN
Hardware/Controller_Case/flap_v2.3mf
Normal file
BIN
Hardware/Controller_Case/flap_v2.3mf
Normal file
Binary file not shown.
731
Software/MainBoard/rust/Cargo.lock
generated
731
Software/MainBoard/rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -46,57 +46,34 @@ canapi = { path = "../../Shared/canapi" }
|
||||
|
||||
# Platform and ESP-specific runtime/boot/runtime utilities
|
||||
log = "0.4.28"
|
||||
esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c6", "log-04"] }
|
||||
esp-hal = { version = "1.0.0", features = ["esp32c6", "log-04"] }
|
||||
esp-rtos = { version = "0.2.0", features = ["esp32c6", "embassy", "esp-radio"] }
|
||||
esp-backtrace = { version = "0.18.1", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
|
||||
esp-println = { version = "0.16.1", features = ["esp32c6", "log-04", "auto"] }
|
||||
esp-storage = { version = "0.8.1", features = ["esp32c6"] }
|
||||
esp-radio = { version = "0.17.0", features = ["esp32c6", "log-04", "smoltcp", "wifi", "unstable"] }
|
||||
esp-alloc = { version = "0.9.0", features = ["esp32c6", "internal-heap-stats"] }
|
||||
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6", "log-04"] }
|
||||
esp-hal = { version = "1.1.0", features = ["esp32c6", "log-04"] }
|
||||
esp-rtos = { version = "0.3.0", features = ["esp32c6", "embassy", "esp-radio"] }
|
||||
esp-backtrace = { version = "0.19.0", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
|
||||
esp-println = { version = "0.17.0", features = ["esp32c6", "log-04", "auto"] }
|
||||
esp-storage = { version = "0.9.0", features = ["esp32c6"] }
|
||||
esp-radio = { version = "0.18.0", features = ["esp32c6", "log-04", "wifi", "unstable"] }
|
||||
esp-alloc = { version = "0.10.0", features = ["esp32c6", "internal-heap-stats"] }
|
||||
|
||||
# Async runtime (Embassy core)
|
||||
embassy-executor = { version = "0.9.1", features = ["log", "nightly"] }
|
||||
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
|
||||
embassy-sync = { version = "0.7.2", features = ["log"] }
|
||||
embassy-executor = { version = "0.10.0", features = ["log", "nightly"] }
|
||||
embassy-time = { version = "0.5.1", features = ["log"], default-features = false }
|
||||
embassy-sync = { version = "0.8.0", features = ["log"] }
|
||||
|
||||
# Networking and protocol stacks
|
||||
embassy-net = { version = "0.7.1", features = [
|
||||
"dhcpv4",
|
||||
"log",
|
||||
"medium-ethernet",
|
||||
"tcp",
|
||||
"udp",
|
||||
"proto-ipv4",
|
||||
"dns"
|
||||
] }
|
||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||
"alloc",
|
||||
"log",
|
||||
"medium-ethernet",
|
||||
"multicast",
|
||||
"proto-dhcpv4",
|
||||
"proto-ipv6",
|
||||
"proto-dns",
|
||||
"proto-ipv4",
|
||||
"socket-dns",
|
||||
"socket-icmp",
|
||||
"socket-raw",
|
||||
"socket-tcp",
|
||||
"socket-udp",
|
||||
] }
|
||||
embassy-net = { version = "0.8.0", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp", "proto-ipv4", "dns", "proto-ipv6"] }
|
||||
sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
|
||||
edge-dhcp = "0.6.0"
|
||||
edge-nal = "0.5.0"
|
||||
edge-nal-embassy = "0.6.0"
|
||||
edge-http = { version = "0.6.1", features = ["log"] }
|
||||
edge-dhcp = "0.7.0"
|
||||
edge-nal = "0.6.0"
|
||||
edge-nal-embassy = "0.8.1"
|
||||
edge-http = { version = "0.7.0", features = ["log"] }
|
||||
|
||||
esp32c6 = { version = "0.22.0" }
|
||||
esp32c6 = { version = "0.23.2" }
|
||||
|
||||
# Hardware abstraction traits and HAL adapters
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-storage = "0.3.1"
|
||||
embassy-embedded-hal = "0.5.0"
|
||||
embassy-embedded-hal = "0.6.0"
|
||||
embedded-can = "0.4.1"
|
||||
nb = "1.1.0"
|
||||
|
||||
@@ -114,7 +91,7 @@ embedded-savegame = { version = "0.3.0" }
|
||||
# Serialization / codecs
|
||||
serde = { version = "1.0.228", features = ["derive", "alloc"], default-features = false }
|
||||
serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] }
|
||||
bincode = { version = "2.0.1", default-features = false, features = ["derive", "alloc"] }
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
|
||||
# Time and time zones
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
|
||||
@@ -132,13 +109,8 @@ unit-enum = "1.4.3"
|
||||
async-trait = "0.1.89"
|
||||
option-lock = { version = "0.3.1", default-features = false }
|
||||
measurements = "0.11.1"
|
||||
mcutie = { path = "src/mcutie_3_0_0", features = ["log"] }
|
||||
|
||||
# Project-specific
|
||||
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
|
||||
no-panic = "0.1.36"
|
||||
|
||||
[patch.crates-io]
|
||||
mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }
|
||||
#bq34z100 = { path = "../../bq34z100_rust" }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::hal::PLANT_COUNT;
|
||||
use crate::plant_state::PlantWateringMode;
|
||||
use alloc::string::{String, ToString};
|
||||
use bincode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkConfig {
|
||||
pub ap_ssid: String,
|
||||
@@ -31,7 +30,7 @@ impl Default for NetworkConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct NightLampConfig {
|
||||
pub enabled: bool,
|
||||
@@ -54,7 +53,7 @@ impl Default for NightLampConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct TankConfig {
|
||||
pub tank_sensor_enabled: bool,
|
||||
@@ -79,20 +78,20 @@ impl Default for TankConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub enum BatteryBoardVersion {
|
||||
#[default]
|
||||
Disabled,
|
||||
WchI2cSlave,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub enum BoardVersion {
|
||||
Initial,
|
||||
#[default]
|
||||
V4,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub struct BoardHardware {
|
||||
pub board: BoardVersion,
|
||||
pub battery: BatteryBoardVersion,
|
||||
@@ -100,7 +99,7 @@ pub struct BoardHardware {
|
||||
pub pump_corrosion_protection: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
#[serde(default)]
|
||||
pub struct PlantControllerConfig {
|
||||
pub hardware: BoardHardware,
|
||||
@@ -111,7 +110,7 @@ pub struct PlantControllerConfig {
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct PlantConfig {
|
||||
pub mode: PlantWateringMode,
|
||||
|
||||
@@ -230,16 +230,8 @@ impl<E: fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bincode::error::DecodeError> for FatError {
|
||||
fn from(value: bincode::error::DecodeError) -> Self {
|
||||
FatError::Eeprom24x {
|
||||
error: format!("{value:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bincode::error::EncodeError> for FatError {
|
||||
fn from(value: bincode::error::EncodeError) -> Self {
|
||||
impl From<postcard::Error> for FatError {
|
||||
fn from(value: postcard::Error) -> Self {
|
||||
FatError::Eeprom24x {
|
||||
error: format!("{value:?}"),
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ use alloc::{format, string::String, vec, vec::Vec};
|
||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
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;
|
||||
use embassy_sync::once_lock::OnceLock;
|
||||
@@ -33,19 +34,17 @@ use esp_hal::system::software_reset;
|
||||
use esp_hal::uart::Uart;
|
||||
use esp_hal::Blocking;
|
||||
use esp_println::println;
|
||||
use esp_radio::wifi::{
|
||||
AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig,
|
||||
ScanTypeConfig, WifiController, WifiDevice, WifiStaState,
|
||||
};
|
||||
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 log::{error, 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};
|
||||
use sntpc::{get_time, NtpContext, NtpTimestampGenerator, NtpUdpSocket};
|
||||
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||
@@ -70,6 +69,39 @@ struct Timestamp {
|
||||
stamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
|
||||
extern "C" {
|
||||
fn gpio_pad_hold(gpio_num: u32);
|
||||
@@ -104,8 +136,8 @@ pub struct Esp<'a> {
|
||||
pub savegame: SavegameManager,
|
||||
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>,
|
||||
@@ -249,6 +281,7 @@ impl Esp<'_> {
|
||||
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)
|
||||
@@ -264,7 +297,7 @@ impl Esp<'_> {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
let addr: IpAddr = ntp.into();
|
||||
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
|
||||
let timeout = get_time(SocketAddr::from((addr, 123)), &ntp_socket, context)
|
||||
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
||||
.await;
|
||||
|
||||
@@ -292,10 +325,10 @@ impl Esp<'_> {
|
||||
let mut lock = self.controller.try_lock()?;
|
||||
info!("start wifi scan lock");
|
||||
let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
|
||||
min: Default::default(),
|
||||
max: Default::default(),
|
||||
min: esp_hal::time::Duration::from_millis(0),
|
||||
max: esp_hal::time::Duration::from_millis(0),
|
||||
});
|
||||
let rv = lock.scan_with_config_async(scan_config).await?;
|
||||
let rv = lock.scan_async(&scan_config).await?;
|
||||
info!("end wifi scan lock");
|
||||
Ok(rv)
|
||||
}
|
||||
@@ -383,15 +416,13 @@ impl Esp<'_> {
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config =
|
||||
ModeConfig::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
self.controller.lock().await.set_config(&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, gw_ip_addr)).ok();
|
||||
spawner.spawn(run_dhcp(*stack, gw_ip_addr)?);
|
||||
|
||||
loop {
|
||||
if stack.is_link_up() {
|
||||
@@ -451,52 +482,28 @@ impl Esp<'_> {
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config = ClientConfig::default()
|
||||
let auth_method = if password.is_empty() {
|
||||
AuthenticationMethod::None
|
||||
} else {
|
||||
AuthenticationMethod::Wpa2Personal
|
||||
};
|
||||
let client_config = StationConfig::default()
|
||||
.with_ssid(ssid)
|
||||
.with_auth_method(AuthMethod::Wpa2Personal)
|
||||
.with_auth_method(auth_method)
|
||||
.with_password(password);
|
||||
|
||||
self.controller
|
||||
.lock()
|
||||
.await
|
||||
.set_config(&ModeConfig::Client(client_config))?;
|
||||
spawner.spawn(net_task(runner)).ok();
|
||||
self.controller.lock().await.start_async().await?;
|
||||
|
||||
let res = async {
|
||||
loop {
|
||||
let state = esp_radio::wifi::sta_state();
|
||||
if state == WifiStaState::Started {
|
||||
self.controller.lock().await.connect()?;
|
||||
break;
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting for wifi sta ready")
|
||||
}
|
||||
|
||||
let res = async {
|
||||
loop {
|
||||
let state = esp_radio::wifi::sta_state();
|
||||
if state == WifiStaState::Connected {
|
||||
break;
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
Ok::<(), FatError>(())
|
||||
}
|
||||
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
bail!("Timeout waiting for wifi sta connected")
|
||||
}
|
||||
.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 res = async {
|
||||
while !stack.is_link_up() {
|
||||
@@ -575,7 +582,15 @@ impl Esp<'_> {
|
||||
/// Retries once on flash error.
|
||||
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
|
||||
let timestamp = self.get_time().to_rfc3339();
|
||||
self.savegame.save(config.as_slice(), ×tamp)
|
||||
self.savegame.save(config.as_slice(), ×tamp)?;
|
||||
|
||||
match self.savegame.load_latest()? {
|
||||
None => bail!("Config save verification failed: no latest save found"),
|
||||
Some(data) => {
|
||||
let _: PlantControllerConfig = serde_json::from_slice(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a specific save slot by erasing it on flash.
|
||||
@@ -701,8 +716,8 @@ impl Esp<'_> {
|
||||
receiver,
|
||||
round_trip_topic.clone(),
|
||||
stay_alive_topic.clone(),
|
||||
))?;
|
||||
spawner.spawn(mqtt_runner(task))?;
|
||||
)?);
|
||||
spawner.spawn(mqtt_runner(task)?);
|
||||
|
||||
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
|
||||
|
||||
@@ -774,6 +789,7 @@ impl Esp<'_> {
|
||||
Error::TooLarge => false,
|
||||
Error::PacketError => false,
|
||||
Error::Invalid => false,
|
||||
Error::Rejected => false,
|
||||
};
|
||||
if !retry {
|
||||
bail!(
|
||||
@@ -859,15 +875,12 @@ async fn mqtt_incoming_task(
|
||||
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>>) {
|
||||
async fn net_task(mut runner: Runner<'static, Interface<'static>>) {
|
||||
runner.run().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bincode::{Decode, Encode};
|
||||
use canapi::SensorSlot;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use core::cell::RefCell;
|
||||
@@ -95,7 +94,6 @@ use esp_hal::system::reset_reason;
|
||||
use esp_hal::timer::timg::{MwdtStage, TimerGroup, Wdt};
|
||||
use esp_hal::uart::Uart;
|
||||
use esp_hal::Blocking;
|
||||
use esp_radio::{init, Controller};
|
||||
use esp_storage::FlashStorage;
|
||||
use log::{info, warn};
|
||||
use portable_atomic::AtomicBool;
|
||||
@@ -119,7 +117,7 @@ pub static I2C_DRIVER: OnceLock<
|
||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Sensor {
|
||||
A,
|
||||
B,
|
||||
@@ -280,19 +278,11 @@ impl PlantHal {
|
||||
let wake_gpio1 = peripherals.GPIO1;
|
||||
|
||||
let rng = Rng::new();
|
||||
let esp_wifi_ctrl = &*mk_static!(
|
||||
Controller<'static>,
|
||||
init().map_err(|e| FatError::String {
|
||||
error: format!("Could not init wifi controller: {:?}", e)
|
||||
})?
|
||||
);
|
||||
|
||||
let (controller, interfaces) =
|
||||
esp_radio::wifi::new(esp_wifi_ctrl, peripherals.WIFI, Default::default()).map_err(
|
||||
|e| FatError::String {
|
||||
error: format!("Could not init wifi: {:?}", e),
|
||||
},
|
||||
)?;
|
||||
let (controller, interfaces) = esp_radio::wifi::new(peripherals.WIFI, Default::default())
|
||||
.map_err(|e| FatError::String {
|
||||
error: format!("Could not init wifi: {:?}", e),
|
||||
})?;
|
||||
|
||||
let pcnt_module = Pcnt::new(peripherals.PCNT);
|
||||
|
||||
@@ -349,16 +339,13 @@ impl PlantHal {
|
||||
.context("No OTA data partition found")?
|
||||
);
|
||||
|
||||
let ota_data = mk_static!(
|
||||
FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
|
||||
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_data = ota_data.as_embedded_storage(mk_static!(
|
||||
RmwNorFlashStorage<&mut MutexFlashStorage>,
|
||||
RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
|
||||
));
|
||||
|
||||
let state_0 = ota_state(AppPartitionSubType::Ota0, ota_data);
|
||||
let state_1 = ota_state(AppPartitionSubType::Ota1, 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)?;
|
||||
@@ -411,8 +398,8 @@ impl PlantHal {
|
||||
error: "Uart creation failed".to_string(),
|
||||
})?;
|
||||
|
||||
let ap = interfaces.ap;
|
||||
let sta = interfaces.sta;
|
||||
let ap = interfaces.access_point;
|
||||
let sta = interfaces.station;
|
||||
let mut esp = Esp {
|
||||
savegame,
|
||||
rng,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::hal::Box;
|
||||
use async_trait::async_trait;
|
||||
use bincode::{Decode, Encode};
|
||||
use chrono::{DateTime, Utc};
|
||||
use ds323x::ic::DS3231;
|
||||
use ds323x::interface::I2cInterface;
|
||||
@@ -29,7 +28,7 @@ pub trait RTCModuleInteraction {
|
||||
fn read(&mut self, offset: u32, data: &mut [u8]) -> FatResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default)]
|
||||
pub struct BackupHeader {
|
||||
pub timestamp: i64,
|
||||
pub(crate) crc16: u16,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use bincode::{Decode, Encode};
|
||||
use embedded_savegame::storage::{Flash, Storage};
|
||||
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
|
||||
use esp_bootloader_esp_idf::partitions::{Error as PartitionError, FlashRegion};
|
||||
@@ -25,13 +25,57 @@ pub struct SaveInfo {
|
||||
pub created_at: Option<alloc::string::String>,
|
||||
}
|
||||
|
||||
/// Wrapper that includes both the config data and metadata like creation timestamp.
|
||||
#[derive(Serialize, Debug, Encode, Decode)]
|
||||
struct SaveWrapper {
|
||||
/// UTC timestamp in RFC3339 format
|
||||
created_at: alloc::string::String,
|
||||
/// Raw config JSON data
|
||||
data: Vec<u8>,
|
||||
const SAVE_MAGIC: [u8; 4] = *b"SGM1";
|
||||
const SAVE_HEADER_LEN: usize = 6; // magic (4) + timestamp_len (u16)
|
||||
|
||||
struct ParsedSave<'a> {
|
||||
created_at: &'a str,
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
fn strip_padding(data: &[u8]) -> &[u8] {
|
||||
let mut end = data.len();
|
||||
while end > 0 {
|
||||
let b = data[end - 1];
|
||||
if b == 0x00 || b == 0xFF {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&data[..end]
|
||||
}
|
||||
|
||||
fn parse_save(data: &[u8]) -> FatResult<ParsedSave<'_>> {
|
||||
if data.len() < SAVE_HEADER_LEN {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload too short".into(),
|
||||
});
|
||||
}
|
||||
if data[..4] != SAVE_MAGIC {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload has invalid magic".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let timestamp_len = u16::from_le_bytes([data[4], data[5]]) as usize;
|
||||
let timestamp_end = SAVE_HEADER_LEN + timestamp_len;
|
||||
if timestamp_end > data.len() {
|
||||
return Err(FatError::String {
|
||||
error: "Save payload timestamp length exceeds data".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let created_at = core::str::from_utf8(&data[SAVE_HEADER_LEN..timestamp_end]).map_err(|e| {
|
||||
FatError::String {
|
||||
error: alloc::format!("Save payload contains invalid timestamp UTF-8: {e:?}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(ParsedSave {
|
||||
created_at,
|
||||
data: strip_padding(&data[timestamp_end..]),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Flash adapter ──────────────────────────────────────────────────────────────
|
||||
@@ -134,11 +178,21 @@ impl SavegameManager {
|
||||
/// slot before `append()` writes to the next free one.
|
||||
/// Both operations are performed atomically on the same Storage instance.
|
||||
pub fn save(&mut self, data: &[u8], timestamp: &str) -> FatResult<()> {
|
||||
let wrapper = SaveWrapper {
|
||||
created_at: alloc::string::String::from(timestamp),
|
||||
data: data.to_vec(),
|
||||
};
|
||||
let mut serialized = bincode::encode_to_vec(&wrapper, bincode::config::standard())?;
|
||||
let timestamp_bytes = timestamp.as_bytes();
|
||||
let timestamp_len: u16 =
|
||||
timestamp_bytes
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| FatError::String {
|
||||
error: "Timestamp too long for save header".into(),
|
||||
})?;
|
||||
|
||||
let mut serialized =
|
||||
Vec::with_capacity(SAVE_HEADER_LEN + timestamp_bytes.len() + data.len());
|
||||
serialized.extend_from_slice(&SAVE_MAGIC);
|
||||
serialized.extend_from_slice(×tamp_len.to_le_bytes());
|
||||
serialized.extend_from_slice(timestamp_bytes);
|
||||
serialized.extend_from_slice(data);
|
||||
|
||||
// Flash storage often requires length to be a multiple of 4.
|
||||
let padding = (4 - (serialized.len() % 4)) % 4;
|
||||
@@ -169,12 +223,8 @@ impl SavegameManager {
|
||||
match self.storage.read(idx, &mut buf)? {
|
||||
None => Ok(None),
|
||||
Some(data) => {
|
||||
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||
let (wrapper, _) = bincode::decode_from_slice::<SaveWrapper, _>(
|
||||
data,
|
||||
bincode::config::standard(),
|
||||
)?;
|
||||
Ok(Some(wrapper.data))
|
||||
let parsed = parse_save(data)?;
|
||||
Ok(Some(parsed.data.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,17 +242,22 @@ impl SavegameManager {
|
||||
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
|
||||
for idx in 0..SAVEGAME_SLOT_COUNT {
|
||||
if let Some(data) = self.storage.read(idx, &mut buf)? {
|
||||
// Try to deserialize as SaveWrapper (new Bincode format)
|
||||
let (wrapper, _) = bincode::decode_from_slice::<SaveWrapper, _>(
|
||||
data,
|
||||
bincode::config::standard(),
|
||||
)?;
|
||||
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: wrapper.data.len() as u32,
|
||||
created_at: Some(wrapper.created_at),
|
||||
});
|
||||
match parse_save(data) {
|
||||
Ok(save) => {
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: save.data.len() as u32,
|
||||
created_at: Some(alloc::string::String::from(save.created_at)),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
saves.push(SaveInfo {
|
||||
idx,
|
||||
len: 0,
|
||||
created_at: Some(err.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(saves)
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::log::{log, LogMessage};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use async_trait::async_trait;
|
||||
use bincode::config;
|
||||
use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET};
|
||||
use canapi::SensorSlot;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
@@ -36,7 +35,6 @@ use measurements::{Current, Voltage};
|
||||
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
|
||||
|
||||
pub const BACKUP_HEADER_MAX_SIZE: usize = 64;
|
||||
const CONFIG: config::Configuration = config::standard();
|
||||
|
||||
const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
|
||||
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::Custom(twai::TimingConfig {
|
||||
@@ -398,6 +396,145 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
res
|
||||
}
|
||||
|
||||
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].unwrap_or(0.0) as u32;
|
||||
let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32;
|
||||
log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "");
|
||||
}
|
||||
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 can_power(&mut self, state: bool) -> FatResult<()> {
|
||||
if state && self.can_power.is_set_low() {
|
||||
self.can_power.set_high();
|
||||
} else {
|
||||
self.can_power.set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn backup_config(&mut self, controller_config: &PlantControllerConfig) -> FatResult<()> {
|
||||
let mut buffer: [u8; 4096 - BACKUP_HEADER_MAX_SIZE] = [0; 4096 - BACKUP_HEADER_MAX_SIZE];
|
||||
let length = postcard::to_slice(controller_config, &mut buffer)?.len();
|
||||
info!("Writing backup config of size {}", length);
|
||||
let mut checksum = X25.digest();
|
||||
checksum.update(&buffer[..length]);
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
let time = self.rtc_module.get_rtc_time().await?.timestamp_millis();
|
||||
let header = BackupHeader {
|
||||
crc16: checksum.finalize(),
|
||||
timestamp: time,
|
||||
size: length as u16,
|
||||
};
|
||||
info!("Header is {:?}", header);
|
||||
postcard::to_slice(&header, &mut header_page_buffer)?;
|
||||
info!("Header is serialized");
|
||||
self.get_rtc_module().write(0, &header_page_buffer)?;
|
||||
info!("Header written");
|
||||
let mut to_write = length;
|
||||
let mut chunk: usize = 0;
|
||||
|
||||
while to_write > 0 {
|
||||
self.progress(chunk as u32).await;
|
||||
let start = chunk * EEPROM_PAGE;
|
||||
let end = start + min(EEPROM_PAGE, to_write);
|
||||
let part = &buffer[start..end];
|
||||
info!(
|
||||
"Writing chunk {} of size {} to offset {}",
|
||||
chunk,
|
||||
part.len(),
|
||||
start
|
||||
);
|
||||
to_write -= part.len();
|
||||
self.get_rtc_module()
|
||||
.write((BACKUP_HEADER_MAX_SIZE + chunk * EEPROM_PAGE) as u32, part)?;
|
||||
chunk += 1;
|
||||
}
|
||||
info!("Backup complete");
|
||||
self.clear_progress().await;
|
||||
Ok(())
|
||||
}
|
||||
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig> {
|
||||
let info = self.backup_info().await?;
|
||||
let mut store = alloc::vec![0_u8; info.size as usize];
|
||||
self.rtc_module
|
||||
.read(BACKUP_HEADER_MAX_SIZE as u32, store.as_mut_slice())?;
|
||||
info!("Read backup data of size {}", store.len());
|
||||
let mut checksum = X25.digest();
|
||||
info!("Calculating CRC");
|
||||
checksum.update(&store[..]);
|
||||
let crc = checksum.finalize();
|
||||
info!("CRC is {:04x}", crc);
|
||||
if crc != info.crc16 {
|
||||
warn!("CRC mismatch in backup data");
|
||||
bail!("CRC mismatch in backup data")
|
||||
}
|
||||
info!("CRC is correct");
|
||||
let decoded = postcard::from_bytes(&store[..])?;
|
||||
info!("Backup data decoded");
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
async fn backup_info(&mut self) -> FatResult<BackupHeader> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
self.get_rtc_module().read(0, &mut header_page_buffer)?;
|
||||
info!("Read header page");
|
||||
let info = postcard::take_from_bytes::<BackupHeader>(&header_page_buffer[..]);
|
||||
info!("decoding header: {:?}", info);
|
||||
let (header, _) = info.context("Could not read backup header")?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
@@ -475,146 +612,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
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].unwrap_or(0.0) as u32;
|
||||
let b = moisture.sensor_b_hz[plant].unwrap_or(0.0) as u32;
|
||||
log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "");
|
||||
}
|
||||
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 can_power(&mut self, state: bool) -> FatResult<()> {
|
||||
if state && self.can_power.is_set_low() {
|
||||
self.can_power.set_high();
|
||||
} else {
|
||||
self.can_power.set_low();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn backup_config(&mut self, controller_config: &PlantControllerConfig) -> FatResult<()> {
|
||||
let mut buffer: [u8; 4096 - BACKUP_HEADER_MAX_SIZE] = [0; 4096 - BACKUP_HEADER_MAX_SIZE];
|
||||
let length = bincode::encode_into_slice(controller_config, &mut buffer, CONFIG)?;
|
||||
info!("Writing backup config of size {}", length);
|
||||
let mut checksum = X25.digest();
|
||||
checksum.update(&buffer[..length]);
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
|
||||
let time = self.rtc_module.get_rtc_time().await?.timestamp_millis();
|
||||
let header = BackupHeader {
|
||||
crc16: checksum.finalize(),
|
||||
timestamp: time,
|
||||
size: length as u16,
|
||||
};
|
||||
info!("Header is {:?}", header);
|
||||
bincode::encode_into_slice(&header, &mut header_page_buffer, CONFIG)?;
|
||||
info!("Header is serialized");
|
||||
self.get_rtc_module().write(0, &header_page_buffer)?;
|
||||
info!("Header written");
|
||||
let mut to_write = length;
|
||||
let mut chunk: usize = 0;
|
||||
|
||||
while to_write > 0 {
|
||||
self.progress(chunk as u32).await;
|
||||
let start = chunk * EEPROM_PAGE;
|
||||
let end = start + min(EEPROM_PAGE, to_write);
|
||||
let part = &buffer[start..end];
|
||||
info!(
|
||||
"Writing chunk {} of size {} to offset {}",
|
||||
chunk,
|
||||
part.len(),
|
||||
start
|
||||
);
|
||||
to_write -= part.len();
|
||||
self.get_rtc_module()
|
||||
.write((BACKUP_HEADER_MAX_SIZE + chunk * EEPROM_PAGE) as u32, part)?;
|
||||
chunk += 1;
|
||||
}
|
||||
info!("Backup complete");
|
||||
self.clear_progress().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_backup(&mut self) -> FatResult<PlantControllerConfig> {
|
||||
let info = self.backup_info().await?;
|
||||
let mut store = alloc::vec![0_u8; info.size as usize];
|
||||
self.rtc_module
|
||||
.read(BACKUP_HEADER_MAX_SIZE as u32, store.as_mut_slice())?;
|
||||
info!("Read backup data of size {}", store.len());
|
||||
let mut checksum = X25.digest();
|
||||
info!("Calculating CRC");
|
||||
checksum.update(&store[..]);
|
||||
let crc = checksum.finalize();
|
||||
info!("CRC is {:04x}", crc);
|
||||
if crc != info.crc16 {
|
||||
warn!("CRC mismatch in backup data");
|
||||
bail!("CRC mismatch in backup data")
|
||||
}
|
||||
info!("CRC is correct");
|
||||
let (decoded, _) = bincode::decode_from_slice(&store[..], CONFIG)?;
|
||||
info!("Backup data decoded");
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
async fn backup_info(&mut self) -> FatResult<BackupHeader> {
|
||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||
self.get_rtc_module().read(0, &mut header_page_buffer)?;
|
||||
info!("Read header page");
|
||||
let info: Result<(BackupHeader, usize), bincode::error::DecodeError> =
|
||||
bincode::decode_from_slice(&header_page_buffer[..], CONFIG);
|
||||
info!("decoding header: {:?}", info);
|
||||
let (header, _) = info.context("Could not read backup header")?;
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_can_measurements(
|
||||
|
||||
@@ -4,18 +4,20 @@ use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
|
||||
use embassy_time::Timer;
|
||||
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>, AdcCalLine<ADC1<'a>>>,
|
||||
flow_counter: Unit<'a, 1>,
|
||||
@@ -30,12 +32,20 @@ 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_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
|
||||
let tank_channel = Adc::new(adc1, adc1_config);
|
||||
let tank_channel = Adc::new(adc1, adc1_config).into_async();
|
||||
|
||||
let one_wire_bus = OneWire::new(one_wire_pin, false);
|
||||
|
||||
@@ -139,15 +149,9 @@ impl<'a> TankSensor<'a> {
|
||||
|
||||
let mut store = [0_u16; TANK_MULTI_SAMPLE];
|
||||
for sample in store.iter_mut() {
|
||||
let value = self.tank_channel.read_oneshot(&mut self.tank_pin);
|
||||
//force yield
|
||||
*sample = self.tank_channel.read_oneshot(&mut self.tank_pin).await;
|
||||
//force yield between successful samples
|
||||
Timer::after_millis(10).await;
|
||||
match value {
|
||||
Ok(v) => *sample = v,
|
||||
Err(e) => {
|
||||
bail!("ADC Hardware error: {:?}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
self.tank_power.set_low();
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
let stack_val = stack.take();
|
||||
if let Some(s) = stack_val {
|
||||
spawner.spawn(http_server(reboot_now.clone(), s))?;
|
||||
spawner.spawn(http_server(reboot_now.clone(), s)?);
|
||||
} else {
|
||||
bail!("Network stack missing, hard abort")
|
||||
}
|
||||
@@ -673,7 +673,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
if stay_alive {
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
if let Some(s) = stack.take() {
|
||||
spawner.spawn(http_server(reboot_now.clone(), s))?;
|
||||
spawner.spawn(http_server(reboot_now.clone(), s)?);
|
||||
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone(), timezone).await;
|
||||
} else {
|
||||
bail!("Network Stack missing, hard abort");
|
||||
@@ -690,7 +690,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
Timer::after_millis(5000).await;
|
||||
|
||||
board.board_hal.get_esp().set_restart_to_conf(false);
|
||||
let _ = board
|
||||
board
|
||||
.board_hal
|
||||
.deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64)
|
||||
.await;
|
||||
@@ -1282,7 +1282,7 @@ use embassy_time::WithTimeout;
|
||||
async fn main(spawner: Spawner) -> ! {
|
||||
// intialize embassy
|
||||
crate::log::INTERCEPTOR.init();
|
||||
spawner.must_spawn(crate::log::log_task());
|
||||
spawner.spawn(log::log_task().unwrap());
|
||||
//force init here!
|
||||
match BOARD_ACCESS.init(
|
||||
PlantHal::create()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::hal::Moistures;
|
||||
use crate::{config::PlantConfig, hal::HAL, in_time_range};
|
||||
use bincode::{Decode, Encode};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -73,7 +72,7 @@ impl PumpState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Encode, Decode)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PlantWateringMode {
|
||||
Off,
|
||||
TargetMoisture,
|
||||
|
||||
@@ -3,11 +3,13 @@ use crate::fat_error::FatResult;
|
||||
use crate::hal::Detection;
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::{do_secure_pump, BOARD_ACCESS};
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use chrono::DateTime;
|
||||
use edge_http::io::server::Connection;
|
||||
use edge_nal::io::{Read, Write};
|
||||
use esp_radio::wifi::ap::AccessPointInfo;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -40,10 +42,10 @@ pub(crate) async fn wifi_scan<T, const N: usize>(
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
info!("start wifi scan");
|
||||
let mut ssids: Vec<String> = Vec::new();
|
||||
let scan_result = board.board_hal.get_esp().wifi_scan().await?;
|
||||
let scan_result: Vec<AccessPointInfo> = board.board_hal.get_esp().wifi_scan().await?;
|
||||
scan_result
|
||||
.iter()
|
||||
.for_each(|s| ssids.push(s.ssid.to_string()));
|
||||
.for_each(|s| ssids.push(s.ssid.as_str().to_owned()));
|
||||
let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
|
||||
info!("Sending ssid list {}", &ssid_json);
|
||||
Ok(Some(ssid_json))
|
||||
|
||||
Reference in New Issue
Block a user