Compare commits
7 Commits
6b419dba6c
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| c112d133db | |||
| 95281d617f | |||
| a2abc99275 | |||
| 4b3c003996 | |||
| bba959f2a2 | |||
| c9a96f37f0 | |||
| fbf97732a4 |
@@ -893,7 +893,7 @@
|
||||
(uuid "f5c71e09-1c64-4699-a872-7959060ff54c")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -2381,7 +2381,7 @@
|
||||
(uuid "2619df3e-b807-4d9d-86fd-1890ae6a2950")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -3089,7 +3089,7 @@
|
||||
(uuid "29bb50c2-8c3f-4ba0-bb86-3c75e758d317")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -3622,7 +3622,7 @@
|
||||
(uuid "ac7efe3b-be6c-4633-baa1-c7a44a15dc73")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Package_TO_SOT_SMD.3dshapes/SOT-23.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Package_TO_SOT_SMD.3dshapes/SOT-23.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -6371,7 +6371,7 @@
|
||||
(uuid "f844f701-66c7-484d-a7cf-cff3b6cdd615")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_1206_3216Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_1206_3216Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -8173,7 +8173,7 @@
|
||||
(uuid "09bb897b-8ad7-4832-82c0-09ce6f9fd4df")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -10055,7 +10055,7 @@
|
||||
(uuid "3e5e9acf-dab8-4180-8dd7-e2e038a511be")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
@@ -13870,7 +13870,7 @@
|
||||
(uuid "a9590be1-1db5-48eb-8721-a2ce5049db3f")
|
||||
)
|
||||
(embedded_fonts no)
|
||||
(model "${KICAD6_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.wrl"
|
||||
(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"
|
||||
(offset
|
||||
(xyz 0 0 0)
|
||||
)
|
||||
|
||||
@@ -50,7 +50,23 @@
|
||||
"silk_text_thickness": 0.1,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"min_clearance": 0.5
|
||||
"border_display_style": 2,
|
||||
"border_hatch_pitch": 0.5,
|
||||
"corner_radius": 0.0,
|
||||
"corner_smoothing": 0,
|
||||
"fill_mode": 0,
|
||||
"hatch_gap": 1.5,
|
||||
"hatch_orientation": 0.0,
|
||||
"hatch_smoothing_level": 0,
|
||||
"hatch_smoothing_value": 0.1,
|
||||
"hatch_thickness": 1.0,
|
||||
"min_clearance": 0.5,
|
||||
"min_island_area": 10.0,
|
||||
"min_thickness": 0.25,
|
||||
"pad_connection": 1,
|
||||
"remove_islands": 0,
|
||||
"thermal_relief_gap": 0.5,
|
||||
"thermal_relief_spoke_width": 0.5
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
|
||||
@@ -112,6 +112,14 @@ pub struct PlantControllerConfig {
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub enum SensorCombineMode {
|
||||
Min,
|
||||
Max,
|
||||
#[default]
|
||||
Avg,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct PlantConfig {
|
||||
@@ -133,6 +141,7 @@ pub struct PlantConfig {
|
||||
pub ignore_current_error: bool,
|
||||
pub fertilizer_s: u16,
|
||||
pub fertilizer_cooldown_min: u16,
|
||||
pub sensor_combine_mode: SensorCombineMode,
|
||||
}
|
||||
|
||||
impl Default for PlantConfig {
|
||||
@@ -156,6 +165,7 @@ impl Default for PlantConfig {
|
||||
ignore_current_error: true,
|
||||
fertilizer_s: 0,
|
||||
fertilizer_cooldown_min: 1440, // 1 day default
|
||||
sensor_combine_mode: SensorCombineMode::Avg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::hal::Box;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use alloc::string::String;
|
||||
use async_trait::async_trait;
|
||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||
@@ -81,7 +81,8 @@ impl BatteryInteraction for WCHI2CSlave<'_> {
|
||||
|
||||
let state_of_charge =
|
||||
state.remaining_capacity_mah as f32 * 100. / state.lifetime_capacity_mah as f32;
|
||||
let state_of_health = state.lifetime_capacity_mah as f32 / config.capacity_mah as f32 * 100.;
|
||||
let state_of_health =
|
||||
state.lifetime_capacity_mah as f32 / config.capacity_mah as f32 * 100.;
|
||||
|
||||
Ok(BatteryState::Info(BatteryInfo {
|
||||
voltage_mv: Some(state.current_mv),
|
||||
@@ -91,7 +92,7 @@ impl BatteryInteraction for WCHI2CSlave<'_> {
|
||||
soc_pct: Some(state_of_charge),
|
||||
soh_pct: Some(state_of_health),
|
||||
temperature_c: Some(state.temperature_celcius),
|
||||
error: None
|
||||
error: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::bail;
|
||||
use crate::config::{PlantControllerConfig};
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::hal::savegame_manager::SavegameManager;
|
||||
use crate::hal::PLANT_COUNT;
|
||||
use crate::log::{log, LogMessage};
|
||||
@@ -16,7 +16,6 @@ use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFla
|
||||
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||
use esp_hal::rng::Rng;
|
||||
use esp_hal::rtc_cntl::{
|
||||
@@ -26,10 +25,11 @@ use esp_hal::rtc_cntl::{
|
||||
use esp_hal::system::software_reset;
|
||||
use esp_hal::uart::Uart;
|
||||
use esp_hal::Blocking;
|
||||
use esp_radio::wifi::ap::{AccessPointInfo};
|
||||
use esp_radio::wifi::ap::AccessPointInfo;
|
||||
use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
|
||||
use esp_radio::wifi::{Interface, WifiController};
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Detailed Wi-Fi scan information including signal strength
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -45,11 +45,15 @@ pub struct WifiScanDetails {
|
||||
fn format_bssid(bssid: &[u8; 6]) -> String {
|
||||
alloc::format!(
|
||||
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]
|
||||
bssid[0],
|
||||
bssid[1],
|
||||
bssid[2],
|
||||
bssid[3],
|
||||
bssid[4],
|
||||
bssid[5]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||
@@ -222,7 +226,7 @@ impl Esp<'_> {
|
||||
ssid: ap.ssid.as_str().to_string(),
|
||||
bssid: format_bssid(&ap.bssid),
|
||||
rssi: ap.signal_strength as i32,
|
||||
channel: ap.channel as u8,
|
||||
channel: ap.channel,
|
||||
auth_method: format!("{:?}", ap.auth_method),
|
||||
})
|
||||
.collect();
|
||||
@@ -414,5 +418,4 @@ impl Esp<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use esp_hal::delay::Delay;
|
||||
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::pcnt::unit::Unit;
|
||||
use esp_hal::peripherals::GPIO5;
|
||||
use esp_hal::Async;
|
||||
use log::{error, info};
|
||||
@@ -145,7 +145,11 @@ impl<'a> TankSensor<'a> {
|
||||
water_temp_sensor = Some(device);
|
||||
break;
|
||||
} else {
|
||||
info!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE);
|
||||
info!(
|
||||
"OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})",
|
||||
device.address[0],
|
||||
ds18b20::FAMILY_CODE
|
||||
);
|
||||
}
|
||||
}
|
||||
if devices_found == 0 {
|
||||
|
||||
@@ -32,7 +32,8 @@ impl LiveLogBuffer {
|
||||
match after {
|
||||
None => (self.entries.clone(), false, next_seq),
|
||||
Some(after_seq) => {
|
||||
let result: Vec<_> = self.entries
|
||||
let result: Vec<_> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(seq, _)| *seq > after_seq)
|
||||
.cloned()
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
esp_bootloader_esp_idf::esp_app_desc!();
|
||||
use esp_backtrace as _;
|
||||
|
||||
use crate::hal::PROGRESS_ACTIVE;
|
||||
use crate::config::{PlantConfig, PlantControllerConfig};
|
||||
use crate::fat_error::{ContextExt, FatResult};
|
||||
use crate::hal::PROGRESS_ACTIVE;
|
||||
|
||||
use crate::log::log;
|
||||
use crate::tank::{determine_tank_state, TankError, WATER_FROZEN_THRESH};
|
||||
@@ -42,8 +42,8 @@ use embassy_sync::once_lock::OnceLock;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use esp_hal::rom::ets_delay_us;
|
||||
use esp_hal::system::software_reset;
|
||||
use esp_println::{println};
|
||||
use hal::battery::{BatteryState};
|
||||
use esp_println::println;
|
||||
use hal::battery::BatteryState;
|
||||
use log::LogMessage;
|
||||
use option_lock::OptionLock;
|
||||
use plant_state::PlantState;
|
||||
@@ -123,8 +123,6 @@ pub struct PumpResult {
|
||||
overcurrent_ma: Option<u16>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
info!("Startup Rust");
|
||||
|
||||
@@ -208,10 +206,15 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
info!("No wifi configured, starting initial config mode");
|
||||
|
||||
let esp = board.board_hal.get_esp();
|
||||
let ssid = esp.load_config().await
|
||||
let ssid = esp
|
||||
.load_config()
|
||||
.await
|
||||
.map(|config| config.network.ap_ssid.to_string())
|
||||
.unwrap_or_else(|_| String::from("PlantCtrl Emergency Mode"));
|
||||
let device = esp.interface_ap.take().context("AP interface already taken")?;
|
||||
let device = esp
|
||||
.interface_ap
|
||||
.take()
|
||||
.context("AP interface already taken")?;
|
||||
let stack = network::wifi_ap(ssid, device, &esp.controller, &mut esp.rng, spawner).await?;
|
||||
|
||||
let reboot_now = Arc::new(AtomicBool::new(false));
|
||||
@@ -236,14 +239,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
|
||||
let res = {
|
||||
let esp = board.board_hal.get_esp();
|
||||
let ssid = esp.load_config().await
|
||||
let ssid = esp
|
||||
.load_config()
|
||||
.await
|
||||
.map(|config| config.network.ap_ssid.to_string())
|
||||
.unwrap_or_else(|_| String::from("PlantCtrl Emergency Mode"));
|
||||
let device = match esp.interface_ap.take() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
use crate::fat_error::FatError;
|
||||
return Err(FatError::String { error: "AP interface already taken".to_string() });
|
||||
return Err(FatError::String {
|
||||
error: "AP interface already taken".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
network::wifi_ap(ssid, device, &esp.controller, &mut esp.rng, spawner).await
|
||||
@@ -276,7 +283,9 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
|
||||
if let network::NetworkMode::WIFI { ref ip_address, .. } = network_mode {
|
||||
mqtt::publish_firmware_info(version, ip_address, &timezone_time.to_rfc3339()).await;
|
||||
mqtt::publish_battery_state(&mut board).await.unwrap_or_else(|e| {
|
||||
mqtt::publish_battery_state(&mut board)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Error publishing battery state {e}");
|
||||
});
|
||||
let _ = mqtt::publish_mppt_state(&mut board).await;
|
||||
@@ -325,7 +334,9 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) {
|
||||
match err {
|
||||
TankError::SensorDisabled => { /* unreachable */ }
|
||||
TankError::SensorMissing { raw_mv: raw_value_mv } => log(
|
||||
TankError::SensorMissing {
|
||||
raw_mv: raw_value_mv,
|
||||
} => log(
|
||||
LogMessage::TankSensorMissing,
|
||||
raw_value_mv as u32,
|
||||
0,
|
||||
@@ -355,7 +366,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
}
|
||||
|
||||
let mut water_frozen = false;
|
||||
let water_temp: FatResult<f32> = board.board_hal.get_tank_sensor().water_temperature_c().await;
|
||||
let water_temp: FatResult<f32> = board
|
||||
.board_hal
|
||||
.get_tank_sensor()
|
||||
.water_temperature_c()
|
||||
.await;
|
||||
|
||||
if let Ok(res) = water_temp {
|
||||
if res < WATER_FROZEN_THRESH {
|
||||
@@ -581,11 +596,15 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
light_state.battery_low = false;
|
||||
}
|
||||
BatteryState::Info(data) => {
|
||||
if data.soc_pct.is_some_and(|soc| soc < board.board_hal.get_config().night_lamp.low_soc_cutoff as f32) {
|
||||
if data.soc_pct.is_some_and(|soc| {
|
||||
soc < board.board_hal.get_config().night_lamp.low_soc_cutoff as f32
|
||||
}) {
|
||||
board.board_hal.get_esp().set_low_voltage_in_cycle();
|
||||
info!("Set low voltage in cycle");
|
||||
}
|
||||
if data.soc_pct.is_some_and(|soc| soc > board.board_hal.get_config().night_lamp.low_soc_restore as f32) {
|
||||
if data.soc_pct.is_some_and(|soc| {
|
||||
soc > board.board_hal.get_config().night_lamp.low_soc_restore as f32
|
||||
}) {
|
||||
board.board_hal.get_esp().clear_low_voltage_in_cycle();
|
||||
info!("Clear low voltage in cycle");
|
||||
}
|
||||
@@ -988,8 +1007,7 @@ async fn wait_infinity(
|
||||
let timezone_time = cur.with_timezone(&timezone);
|
||||
|
||||
mqtt::publish("/state", "config").await;
|
||||
mqtt::publish("/firmware/last_online", &timezone_time.to_rfc3339())
|
||||
.await;
|
||||
mqtt::publish("/firmware/last_online", &timezone_time.to_rfc3339()).await;
|
||||
last_mqtt_update = Some(now);
|
||||
}
|
||||
|
||||
|
||||
@@ -288,11 +288,7 @@ pub async fn publish_plant_states(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_firmware_info(
|
||||
version: VersionInfo,
|
||||
ip_address: &str,
|
||||
timezone_time: &str,
|
||||
) {
|
||||
pub async fn publish_firmware_info(version: VersionInfo, ip_address: &str, timezone_time: &str) {
|
||||
publish("/firmware/address", ip_address).await;
|
||||
let version = &serde_json::to_string(&version);
|
||||
match version {
|
||||
@@ -384,12 +380,7 @@ pub async fn publish_mppt_state(
|
||||
pub async fn publish_battery_state(
|
||||
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
|
||||
) -> FatResult<()> {
|
||||
let telemetry = match board
|
||||
.board_hal
|
||||
.get_battery_monitor()
|
||||
.get_state()
|
||||
.await
|
||||
{
|
||||
let telemetry = match board.board_hal.get_battery_monitor().get_state().await {
|
||||
Ok(BatteryState::Info(info)) => info,
|
||||
Ok(BatteryState::Unknown) => BatteryInfo {
|
||||
voltage_mv: None,
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use crate::bail;
|
||||
use crate::config::NetworkConfig;
|
||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||
use crate::hal::{HAL};
|
||||
use crate::hal::HAL;
|
||||
use crate::mqtt;
|
||||
use crate::util::mk_static;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use edge_dhcp::{
|
||||
io::{self, DEFAULT_SERVER_PORT},
|
||||
server::{Server, ServerOptions},
|
||||
};
|
||||
use edge_nal::UdpBind;
|
||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::dns::DnsQueryType;
|
||||
use embassy_net::udp::{PacketMetadata, UdpSocket};
|
||||
@@ -15,21 +21,15 @@ use embassy_net::{DhcpConfig, Runner, Stack, StackResources, StaticConfigV4};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||
use embassy_time::{Duration, Timer, WithTimeout};
|
||||
use option_lock::OptionLock;
|
||||
use edge_dhcp::{
|
||||
io::{self, DEFAULT_SERVER_PORT},
|
||||
server::{Server, ServerOptions},
|
||||
};
|
||||
use edge_nal::UdpBind;
|
||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
||||
use esp_hal::rng::Rng;
|
||||
use esp_println::println;
|
||||
use esp_radio::wifi::ap::AccessPointConfig;
|
||||
use esp_radio::wifi::sta::StationConfig;
|
||||
use esp_radio::wifi::{AuthenticationMethod, Config, Interface};
|
||||
use log::{info, warn, error};
|
||||
use log::{error, info, warn};
|
||||
use option_lock::OptionLock;
|
||||
use serde::Serialize;
|
||||
use sntpc::{NtpContext, NtpTimestampGenerator, NtpUdpSocket, get_time};
|
||||
use sntpc::{get_time, NtpContext, NtpTimestampGenerator, NtpUdpSocket};
|
||||
|
||||
const NTP_SERVER: &str = "pool.ntp.org";
|
||||
|
||||
@@ -225,8 +225,7 @@ pub async fn wifi_ap(
|
||||
);
|
||||
let stack = mk_static!(Stack, stack);
|
||||
|
||||
let client_config =
|
||||
Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
let client_config = Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
|
||||
controller.lock().await.set_config(&client_config)?;
|
||||
|
||||
println!("start net task");
|
||||
@@ -352,7 +351,10 @@ pub async fn wifi(
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
warn!("WiFi connection attempt {} failed: link up timeout", attempts + 1);
|
||||
warn!(
|
||||
"WiFi connection attempt {} failed: link up timeout",
|
||||
attempts + 1
|
||||
);
|
||||
attempts += 1;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
@@ -368,7 +370,10 @@ pub async fn wifi(
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
warn!("WiFi connection attempt {} failed: config up timeout", attempts + 1);
|
||||
warn!(
|
||||
"WiFi connection attempt {} failed: config up timeout",
|
||||
attempts + 1
|
||||
);
|
||||
attempts += 1;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::config::SensorCombineMode;
|
||||
use crate::hal::Moistures;
|
||||
use crate::plant_state::PlantWateringMode::TargetMoisture;
|
||||
use crate::{config::PlantConfig, hal::HAL, in_time_range};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Embedded environments may not have floating-point math functions.
|
||||
// For no_std with k=0.5 (square root), we use Newton's method approximation.
|
||||
// Formula: sqrt(t) ≈ iterative refinement for better wet-range discrimination.
|
||||
|
||||
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 160000.; // 160kHz -> very wet
|
||||
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 400.; // this is really, really dry, think like cactus levels
|
||||
|
||||
@@ -17,7 +15,7 @@ pub enum MoistureSensorError {
|
||||
MissingMessage,
|
||||
NotExpectedMessage { hz: f32 },
|
||||
ShortCircuit { hz: f32, max: f32 },
|
||||
OpenLoop { hz: f32, min: f32 }
|
||||
OpenLoop { hz: f32, min: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
@@ -113,8 +111,8 @@ pub struct PlantState {
|
||||
pub sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// Last known firmware build timestamp for sensor B.
|
||||
pub sensor_b_firmware_build_minutes: Option<u32>,
|
||||
/// Last time fertilizer was applied (Unix timestamp in seconds).
|
||||
pub last_fertilizer_time: i64,
|
||||
/// Last time fertilizer was applied.
|
||||
pub last_fertilizer_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Map sensor frequency to moisture percentage using inverse power-law scaling (quadratic).
|
||||
@@ -228,8 +226,12 @@ impl PlantState {
|
||||
|
||||
let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id);
|
||||
let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id);
|
||||
let last_fertilizer_time = board.board_hal.get_esp().last_fertilizer_time(plant_id);
|
||||
let last_fertilizer_timestamp = board.board_hal.get_esp().last_fertilizer_time(plant_id);
|
||||
let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes();
|
||||
|
||||
let last_fertilizer_time = DateTime::from_timestamp_millis(last_fertilizer_timestamp);
|
||||
|
||||
// Create plant state first, then check for warnings
|
||||
let state = Self {
|
||||
sensor_a,
|
||||
sensor_b,
|
||||
@@ -242,7 +244,20 @@ impl PlantState {
|
||||
sensor_b_firmware_build_minutes: b_builds[plant_id],
|
||||
last_fertilizer_time,
|
||||
};
|
||||
if state.is_err() {
|
||||
|
||||
// Check for sensor warning condition (expected 2 sensors, only 1 responding)
|
||||
let has_a =
|
||||
state.sensor_a.moisture_percent().is_some() && state.sensor_a.is_err().is_none();
|
||||
let has_b =
|
||||
state.sensor_b.moisture_percent().is_some() && state.sensor_b.is_err().is_none();
|
||||
|
||||
// Check if we expected two sensors but only got one
|
||||
let has_sensor_warning =
|
||||
expected_a && expected_b && ((has_a && !has_b) || (!has_a && has_b));
|
||||
|
||||
// Set fault LED for both errors AND sensor warnings
|
||||
let has_issue = state.is_err() || has_sensor_warning;
|
||||
if has_issue {
|
||||
let _ = board.board_hal.fault(plant_id, true).await;
|
||||
}
|
||||
state
|
||||
@@ -265,26 +280,25 @@ impl PlantState {
|
||||
self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some()
|
||||
}
|
||||
|
||||
pub fn plant_moisture(
|
||||
&self,
|
||||
) -> (
|
||||
Option<f32>,
|
||||
(Option<&MoistureSensorError>, Option<&MoistureSensorError>),
|
||||
) {
|
||||
/// Get combined moisture value with configurable combination mode and sensor warning.
|
||||
///
|
||||
/// Returns:
|
||||
/// - Combined moisture percentage (or None if no valid readings)
|
||||
/// - Tuple of errors from sensor A and B
|
||||
/// - Sensor warning indicating if warning LED should be lit (MissingSecondSensor)
|
||||
pub fn plant_moisture_with_warning(&self, plant_conf: &PlantConfig) -> Option<f32> {
|
||||
match (
|
||||
self.sensor_a.moisture_percent(),
|
||||
self.sensor_b.moisture_percent(),
|
||||
) {
|
||||
(Some(moisture_a), Some(moisture_b)) => {
|
||||
(Some((moisture_a + moisture_b) / 2.), (None, None))
|
||||
}
|
||||
(Some(moisture_percent), _) => {
|
||||
(Some(moisture_percent), (None, self.sensor_b.is_err()))
|
||||
}
|
||||
(_, Some(moisture_percent)) => {
|
||||
(Some(moisture_percent), (self.sensor_a.is_err(), None))
|
||||
}
|
||||
_ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
|
||||
(Some(moisture_a), Some(moisture_b)) => match plant_conf.sensor_combine_mode {
|
||||
SensorCombineMode::Min => Some(moisture_a.min(moisture_b)),
|
||||
SensorCombineMode::Max => Some(moisture_a.max(moisture_b)),
|
||||
SensorCombineMode::Avg => Some((moisture_a + moisture_b) / 2.0),
|
||||
},
|
||||
(Some(moisture), _) => Some(moisture),
|
||||
(_, Some(moisture)) => Some(moisture),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +310,7 @@ impl PlantState {
|
||||
match plant_conf.mode {
|
||||
PlantWateringMode::Off => false,
|
||||
PlantWateringMode::TargetMoisture => {
|
||||
let (moisture_percent, _) = self.plant_moisture();
|
||||
let moisture_percent = self.plant_moisture_with_warning(plant_conf);
|
||||
if let Some(moisture_percent) = moisture_percent {
|
||||
if self.pump_in_timeout(plant_conf, current_time) {
|
||||
false
|
||||
@@ -322,17 +336,18 @@ impl PlantState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mqtt_info(
|
||||
&self,
|
||||
plant_conf: &PlantConfig,
|
||||
current_time: &DateTime<Tz>,
|
||||
) -> PlantInfo {
|
||||
let (moisture_pct, _) = self.plant_moisture();
|
||||
pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo {
|
||||
let moisture_pct = self.plant_moisture_with_warning(plant_conf);
|
||||
PlantInfo {
|
||||
moisture_pct,
|
||||
sensor_a: Self::sensor_to_telemetry(&self.sensor_a),
|
||||
sensor_b: Self::sensor_to_telemetry(&self.sensor_b),
|
||||
mode: plant_conf.mode,
|
||||
target_pct: if plant_conf.mode == TargetMoisture {
|
||||
Some(plant_conf.target_moisture as f32)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
do_water: self.needs_to_be_watered(plant_conf, current_time),
|
||||
dry: if let Some(moisture_percent) = moisture_pct {
|
||||
moisture_percent < plant_conf.target_moisture.into()
|
||||
@@ -365,21 +380,39 @@ impl PlantState {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
last_fertilizer: self
|
||||
.last_fertilizer_time
|
||||
.map(|t| t.with_timezone(¤t_time.timezone())),
|
||||
next_fertilizer: if matches!(
|
||||
plant_conf.mode,
|
||||
PlantWateringMode::TimerOnly
|
||||
| PlantWateringMode::TargetMoisture
|
||||
| PlantWateringMode::MinMoisture
|
||||
) {
|
||||
self.last_fertilizer_time.and_then(|last_fert| {
|
||||
// Convert to Tz for calculation, then back
|
||||
let tz_last_fert = last_fert.with_timezone(¤t_time.timezone());
|
||||
tz_last_fert
|
||||
.checked_add_signed(TimeDelta::minutes(
|
||||
plant_conf.fertilizer_cooldown_min.into(),
|
||||
))
|
||||
.map(|t| t.with_timezone(¤t_time.timezone()))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes,
|
||||
sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes,
|
||||
last_fertilizer_time: self.last_fertilizer_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn sensor_to_telemetry(sensor: &MoistureSensorState) -> SensorTelemetry {
|
||||
match sensor {
|
||||
MoistureSensorState::NoMessage => {
|
||||
SensorTelemetry {
|
||||
MoistureSensorState::NoMessage => SensorTelemetry {
|
||||
moisture_pct: None,
|
||||
raw_hz: None,
|
||||
error: None
|
||||
}
|
||||
}
|
||||
error: None,
|
||||
},
|
||||
MoistureSensorState::MoistureValue {
|
||||
hz,
|
||||
moisture_percent,
|
||||
@@ -402,6 +435,8 @@ impl PlantState {
|
||||
pub struct PlantInfo {
|
||||
/// combined plant moisture from available sensors
|
||||
moisture_pct: Option<f32>,
|
||||
/// moisture target, if in targetmode
|
||||
target_pct: Option<f32>,
|
||||
/// state of humidity sensor on bank a
|
||||
sensor_a: SensorTelemetry,
|
||||
/// state of humidity sensor on bank b
|
||||
@@ -423,10 +458,12 @@ pub struct PlantInfo {
|
||||
last_pump: Option<DateTime<Tz>>,
|
||||
/// next time when pump should activate
|
||||
next_pump: Option<DateTime<Tz>>,
|
||||
/// last time when fertilizer was applied
|
||||
last_fertilizer: Option<DateTime<Tz>>,
|
||||
/// next time when fertilizer should be applied
|
||||
next_fertilizer: Option<DateTime<Tz>>,
|
||||
/// firmware build timestamp of sensor A (minutes since Unix epoch); None if unknown
|
||||
sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown
|
||||
sensor_b_firmware_build_minutes: Option<u32>,
|
||||
/// last time when fertilizer was applied
|
||||
last_fertilizer_time: i64,
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ pub enum TankState {
|
||||
|
||||
fn raw_voltage_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
|
||||
if raw_value_mv > OPEN_TANK_VOLTAGE {
|
||||
return Err(TankError::SensorMissing { raw_mv: raw_value_mv });
|
||||
return Err(TankError::SensorMissing {
|
||||
raw_mv: raw_value_mv,
|
||||
});
|
||||
}
|
||||
|
||||
let r2 = raw_value_mv * 50.0 / (3.3 - raw_value_mv);
|
||||
@@ -161,12 +163,14 @@ pub async fn determine_tank_state(
|
||||
if board.board_hal.get_config().tank.tank_sensor_enabled {
|
||||
match board
|
||||
.board_hal
|
||||
.get_tank_sensor().tank_sensor_voltage().await
|
||||
.get_tank_sensor()
|
||||
.tank_sensor_voltage()
|
||||
.await
|
||||
{
|
||||
Ok(raw_sensor_value_mv) => {
|
||||
TankState::Present(raw_sensor_value_mv)
|
||||
},
|
||||
Err(err) => TankState::Error(TankError::BoardError { message: err.to_string() }),
|
||||
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv),
|
||||
Err(err) => TankState::Error(TankError::BoardError {
|
||||
message: err.to_string(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
TankState::Disabled
|
||||
|
||||
@@ -93,7 +93,11 @@ where
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let tank_state = determine_tank_state(&mut board).await;
|
||||
let water_temp: FatResult<f32> = board.board_hal.get_tank_sensor().water_temperature_c().await;
|
||||
let water_temp: FatResult<f32> = board
|
||||
.board_hal
|
||||
.get_tank_sensor()
|
||||
.water_temperature_c()
|
||||
.await;
|
||||
Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info(
|
||||
&board.board_hal.get_config().tank,
|
||||
&water_temp,
|
||||
|
||||
@@ -10,9 +10,9 @@ mod post_json;
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
|
||||
use crate::webserver::get_json::{
|
||||
delete_save, get_battery_state, get_config, get_live_moisture, get_log_localization_config,
|
||||
get_firmware_info_web, get_solar_state, get_time, get_timezones, list_saves, tank_info,
|
||||
get_wifi_details,
|
||||
delete_save, get_battery_state, get_config, get_firmware_info_web, get_live_moisture,
|
||||
get_log_localization_config, get_solar_state, get_time, get_timezones, get_wifi_details,
|
||||
list_saves, tank_info,
|
||||
};
|
||||
use crate::webserver::get_log::{get_live_log, get_log};
|
||||
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::hal::savegame_manager::SAVEGAME_SLOT_SIZE;
|
||||
use crate::hal::DetectionRequest;
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::{do_secure_pump, BOARD_ACCESS};
|
||||
@@ -135,7 +136,8 @@ pub(crate) async fn set_config<T, const N: usize>(
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let all = read_up_to_bytes_from_request(request, Some(4096)).await?;
|
||||
//accept nearly full slotsize leave some space for header
|
||||
let all = read_up_to_bytes_from_request(request, Some(SAVEGAME_SLOT_SIZE - 512)).await?;
|
||||
let length = all.len();
|
||||
let config: PlantControllerConfig = serde_json::from_slice(&all)?;
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export interface PlantConfig {
|
||||
min_pump_current_ma: number,
|
||||
max_pump_current_ma: number,
|
||||
ignore_current_error: boolean,
|
||||
sensor_combine_mode: string,
|
||||
}
|
||||
|
||||
export interface PumpTestResult {
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
.plantSensorEnabledOnly_ ${plantId} {
|
||||
}
|
||||
|
||||
.plantBothSensorsOnly_ ${plantId} {
|
||||
}
|
||||
|
||||
.plantHidden_ ${plantId} {
|
||||
display: none;
|
||||
}
|
||||
@@ -48,6 +51,14 @@
|
||||
<div class="plantkey">Sensor B installed:</div>
|
||||
<input class="plantcheckbox" id="plant_${plantId}_sensor_b" type="checkbox">
|
||||
</div>
|
||||
<div class="flexcontainer plantBothSensorsOnly_${plantId}">
|
||||
<div class="plantkey">Sensor Combine Mode:</div>
|
||||
<select class="plantvalue" id="plant_${plantId}_sensor_combine_mode">
|
||||
<option value="Min">Min</option>
|
||||
<option value="Max">Max</option>
|
||||
<option value="Avg">Average</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<div class="plantkey">
|
||||
Mode:
|
||||
|
||||
@@ -93,6 +93,7 @@ export class PlantView {
|
||||
private readonly pumpHourEnd: HTMLSelectElement;
|
||||
private readonly sensorAInstalled: HTMLInputElement;
|
||||
private readonly sensorBInstalled: HTMLInputElement;
|
||||
private readonly sensorCombineMode: HTMLSelectElement;
|
||||
private readonly mode: HTMLSelectElement;
|
||||
private readonly moistureA: HTMLElement;
|
||||
private readonly moistureB: HTMLElement;
|
||||
@@ -236,6 +237,14 @@ export class PlantView {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
this.sensorCombineMode = document.getElementById("plant_" + plantId + "_sensor_combine_mode") as HTMLSelectElement;
|
||||
this.sensorCombineMode.onchange = function () {
|
||||
controller.configChanged()
|
||||
}
|
||||
|
||||
// Initial visibility update for sensor combine mode
|
||||
this.updateSensorCombineModeState();
|
||||
|
||||
this.minPumpCurrentMa = document.getElementById("plant_" + plantId + "_min_pump_current_ma") as HTMLInputElement;
|
||||
this.minPumpCurrentMa.onchange = function () {
|
||||
controller.configChanged()
|
||||
@@ -271,6 +280,19 @@ export class PlantView {
|
||||
};
|
||||
}
|
||||
|
||||
updateSensorCombineModeState() {
|
||||
const bothActive = this.sensorAInstalled.checked && this.sensorBInstalled.checked;
|
||||
const bothOnlyElements = document.getElementsByClassName("plantBothSensorsOnly_" + this.plantId);
|
||||
for (const element of Array.from(bothOnlyElements)) {
|
||||
if (bothActive) {
|
||||
element.classList.remove("plantHidden_" + this.plantId);
|
||||
} else {
|
||||
element.classList.add("plantHidden_" + this.plantId);
|
||||
}
|
||||
}
|
||||
this.sensorCombineMode.disabled = !bothActive;
|
||||
}
|
||||
|
||||
updateVisibility(plantConfig: PlantConfig) {
|
||||
let sensorOnly = document.getElementsByClassName("plantSensorEnabledOnly_" + this.plantId)
|
||||
let pumpOnly = document.getElementsByClassName("plantPumpEnabledOnly_" + this.plantId)
|
||||
@@ -324,6 +346,9 @@ export class PlantView {
|
||||
// element.classList.add("plantHidden_" + this.plantId)
|
||||
// }
|
||||
// }
|
||||
|
||||
// Update sensor combine mode visibility based on whether both sensors are active
|
||||
this.updateSensorCombineModeState();
|
||||
}
|
||||
|
||||
setTestResult(result: PumpTestResult) {
|
||||
@@ -354,6 +379,7 @@ export class PlantView {
|
||||
this.pumpHourEnd.value = plantConfig.pump_hour_end.toString();
|
||||
this.sensorBInstalled.checked = plantConfig.sensor_b;
|
||||
this.sensorAInstalled.checked = plantConfig.sensor_a;
|
||||
this.sensorCombineMode.value = plantConfig.sensor_combine_mode || "Min";
|
||||
this.maxConsecutivePumpCount.value = plantConfig.max_consecutive_pump_count.toString();
|
||||
this.minPumpCurrentMa.value = plantConfig.min_pump_current_ma.toString();
|
||||
this.maxPumpCurrentMa.value = plantConfig.max_pump_current_ma.toString();
|
||||
@@ -383,6 +409,7 @@ export class PlantView {
|
||||
pump_hour_end: +this.pumpHourEnd.value,
|
||||
sensor_b: this.sensorBInstalled.checked,
|
||||
sensor_a: this.sensorAInstalled.checked,
|
||||
sensor_combine_mode: this.sensorCombineMode.value,
|
||||
max_consecutive_pump_count: this.maxConsecutivePumpCount.valueAsNumber,
|
||||
moisture_sensor_min_frequency: this.moistureSensorMinFrequency.valueAsNumber || null,
|
||||
moisture_sensor_max_frequency: this.moistureSensorMaxFrequency.valueAsNumber || null,
|
||||
|
||||
Reference in New Issue
Block a user