8 Commits

Author SHA1 Message Date
016047ab23 Update Water HAL: enhance GPIO config with drive mode and input settings
- Added `DriveMode` configuration and input enablement for `one_wire_pin`.
- Improved GPIO initialization with `InputConfig` and default pull settings.
2026-04-26 21:24:51 +02:00
eb276cfa68 Refactor HAL modules: update async support in Water module and reorganize detect_sensors logic
- Replaced `Blocking` with `Async` for ADC operations in `Water` module.
- Improved `detect_sensors` implementation with better structure for sensor messages and autodetection.
- Updated tank ADC sampling to yield between readings, improving efficiency.
2026-04-26 21:01:27 +02:00
f1c85d1d74 Migrate serialization from Bincode to Postcard
- Replaced Bincode with Postcard for serialization/deserialization across configs and save operations.
- Simplified struct derives by removing `bincode`-specific traits.
- Updated `Cargo.toml` and `Cargo.lock` to include `postcard` and dependencies.
- Added padding stripping for deserialization and improved error handling.
- Adjusted serialization logic in `savegame_manager.rs` and related modules.
2026-04-26 20:46:52 +02:00
097aff5360 Switch savegame serialization format from Bincode to custom parsing
- Replaced Bincode-based serialization/deserialization with a custom save format for better control.
- Introduced save header with magic bytes, timestamp handling, and UTF-8 validation.
- Enhanced error handling for save parsing and increased format flexibility.
- Removed
2026-04-26 20:31:56 +02:00
fc0e18da56 Integrate mcutie library for MQTT functionality
- Added `mcutie` as a dependency in `Cargo.toml` and updated `Cargo.lock`.
- Replaced commented-out MQTT logic with fully implemented functionality in `esp.rs`.
- Enhanced MQTT publish and subscription handling with configurable topics and error handling.
- Updated MQTT connection logic to improve reliability and logging.
2026-04-26 19:56:16 +02:00
2e4eb283b5 Add AGENTS.md to document repository structure and development guidelines
- Introduced `AGENTS.md` to provide an overview of the repository layout and working conventions.
- Included guidance for firmware, hardware, and website contributions.
- Added validation, file hygiene, and handoff expectations for consistent development practices.
2026-04-26 19:47:07 +02:00
cc92c82ac9 Fix incorrect spawn function call and update dependencies
- Corrected usage of `spawner.spawn` by fixing misplaced error propagation.
- Updated `Cargo.lock` with new and upgraded dependencies, including `base64`, `darling`, and `smoltcp` upgrades.
2026-04-26 19:46:46 +02:00
b8f01f0de9 Remove unused dependencies and imports, cleanup Cargo.lock
- Removed `smoltcp`, `defmt`, and associated dependencies as they are no longer used.
- Updated `Cargo.toml` to exclude unused features from `esp-radio`.
- Cleaned up imports in `esp.rs` for better clarity and consistency.
2026-04-26 19:08:18 +02:00
15 changed files with 939 additions and 583 deletions

62
AGENTS.md Normal file
View 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.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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,

View File

@@ -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:?}"),
}

View File

@@ -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(), &timestamp)
self.savegame.save(config.as_slice(), &timestamp)?;
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;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(&timestamp_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)

View File

@@ -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(

View File

@@ -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();

View File

@@ -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()

View File

@@ -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,

View File

@@ -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))