retry wifi connection, show canbus FW version, adjust measurement formular
This commit is contained in:
1
Software/MainBoard/rust/.idea/vcs.xml
generated
1
Software/MainBoard/rust/.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/../../../website/themes/blowfish" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -14,6 +14,7 @@ pub struct NetworkConfig {
|
|||||||
pub mqtt_user: Option<String>,
|
pub mqtt_user: Option<String>,
|
||||||
pub mqtt_password: Option<String>,
|
pub mqtt_password: Option<String>,
|
||||||
pub max_wait: u32,
|
pub max_wait: u32,
|
||||||
|
pub retry_count: u32,
|
||||||
}
|
}
|
||||||
impl Default for NetworkConfig {
|
impl Default for NetworkConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -26,6 +27,7 @@ impl Default for NetworkConfig {
|
|||||||
mqtt_user: None,
|
mqtt_user: None,
|
||||||
mqtt_password: None,
|
mqtt_password: None,
|
||||||
max_wait: 10000,
|
max_wait: 10000,
|
||||||
|
retry_count: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,12 +270,12 @@ pub async fn wifi(
|
|||||||
bail!("Wifi ssid was empty")
|
bail!("Wifi ssid was empty")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!("attempting to connect wifi {ssid}");
|
|
||||||
let password = match network_config.password {
|
let password = match network_config.password {
|
||||||
Some(ref password) => password.as_str().to_string(),
|
Some(ref password) => password.as_str().to_string(),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
};
|
};
|
||||||
let max_wait = network_config.max_wait;
|
let max_wait = network_config.max_wait;
|
||||||
|
let retry_count = network_config.retry_count;
|
||||||
|
|
||||||
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
|
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
|
||||||
|
|
||||||
@@ -294,56 +294,93 @@ pub async fn wifi(
|
|||||||
} else {
|
} else {
|
||||||
AuthenticationMethod::Wpa2Personal
|
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
|
// Spawn the network task once
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.set_config(&Config::Station(client_config))?;
|
|
||||||
spawner.spawn(net_task(runner)?);
|
spawner.spawn(net_task(runner)?);
|
||||||
controller
|
|
||||||
.lock()
|
let mut attempts = 0;
|
||||||
.await
|
|
||||||
.connect_async()
|
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))
|
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
|
||||||
.await
|
.await;
|
||||||
.context("Timeout waiting for wifi sta connected")??;
|
|
||||||
|
|
||||||
let res = async {
|
if res.is_err() {
|
||||||
while !stack.is_link_up() {
|
warn!("WiFi connection attempt {} failed: link up timeout", attempts + 1);
|
||||||
|
attempts += 1;
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
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() {
|
let res = async {
|
||||||
bail!("Timeout waiting for wifi link up")
|
while !stack.is_config_up() {
|
||||||
}
|
Timer::after(Duration::from_millis(100)).await
|
||||||
|
}
|
||||||
let res = async {
|
Ok::<(), FatError>(())
|
||||||
while !stack.is_config_up() {
|
|
||||||
Timer::after(Duration::from_millis(100)).await
|
|
||||||
}
|
}
|
||||||
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() {
|
if res.is_err() {
|
||||||
bail!("Timeout waiting for wifi config up")
|
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());
|
// All retries exhausted
|
||||||
Ok(*stack)
|
bail!("WiFi connection failed after all retries");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn try_connect_wifi_sntp_mqtt(
|
pub async fn try_connect_wifi_sntp_mqtt(
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ use chrono::{DateTime, TimeDelta, Utc};
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use serde::{Deserialize, Serialize};
|
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
|
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 400.; // this is really, really dry, think like cactus levels
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||||
@@ -113,6 +117,27 @@ pub struct PlantState {
|
|||||||
pub last_fertilizer_time: i64,
|
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(
|
fn map_range_moisture(
|
||||||
s: f32,
|
s: f32,
|
||||||
min_frequency: Option<f32>,
|
min_frequency: Option<f32>,
|
||||||
@@ -134,9 +159,28 @@ fn map_range_moisture(
|
|||||||
max: max_freq,
|
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 {
|
impl PlantState {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ struct LoadData<'a> {
|
|||||||
struct Moistures {
|
struct Moistures {
|
||||||
moisture_a: Vec<String>,
|
moisture_a: Vec<String>,
|
||||||
moisture_b: Vec<String>,
|
moisture_b: Vec<String>,
|
||||||
|
sensor_a_build_minutes: Vec<Option<u32>>,
|
||||||
|
sensor_b_build_minutes: Vec<Option<u32>>,
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
struct SolarState {
|
struct SolarState {
|
||||||
@@ -63,9 +65,20 @@ where
|
|||||||
MoistureSensorState::NoMessage => "No Message".to_string(),
|
MoistureSensorState::NoMessage => "No Message".to_string(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let sensor_a_build_minutes: Vec<Option<u32>> = plant_state
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.sensor_a_firmware_build_minutes)
|
||||||
|
.collect();
|
||||||
|
let sensor_b_build_minutes: Vec<Option<u32>> = plant_state
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.sensor_b_firmware_build_minutes)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let data = Moistures {
|
let data = Moistures {
|
||||||
moisture_a: a,
|
moisture_a: a,
|
||||||
moisture_b: b,
|
moisture_b: b,
|
||||||
|
sensor_a_build_minutes,
|
||||||
|
sensor_b_build_minutes,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&data)?;
|
let json = serde_json::to_string(&data)?;
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ export interface GetTime {
|
|||||||
export interface Moistures {
|
export interface Moistures {
|
||||||
moisture_a: [string],
|
moisture_a: [string],
|
||||||
moisture_b: [string],
|
moisture_b: [string],
|
||||||
|
sensor_a_build_minutes: Array<number | null>,
|
||||||
|
sensor_b_build_minutes: Array<number | null>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
|
|||||||
@@ -556,8 +556,8 @@ export class Controller {
|
|||||||
return fetch(PUBLIC_URL + "/moisture")
|
return fetch(PUBLIC_URL + "/moisture")
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(json => json as Moistures)
|
.then(json => json as Moistures)
|
||||||
.then(time => {
|
.then(data => {
|
||||||
controller.plantViews.update(time.moisture_a, time.moisture_b)
|
controller.plantViews.update(data.moisture_a, data.moisture_b, data.sensor_a_build_minutes, data.sensor_b_build_minutes)
|
||||||
clearTimeout(timerId);
|
clearTimeout(timerId);
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
controller.progressview.removeProgress("measure_moisture");
|
controller.progressview.removeProgress("measure_moisture");
|
||||||
@@ -731,6 +731,7 @@ const tasks = [
|
|||||||
{task: controller.updateRTCData, displayString: "Updating RTC Data"},
|
{task: controller.updateRTCData, displayString: "Updating RTC Data"},
|
||||||
{task: controller.updateBatteryData, displayString: "Updating Battery Data"},
|
{task: controller.updateBatteryData, displayString: "Updating Battery Data"},
|
||||||
{task: controller.updateSolarData, displayString: "Updating Solar Data"},
|
{task: controller.updateSolarData, displayString: "Updating Solar Data"},
|
||||||
|
{task: () => controller.measure_moisture(true), displayString: "Measuring Moisture"},
|
||||||
{task: controller.downloadConfig, displayString: "Downloading Configuration"},
|
{task: controller.downloadConfig, displayString: "Downloading Configuration"},
|
||||||
{task: controller.version, displayString: "Fetching Version Information"},
|
{task: controller.version, displayString: "Fetching Version Information"},
|
||||||
{task: controller.updateSaveList, displayString: "Updating Save Slots"},
|
{task: controller.updateSaveList, displayString: "Updating Save Slots"},
|
||||||
|
|||||||
@@ -36,11 +36,19 @@ export class PlantViews {
|
|||||||
return rv
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
update(moisture_a: [string], moisture_b: [string]) {
|
update(moisture_a: [string], moisture_b: [string], sensor_a_build_minutes?: Array<number | null>, sensor_b_build_minutes?: Array<number | null>) {
|
||||||
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
|
||||||
const a = moisture_a[plantId]
|
const a = moisture_a[plantId]
|
||||||
const b = moisture_b[plantId]
|
const b = moisture_b[plantId]
|
||||||
this.plants[plantId].setMeasurementResult(a, b)
|
this.plants[plantId].setMeasurementResult(a, b)
|
||||||
|
|
||||||
|
// Update firmware build timestamps if provided
|
||||||
|
if (sensor_a_build_minutes && sensor_a_build_minutes[plantId] !== undefined) {
|
||||||
|
this.plants[plantId].setFirmwareBuild("sensor_a", sensor_a_build_minutes[plantId])
|
||||||
|
}
|
||||||
|
if (sensor_b_build_minutes && sensor_b_build_minutes[plantId] !== undefined) {
|
||||||
|
this.plants[plantId].setFirmwareBuild("sensor_b", sensor_b_build_minutes[plantId])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,4 +414,12 @@ export class PlantView {
|
|||||||
this.sensorAFwBuild.innerText = formatBuildMinutes(plantResult.sensor_a);
|
this.sensorAFwBuild.innerText = formatBuildMinutes(plantResult.sensor_a);
|
||||||
this.sensorBFwBuild.innerText = formatBuildMinutes(plantResult.sensor_b);
|
this.sensorBFwBuild.innerText = formatBuildMinutes(plantResult.sensor_b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFirmwareBuild(sensor: "sensor_a" | "sensor_b", buildMinutes: number | null) {
|
||||||
|
if (sensor === "sensor_a") {
|
||||||
|
this.sensorAFwBuild.innerText = formatBuildMinutes(buildMinutes);
|
||||||
|
} else {
|
||||||
|
this.sensorBFwBuild.innerText = formatBuildMinutes(buildMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,13 +27,7 @@ export class SubmitView {
|
|||||||
this.submit_status = document.getElementById("submit_status") as HTMLElement
|
this.submit_status = document.getElementById("submit_status") as HTMLElement
|
||||||
this.submitFormBtn.onclick = () => {
|
this.submitFormBtn.onclick = () => {
|
||||||
controller.uploadConfig(this.json.textContent as string, (status: string) => {
|
controller.uploadConfig(this.json.textContent as string, (status: string) => {
|
||||||
if (status != "OK") {
|
toast.info(status);
|
||||||
// Show error toast (click to dismiss only)
|
|
||||||
toast.error(status);
|
|
||||||
} else {
|
|
||||||
// Show info toast (auto hides after 5s, or click to dismiss sooner)
|
|
||||||
toast.info('Config uploaded successfully');
|
|
||||||
}
|
|
||||||
this.submit_status.innerHTML = status;
|
this.submit_status.innerHTML = status;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user