From 6b419dba6cbd1e27b3ff3c01ed3be89181ea1696 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Thu, 28 May 2026 00:46:14 +0200 Subject: [PATCH] Add Wi-Fi scan details display and MQTT publish - HTML: Add Wi-Fi scan results container to network.html - Rust: Implement `wifi_scan_details()` with RSSI, channel, auth method - API & UI: Fetch and display scan results in table format - MQTT: Publish top 10 networks sorted by RSSI to `/wifi_scan` --- Software/MainBoard/rust/src/hal/esp.rs | 42 ++++++++++++++- Software/MainBoard/rust/src/main.rs | 1 + Software/MainBoard/rust/src/mqtt.rs | 42 ++++++++++++++- .../MainBoard/rust/src/webserver/get_json.rs | 9 ++++ Software/MainBoard/rust/src/webserver/mod.rs | 2 + .../MainBoard/rust/src_webpack/src/api.ts | 9 ++++ .../MainBoard/rust/src_webpack/src/log.html | 2 +- .../MainBoard/rust/src_webpack/src/main.ts | 19 ++++++- .../rust/src_webpack/src/network.html | 10 +++- .../MainBoard/rust/src_webpack/src/network.ts | 54 +++++++++++++++++-- 10 files changed, 180 insertions(+), 10 deletions(-) diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index ead2389..b502474 100644 --- a/Software/MainBoard/rust/src/hal/esp.rs +++ b/Software/MainBoard/rust/src/hal/esp.rs @@ -7,15 +7,16 @@ use chrono::{DateTime, Utc}; use crate::fat_error::{FatError, FatResult}; use crate::hal::shared_flash::MutexFlashStorage; -use alloc::string::ToString; +use alloc::string::{String, ToString}; use alloc::sync::Arc; -use alloc::{string::String, vec, vec::Vec}; +use alloc::{format, vec, vec::Vec}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::mutex::Mutex; use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage}; 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::{ @@ -30,6 +31,24 @@ use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig}; use esp_radio::wifi::{Interface, WifiController}; use log::{error, info}; +/// Detailed Wi-Fi scan information including signal strength +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WifiScanDetails { + pub ssid: String, + pub bssid: String, + pub rssi: i32, + pub channel: u8, + pub auth_method: String, +} + +// Helper function to format BSSID as MAC address string +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] + ) +} + #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT]; @@ -192,6 +211,25 @@ impl Esp<'_> { Ok(rv) } + /// Return detailed Wi-Fi scan information including signal strength + pub(crate) async fn wifi_scan_details(&mut self) -> FatResult> { + let ap_infos = self.wifi_scan().await?; + + // Convert AccessPointInfo to WifiScanDetails + let details: Vec = ap_infos + .iter() + .map(|ap| WifiScanDetails { + ssid: ap.ssid.as_str().to_string(), + bssid: format_bssid(&ap.bssid), + rssi: ap.signal_strength as i32, + channel: ap.channel as u8, + auth_method: format!("{:?}",ap.auth_method), + }) + .collect(); + + Ok(details) + } + pub(crate) fn last_pump_time(&self, plant: usize) -> Option> { let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant]; DateTime::from_timestamp_millis(ts) diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index d51fb25..d1e0e51 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -280,6 +280,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { error!("Error publishing battery state {e}"); }); let _ = mqtt::publish_mppt_state(&mut board).await; + let _ = mqtt::publish_wifi_scan(&mut board).await; } log( diff --git a/Software/MainBoard/rust/src/mqtt.rs b/Software/MainBoard/rust/src/mqtt.rs index 441a8f4..0249c27 100644 --- a/Software/MainBoard/rust/src/mqtt.rs +++ b/Software/MainBoard/rust/src/mqtt.rs @@ -6,8 +6,9 @@ use crate::log::{log, LogMessage}; use crate::plant_state::PlantState; use crate::tank::TankState; use crate::{bail, VersionInfo}; -use alloc::string::String; -use alloc::{format, string::ToString}; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; use chrono::DateTime; use chrono_tz::Tz; use core::sync::atomic::Ordering; @@ -349,6 +350,16 @@ pub async fn pump_info( }; } +/// Wi-Fi scan result details for MQTT +#[derive(Serialize, Debug, PartialEq)] +pub struct WifiScanResult { + pub ssid: String, + pub bssid: String, + pub rssi: i32, + pub channel: u8, + pub auth_method: String, +} + #[derive(Serialize, Debug, PartialEq)] pub struct Solar { pub current_ma: u32, @@ -407,3 +418,30 @@ pub async fn publish_battery_state( publish("/battery", &json).await; Ok(()) } + +/// Publish Wi-Fi scan details to MQTT +pub async fn publish_wifi_scan( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, +) -> FatResult<()> { + let mut wifi_details = board.board_hal.get_esp().wifi_scan_details().await?; + + // Sort by RSSI in descending order (strongest first) + wifi_details.sort_by(|a, b| b.rssi.cmp(&a.rssi)); + + // Take only the strongest 10 results + let wifi_results: Vec = wifi_details + .iter() + .take(10) + .map(|d| WifiScanResult { + ssid: d.ssid.clone(), + bssid: d.bssid.clone(), + rssi: d.rssi, + channel: d.channel, + auth_method: d.auth_method.clone(), + }) + .collect(); + + let json = serde_json::to_string(&wifi_results)?; + publish("/wifi_scan", &json).await; + Ok(()) +} diff --git a/Software/MainBoard/rust/src/webserver/get_json.rs b/Software/MainBoard/rust/src/webserver/get_json.rs index 9071aa5..4e57586 100644 --- a/Software/MainBoard/rust/src/webserver/get_json.rs +++ b/Software/MainBoard/rust/src/webserver/get_json.rs @@ -214,3 +214,12 @@ pub(crate) async fn get_log_localization_config( &LogMessage::log_localisation_config(), )?)) } + +/// Return Wi-Fi scan details including signal strength (RSSI) +pub(crate) async fn get_wifi_details( + _request: &mut Connection<'_, T, N>, +) -> FatResult> { + let mut board = BOARD_ACCESS.get().await.lock().await; + let wifi_details = board.board_hal.get_esp().wifi_scan_details().await?; + Ok(Some(serde_json::to_string(&wifi_details)?)) +} diff --git a/Software/MainBoard/rust/src/webserver/mod.rs b/Software/MainBoard/rust/src/webserver/mod.rs index 5a6f382..62b6d8c 100644 --- a/Software/MainBoard/rust/src/webserver/mod.rs +++ b/Software/MainBoard/rust/src/webserver/mod.rs @@ -12,6 +12,7 @@ use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_co 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, }; use crate::webserver::get_log::{get_live_log, get_log}; use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index}; @@ -83,6 +84,7 @@ impl Handler for HTTPRequestRouter { "/timezones" => Some(get_timezones().await), "/moisture" => Some(get_live_moisture(conn).await), "/list_saves" => Some(list_saves(conn).await), + "/wifi_details" => Some(get_wifi_details(conn).await), // /live_log accepts an optional ?after=N query parameter p if p == "/live_log" || p.starts_with("/live_log?") => { let after: Option = p diff --git a/Software/MainBoard/rust/src_webpack/src/api.ts b/Software/MainBoard/rust/src_webpack/src/api.ts index 2b2aa24..6436f4d 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -225,6 +225,15 @@ export interface Detection { plant: DetectionPlant[] } +/// Wi-Fi scan result details for UI display +export interface WifiScanResult { + ssid: string, + bssid: string, + rssi: number, // signal strength in dBm + channel: number, + auth_method: string +} + export interface TankInfo { /// there is enough water in the tank enough_water: boolean, diff --git a/Software/MainBoard/rust/src_webpack/src/log.html b/Software/MainBoard/rust/src_webpack/src/log.html index 3863bf4..4e5c975 100644 --- a/Software/MainBoard/rust/src_webpack/src/log.html +++ b/Software/MainBoard/rust/src_webpack/src/log.html @@ -37,7 +37,7 @@ } #logpanel { - display: none; + } diff --git a/Software/MainBoard/rust/src_webpack/src/main.ts b/Software/MainBoard/rust/src_webpack/src/main.ts index f270740..32ac3c2 100644 --- a/Software/MainBoard/rust/src_webpack/src/main.ts +++ b/Software/MainBoard/rust/src_webpack/src/main.ts @@ -26,7 +26,7 @@ import { Moistures, NightLampCommand, PlantControllerConfig, - SetTime, SSIDList, TankInfo, + SetTime, SSIDList, TankInfo, WifiScanResult, TestPump, VersionInfo, SaveInfo, SolarState, PumpTestResult, Detection, DetectionRequest, CanPower @@ -172,6 +172,21 @@ export class Controller { } } + async scanWifiDetails(): Promise { + try { + const response = await fetch(PUBLIC_URL + "/wifi_details"); + if (response.ok) { + const wifiDetails = await response.json(); + controller.networkView.displayWifiResults(wifiDetails as WifiScanResult[]); + } else { + toast.error(`Failed to fetch Wi-Fi details: ${response.status}`); + } + } catch (error) { + toast.error(`Wi-Fi details error: ${error}`); + console.log(error); + } + } + uploadNewFirmware(file: File) { let current = 0; let max = 100; @@ -501,6 +516,8 @@ export class Controller { if (ajax.status >= 200 && ajax.status < 300) { this.networkView.setScanResult(ajax.response as SSIDList); toast.success("WiFi scan completed"); + // Also fetch detailed Wi-Fi information + this.scanWifiDetails(); } else { toast.error(`WiFi scan failed: ${ajax.status}`); } diff --git a/Software/MainBoard/rust/src_webpack/src/network.html b/Software/MainBoard/rust/src_webpack/src/network.html index 7d0a56b..c8aba8c 100644 --- a/Software/MainBoard/rust/src_webpack/src/network.html +++ b/Software/MainBoard/rust/src_webpack/src/network.html @@ -85,7 +85,15 @@ +
+
+
Wi-Fi Scan Results
+
+ +
+

Scan for available networks to see signal strength

+
+
- diff --git a/Software/MainBoard/rust/src_webpack/src/network.ts b/Software/MainBoard/rust/src_webpack/src/network.ts index f26b659..74d7f6b 100644 --- a/Software/MainBoard/rust/src_webpack/src/network.ts +++ b/Software/MainBoard/rust/src_webpack/src/network.ts @@ -1,7 +1,9 @@ import { Controller } from "./main"; -import {NetworkConfig, SSIDList} from "./api"; +import {NetworkConfig, SSIDList, WifiScanResult} from "./api"; export class NetworkConfigView { + private wifiResults: HTMLElement; + setScanResult(ssidList: SSIDList) { this.ssidlist.innerHTML = '' for (const ssid of ssidList.ssids) { @@ -10,6 +12,47 @@ export class NetworkConfigView { this.ssidlist.appendChild(wi); } } + + async scanAndDisplayWifiDetails() { + try { + const response = await fetch('/wifi_details'); + if (response.ok) { + const data: WifiScanResult[] = await response.json(); + this.displayWifiResults(data); + } + } catch (error) { + console.error('Error fetching Wi-Fi details:', error); + this.displayWifiResults([]); + } + } + + displayWifiResults(results: WifiScanResult[]) { + const wifiContainer = document.getElementById('wifi-results'); + if (!wifiContainer) return; + + if (results.length === 0) { + wifiContainer.innerHTML = '

No Wi-Fi networks found

'; + return; + } + + let html = ''; + html += ''; + html += ''; + html += ''; + html += ''; + + results.forEach(result => { + html += ''; + html += ``; + html += ``; + html += ``; + html += ``; + html += ''; + }); + + html += '
SSIDSignal (RSSI)ChannelAuthentication
${result.ssid}${result.rssi} dBm${result.channel}${result.auth_method}
'; + wifiContainer.innerHTML = html; + } private readonly ap_ssid: HTMLInputElement; private readonly ssid: HTMLInputElement; private readonly password: HTMLInputElement; @@ -47,9 +90,14 @@ export class NetworkConfigView { this.ssidlist = document.getElementById("ssidlist") as HTMLElement let scanWifiBtn = document.getElementById("scan") as HTMLButtonElement; - scanWifiBtn.onclick = function (){ + scanWifiBtn.onclick = async () => { controller.scanWifi(); - } + // After Wi-Fi scan, fetch and display detailed results + await this.scanAndDisplayWifiDetails(); + }; + + // Store wifiResults reference for later use + this.wifiResults = document.getElementById('wifi-results') as HTMLElement; } setConfig(network: NetworkConfig) {