feat: persistent config

This commit is contained in:
2025-10-15 21:52:24 +02:00
parent 7a7400422e
commit 993a431310
9 changed files with 1195 additions and 29 deletions

95
ctl.sh
View File

@@ -4,6 +4,25 @@ set -e
source .env
## Spore Control Script
## Usage: ./ctl.sh <command> [options]
##
## Commands:
## build [target] - Build firmware for target (base, d1_mini, etc.)
## flash [target] - Flash firmware to device
## uploadfs [target] - Upload filesystem to device
## ota update <ip> <target> - OTA update specific node
## ota all <target> - OTA update all nodes in cluster
## cluster members - List cluster members
## node wifi <ssid> <password> [ip] - Configure WiFi on node
## monitor - Monitor serial output
##
## Examples:
## ./ctl.sh build base
## ./ctl.sh flash d1_mini
## ./ctl.sh node wifi "MyNetwork" "MyPassword"
## ./ctl.sh node wifi "MyNetwork" "MyPassword" 192.168.1.100
function info {
sed -n 's/^##//p' ctl.sh
}
@@ -69,6 +88,82 @@ function cluster {
${@:-info}
}
function node {
function wifi {
if [ $# -lt 2 ]; then
echo "Usage: $0 node wifi <ssid> <password> [node_ip]"
echo " ssid: WiFi network name"
echo " password: WiFi password"
echo " node_ip: Optional IP address (defaults to API_NODE from .env)"
return 1
fi
local ssid="$1"
local password="$2"
local node_ip="${3:-$API_NODE}"
echo "Configuring WiFi on node $node_ip..."
echo "SSID: $ssid"
# Configure WiFi using the API endpoint
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "ssid=$ssid&password=$password" \
"http://$node_ip/api/network/wifi/config" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response
status=$(echo "$response_body" | jq -r '.status // "unknown"')
message=$(echo "$response_body" | jq -r '.message // "No message"')
config_saved=$(echo "$response_body" | jq -r '.config_saved // false')
restarting=$(echo "$response_body" | jq -r '.restarting // false')
connected=$(echo "$response_body" | jq -r '.connected // false')
ip=$(echo "$response_body" | jq -r '.ip // "N/A"')
echo "Status: $status"
echo "Message: $message"
echo "Config saved: $config_saved"
if [ "$restarting" = "true" ]; then
echo "Restarting: true"
echo "Note: Node will restart to apply new WiFi settings"
fi
echo "Connected: $connected"
if [ "$connected" = "true" ]; then
echo "IP Address: $ip"
fi
# Return appropriate exit code
if [ "$status" = "success" ]; then
echo "WiFi configuration completed successfully!"
return 0
else
echo "WiFi configuration failed!"
return 1
fi
}
${@:-info}
}
function monitor {
pio run --target monitor
}

View File

@@ -291,6 +291,51 @@ The system includes automatic WiFi fallback for robust operation:
## Configuration Management
SPORE implements a persistent configuration system that manages device settings across reboots and provides runtime reconfiguration capabilities.
### Configuration Architecture
The configuration system consists of several key components:
- **`Config` Class**: Central configuration management with default constants
- **LittleFS Storage**: Persistent file-based storage (`/config.json`)
- **Runtime Updates**: Live configuration changes via HTTP API
- **Automatic Persistence**: Configuration changes are automatically saved
### Configuration Categories
| Category | Description | Examples |
|----------|-------------|----------|
| **WiFi Configuration** | Network connection settings | SSID, password, timeouts |
| **Network Configuration** | Network service settings | UDP port, API server port |
| **Cluster Configuration** | Cluster management settings | Discovery intervals, heartbeat timing |
| **Node Status Thresholds** | Health monitoring thresholds | Active/inactive/dead timeouts |
| **System Configuration** | Core system settings | Restart delay, JSON document size |
| **Memory Management** | Resource management settings | Memory thresholds, HTTP request limits |
### Configuration Lifecycle
1. **Boot Process**: Load configuration from `/config.json` or use defaults
2. **Runtime Updates**: Configuration changes via HTTP API
3. **Persistent Storage**: Changes automatically saved to LittleFS
4. **Service Integration**: Configuration applied to all system services
### Default Value Management
All default values are defined as `constexpr` constants in the `Config` class:
```cpp
static constexpr const char* DEFAULT_WIFI_SSID = "shroud";
static constexpr uint16_t DEFAULT_UDP_PORT = 4210;
static constexpr unsigned long DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
```
This ensures:
- **Single Source of Truth**: All defaults defined once
- **Type Safety**: Compile-time type checking
- **Maintainability**: Easy to update default values
- **Consistency**: Same defaults used in `setDefaults()` and `loadFromFile()`
### Environment Variables
```bash
@@ -380,6 +425,8 @@ pio device monitor
## Related Documentation
- **[Configuration Management](./ConfigurationManagement.md)** - Persistent configuration system
- **[WiFi Configuration](./WiFiConfiguration.md)** - WiFi setup and reconfiguration process
- **[Task Management](./TaskManagement.md)** - Background task system
- **[API Reference](./API.md)** - REST API documentation
- **[TaskManager API](./TaskManager.md)** - TaskManager class reference

View File

@@ -0,0 +1,340 @@
# SPORE Configuration Management
## Overview
SPORE implements a comprehensive persistent configuration system that manages device settings across reboots and provides runtime reconfiguration capabilities. The system uses LittleFS for persistent storage and provides both programmatic and HTTP API access to configuration parameters.
## Configuration Architecture
### Core Components
- **`Config` Class**: Central configuration management
- **LittleFS Storage**: Persistent file-based storage (`/config.json`)
- **Default Constants**: Single source of truth for all default values
- **Runtime Updates**: Live configuration changes via HTTP API
- **Automatic Persistence**: Configuration changes are automatically saved
### Configuration Categories
The configuration system manages several categories of settings:
| Category | Description | Examples |
|----------|-------------|----------|
| **WiFi Configuration** | Network connection settings | SSID, password, timeouts |
| **Network Configuration** | Network service settings | UDP port, API server port |
| **Cluster Configuration** | Cluster management settings | Discovery intervals, heartbeat timing |
| **Node Status Thresholds** | Health monitoring thresholds | Active/inactive/dead timeouts |
| **System Configuration** | Core system settings | Restart delay, JSON document size |
| **Memory Management** | Resource management settings | Memory thresholds, HTTP request limits |
## Configuration Lifecycle
### 1. Boot Process
```mermaid
graph TD
A[System Boot] --> B[Initialize LittleFS]
B --> C{Config File Exists?}
C -->|Yes| D[Load from File]
C -->|No| E[Use Defaults]
D --> F[Apply Configuration]
E --> G[Save Defaults to File]
G --> F
F --> H[Start Services]
```
**Boot Sequence:**
1. **LittleFS Initialization**: Mount the filesystem for persistent storage
2. **Configuration Loading**: Attempt to load `/config.json`
3. **Fallback to Defaults**: If no config file exists, use hardcoded defaults
4. **Default Persistence**: Save default configuration to file for future boots
5. **Service Initialization**: Apply configuration to all system services
### 2. Runtime Configuration
```mermaid
graph TD
A[HTTP API Request] --> B[Validate Parameters]
B --> C[Update Config Object]
C --> D[Save to File]
D --> E{Requires Restart?}
E -->|Yes| F[Schedule Restart]
E -->|No| G[Apply Changes]
F --> H[Send Response]
G --> H
```
**Runtime Update Process:**
1. **API Request**: Configuration change via HTTP API
2. **Parameter Validation**: Validate input parameters
3. **Memory Update**: Update configuration object in memory
4. **Persistent Save**: Save changes to `/config.json`
5. **Service Notification**: Notify affected services of changes
6. **Restart if Needed**: Restart system for certain configuration changes
## Configuration File Format
### JSON Structure
The configuration is stored as a JSON file with the following structure:
```json
{
"wifi": {
"ssid": "MyNetwork",
"password": "mypassword",
"connect_timeout_ms": 15000,
"retry_delay_ms": 500
},
"network": {
"udp_port": 4210,
"api_server_port": 80
},
"cluster": {
"discovery_interval_ms": 1000,
"heartbeat_interval_ms": 5000,
"cluster_listen_interval_ms": 10,
"status_update_interval_ms": 1000,
"member_info_update_interval_ms": 10000,
"print_interval_ms": 5000
},
"thresholds": {
"node_active_threshold_ms": 10000,
"node_inactive_threshold_ms": 60000,
"node_dead_threshold_ms": 120000
},
"system": {
"restart_delay_ms": 10,
"json_doc_size": 1024
},
"memory": {
"low_memory_threshold_bytes": 10000,
"critical_memory_threshold_bytes": 5000,
"max_concurrent_http_requests": 3
},
"_meta": {
"version": "1.0",
"saved_at": 1234567890
}
}
```
### Metadata Fields
- **`version`**: Configuration schema version for future compatibility
- **`saved_at`**: Timestamp when configuration was saved (millis())
## Default Configuration Constants
All default values are defined as `constexpr` constants in the `Config` class header:
```cpp
// Default Configuration Constants
static constexpr const char* DEFAULT_WIFI_SSID = "shroud";
static constexpr const char* DEFAULT_WIFI_PASSWORD = "th3r31sn0sp00n";
static constexpr uint16_t DEFAULT_UDP_PORT = 4210;
static constexpr uint16_t DEFAULT_API_SERVER_PORT = 80;
// ... additional constants
```
### Benefits of Constants
- **Single Source of Truth**: All defaults defined once
- **Type Safety**: Compile-time type checking
- **Maintainability**: Easy to update default values
- **Consistency**: Same defaults used in `setDefaults()` and `loadFromFile()`
## Configuration Methods
### Core Methods
| Method | Purpose | Parameters |
|--------|---------|------------|
| `setDefaults()` | Initialize with default values | None |
| `loadFromFile()` | Load configuration from persistent storage | `filename` (optional) |
| `saveToFile()` | Save configuration to persistent storage | `filename` (optional) |
### Loading Process
```cpp
bool Config::loadFromFile(const String& filename) {
// 1. Initialize LittleFS
if (!LittleFS.begin()) {
LOG_ERROR("Config", "LittleFS not initialized");
return false;
}
// 2. Check file existence
if (!LittleFS.exists(filename)) {
LOG_DEBUG("Config", "Config file does not exist");
return false;
}
// 3. Parse JSON with fallback defaults
wifi_ssid = doc["wifi"]["ssid"] | DEFAULT_WIFI_SSID;
wifi_password = doc["wifi"]["password"] | DEFAULT_WIFI_PASSWORD;
// ... additional fields
return true;
}
```
### Saving Process
```cpp
bool Config::saveToFile(const String& filename) {
// 1. Create JSON document
JsonDocument doc;
// 2. Serialize all configuration fields
doc["wifi"]["ssid"] = wifi_ssid;
doc["wifi"]["password"] = wifi_password;
// ... additional fields
// 3. Add metadata
doc["_meta"]["version"] = "1.0";
doc["_meta"]["saved_at"] = millis();
// 4. Write to file
size_t bytesWritten = serializeJson(doc, file);
return bytesWritten > 0;
}
```
## Error Handling
### Common Error Scenarios
| Scenario | Error Handling | Recovery |
|----------|----------------|----------|
| **LittleFS Init Failure** | Log warning, use defaults | Continue with default configuration |
| **File Not Found** | Log debug message, return false | Caller handles fallback to defaults |
| **JSON Parse Error** | Log error, return false | Caller handles fallback to defaults |
| **Write Failure** | Log error, return false | Configuration not persisted |
| **Memory Allocation Failure** | Log error, return false | Operation aborted |
### Logging Levels
- **ERROR**: Critical failures that prevent operation
- **WARN**: Non-critical issues that affect functionality
- **INFO**: Normal operation events
- **DEBUG**: Detailed diagnostic information
## Configuration Validation
### Input Validation
- **Required Fields**: SSID and password are mandatory for WiFi configuration
- **Range Validation**: Numeric values are validated against reasonable ranges
- **Type Validation**: JSON parsing ensures correct data types
- **Length Limits**: String fields have maximum length constraints
### Default Value Fallback
The system uses the `|` operator for safe fallback to defaults:
```cpp
// Safe loading with fallback
wifi_ssid = doc["wifi"]["ssid"] | DEFAULT_WIFI_SSID;
udp_port = doc["network"]["udp_port"] | DEFAULT_UDP_PORT;
```
This ensures that:
- Missing fields use default values
- Invalid values are replaced with defaults
- System remains functional with partial configuration
## Performance Considerations
### Memory Usage
- **Configuration Object**: ~200 bytes in RAM
- **JSON Document**: ~1KB during parsing/saving
- **LittleFS Overhead**: ~2-4KB for filesystem
### Storage Requirements
- **Config File**: ~500-800 bytes on disk
- **LittleFS Minimum**: ~64KB partition size
- **Available Space**: Depends on flash size (1MB+ recommended)
### Processing Overhead
- **Load Time**: ~10-50ms for JSON parsing
- **Save Time**: ~20-100ms for JSON serialization
- **File I/O**: Minimal impact on system performance
## Security Considerations
### Current Implementation
- **Local Storage Only**: Configuration stored on device filesystem
- **No Encryption**: Plain text storage (LAN-only access assumed)
- **Access Control**: No authentication for configuration changes
### Future Enhancements
- **Configuration Encryption**: Encrypt sensitive fields (passwords)
- **Access Control**: Authentication for configuration changes
- **Audit Logging**: Track configuration modifications
- **Backup/Restore**: Configuration backup and restore capabilities
## Troubleshooting
### Common Issues
1. **Configuration Not Persisting**
- Check LittleFS initialization
- Verify file write permissions
- Monitor available flash space
2. **Default Values Not Applied**
- Verify constants are properly defined
- Check JSON parsing errors
- Ensure fallback logic is working
3. **Configuration Corruption**
- Delete `/config.json` to reset to defaults
- Check for JSON syntax errors
- Verify file system integrity
### Debug Commands
```bash
# Check configuration status
curl -s http://192.168.1.100/api/network/status | jq '.'
# View current WiFi settings
curl -s http://192.168.1.100/api/network/status | jq '.wifi'
# Test configuration save
curl -X POST http://192.168.1.100/api/network/wifi/config \
-d "ssid=TestNetwork&password=testpass"
```
## Best Practices
### Configuration Management
1. **Use Constants**: Always define defaults as constants
2. **Validate Input**: Check all configuration parameters
3. **Handle Errors**: Implement proper error handling
4. **Log Changes**: Log configuration modifications
5. **Test Fallbacks**: Ensure default fallbacks work correctly
### Development Guidelines
1. **Single Source**: Define each default value only once
2. **Type Safety**: Use appropriate data types
3. **Documentation**: Document all configuration parameters
4. **Versioning**: Include version metadata in config files
5. **Backward Compatibility**: Handle old configuration formats
## Related Documentation
- **[WiFi Configuration Process](./WiFiConfiguration.md)** - Detailed WiFi setup workflow
- **[API Reference](./API.md)** - HTTP API for configuration management
- **[Architecture Overview](./Architecture.md)** - System architecture and components
- **[OpenAPI Specification](../api/)** - Machine-readable API specification

478
docs/WiFiConfiguration.md Normal file
View File

@@ -0,0 +1,478 @@
# SPORE WiFi Configuration Process
## Overview
SPORE implements a WiFi configuration system that handles initial setup, runtime reconfiguration, and automatic fallback mechanisms. The system supports both Station (STA) and Access Point (AP) modes with seamless switching between them.
## WiFi Configuration Architecture
### Core Components
- **`NetworkManager`**: Handles WiFi operations and configuration
- **`NetworkService`**: Provides HTTP API endpoints for WiFi management
- **`Config`**: Stores WiFi credentials and connection parameters
- **LittleFS**: Persistent storage for WiFi configuration
- **ESP8266 WiFi Library**: Low-level WiFi operations
### Configuration Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `wifi_ssid` | String | "shroud" | Network SSID for connection |
| `wifi_password` | String | "th3r31sn0sp00n" | Network password |
| `wifi_connect_timeout_ms` | uint32_t | 15000 | Connection timeout (15 seconds) |
| `wifi_retry_delay_ms` | uint32_t | 500 | Delay between connection attempts |
## WiFi Configuration Lifecycle
### 1. Boot Process
```mermaid
graph TD
A[System Boot] --> B[Initialize LittleFS]
B --> C[Load Configuration]
C --> D[Initialize WiFi Mode]
D --> E[Set Hostname from MAC]
E --> F[Attempt STA Connection]
F --> G{Connection Successful?}
G -->|Yes| H[STA Mode Active]
G -->|No| I[Switch to AP Mode]
I --> J[Create Access Point]
J --> K[AP Mode Active]
H --> L[Start UDP Services]
K --> L
L --> M[Initialize Node Context]
M --> N[Start Cluster Services]
```
**Detailed Boot Sequence:**
1. **LittleFS Initialization**
```cpp
if (!LittleFS.begin()) {
LOG_WARN("Config", "Failed to initialize LittleFS, using defaults");
setDefaults();
return;
}
```
2. **Configuration Loading**
- Load WiFi credentials from `/config.json`
- Fall back to defaults if file doesn't exist
- Validate configuration parameters
3. **WiFi Mode Initialization**
```cpp
WiFi.mode(WIFI_STA);
WiFi.begin(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
```
4. **Hostname Generation**
```cpp
void NetworkManager::setHostnameFromMac() {
uint8_t mac[6];
WiFi.macAddress(mac);
char buf[32];
sprintf(buf, "esp-%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
WiFi.hostname(buf);
ctx.hostname = String(buf);
}
```
5. **Connection Attempt**
```cpp
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - startAttemptTime < ctx.config.wifi_connect_timeout_ms) {
delay(ctx.config.wifi_retry_delay_ms);
}
```
6. **Fallback to AP Mode**
```cpp
if (WiFi.status() != WL_CONNECTED) {
LOG_WARN("WiFi", "Failed to connect to AP. Creating AP...");
WiFi.mode(WIFI_AP);
WiFi.softAP(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
}
```
### 2. Runtime Reconfiguration
```mermaid
graph TD
A[HTTP API Request] --> B[Validate Parameters]
B --> C[Update Config Object]
C --> D[Save to Persistent Storage]
D --> E[Send Response to Client]
E --> F[Schedule Node Restart]
F --> G[Node Restarts]
G --> H[Apply New Configuration]
H --> I[Attempt New Connection]
```
**Runtime Reconfiguration Process:**
1. **API Request Validation**
```cpp
if (!request->hasParam("ssid", true) || !request->hasParam("password", true)) {
request->send(400, "application/json", "{\"error\": \"Missing required parameters\"}");
return;
}
```
2. **Configuration Update**
```cpp
void NetworkManager::setWiFiConfig(const String& ssid, const String& password,
uint32_t connect_timeout_ms, uint32_t retry_delay_ms) {
ctx.config.wifi_ssid = ssid;
ctx.config.wifi_password = password;
ctx.config.wifi_connect_timeout_ms = connect_timeout_ms;
ctx.config.wifi_retry_delay_ms = retry_delay_ms;
}
```
3. **Persistent Storage**
```cpp
bool configSaved = networkManager.saveConfig();
if (!configSaved) {
LOG_WARN("NetworkService", "Failed to save WiFi configuration to persistent storage");
}
```
4. **Restart Scheduling**
```cpp
request->onDisconnect([this]() {
LOG_INFO("NetworkService", "Restarting node to apply WiFi configuration...");
delay(100); // Give time for response to be sent
networkManager.restartNode();
});
```
## WiFi Modes
### Station Mode (STA)
**Purpose**: Connect to existing WiFi network as a client
**Configuration**:
- SSID and password required
- Automatic IP assignment via DHCP
- Hostname set from MAC address
- UDP services on configured port
**Connection Process**:
1. Set WiFi mode to STA
2. Begin connection with credentials
3. Wait for connection with timeout
4. Set hostname and start services
### Access Point Mode (AP)
**Purpose**: Create WiFi hotspot when STA connection fails
**Configuration**:
- Uses configured SSID/password for AP
- Fixed IP address (usually 192.168.4.1)
- Allows other devices to connect
- Maintains cluster functionality
**AP Creation Process**:
1. Switch WiFi mode to AP
2. Create soft access point
3. Set AP IP address
4. Start services on AP IP
## HTTP API Endpoints
### Network Status
#### GET `/api/network/status`
Returns comprehensive WiFi and network status information.
**Response Fields**:
```json
{
"wifi": {
"connected": true,
"mode": "STA",
"ssid": "MyNetwork",
"ip": "192.168.1.100",
"mac": "AA:BB:CC:DD:EE:FF",
"hostname": "esp-AABBCCDDEEFF",
"rssi": -45,
"ap_ip": "192.168.4.1",
"ap_mac": "AA:BB:CC:DD:EE:FF",
"stations_connected": 0
}
}
```
### WiFi Scanning
#### GET `/api/network/wifi/scan`
Returns list of available WiFi networks from last scan.
**Response Fields**:
```json
{
"access_points": [
{
"ssid": "MyNetwork",
"rssi": -45,
"channel": 6,
"encryption_type": 4,
"hidden": false,
"bssid": "AA:BB:CC:DD:EE:FF"
}
]
}
```
#### POST `/api/network/wifi/scan`
Initiates a new WiFi network scan.
**Response**:
```json
{
"status": "scanning",
"message": "WiFi scan started"
}
```
### WiFi Configuration
#### POST `/api/network/wifi/config`
Configures WiFi connection with new credentials.
**Parameters**:
- `ssid` (required): Network SSID
- `password` (required): Network password
- `connect_timeout_ms` (optional): Connection timeout (default: 10000)
- `retry_delay_ms` (optional): Retry delay (default: 500)
**Request Example**:
```bash
curl -X POST http://192.168.1.100/api/network/wifi/config \
-d "ssid=MyNewNetwork&password=newpassword&connect_timeout_ms=15000"
```
**Response**:
```json
{
"status": "success",
"message": "WiFi configuration updated and saved",
"config_saved": true,
"restarting": true
}
```
## WiFi Scanning Process
### Scanning Implementation
```cpp
void NetworkManager::scanWifi() {
if (!isScanning) {
isScanning = true;
LOG_INFO("WiFi", "Starting WiFi scan...");
WiFi.scanNetworksAsync([this](int networksFound) {
LOG_INFO("WiFi", "Scan completed, found " + String(networksFound) + " networks");
this->processAccessPoints();
this->isScanning = false;
}, true);
}
}
```
### Access Point Processing
```cpp
void NetworkManager::processAccessPoints() {
int numNetworks = WiFi.scanComplete();
if (numNetworks <= 0) return;
accessPoints.clear();
for (int i = 0; i < numNetworks; i++) {
AccessPoint ap;
ap.ssid = WiFi.SSID(i);
ap.rssi = WiFi.RSSI(i);
ap.encryptionType = WiFi.encryptionType(i);
ap.channel = WiFi.channel(i);
ap.isHidden = ap.ssid.length() == 0;
uint8_t* newBssid = new uint8_t[6];
memcpy(newBssid, WiFi.BSSID(i), 6);
ap.bssid = newBssid;
accessPoints.push_back(ap);
}
WiFi.scanDelete();
}
```
## Error Handling
### Connection Failures
| Error Type | Handling | Recovery |
|------------|----------|----------|
| **Invalid Credentials** | Log error, switch to AP mode | Manual reconfiguration via API |
| **Network Unavailable** | Log warning, switch to AP mode | Automatic retry on next boot |
| **Timeout** | Log timeout, switch to AP mode | Increase timeout or check network |
| **Hardware Failure** | Log error, continue in AP mode | Hardware replacement required |
### API Error Responses
```json
{
"error": "Missing required parameters",
"status": "error"
}
```
```json
{
"error": "Failed to save configuration",
"status": "error",
"config_saved": false
}
```
## Security Considerations
### Current Implementation
- **Plain Text Storage**: WiFi passwords stored unencrypted
- **Local Network Only**: No internet exposure
- **No Authentication**: API access without authentication
- **MAC-based Hostnames**: Predictable hostname generation
### Security Best Practices
1. **Network Isolation**: Keep SPORE devices on isolated network
2. **Strong Passwords**: Use complex WiFi passwords
3. **Regular Updates**: Keep firmware updated
4. **Access Control**: Implement network-level access control
### Future Security Enhancements
- **Password Encryption**: Encrypt stored WiFi credentials
- **API Authentication**: Add authentication to configuration API
- **Certificate-based Security**: Implement TLS/SSL
- **Access Control Lists**: Role-based configuration access
## Troubleshooting
### Common Issues
1. **WiFi Connection Fails**
- Check SSID and password
- Verify network availability
- Check signal strength
- Increase connection timeout
2. **Configuration Not Persisting**
- Check LittleFS initialization
- Verify file write permissions
- Monitor available flash space
3. **AP Mode Not Working**
- Check AP credentials
- Verify IP address assignment
- Check for IP conflicts
4. **Scan Not Finding Networks**
- Wait for scan completion
- Check WiFi hardware
- Verify scan permissions
### Debug Commands
```bash
# Check WiFi status
curl -s http://192.168.1.100/api/network/status | jq '.wifi'
# Scan for networks
curl -X POST http://192.168.1.100/api/network/wifi/scan
# View scan results
curl -s http://192.168.1.100/api/network/wifi/scan | jq '.'
# Test configuration
curl -X POST http://192.168.1.100/api/network/wifi/config \
-d "ssid=TestNetwork&password=testpass"
```
### Log Analysis
**Successful Connection**:
```
[INFO] WiFi: Connected to AP, IP: 192.168.1.100
[INFO] WiFi: Hostname set to: esp-AABBCCDDEEFF
[INFO] WiFi: UDP listening on port 4210
```
**Connection Failure**:
```
[WARN] WiFi: Failed to connect to AP. Creating AP...
[INFO] WiFi: AP created, IP: 192.168.4.1
```
**Configuration Update**:
```
[INFO] NetworkService: Restarting node to apply WiFi configuration...
[INFO] WiFi: Connecting to AP...
```
## Performance Considerations
### Connection Time
- **STA Connection**: 5-15 seconds typical
- **AP Creation**: 1-3 seconds
- **Scan Duration**: 5-10 seconds
- **Configuration Save**: 100-500ms
### Memory Usage
- **WiFi Stack**: ~20-30KB RAM
- **Scan Results**: ~1KB per network
- **Configuration**: ~200 bytes
- **API Buffers**: ~2-4KB
### Network Overhead
- **Scan Packets**: Minimal impact
- **Configuration API**: ~500 bytes per request
- **Status Updates**: ~200 bytes per response
## Best Practices
### Configuration Management
1. **Use Strong Passwords**: Implement password complexity requirements
2. **Set Appropriate Timeouts**: Balance connection speed vs reliability
3. **Monitor Connection Quality**: Track RSSI and connection stability
4. **Implement Retry Logic**: Handle temporary network issues
5. **Log Configuration Changes**: Audit trail for troubleshooting
### Development Guidelines
1. **Handle All Error Cases**: Implement comprehensive error handling
2. **Provide Clear Feedback**: Inform users of connection status
3. **Optimize Scan Frequency**: Balance discovery vs performance
4. **Test Fallback Scenarios**: Ensure AP mode works correctly
5. **Document Configuration Options**: Clear parameter documentation
## Related Documentation
- **[Configuration Management](./ConfigurationManagement.md)** - Persistent configuration system
- **[API Reference](./API.md)** - Complete HTTP API documentation
- **[Architecture Overview](./Architecture.md)** - System architecture and components
- **[OpenAPI Specification](../api/)** - Machine-readable API specification

View File

@@ -27,6 +27,8 @@ public:
void setWiFiConfig(const String& ssid, const String& password,
uint32_t connect_timeout_ms = 10000,
uint32_t retry_delay_ms = 500);
bool saveConfig();
void restartNode();
// Network status methods
bool isConnected() const { return WiFi.isConnected(); }

View File

@@ -1,9 +1,33 @@
#pragma once
#include <Arduino.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
class Config {
public:
// Default Configuration Constants
static constexpr const char* DEFAULT_WIFI_SSID = "shroud";
static constexpr const char* DEFAULT_WIFI_PASSWORD = "th3r31sn0sp00n";
static constexpr uint16_t DEFAULT_UDP_PORT = 4210;
static constexpr uint16_t DEFAULT_API_SERVER_PORT = 80;
static constexpr unsigned long DEFAULT_DISCOVERY_INTERVAL_MS = 1000;
static constexpr unsigned long DEFAULT_CLUSTER_LISTEN_INTERVAL_MS = 10;
static constexpr unsigned long DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
static constexpr unsigned long DEFAULT_STATUS_UPDATE_INTERVAL_MS = 1000;
static constexpr unsigned long DEFAULT_MEMBER_INFO_UPDATE_INTERVAL_MS = 10000;
static constexpr unsigned long DEFAULT_PRINT_INTERVAL_MS = 5000;
static constexpr unsigned long DEFAULT_NODE_ACTIVE_THRESHOLD_MS = 10000;
static constexpr unsigned long DEFAULT_NODE_INACTIVE_THRESHOLD_MS = 60000;
static constexpr unsigned long DEFAULT_NODE_DEAD_THRESHOLD_MS = 120000;
static constexpr unsigned long DEFAULT_WIFI_CONNECT_TIMEOUT_MS = 15000;
static constexpr unsigned long DEFAULT_WIFI_RETRY_DELAY_MS = 500;
static constexpr unsigned long DEFAULT_RESTART_DELAY_MS = 10;
static constexpr uint16_t DEFAULT_JSON_DOC_SIZE = 1024;
static constexpr uint32_t DEFAULT_LOW_MEMORY_THRESHOLD_BYTES = 10000;
static constexpr uint32_t DEFAULT_CRITICAL_MEMORY_THRESHOLD_BYTES = 5000;
static constexpr size_t DEFAULT_MAX_CONCURRENT_HTTP_REQUESTS = 3;
// WiFi Configuration
String wifi_ssid;
String wifi_password;
@@ -40,4 +64,12 @@ public:
// Constructor
Config();
// Persistence methods
bool saveToFile(const String& filename = "/config.json");
bool loadFromFile(const String& filename = "/config.json");
void setDefaults();
private:
static const char* CONFIG_FILE_PATH;
};

View File

@@ -65,6 +65,16 @@ void NetworkManager::setWiFiConfig(const String& ssid, const String& password,
ctx.config.wifi_retry_delay_ms = retry_delay_ms;
}
bool NetworkManager::saveConfig() {
return ctx.config.saveToFile();
}
void NetworkManager::restartNode() {
LOG_INFO("NetworkManager", "Restarting node after WiFi configuration change...");
delay(100); // Give time for response to be sent
ESP.restart();
}
void NetworkManager::setHostnameFromMac() {
uint8_t mac[6];
WiFi.macAddress(mac);

View File

@@ -1,4 +1,5 @@
#include "spore/services/NetworkService.h"
#include "spore/util/Logging.h"
#include <ArduinoJson.h>
NetworkService::NetworkService(NetworkManager& networkManager)
@@ -116,21 +117,34 @@ void NetworkService::handleSetWifiConfig(AsyncWebServerRequest* request) {
retry_delay_ms = request->getParam("retry_delay_ms", true)->value().toInt();
}
// Update configuration
// Update configuration in memory
networkManager.setWiFiConfig(ssid, password, connect_timeout_ms, retry_delay_ms);
// Attempt to connect with new settings
networkManager.setupWiFi();
// Save configuration to persistent storage
bool configSaved = networkManager.saveConfig();
if (!configSaved) {
LOG_WARN("NetworkService", "Failed to save WiFi configuration to persistent storage");
}
// Prepare response
JsonDocument doc;
doc["status"] = "success";
doc["message"] = "WiFi configuration updated";
doc["connected"] = WiFi.isConnected();
if (WiFi.isConnected()) {
doc["ip"] = WiFi.localIP().toString();
}
doc["message"] = "WiFi configuration updated and saved";
doc["config_saved"] = configSaved;
doc["restarting"] = true;
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
// Send response before restarting
AsyncWebServerResponse* response = request->beginResponse(200, "application/json", json);
response->addHeader("Connection", "close");
request->send(response);
// Restart the node to apply new WiFi settings
request->onDisconnect([this]() {
LOG_INFO("NetworkService", "Restarting node to apply WiFi configuration...");
delay(100); // Give time for response to be sent
networkManager.restartNode();
});
}

View File

@@ -1,37 +1,185 @@
#include "spore/types/Config.h"
#include "spore/util/Logging.h"
const char* Config::CONFIG_FILE_PATH = "/config.json";
Config::Config() {
// Initialize LittleFS
if (!LittleFS.begin()) {
LOG_WARN("Config", "Failed to initialize LittleFS, using defaults");
setDefaults();
return;
}
// Try to load configuration from file
if (!loadFromFile()) {
LOG_INFO("Config", "No config file found, using defaults");
setDefaults();
// Save defaults to file for future use
saveToFile();
} else {
LOG_INFO("Config", "Configuration loaded from file");
}
}
void Config::setDefaults() {
// WiFi Configuration
wifi_ssid = "shroud";
wifi_password = "th3r31sn0sp00n";
wifi_ssid = DEFAULT_WIFI_SSID;
wifi_password = DEFAULT_WIFI_PASSWORD;
// Network Configuration
udp_port = 4210;
api_server_port = 80;
udp_port = DEFAULT_UDP_PORT;
api_server_port = DEFAULT_API_SERVER_PORT;
// Cluster Configuration
discovery_interval_ms = 1000; // TODO retire this in favor of heartbeat_interval_ms
cluster_listen_interval_ms = 10;
heartbeat_interval_ms = 5000;
status_update_interval_ms = 1000;
member_info_update_interval_ms = 10000; // TODO retire this in favor of heartbeat_interval_ms
print_interval_ms = 5000;
discovery_interval_ms = DEFAULT_DISCOVERY_INTERVAL_MS; // TODO retire this in favor of heartbeat_interval_ms
cluster_listen_interval_ms = DEFAULT_CLUSTER_LISTEN_INTERVAL_MS;
heartbeat_interval_ms = DEFAULT_HEARTBEAT_INTERVAL_MS;
status_update_interval_ms = DEFAULT_STATUS_UPDATE_INTERVAL_MS;
member_info_update_interval_ms = DEFAULT_MEMBER_INFO_UPDATE_INTERVAL_MS; // TODO retire this in favor of heartbeat_interval_ms
print_interval_ms = DEFAULT_PRINT_INTERVAL_MS;
// Node Status Thresholds
node_active_threshold_ms = 10000;
node_inactive_threshold_ms = 60000;
node_dead_threshold_ms = 120000;
node_active_threshold_ms = DEFAULT_NODE_ACTIVE_THRESHOLD_MS;
node_inactive_threshold_ms = DEFAULT_NODE_INACTIVE_THRESHOLD_MS;
node_dead_threshold_ms = DEFAULT_NODE_DEAD_THRESHOLD_MS;
// WiFi Connection
wifi_connect_timeout_ms = 15000;
wifi_retry_delay_ms = 500;
wifi_connect_timeout_ms = DEFAULT_WIFI_CONNECT_TIMEOUT_MS;
wifi_retry_delay_ms = DEFAULT_WIFI_RETRY_DELAY_MS;
// System Configuration
restart_delay_ms = 10;
json_doc_size = 1024;
restart_delay_ms = DEFAULT_RESTART_DELAY_MS;
json_doc_size = DEFAULT_JSON_DOC_SIZE;
// Memory Management
low_memory_threshold_bytes = 10000; // 10KB
critical_memory_threshold_bytes = 5000; // 5KB
max_concurrent_http_requests = 3;
low_memory_threshold_bytes = DEFAULT_LOW_MEMORY_THRESHOLD_BYTES; // 10KB
critical_memory_threshold_bytes = DEFAULT_CRITICAL_MEMORY_THRESHOLD_BYTES; // 5KB
max_concurrent_http_requests = DEFAULT_MAX_CONCURRENT_HTTP_REQUESTS;
}
bool Config::saveToFile(const String& filename) {
if (!LittleFS.begin()) {
LOG_ERROR("Config", "LittleFS not initialized, cannot save config");
return false;
}
File file = LittleFS.open(filename, "w");
if (!file) {
LOG_ERROR("Config", "Failed to open config file for writing: " + filename);
return false;
}
JsonDocument doc;
// WiFi Configuration
doc["wifi"]["ssid"] = wifi_ssid;
doc["wifi"]["password"] = wifi_password;
doc["wifi"]["connect_timeout_ms"] = wifi_connect_timeout_ms;
doc["wifi"]["retry_delay_ms"] = wifi_retry_delay_ms;
// Network Configuration
doc["network"]["udp_port"] = udp_port;
doc["network"]["api_server_port"] = api_server_port;
// Cluster Configuration
doc["cluster"]["discovery_interval_ms"] = discovery_interval_ms;
doc["cluster"]["heartbeat_interval_ms"] = heartbeat_interval_ms;
doc["cluster"]["cluster_listen_interval_ms"] = cluster_listen_interval_ms;
doc["cluster"]["status_update_interval_ms"] = status_update_interval_ms;
doc["cluster"]["member_info_update_interval_ms"] = member_info_update_interval_ms;
doc["cluster"]["print_interval_ms"] = print_interval_ms;
// Node Status Thresholds
doc["thresholds"]["node_active_threshold_ms"] = node_active_threshold_ms;
doc["thresholds"]["node_inactive_threshold_ms"] = node_inactive_threshold_ms;
doc["thresholds"]["node_dead_threshold_ms"] = node_dead_threshold_ms;
// System Configuration
doc["system"]["restart_delay_ms"] = restart_delay_ms;
doc["system"]["json_doc_size"] = json_doc_size;
// Memory Management
doc["memory"]["low_memory_threshold_bytes"] = low_memory_threshold_bytes;
doc["memory"]["critical_memory_threshold_bytes"] = critical_memory_threshold_bytes;
doc["memory"]["max_concurrent_http_requests"] = max_concurrent_http_requests;
// Add metadata
doc["_meta"]["version"] = "1.0";
doc["_meta"]["saved_at"] = millis();
size_t bytesWritten = serializeJson(doc, file);
file.close();
if (bytesWritten > 0) {
LOG_INFO("Config", "Configuration saved to " + filename + " (" + String(bytesWritten) + " bytes)");
return true;
} else {
LOG_ERROR("Config", "Failed to write configuration to file");
return false;
}
}
bool Config::loadFromFile(const String& filename) {
if (!LittleFS.begin()) {
LOG_ERROR("Config", "LittleFS not initialized, cannot load config");
return false;
}
if (!LittleFS.exists(filename)) {
LOG_DEBUG("Config", "Config file does not exist: " + filename);
return false;
}
File file = LittleFS.open(filename, "r");
if (!file) {
LOG_ERROR("Config", "Failed to open config file for reading: " + filename);
return false;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
LOG_ERROR("Config", "Failed to parse config file: " + String(error.c_str()));
return false;
}
// Load WiFi Configuration with defaults
wifi_ssid = doc["wifi"]["ssid"] | DEFAULT_WIFI_SSID;
wifi_password = doc["wifi"]["password"] | DEFAULT_WIFI_PASSWORD;
wifi_connect_timeout_ms = doc["wifi"]["connect_timeout_ms"] | DEFAULT_WIFI_CONNECT_TIMEOUT_MS;
wifi_retry_delay_ms = doc["wifi"]["retry_delay_ms"] | DEFAULT_WIFI_RETRY_DELAY_MS;
// Load Network Configuration with defaults
udp_port = doc["network"]["udp_port"] | DEFAULT_UDP_PORT;
api_server_port = doc["network"]["api_server_port"] | DEFAULT_API_SERVER_PORT;
// Load Cluster Configuration with defaults
discovery_interval_ms = doc["cluster"]["discovery_interval_ms"] | DEFAULT_DISCOVERY_INTERVAL_MS;
heartbeat_interval_ms = doc["cluster"]["heartbeat_interval_ms"] | DEFAULT_HEARTBEAT_INTERVAL_MS;
cluster_listen_interval_ms = doc["cluster"]["cluster_listen_interval_ms"] | DEFAULT_CLUSTER_LISTEN_INTERVAL_MS;
status_update_interval_ms = doc["cluster"]["status_update_interval_ms"] | DEFAULT_STATUS_UPDATE_INTERVAL_MS;
member_info_update_interval_ms = doc["cluster"]["member_info_update_interval_ms"] | DEFAULT_MEMBER_INFO_UPDATE_INTERVAL_MS;
print_interval_ms = doc["cluster"]["print_interval_ms"] | DEFAULT_PRINT_INTERVAL_MS;
// Load Node Status Thresholds with defaults
node_active_threshold_ms = doc["thresholds"]["node_active_threshold_ms"] | DEFAULT_NODE_ACTIVE_THRESHOLD_MS;
node_inactive_threshold_ms = doc["thresholds"]["node_inactive_threshold_ms"] | DEFAULT_NODE_INACTIVE_THRESHOLD_MS;
node_dead_threshold_ms = doc["thresholds"]["node_dead_threshold_ms"] | DEFAULT_NODE_DEAD_THRESHOLD_MS;
// Load System Configuration with defaults
restart_delay_ms = doc["system"]["restart_delay_ms"] | DEFAULT_RESTART_DELAY_MS;
json_doc_size = doc["system"]["json_doc_size"] | DEFAULT_JSON_DOC_SIZE;
// Load Memory Management with defaults
low_memory_threshold_bytes = doc["memory"]["low_memory_threshold_bytes"] | DEFAULT_LOW_MEMORY_THRESHOLD_BYTES;
critical_memory_threshold_bytes = doc["memory"]["critical_memory_threshold_bytes"] | DEFAULT_CRITICAL_MEMORY_THRESHOLD_BYTES;
max_concurrent_http_requests = doc["memory"]["max_concurrent_http_requests"] | DEFAULT_MAX_CONCURRENT_HTTP_REQUESTS;
LOG_DEBUG("Config", "Loaded WiFi SSID: " + wifi_ssid);
LOG_DEBUG("Config", "Config file version: " + String(doc["_meta"]["version"] | "unknown"));
return true;
}