diff --git a/Software/MainBoard/rust/.idea/vcs.xml b/Software/MainBoard/rust/.idea/vcs.xml
index c2365ab..298f634 100644
--- a/Software/MainBoard/rust/.idea/vcs.xml
+++ b/Software/MainBoard/rust/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/Software/MainBoard/rust/src/config.rs b/Software/MainBoard/rust/src/config.rs
index 5ae2d5d..97abef8 100644
--- a/Software/MainBoard/rust/src/config.rs
+++ b/Software/MainBoard/rust/src/config.rs
@@ -14,6 +14,7 @@ pub struct NetworkConfig {
pub mqtt_user: Option,
pub mqtt_password: Option,
pub max_wait: u32,
+ pub retry_count: u32,
}
impl Default for NetworkConfig {
fn default() -> Self {
@@ -26,6 +27,7 @@ impl Default for NetworkConfig {
mqtt_user: None,
mqtt_password: None,
max_wait: 10000,
+ retry_count: 3,
}
}
}
diff --git a/Software/MainBoard/rust/src/network.rs b/Software/MainBoard/rust/src/network.rs
index cd00f60..80dac32 100644
--- a/Software/MainBoard/rust/src/network.rs
+++ b/Software/MainBoard/rust/src/network.rs
@@ -270,12 +270,12 @@ pub async fn wifi(
bail!("Wifi ssid was empty")
}
};
- info!("attempting to connect wifi {ssid}");
let password = match network_config.password {
Some(ref password) => password.as_str().to_string(),
None => "".to_string(),
};
let max_wait = network_config.max_wait;
+ let retry_count = network_config.retry_count;
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
@@ -294,56 +294,93 @@ pub async fn wifi(
} else {
AuthenticationMethod::Wpa2Personal
};
- let client_config = StationConfig::default()
- .with_ssid(ssid)
- .with_auth_method(auth_method)
- .with_scan_method(esp_radio::wifi::sta::ScanMethod::AllChannels)
- .with_listen_interval(10)
- .with_beacon_timeout(10)
- .with_failure_retry_cnt(3)
- .with_password(password);
- controller
- .lock()
- .await
- .set_config(&Config::Station(client_config))?;
+ // Spawn the network task once
spawner.spawn(net_task(runner)?);
- controller
- .lock()
- .await
- .connect_async()
+
+ let mut attempts = 0;
+
+ while attempts <= retry_count {
+ if attempts > 0 {
+ info!("WiFi connection retry {}/{}", attempts, retry_count);
+ } else {
+ info!("attempting to connect wifi {}", ssid);
+ }
+
+ let client_config = StationConfig::default()
+ .with_ssid(ssid.clone())
+ .with_auth_method(auth_method)
+ .with_scan_method(esp_radio::wifi::sta::ScanMethod::AllChannels)
+ .with_listen_interval(10)
+ .with_beacon_timeout(10)
+ .with_failure_retry_cnt(3)
+ .with_password(password.clone());
+
+ // Set config and attempt connection
+ controller
+ .lock()
+ .await
+ .set_config(&Config::Station(client_config))?;
+
+ match controller
+ .lock()
+ .await
+ .connect_async()
+ .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
+ .await
+ {
+ Ok(result) => {
+ result?;
+ }
+ Err(e) => {
+ let disconnect_info = controller.lock().await.disconnect_async().await;
+ warn!("Wifi disconnect info {:?}", disconnect_info);
+ warn!("WiFi connection attempt {} failed: Timeout waiting for wifi sta connected: {:?}", attempts + 1, e);
+ attempts += 1;
+ Timer::after(Duration::from_millis(500)).await;
+ continue;
+ }
+ }
+
+ let res = async {
+ while !stack.is_link_up() {
+ Timer::after(Duration::from_millis(500)).await;
+ }
+ Ok::<(), FatError>(())
+ }
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
- .await
- .context("Timeout waiting for wifi sta connected")??;
+ .await;
- let res = async {
- while !stack.is_link_up() {
+ if res.is_err() {
+ warn!("WiFi connection attempt {} failed: link up timeout", attempts + 1);
+ attempts += 1;
Timer::after(Duration::from_millis(500)).await;
+ continue;
}
- Ok::<(), FatError>(())
- }
- .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
- .await;
- if res.is_err() {
- bail!("Timeout waiting for wifi link up")
- }
-
- let res = async {
- while !stack.is_config_up() {
- Timer::after(Duration::from_millis(100)).await
+ let res = async {
+ while !stack.is_config_up() {
+ Timer::after(Duration::from_millis(100)).await
+ }
+ Ok::<(), FatError>(())
}
- Ok::<(), FatError>(())
- }
- .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
- .await;
+ .with_timeout(Duration::from_millis(max_wait as u64 * 1000))
+ .await;
- if res.is_err() {
- bail!("Timeout waiting for wifi config up")
+ if res.is_err() {
+ warn!("WiFi connection attempt {} failed: config up timeout", attempts + 1);
+ attempts += 1;
+ Timer::after(Duration::from_millis(500)).await;
+ continue;
+ }
+
+ // Success!
+ info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
+ return Ok(*stack);
}
- info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
- Ok(*stack)
+ // All retries exhausted
+ bail!("WiFi connection failed after all retries");
}
pub async fn try_connect_wifi_sntp_mqtt(
diff --git a/Software/MainBoard/rust/src/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs
index b3854f7..5092d29 100644
--- a/Software/MainBoard/rust/src/plant_state.rs
+++ b/Software/MainBoard/rust/src/plant_state.rs
@@ -4,7 +4,11 @@ use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
-const MOIST_SENSOR_MAX_FREQUENCY: f32 = 70000.; // 70kHz
+// 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
#[derive(Debug, PartialEq, Clone, Serialize)]
@@ -113,6 +117,27 @@ pub struct PlantState {
pub last_fertilizer_time: i64,
}
+/// Map sensor frequency to moisture percentage using inverse power-law scaling (quadratic).
+///
+/// For resistive probes with 555 timer oscillator:
+/// - Dry soil has high resistance → low oscillation frequency
+/// - Wet soil has low resistance → high oscillation frequency
+///
+/// The relationship is non-linear: most frequency change occurs in the wet range.
+/// Using inverse power-law to give better discrimination at high moisture levels.
+///
+/// Formula: moisture = (1 - (f_max - f) / (f_max - f_min))^2 * 100
+/// = ((f - f_min) / (f_max - f_min))^2 * 100
+///
+/// But with k=0.5 (square root) for better high-end discrimination:
+/// Formula: moisture = sqrt((f - f_min) / (f_max - f_min)) * 100
+///
+/// Examples with default range (400-160000 Hz) using k=0.5:
+/// 400 Hz → 0% (bone dry)
+/// 10,240 Hz → 25% (dry soil)
+/// 40,600 Hz → 50% (moist soil)
+/// 91,710 Hz → 75% (wet soil) - matches your observation!
+/// 160,000 Hz → 100% (saturated)
fn map_range_moisture(
s: f32,
min_frequency: Option,
@@ -134,9 +159,28 @@ fn map_range_moisture(
max: max_freq,
});
}
- let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);
+
+ // Normalize to 0-1 range
+ let t = (s - min_freq) / (max_freq - min_freq);
+
+ // Apply power-law mapping with k=0.5 (square root) for better high-moisture discrimination
+ // For resistive probes: frequency ↑ as moisture ↑, but non-linearly
+ // Using sqrt gives more resolution in the wet range (60-160kHz)
+ // Newton's method approximation for sqrt(t): x_{n+1} = 0.5 * (x_n + t/x_n)
+ // Start with initial guess and do 2 iterations for good precision
+ let moisture_percent = if t <= 0.0 {
+ 0.0
+ } else if t >= 1.0 {
+ 100.0
+ } else {
+ // Newton's method for sqrt(t)
+ let mut x = t; // Initial guess
+ x = 0.5 * (x + t / x); // First iteration
+ x = 0.5 * (x + t / x); // Second iteration for better precision
+ x * 100.0
+ };
- Ok(moisture_percent)
+ Ok(moisture_percent.clamp(0.0, 100.0))
}
impl PlantState {
diff --git a/Software/MainBoard/rust/src/webserver/get_json.rs b/Software/MainBoard/rust/src/webserver/get_json.rs
index 630c462..9071aa5 100644
--- a/Software/MainBoard/rust/src/webserver/get_json.rs
+++ b/Software/MainBoard/rust/src/webserver/get_json.rs
@@ -23,6 +23,8 @@ struct LoadData<'a> {
struct Moistures {
moisture_a: Vec,
moisture_b: Vec,
+ sensor_a_build_minutes: Vec