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`
This commit is contained in:
@@ -7,15 +7,16 @@ use chrono::{DateTime, Utc};
|
|||||||
|
|
||||||
use crate::fat_error::{FatError, FatResult};
|
use crate::fat_error::{FatError, FatResult};
|
||||||
use crate::hal::shared_flash::MutexFlashStorage;
|
use crate::hal::shared_flash::MutexFlashStorage;
|
||||||
use alloc::string::ToString;
|
use alloc::string::{String, ToString};
|
||||||
use alloc::sync::Arc;
|
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::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::mutex::Mutex;
|
use embassy_sync::mutex::Mutex;
|
||||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
|
||||||
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||||
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||||
use esp_hal::rng::Rng;
|
use esp_hal::rng::Rng;
|
||||||
use esp_hal::rtc_cntl::{
|
use esp_hal::rtc_cntl::{
|
||||||
@@ -30,6 +31,24 @@ use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
|
|||||||
use esp_radio::wifi::{Interface, WifiController};
|
use esp_radio::wifi::{Interface, WifiController};
|
||||||
use log::{error, info};
|
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))]
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||||
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||||
@@ -192,6 +211,25 @@ impl Esp<'_> {
|
|||||||
Ok(rv)
|
Ok(rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return detailed Wi-Fi scan information including signal strength
|
||||||
|
pub(crate) async fn wifi_scan_details(&mut self) -> FatResult<Vec<WifiScanDetails>> {
|
||||||
|
let ap_infos = self.wifi_scan().await?;
|
||||||
|
|
||||||
|
// Convert AccessPointInfo to WifiScanDetails
|
||||||
|
let details: Vec<WifiScanDetails> = 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<DateTime<Utc>> {
|
pub(crate) fn last_pump_time(&self, plant: usize) -> Option<DateTime<Utc>> {
|
||||||
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
|
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
|
||||||
DateTime::from_timestamp_millis(ts)
|
DateTime::from_timestamp_millis(ts)
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
|||||||
error!("Error publishing battery state {e}");
|
error!("Error publishing battery state {e}");
|
||||||
});
|
});
|
||||||
let _ = mqtt::publish_mppt_state(&mut board).await;
|
let _ = mqtt::publish_mppt_state(&mut board).await;
|
||||||
|
let _ = mqtt::publish_wifi_scan(&mut board).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use crate::log::{log, LogMessage};
|
|||||||
use crate::plant_state::PlantState;
|
use crate::plant_state::PlantState;
|
||||||
use crate::tank::TankState;
|
use crate::tank::TankState;
|
||||||
use crate::{bail, VersionInfo};
|
use crate::{bail, VersionInfo};
|
||||||
use alloc::string::String;
|
use alloc::format;
|
||||||
use alloc::{format, string::ToString};
|
use alloc::string::{String, ToString};
|
||||||
|
use alloc::vec::Vec;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use core::sync::atomic::Ordering;
|
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)]
|
#[derive(Serialize, Debug, PartialEq)]
|
||||||
pub struct Solar {
|
pub struct Solar {
|
||||||
pub current_ma: u32,
|
pub current_ma: u32,
|
||||||
@@ -407,3 +418,30 @@ pub async fn publish_battery_state(
|
|||||||
publish("/battery", &json).await;
|
publish("/battery", &json).await;
|
||||||
Ok(())
|
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<WifiScanResult> = 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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,3 +214,12 @@ pub(crate) async fn get_log_localization_config<T, const N: usize>(
|
|||||||
&LogMessage::log_localisation_config(),
|
&LogMessage::log_localisation_config(),
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return Wi-Fi scan details including signal strength (RSSI)
|
||||||
|
pub(crate) async fn get_wifi_details<T, const N: usize>(
|
||||||
|
_request: &mut Connection<'_, T, N>,
|
||||||
|
) -> FatResult<Option<String>> {
|
||||||
|
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)?))
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_co
|
|||||||
use crate::webserver::get_json::{
|
use crate::webserver::get_json::{
|
||||||
delete_save, get_battery_state, get_config, get_live_moisture, get_log_localization_config,
|
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_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_log::{get_live_log, get_log};
|
||||||
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
||||||
@@ -83,6 +84,7 @@ impl Handler for HTTPRequestRouter {
|
|||||||
"/timezones" => Some(get_timezones().await),
|
"/timezones" => Some(get_timezones().await),
|
||||||
"/moisture" => Some(get_live_moisture(conn).await),
|
"/moisture" => Some(get_live_moisture(conn).await),
|
||||||
"/list_saves" => Some(list_saves(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
|
// /live_log accepts an optional ?after=N query parameter
|
||||||
p if p == "/live_log" || p.starts_with("/live_log?") => {
|
p if p == "/live_log" || p.starts_with("/live_log?") => {
|
||||||
let after: Option<u64> = p
|
let after: Option<u64> = p
|
||||||
|
|||||||
@@ -225,6 +225,15 @@ export interface Detection {
|
|||||||
plant: DetectionPlant[]
|
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 {
|
export interface TankInfo {
|
||||||
/// there is enough water in the tank
|
/// there is enough water in the tank
|
||||||
enough_water: boolean,
|
enough_water: boolean,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#logpanel {
|
#logpanel {
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
Moistures,
|
Moistures,
|
||||||
NightLampCommand,
|
NightLampCommand,
|
||||||
PlantControllerConfig,
|
PlantControllerConfig,
|
||||||
SetTime, SSIDList, TankInfo,
|
SetTime, SSIDList, TankInfo, WifiScanResult,
|
||||||
TestPump,
|
TestPump,
|
||||||
VersionInfo,
|
VersionInfo,
|
||||||
SaveInfo, SolarState, PumpTestResult, Detection, DetectionRequest, CanPower
|
SaveInfo, SolarState, PumpTestResult, Detection, DetectionRequest, CanPower
|
||||||
@@ -172,6 +172,21 @@ export class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanWifiDetails(): Promise<void> {
|
||||||
|
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) {
|
uploadNewFirmware(file: File) {
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let max = 100;
|
let max = 100;
|
||||||
@@ -501,6 +516,8 @@ export class Controller {
|
|||||||
if (ajax.status >= 200 && ajax.status < 300) {
|
if (ajax.status >= 200 && ajax.status < 300) {
|
||||||
this.networkView.setScanResult(ajax.response as SSIDList);
|
this.networkView.setScanResult(ajax.response as SSIDList);
|
||||||
toast.success("WiFi scan completed");
|
toast.success("WiFi scan completed");
|
||||||
|
// Also fetch detailed Wi-Fi information
|
||||||
|
this.scanWifiDetails();
|
||||||
} else {
|
} else {
|
||||||
toast.error(`WiFi scan failed: ${ajax.status}`);
|
toast.error(`WiFi scan failed: ${ajax.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,15 @@
|
|||||||
<input class="mqttvalue" type="text" id="mqtt_password" placeholder="">
|
<input class="mqttvalue" type="text" id="mqtt_password" placeholder="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="subcontainer">
|
||||||
|
<div class="flexcontainer">
|
||||||
|
<div class="subtitle">Wi-Fi Scan Results</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-results">
|
||||||
|
<p>Scan for available networks to see signal strength</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Controller } from "./main";
|
import { Controller } from "./main";
|
||||||
import {NetworkConfig, SSIDList} from "./api";
|
import {NetworkConfig, SSIDList, WifiScanResult} from "./api";
|
||||||
|
|
||||||
export class NetworkConfigView {
|
export class NetworkConfigView {
|
||||||
|
private wifiResults: HTMLElement;
|
||||||
|
|
||||||
setScanResult(ssidList: SSIDList) {
|
setScanResult(ssidList: SSIDList) {
|
||||||
this.ssidlist.innerHTML = ''
|
this.ssidlist.innerHTML = ''
|
||||||
for (const ssid of ssidList.ssids) {
|
for (const ssid of ssidList.ssids) {
|
||||||
@@ -10,6 +12,47 @@ export class NetworkConfigView {
|
|||||||
this.ssidlist.appendChild(wi);
|
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 = '<p>No Wi-Fi networks found</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<table style="width:100%; border-collapse: collapse;">';
|
||||||
|
html += '<tr><th style="text-align:left; padding: 8px;">SSID</th>';
|
||||||
|
html += '<th style="text-align:left; padding: 8px;">Signal (RSSI)</th>';
|
||||||
|
html += '<th style="text-align:left; padding: 8px;">Channel</th>';
|
||||||
|
html += '<th style="text-align:left; padding: 8px;">Authentication</th></tr>';
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
html += '<tr style="border-bottom: 1px solid #ddd;">';
|
||||||
|
html += `<td style="padding: 8px;">${result.ssid}</td>`;
|
||||||
|
html += `<td style="padding: 8px;">${result.rssi} dBm</td>`;
|
||||||
|
html += `<td style="padding: 8px;">${result.channel}</td>`;
|
||||||
|
html += `<td style="padding: 8px;">${result.auth_method}</td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</table>';
|
||||||
|
wifiContainer.innerHTML = html;
|
||||||
|
}
|
||||||
private readonly ap_ssid: HTMLInputElement;
|
private readonly ap_ssid: HTMLInputElement;
|
||||||
private readonly ssid: HTMLInputElement;
|
private readonly ssid: HTMLInputElement;
|
||||||
private readonly password: HTMLInputElement;
|
private readonly password: HTMLInputElement;
|
||||||
@@ -47,9 +90,14 @@ export class NetworkConfigView {
|
|||||||
this.ssidlist = document.getElementById("ssidlist") as HTMLElement
|
this.ssidlist = document.getElementById("ssidlist") as HTMLElement
|
||||||
|
|
||||||
let scanWifiBtn = document.getElementById("scan") as HTMLButtonElement;
|
let scanWifiBtn = document.getElementById("scan") as HTMLButtonElement;
|
||||||
scanWifiBtn.onclick = function (){
|
scanWifiBtn.onclick = async () => {
|
||||||
controller.scanWifi();
|
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) {
|
setConfig(network: NetworkConfig) {
|
||||||
|
|||||||
Reference in New Issue
Block a user