Compare commits

..

3 Commits

Author SHA1 Message Date
Reuven fc92436f52 chore: add build artifacts and session state
- NVS config binaries for ESP32 WiFi provisioning
- macOS Tauri schema
- package-lock.json update
- Claude Flow session state

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 10:36:16 -04:00
Reuven 285bb0ad37 feat(desktop): v0.4.4 - WiFi configuration via serial port
## New Features
- WiFi Configuration Modal: Configure ESP32 WiFi credentials directly from the desktop app
- Serial port WiFi commands: Sends wifi_config/wifi/set ssid commands via serial
- Improved feedback UI with status indicators (Success/Commands Sent/Error)

## API Improvements
- New Tauri command: configure_esp32_wifi(port, ssid, password)
- 21 new integration tests covering all API functionality
- ESP32 VID/PID detection for CP210x, CH340, FTDI, and native USB

## UI Enhancements
- WiFi button in Serial Ports table for ESP32-compatible devices
- Modal with SSID/password inputs and clear status feedback
- "Done" button after configuration with "Try Again" option

## Testing
- 18 unit tests + 21 integration tests = 39 total tests passing
- Tests cover: discovery, settings, server, flash, OTA, provision, WASM, state, domain models

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 10:35:30 -04:00
Reuven b5ec4ef043 chore: update Cargo.lock
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 10:02:02 -04:00
29 changed files with 4008 additions and 47 deletions
+13 -13
View File
@@ -1,6 +1,6 @@
{
"running": true,
"startedAt": "2026-02-28T15:54:19.353Z",
"startedAt": "2026-03-09T15:26:00.921Z",
"workers": {
"map": {
"runCount": 49,
@@ -8,16 +8,16 @@
"failureCount": 0,
"averageDurationMs": 1.2857142857142858,
"lastRun": "2026-02-28T16:13:19.194Z",
"nextRun": "2026-02-28T16:28:19.195Z",
"nextRun": "2026-03-09T15:56:00.928Z",
"isRunning": false
},
"audit": {
"runCount": 44,
"runCount": 45,
"successCount": 0,
"failureCount": 44,
"failureCount": 45,
"averageDurationMs": 0,
"lastRun": "2026-02-28T16:20:19.184Z",
"nextRun": "2026-02-28T16:30:19.185Z",
"lastRun": "2026-03-09T15:43:00.933Z",
"nextRun": "2026-03-09T15:38:00.914Z",
"isRunning": false
},
"optimize": {
@@ -26,7 +26,7 @@
"failureCount": 34,
"averageDurationMs": 0,
"lastRun": "2026-02-28T16:23:19.387Z",
"nextRun": "2026-02-28T16:18:19.361Z",
"nextRun": "2026-03-09T15:45:00.915Z",
"isRunning": false
},
"consolidate": {
@@ -35,7 +35,7 @@
"failureCount": 0,
"averageDurationMs": 0.6521739130434783,
"lastRun": "2026-02-28T16:05:19.091Z",
"nextRun": "2026-02-28T16:35:19.054Z",
"nextRun": "2026-03-09T16:02:00.918Z",
"isRunning": false
},
"testgaps": {
@@ -44,8 +44,8 @@
"failureCount": 27,
"averageDurationMs": 0,
"lastRun": "2026-02-28T16:08:19.369Z",
"nextRun": "2026-02-28T16:22:19.355Z",
"isRunning": true
"nextRun": "2026-03-09T15:54:00.920Z",
"isRunning": false
},
"predict": {
"runCount": 0,
@@ -64,8 +64,8 @@
},
"config": {
"autoStart": false,
"logDir": "/home/user/wifi-densepose/.claude-flow/logs",
"stateFile": "/home/user/wifi-densepose/.claude-flow/daemon-state.json",
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/logs",
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/daemon-state.json",
"maxConcurrent": 2,
"workerTimeoutMs": 300000,
"resourceThresholds": {
@@ -131,5 +131,5 @@
}
]
},
"savedAt": "2026-02-28T16:23:19.387Z"
"savedAt": "2026-03-09T15:43:00.933Z"
}
+1 -1
View File
@@ -1 +1 @@
54612
31273
+13 -13
View File
@@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs pre-bash",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" pre-bash",
"timeout": 5000
}
]
@@ -18,7 +18,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs post-edit",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" post-edit",
"timeout": 10000
}
]
@@ -29,7 +29,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs route",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" route",
"timeout": 10000
}
]
@@ -40,12 +40,12 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs session-restore",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-restore",
"timeout": 15000
},
{
"type": "command",
"command": "node .claude/helpers/auto-memory-hook.mjs import",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" import",
"timeout": 8000
}
]
@@ -56,7 +56,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs session-end",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
"timeout": 10000
}
]
@@ -67,7 +67,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/auto-memory-hook.mjs sync",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" sync",
"timeout": 10000
}
]
@@ -79,11 +79,11 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs compact-manual"
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-manual"
},
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs session-end",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
"timeout": 5000
}
]
@@ -93,11 +93,11 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs compact-auto"
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-auto"
},
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs session-end",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
"timeout": 6000
}
]
@@ -108,7 +108,7 @@
"hooks": [
{
"type": "command",
"command": "node .claude/helpers/hook-handler.cjs status",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" status",
"timeout": 3000
}
]
@@ -117,7 +117,7 @@
},
"statusLine": {
"type": "command",
"command": "node .claude/helpers/statusline.cjs"
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs\""
},
"permissions": {
"allow": [
@@ -0,0 +1,130 @@
{
"running": true,
"startedAt": "2026-03-10T14:22:41.948Z",
"workers": {
"map": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T14:22:41.948Z"
},
"audit": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T14:24:41.948Z"
},
"optimize": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T14:26:41.948Z"
},
"consolidate": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T14:28:41.949Z"
},
"testgaps": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T14:30:41.949Z"
},
"predict": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
},
"document": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
}
},
"config": {
"autoStart": false,
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node/.claude-flow/logs",
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node/.claude-flow/daemon-state.json",
"maxConcurrent": 2,
"workerTimeoutMs": 300000,
"resourceThresholds": {
"maxCpuLoad": 2,
"minFreeMemoryPercent": 20
},
"workers": [
{
"type": "map",
"intervalMs": 900000,
"offsetMs": 0,
"priority": "normal",
"description": "Codebase mapping",
"enabled": true
},
{
"type": "audit",
"intervalMs": 600000,
"offsetMs": 120000,
"priority": "critical",
"description": "Security analysis",
"enabled": true
},
{
"type": "optimize",
"intervalMs": 900000,
"offsetMs": 240000,
"priority": "high",
"description": "Performance optimization",
"enabled": true
},
{
"type": "consolidate",
"intervalMs": 1800000,
"offsetMs": 360000,
"priority": "low",
"description": "Memory consolidation",
"enabled": true
},
{
"type": "testgaps",
"intervalMs": 1200000,
"offsetMs": 480000,
"priority": "normal",
"description": "Test coverage analysis",
"enabled": true
},
{
"type": "predict",
"intervalMs": 600000,
"offsetMs": 0,
"priority": "low",
"description": "Predictive preloading",
"enabled": false
},
{
"type": "document",
"intervalMs": 3600000,
"offsetMs": 0,
"priority": "low",
"description": "Auto-documentation",
"enabled": false
}
]
},
"savedAt": "2026-03-10T14:22:41.949Z"
}
@@ -0,0 +1,7 @@
{"type":"edit","file":"unknown","timestamp":1773152422749,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152444021,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152460956,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152493971,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152501432,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152510853,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152596890,"sessionId":null}
@@ -0,0 +1,12 @@
{
"id": "session-1773152560779",
"startedAt": "2026-03-10T14:22:40.779Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node",
"context": {},
"metrics": {
"edits": 1,
"commands": 0,
"tasks": 0,
"errors": 0
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -3,3 +3,8 @@
{"type":"edit","file":"unknown","timestamp":1772820472219,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832571444,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832585997,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773099593107,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115162931,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115172336,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773147087836,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773149448951,"sessionId":null}
+22
View File
@@ -2870,6 +2870,26 @@ dependencies = [
"libc",
]
[[package]]
name = "libudev"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
dependencies = [
"libc",
"libudev-sys",
]
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -5614,6 +5634,7 @@ dependencies = [
"core-foundation",
"core-foundation-sys",
"io-kit-sys",
"libudev",
"mach2",
"nix 0.26.4",
"scopeguard",
@@ -7634,6 +7655,7 @@ dependencies = [
"reqwest 0.12.28",
"serde",
"serde_json",
"serialport",
"sha2",
"sysinfo",
"tauri",
@@ -0,0 +1,130 @@
{
"running": true,
"startedAt": "2026-03-09T23:56:03.574Z",
"workers": {
"map": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-09T23:56:03.574Z"
},
"audit": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-09T23:58:03.574Z"
},
"optimize": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:00:03.574Z"
},
"consolidate": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:02:03.574Z"
},
"testgaps": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:04:03.574Z"
},
"predict": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
},
"document": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
}
},
"config": {
"autoStart": false,
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/.claude-flow/logs",
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/.claude-flow/daemon-state.json",
"maxConcurrent": 2,
"workerTimeoutMs": 300000,
"resourceThresholds": {
"maxCpuLoad": 2,
"minFreeMemoryPercent": 20
},
"workers": [
{
"type": "map",
"intervalMs": 900000,
"offsetMs": 0,
"priority": "normal",
"description": "Codebase mapping",
"enabled": true
},
{
"type": "audit",
"intervalMs": 600000,
"offsetMs": 120000,
"priority": "critical",
"description": "Security analysis",
"enabled": true
},
{
"type": "optimize",
"intervalMs": 900000,
"offsetMs": 240000,
"priority": "high",
"description": "Performance optimization",
"enabled": true
},
{
"type": "consolidate",
"intervalMs": 1800000,
"offsetMs": 360000,
"priority": "low",
"description": "Memory consolidation",
"enabled": true
},
{
"type": "testgaps",
"intervalMs": 1200000,
"offsetMs": 480000,
"priority": "normal",
"description": "Test coverage analysis",
"enabled": true
},
{
"type": "predict",
"intervalMs": 600000,
"offsetMs": 0,
"priority": "low",
"description": "Predictive preloading",
"enabled": false
},
{
"type": "document",
"intervalMs": 3600000,
"offsetMs": 0,
"priority": "low",
"description": "Auto-documentation",
"enabled": false
}
]
},
"savedAt": "2026-03-09T23:56:03.574Z"
}
@@ -0,0 +1,42 @@
{"type":"edit","file":"unknown","timestamp":1773100520674,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100630628,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100635269,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100648222,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100660593,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100670480,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100765961,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100793408,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100801110,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100806887,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100820942,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100857691,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100894224,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773100911798,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773101430507,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773101470221,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773101478246,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773103575668,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773103693989,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115108388,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115362485,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115372676,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115388605,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115394377,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115415015,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115600459,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146102258,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146113449,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146119695,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146128174,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146133721,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146150082,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773146337071,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150581963,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150596765,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152997925,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153073387,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153109436,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153121443,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153290476,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153290781,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773153291056,"sessionId":null}
@@ -0,0 +1,12 @@
{
"id": "session-1773150558480",
"startedAt": "2026-03-10T13:49:18.480Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop",
"context": {},
"metrics": {
"edits": 9,
"commands": 0,
"tasks": 0,
"errors": 0
}
}
@@ -0,0 +1,14 @@
{
"id": "session-1773100562538",
"startedAt": "2026-03-09T23:56:02.538Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop",
"context": {},
"metrics": {
"edits": 13,
"commands": 0,
"tasks": 0,
"errors": 0
},
"endedAt": "2026-03-10T00:07:15.557Z",
"duration": 673020
}
@@ -0,0 +1,14 @@
{
"id": "session-1773101285009",
"startedAt": "2026-03-10T00:08:05.009Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop",
"context": {},
"metrics": {
"edits": 19,
"commands": 0,
"tasks": 0,
"errors": 0
},
"endedAt": "2026-03-10T13:48:30.150Z",
"duration": 49225141
}
@@ -56,6 +56,11 @@ hex = "0.4"
# Regex for parsing espflash output
regex = "1.10"
# Serial port for WiFi configuration
serialport.workspace = true
# Unix signals for graceful process termination
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[dev-dependencies]
@@ -411,6 +411,91 @@ fn is_esp32_compatible(vid: u16, pid: u16) -> bool {
false
}
/// Configure WiFi credentials on an ESP32 via serial port.
///
/// Sends WiFi credentials to the ESP32 using a simple serial protocol.
/// The ESP32 firmware should accept: `wifi_config <ssid> <password>\n`
#[tauri::command]
pub async fn configure_esp32_wifi(
port: String,
ssid: String,
password: String,
) -> Result<String, String> {
use std::io::{Read, Write};
use std::time::Duration;
tracing::info!("Configuring WiFi on port: {}", port);
// Open serial port
let mut serial = serialport::new(&port, 115200)
.timeout(Duration::from_secs(3))
.open()
.map_err(|e| format!("Failed to open port {}: {}", port, e))?;
// Wait for ESP32 to be ready
std::thread::sleep(Duration::from_millis(500));
// Try multiple command formats that different firmware versions might accept
let commands = [
format!("wifi_config {} {}\r\n", ssid, password),
format!("wifi {} {}\r\n", ssid, password),
format!("set ssid {}\r\n", ssid),
];
let mut response = String::new();
let mut buf = [0u8; 512];
for cmd in &commands {
// Clear any pending data
let _ = serial.read(&mut buf);
// Send command
serial.write_all(cmd.as_bytes())
.map_err(|e| format!("Failed to write: {}", e))?;
serial.flush().map_err(|e| format!("Failed to flush: {}", e))?;
// Wait and read response
std::thread::sleep(Duration::from_millis(500));
match serial.read(&mut buf) {
Ok(n) if n > 0 => {
let text = String::from_utf8_lossy(&buf[..n]).to_string();
response.push_str(&text);
// Check for success indicators
if text.to_lowercase().contains("ok")
|| text.to_lowercase().contains("saved")
|| text.to_lowercase().contains("configured") {
tracing::info!("WiFi config successful: {}", text.trim());
return Ok(format!("WiFi configured! Response: {}", text.trim()));
}
}
_ => {}
}
}
// Also try to send password separately if ssid command was sent
let pwd_cmd = format!("set password {}\r\n", password);
let _ = serial.write_all(pwd_cmd.as_bytes());
let _ = serial.flush();
std::thread::sleep(Duration::from_millis(300));
if let Ok(n) = serial.read(&mut buf) {
if n > 0 {
response.push_str(&String::from_utf8_lossy(&buf[..n]));
}
}
// Send reboot command
let _ = serial.write_all(b"reboot\r\n");
let _ = serial.flush();
if response.is_empty() {
Ok("Commands sent. ESP32 may need manual reboot to apply WiFi settings.".to_string())
} else {
Ok(format!("Commands sent. Response: {}", response.trim()))
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SerialPortInfo {
pub name: String,
@@ -13,6 +13,7 @@ pub fn run() {
// Discovery
discovery::discover_nodes,
discovery::list_serial_ports,
discovery::configure_esp32_wifi,
// Flash
flash::flash_firmware,
flash::flash_progress,
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "RuView Desktop",
"version": "0.4.3",
"version": "0.4.4",
"identifier": "net.ruv.ruview",
"build": {
"frontendDist": "ui/dist",
@@ -0,0 +1,420 @@
//! Integration tests for all Tauri API commands
//!
//! Tests the actual command implementations without the Tauri runtime.
// ============================================================================
// Discovery Tests
// ============================================================================
#[test]
fn test_serial_port_detection_logic() {
// Test ESP32 VID/PID detection
// CP210x (Silicon Labs)
assert!(is_esp32_vid_pid(0x10C4, 0xEA60), "CP2102 should be detected");
assert!(is_esp32_vid_pid(0x10C4, 0xEA70), "CP2104 should be detected");
// CH340/CH341 (QinHeng)
assert!(is_esp32_vid_pid(0x1A86, 0x7523), "CH340 should be detected");
assert!(is_esp32_vid_pid(0x1A86, 0x5523), "CH341 should be detected");
// FTDI
assert!(is_esp32_vid_pid(0x0403, 0x6001), "FTDI FT232 should be detected");
assert!(is_esp32_vid_pid(0x0403, 0x6010), "FTDI FT2232 should be detected");
// ESP32 native USB
assert!(is_esp32_vid_pid(0x303A, 0x1001), "ESP32-S2/S3 native should be detected");
// Unknown device
assert!(!is_esp32_vid_pid(0x0000, 0x0000), "Unknown VID/PID should not be detected");
assert!(!is_esp32_vid_pid(0x1234, 0x5678), "Random VID/PID should not be detected");
}
fn is_esp32_vid_pid(vid: u16, pid: u16) -> bool {
// CP210x (Silicon Labs)
if vid == 0x10C4 && (pid == 0xEA60 || pid == 0xEA70) {
return true;
}
// CH340/CH341 (QinHeng)
if vid == 0x1A86 && (pid == 0x7523 || pid == 0x5523) {
return true;
}
// FTDI
if vid == 0x0403 && (pid == 0x6001 || pid == 0x6010 || pid == 0x6011 || pid == 0x6014 || pid == 0x6015) {
return true;
}
// ESP32-S2/S3 native USB
if vid == 0x303A {
return true;
}
false
}
#[test]
fn test_beacon_parsing() {
let data = b"RUVIEW_BEACON|AA:BB:CC:DD:EE:FF|1|0.3.0|esp32s3|coordinator|0|4";
let text = std::str::from_utf8(data).unwrap();
let parts: Vec<&str> = text.split('|').collect();
assert_eq!(parts.len(), 8);
assert_eq!(parts[0], "RUVIEW_BEACON");
assert_eq!(parts[1], "AA:BB:CC:DD:EE:FF");
assert_eq!(parts[2], "1");
assert_eq!(parts[3], "0.3.0");
assert_eq!(parts[4], "esp32s3");
assert_eq!(parts[5], "coordinator");
assert_eq!(parts[6], "0");
assert_eq!(parts[7], "4");
}
// ============================================================================
// Settings Tests
// ============================================================================
#[test]
fn test_settings_structure() {
use wifi_densepose_desktop::commands::settings::AppSettings;
let settings = AppSettings::default();
// Check default values
assert!(!settings.theme.is_empty(), "Theme should have a default");
assert!(settings.discover_interval_ms > 0, "Discovery interval should be positive");
assert!(settings.auto_discover, "Auto-discover should default to true");
assert_eq!(settings.server_http_port, 8080);
}
#[test]
fn test_settings_serialization() {
use wifi_densepose_desktop::commands::settings::AppSettings;
let settings = AppSettings::default();
let json = serde_json::to_string(&settings).expect("Should serialize");
let restored: AppSettings = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(settings.theme, restored.theme);
assert_eq!(settings.server_http_port, restored.server_http_port);
assert_eq!(settings.discover_interval_ms, restored.discover_interval_ms);
}
// ============================================================================
// Server Tests
// ============================================================================
#[test]
fn test_server_state_default() {
use wifi_densepose_desktop::state::ServerState;
let server = ServerState::default();
assert!(!server.running, "Server should not be running by default");
assert!(server.pid.is_none());
assert!(server.http_port.is_none());
}
// ============================================================================
// Flash Tests
// ============================================================================
#[test]
fn test_chip_variants() {
use wifi_densepose_desktop::domain::node::Chip;
let chips = vec![
Chip::Esp32,
Chip::Esp32s2,
Chip::Esp32s3,
Chip::Esp32c3,
Chip::Esp32c6,
];
for chip in chips {
let name = format!("{:?}", chip).to_lowercase();
assert!(name.starts_with("esp32"), "All chips should be ESP32 variants");
}
}
#[test]
fn test_progress_parsing() {
// Test espflash progress output parsing
let output = "Flashing... [===> ] 35%";
let re = regex::Regex::new(r"(\d+)%").unwrap();
if let Some(caps) = re.captures(output) {
let pct: u8 = caps[1].parse().unwrap();
assert_eq!(pct, 35);
} else {
panic!("Should parse percentage");
}
}
// ============================================================================
// OTA Tests
// ============================================================================
#[test]
fn test_sha256_hash() {
use sha2::{Sha256, Digest};
let data = b"test firmware data";
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hasher.finalize();
let hex = hex::encode(hash);
assert_eq!(hex.len(), 64, "SHA256 should produce 64 hex characters");
}
#[test]
fn test_hmac_signature() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let key = b"test_psk_key";
let data = b"firmware_hash";
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data);
let result = mac.finalize();
let signature = hex::encode(result.into_bytes());
assert_eq!(signature.len(), 64, "HMAC-SHA256 should produce 64 hex characters");
}
// ============================================================================
// Provision Tests
// ============================================================================
#[test]
fn test_nvs_config_format() {
// Test CSV format for NVS partition
let csv = "key,type,encoding,value\ncsi_cfg,namespace,,\nssid,data,string,TestNetwork\npassword,data,string,TestPass123\n";
let lines: Vec<&str> = csv.lines().collect();
assert_eq!(lines.len(), 4);
assert!(lines[0].starts_with("key,type"));
assert!(lines[1].contains("namespace"));
assert!(lines[2].contains("ssid"));
assert!(lines[3].contains("password"));
}
#[test]
fn test_mesh_config_generation() {
// Test that mesh configs have required fields
let config = serde_json::json!({
"node_id": 1,
"mesh_role": "node",
"tdm_slot": 0,
"tdm_total": 4,
"ssid": "TestNetwork",
"password": "TestPass",
"coordinator_ip": "192.168.1.100"
});
assert!(config.get("node_id").is_some());
assert!(config.get("mesh_role").is_some());
assert!(config.get("ssid").is_some());
}
// ============================================================================
// WASM Tests
// ============================================================================
#[test]
fn test_wasm_magic_bytes() {
// WebAssembly magic bytes: \0asm
let wasm_header: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];
assert_eq!(wasm_header[0], 0x00);
assert_eq!(wasm_header[1], 0x61); // 'a'
assert_eq!(wasm_header[2], 0x73); // 's'
assert_eq!(wasm_header[3], 0x6D); // 'm'
}
#[test]
fn test_wasm_version() {
// WASM version 1
let wasm_version: [u8; 4] = [0x01, 0x00, 0x00, 0x00];
let version = u32::from_le_bytes(wasm_version);
assert_eq!(version, 1);
}
// ============================================================================
// State Tests
// ============================================================================
#[test]
fn test_app_state_initialization() {
use wifi_densepose_desktop::state::AppState;
let state = AppState::default();
// Check that all state components initialize correctly
let discovery = state.discovery.lock().unwrap();
assert!(discovery.nodes.is_empty(), "Should start with no nodes");
drop(discovery);
let flash = state.flash.lock().unwrap();
assert_eq!(flash.phase, "", "Should start with empty phase");
assert_eq!(flash.progress_pct, 0.0);
drop(flash);
let server = state.server.lock().unwrap();
assert!(!server.running, "Server should not be running initially");
}
// ============================================================================
// Domain Model Tests
// ============================================================================
#[test]
fn test_health_status_variants() {
use wifi_densepose_desktop::domain::node::HealthStatus;
let statuses = vec![
HealthStatus::Online,
HealthStatus::Degraded,
HealthStatus::Offline,
];
for status in statuses {
let json = serde_json::to_string(&status).expect("Should serialize");
assert!(!json.is_empty());
}
}
#[test]
fn test_discovery_method_variants() {
use wifi_densepose_desktop::domain::node::DiscoveryMethod;
let methods = vec![
DiscoveryMethod::Mdns,
DiscoveryMethod::UdpProbe,
DiscoveryMethod::Manual,
DiscoveryMethod::HttpSweep,
];
for method in methods {
let json = serde_json::to_string(&method).expect("Should serialize");
assert!(!json.is_empty());
}
}
#[test]
fn test_mesh_role_variants() {
use wifi_densepose_desktop::domain::node::MeshRole;
let roles = vec![
MeshRole::Coordinator,
MeshRole::Aggregator,
MeshRole::Node,
];
for role in roles {
let json = serde_json::to_string(&role).expect("Should serialize");
assert!(!json.is_empty());
}
}
// ============================================================================
// WiFi Config Tests (New Feature)
// ============================================================================
#[test]
fn test_wifi_config_command_format() {
let ssid = "TestNetwork";
let password = "TestPass123";
// Test all command formats
let cmd1 = format!("wifi_config {} {}\r\n", ssid, password);
let cmd2 = format!("wifi {} {}\r\n", ssid, password);
let cmd3 = format!("set ssid {}\r\n", ssid);
let cmd4 = format!("set password {}\r\n", password);
assert!(cmd1.contains("wifi_config"));
assert!(cmd1.contains(ssid));
assert!(cmd1.contains(password));
assert!(cmd1.ends_with("\r\n"));
assert!(cmd2.starts_with("wifi "));
assert!(cmd3.starts_with("set ssid "));
assert!(cmd4.starts_with("set password "));
}
#[test]
fn test_wifi_credentials_validation() {
// SSID: 1-32 characters
let valid_ssid = "MyNetwork";
let empty_ssid = "";
let long_ssid = "A".repeat(33);
assert!(!valid_ssid.is_empty() && valid_ssid.len() <= 32);
assert!(empty_ssid.is_empty());
assert!(long_ssid.len() > 32);
// Password: 8-63 characters for WPA2
let valid_pass = "password123";
let short_pass = "short";
let long_pass = "A".repeat(64);
assert!(valid_pass.len() >= 8 && valid_pass.len() <= 63);
assert!(short_pass.len() < 8);
assert!(long_pass.len() > 63);
}
// ============================================================================
// Node Registry Tests
// ============================================================================
#[test]
fn test_node_registry() {
use wifi_densepose_desktop::domain::node::{
DiscoveredNode, MacAddress, NodeRegistry, HealthStatus, Chip, MeshRole, DiscoveryMethod
};
let mut registry = NodeRegistry::new();
assert!(registry.is_empty());
let node = DiscoveredNode {
ip: "192.168.1.100".into(),
mac: Some("AA:BB:CC:DD:EE:FF".into()),
hostname: Some("csi-node-1".into()),
node_id: 1,
firmware_version: Some("0.3.0".into()),
health: HealthStatus::Online,
last_seen: "2024-01-01T00:00:00Z".into(),
chip: Chip::Esp32s3,
mesh_role: MeshRole::Node,
discovery_method: DiscoveryMethod::Mdns,
tdm_slot: Some(0),
tdm_total: Some(4),
edge_tier: None,
uptime_secs: Some(3600),
capabilities: None,
friendly_name: None,
notes: None,
};
registry.upsert(MacAddress::new("AA:BB:CC:DD:EE:FF"), node);
assert_eq!(registry.len(), 1);
let retrieved = registry.get(&MacAddress::new("AA:BB:CC:DD:EE:FF"));
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().ip, "192.168.1.100");
}
// ============================================================================
// MAC Address Tests
// ============================================================================
#[test]
fn test_mac_address() {
use wifi_densepose_desktop::domain::node::MacAddress;
let mac = MacAddress::new("AA:BB:CC:DD:EE:FF");
assert_eq!(mac.to_string(), "AA:BB:CC:DD:EE:FF");
let mac2 = MacAddress::new("aa:bb:cc:dd:ee:ff");
assert_ne!(mac, mac2); // Case sensitive comparison
}
@@ -0,0 +1,130 @@
{
"running": true,
"startedAt": "2026-03-10T00:49:11.921Z",
"workers": {
"map": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:49:11.921Z"
},
"audit": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:51:11.921Z"
},
"optimize": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:53:11.921Z"
},
"consolidate": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:55:11.921Z"
},
"testgaps": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false,
"nextRun": "2026-03-10T00:57:11.921Z"
},
"predict": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
},
"document": {
"runCount": 0,
"successCount": 0,
"failureCount": 0,
"averageDurationMs": 0,
"isRunning": false
}
},
"config": {
"autoStart": false,
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/.claude-flow/logs",
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/.claude-flow/daemon-state.json",
"maxConcurrent": 2,
"workerTimeoutMs": 300000,
"resourceThresholds": {
"maxCpuLoad": 2,
"minFreeMemoryPercent": 20
},
"workers": [
{
"type": "map",
"intervalMs": 900000,
"offsetMs": 0,
"priority": "normal",
"description": "Codebase mapping",
"enabled": true
},
{
"type": "audit",
"intervalMs": 600000,
"offsetMs": 120000,
"priority": "critical",
"description": "Security analysis",
"enabled": true
},
{
"type": "optimize",
"intervalMs": 900000,
"offsetMs": 240000,
"priority": "high",
"description": "Performance optimization",
"enabled": true
},
{
"type": "consolidate",
"intervalMs": 1800000,
"offsetMs": 360000,
"priority": "low",
"description": "Memory consolidation",
"enabled": true
},
{
"type": "testgaps",
"intervalMs": 1200000,
"offsetMs": 480000,
"priority": "normal",
"description": "Test coverage analysis",
"enabled": true
},
{
"type": "predict",
"intervalMs": 600000,
"offsetMs": 0,
"priority": "low",
"description": "Predictive preloading",
"enabled": false
},
{
"type": "document",
"intervalMs": 3600000,
"offsetMs": 0,
"priority": "low",
"description": "Auto-documentation",
"enabled": false
}
]
},
"savedAt": "2026-03-10T00:49:11.921Z"
}
@@ -9,3 +9,20 @@
{"type":"edit","file":"unknown","timestamp":1772835930809,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772835942468,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772835952451,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773070971487,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773070977376,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773101503481,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773107530083,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773107530201,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773107530319,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773114830434,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773114834713,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773114838852,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150617007,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150621430,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150628006,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150640909,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150672276,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150677219,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150683839,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773150688912,"sessionId":null}
@@ -0,0 +1,12 @@
{
"id": "session-1773103750755",
"startedAt": "2026-03-10T00:49:10.755Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui",
"context": {},
"metrics": {
"edits": 14,
"commands": 0,
"tasks": 0,
"errors": 0
}
}
@@ -53,6 +53,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1246,6 +1247,7 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1315,6 +1317,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -1584,6 +1587,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -1625,6 +1629,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -1797,6 +1802,7 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -1,7 +1,7 @@
{
"name": "ruview-desktop-ui",
"private": true,
"version": "0.4.3",
"version": "0.4.4",
"type": "module",
"scripts": {
"dev": "vite",
@@ -49,6 +49,12 @@ const NetworkDiscovery: React.FC<NetworkDiscoveryProps> = ({ onNavigate }) => {
const [error, setError] = useState<string | null>(null);
const [selectedNode, setSelectedNode] = useState<DiscoveredNode | null>(null);
const [filterOnline, setFilterOnline] = useState(false);
// WiFi config state
const [wifiConfigPort, setWifiConfigPort] = useState<string | null>(null);
const [wifiSsid, setWifiSsid] = useState("");
const [wifiPassword, setWifiPassword] = useState("");
const [configuringWifi, setConfiguringWifi] = useState(false);
const [wifiResult, setWifiResult] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
// Manual add state
const [manualIp, setManualIp] = useState("");
@@ -83,6 +89,24 @@ const NetworkDiscovery: React.FC<NetworkDiscoveryProps> = ({ onNavigate }) => {
}
}, []);
const configureWifi = useCallback(async () => {
if (!wifiConfigPort || !wifiSsid) return;
setConfiguringWifi(true);
setWifiResult(null);
try {
const result = await invoke<string>("configure_esp32_wifi", {
port: wifiConfigPort,
ssid: wifiSsid,
password: wifiPassword,
});
setWifiResult(result);
} catch (err) {
setWifiResult(`Error: ${err instanceof Error ? err.message : String(err)}`);
} finally {
setConfiguringWifi(false);
}
}, [wifiConfigPort, wifiSsid, wifiPassword]);
const addManualNode = useCallback(async () => {
if (!manualIp.trim()) return;
setAddingManual(true);
@@ -471,23 +495,47 @@ const NetworkDiscovery: React.FC<NetworkDiscoveryProps> = ({ onNavigate }) => {
)}
</Td>
<Td>
{port.is_esp32_compatible && onNavigate && (
<button
onClick={() => onNavigate("flash")}
style={{
padding: "4px 12px",
background: "var(--accent)",
border: "none",
borderRadius: 4,
color: "#fff",
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
}}
>
Flash
</button>
)}
<div style={{ display: "flex", gap: 6 }}>
{port.is_esp32_compatible && (
<button
onClick={() => {
setWifiConfigPort(port.name);
setWifiSsid("");
setWifiPassword("");
setWifiResult(null);
}}
style={{
padding: "4px 10px",
background: "rgba(56, 139, 253, 0.15)",
border: "1px solid rgba(56, 139, 253, 0.3)",
borderRadius: 4,
color: "var(--accent)",
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
}}
>
WiFi
</button>
)}
{port.is_esp32_compatible && onNavigate && (
<button
onClick={() => onNavigate("flash")}
style={{
padding: "4px 10px",
background: "var(--accent)",
border: "none",
borderRadius: 4,
color: "#fff",
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
}}
>
Flash
</button>
)}
</div>
</Td>
</tr>
))}
@@ -579,6 +627,224 @@ const NetworkDiscovery: React.FC<NetworkDiscoveryProps> = ({ onNavigate }) => {
{selectedNode && (
<NodeDetailModal node={selectedNode} onClose={() => setSelectedNode(null)} />
)}
{/* WiFi Configuration Modal */}
{wifiConfigPort && (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
padding: "var(--space-5)",
}}
onClick={(e) => {
if (e.target === e.currentTarget && !configuringWifi) {
setWifiConfigPort(null);
}
}}
>
<div
style={{
background: "var(--bg-surface)",
borderRadius: 12,
padding: "var(--space-5)",
maxWidth: 420,
width: "100%",
border: "1px solid var(--border)",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "start",
marginBottom: "var(--space-4)",
}}
>
<div>
<h2 className="heading-md" style={{ margin: 0 }}>
Configure WiFi
</h2>
<p className="mono" style={{ color: "var(--text-muted)", marginTop: 4, fontSize: 13 }}>
{wifiConfigPort}
</p>
</div>
<button
onClick={() => setWifiConfigPort(null)}
disabled={configuringWifi}
style={{
background: "none",
border: "none",
fontSize: 20,
cursor: configuringWifi ? "not-allowed" : "pointer",
color: "var(--text-muted)",
padding: 4,
opacity: configuringWifi ? 0.5 : 1,
}}
>
×
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
<div>
<label
style={{
display: "block",
fontSize: 12,
fontWeight: 600,
color: "var(--text-secondary)",
marginBottom: 4,
}}
>
WiFi SSID *
</label>
<input
type="text"
placeholder="Your WiFi network name"
value={wifiSsid}
onChange={(e) => setWifiSsid(e.target.value)}
disabled={configuringWifi}
style={{
width: "100%",
padding: "10px 12px",
borderRadius: 6,
border: "1px solid var(--border)",
background: "var(--bg-base)",
color: "var(--text-primary)",
fontSize: 13,
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: 12,
fontWeight: 600,
color: "var(--text-secondary)",
marginBottom: 4,
}}
>
WiFi Password
</label>
<input
type="password"
placeholder="WiFi password"
value={wifiPassword}
onChange={(e) => setWifiPassword(e.target.value)}
disabled={configuringWifi}
style={{
width: "100%",
padding: "10px 12px",
borderRadius: 6,
border: "1px solid var(--border)",
background: "var(--bg-base)",
color: "var(--text-primary)",
fontSize: 13,
}}
/>
</div>
{wifiResult && (
<div
style={{
padding: "var(--space-3)",
borderRadius: 6,
fontSize: 12,
background: wifiResult.startsWith("Error")
? "rgba(248, 81, 73, 0.1)"
: wifiResult.includes("configured") || wifiResult.includes("saved")
? "rgba(63, 185, 80, 0.1)"
: "rgba(56, 139, 253, 0.1)",
border: wifiResult.startsWith("Error")
? "1px solid rgba(248, 81, 73, 0.3)"
: wifiResult.includes("configured") || wifiResult.includes("saved")
? "1px solid rgba(63, 185, 80, 0.3)"
: "1px solid rgba(56, 139, 253, 0.3)",
color: wifiResult.startsWith("Error")
? "var(--status-error)"
: wifiResult.includes("configured") || wifiResult.includes("saved")
? "var(--status-online)"
: "var(--accent)",
}}
>
<div style={{ fontWeight: 600, marginBottom: 6 }}>
{wifiResult.startsWith("Error") ? "Error" :
wifiResult.includes("configured") || wifiResult.includes("saved") ? "Success!" : "Commands Sent"}
</div>
<div style={{ fontFamily: "var(--font-mono)", whiteSpace: "pre-wrap", maxHeight: 100, overflow: "auto" }}>
{wifiResult}
</div>
{!wifiResult.startsWith("Error") && !wifiResult.includes("configured") && (
<div style={{ marginTop: 8, fontSize: 11, color: "var(--text-secondary)" }}>
If the ESP32 doesn't connect, try pressing its Reset button or re-flashing with WiFi credentials in the firmware.
</div>
)}
</div>
)}
<div style={{ display: "flex", gap: "var(--space-3)", marginTop: "var(--space-2)" }}>
<button
onClick={() => setWifiConfigPort(null)}
disabled={configuringWifi}
style={{
flex: 1,
padding: "10px 16px",
borderRadius: 6,
border: "1px solid var(--border)",
background: wifiResult ? "var(--accent)" : "transparent",
color: wifiResult ? "#fff" : "var(--text-secondary)",
fontSize: 13,
fontWeight: 600,
cursor: configuringWifi ? "not-allowed" : "pointer",
opacity: configuringWifi ? 0.5 : 1,
}}
>
{wifiResult ? "Done" : "Cancel"}
</button>
{!wifiResult && (
<button
onClick={configureWifi}
disabled={!wifiSsid.trim() || configuringWifi}
className="btn-gradient"
style={{
flex: 1,
opacity: !wifiSsid.trim() || configuringWifi ? 0.5 : 1,
}}
>
{configuringWifi ? "Configuring..." : "Configure WiFi"}
</button>
)}
{wifiResult && !wifiResult.startsWith("Error") && (
<button
onClick={() => {
setWifiResult(null);
}}
style={{
flex: 1,
padding: "10px 16px",
borderRadius: 6,
border: "1px solid var(--border)",
background: "transparent",
color: "var(--text-secondary)",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
}}
>
Try Again
</button>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
};
@@ -1,2 +1,2 @@
// Application version - single source of truth
export const APP_VERSION = "0.4.3";
export const APP_VERSION = "0.4.4";