diff --git a/.gitignore b/.gitignore index 9485d0c..c354b1a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .DS_Store .clang_complete .gcc-flags.json - +lxc-services/ Thumbs.db arduino-esp32.7z IMPROVEMENT.md @@ -16,11 +16,10 @@ IMPLEMENTATION_SUMMARY.md PHASE2_IMPLEMENTATION_COMPLETE.md PHASE3_IMPLEMENTATION_COMPLETE.md improvements_plan.md - docs/ .serena/ test/ -.github/workflows/ +.github/ .pio/ .vscode/ .pioenvs/ diff --git a/README.md b/README.md index 6602028..2c9b05e 100644 --- a/README.md +++ b/README.md @@ -1,369 +1,264 @@ -# ESP32 Audio Streamer v2.0 +# ESP32 Audio Streamer v2.0 - Quick Start Guide **Professional-grade I2S audio streaming system for ESP32 with comprehensive reliability features.** -[![Build Status](https://img.shields.io/badge/build-SUCCESS-brightgreen)](README.md) -[![RAM Usage](https://img.shields.io/badge/RAM-15.0%25-blue)](README.md) -[![Flash Usage](https://img.shields.io/badge/Flash-59.3%25-blue)](README.md) -[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Build Status](https://img.shields.io/badge/build-SUCCESS-brightgreen)](#) +[![RAM Usage](https://img.shields.io/badge/RAM-15.0%25-blue)](#) +[![Flash Usage](https://img.shields.io/badge/Flash-59.6%25-blue)](#) +[![License](https://img.shields.io/badge/license-MIT-green)](#) --- -## Overview +## ๐Ÿ“š Documentation Structure -The ESP32 Audio Streamer is a robust, production-ready system for streaming high-quality audio from an INMP441 I2S microphone to a TCP server over WiFi. Designed with reliability, error recovery, and adaptive optimization in mind. +This project now uses **3 consolidated documentation files**: -**Key Characteristics:** -- 16kHz, 16-bit mono audio acquisition -- ~256 Kbps streaming rate (32 KB/sec) -- Automatic error detection and recovery -- Adaptive resource management based on signal strength -- Real-time system monitoring and control via serial commands -- Comprehensive diagnostics and troubleshooting +1. **README.md** (this file) - Quick Start & Overview +2. **DEVELOPMENT.md** - Complete Technical Reference +3. **TROUBLESHOOTING.md** - Diagnostics & Solutions --- -## Features - -### ๐ŸŽฏ Core Functionality -โœ… **I2S Audio Acquisition** - Digital audio input from INMP441 microphone -โœ… **WiFi Connectivity** - Automatic reconnection with exponential backoff -โœ… **TCP Streaming** - Reliable data transmission to remote server -โœ… **State Machine** - Explicit system state management with clear transitions - -### ๐Ÿ›ก๏ธ Reliability Features -โœ… **Configuration Validation** - Startup verification of all critical parameters -โœ… **Error Classification** - Intelligent error categorization (transient/permanent/fatal) -โœ… **Memory Leak Detection** - Automatic heap trend monitoring -โœ… **TCP State Machine** - Explicit connection state tracking with validation -โœ… **Health Checks** - Real-time system health scoring - -### ๐Ÿš€ Performance Optimization -โœ… **Adaptive Buffering** - Dynamic buffer sizing based on WiFi signal strength -โœ… **Exponential Backoff** - Intelligent retry strategy for connection failures -โœ… **Watchdog Timer** - Hardware watchdog with timeout validation -โœ… **Non-blocking Operations** - Responsive main loop with configurable task yielding - -### ๐Ÿ”ง System Control & Monitoring -โœ… **Serial Command Interface** - 8 runtime commands for system control -โœ… **Real-time Statistics** - Comprehensive system metrics every 5 minutes -โœ… **Health Monitoring** - Visual status indicators and diagnostic output -โœ… **Debug Modes** - 6 configurable debug levels (production to verbose) - ---- - -## Quick Start +## ๐Ÿš€ Quick Start ### Requirements + - **Hardware**: ESP32-DevKit or Seeed XIAO ESP32-S3 - **Microphone**: INMP441 I2S digital microphone - **Tools**: PlatformIO IDE or CLI -- **Server**: TCP server listening on configured host:port +- **Server**: TCP server listening on port 9000 -### Installation +### Hardware Connections + +**ESP32-DevKit:** + +``` +INMP441 Pin โ†’ ESP32 Pin + CLK โ†’ GPIO 14 + WS โ†’ GPIO 15 + SD โ†’ GPIO 32 + GND โ†’ GND + VCC โ†’ 3V3 +``` + +**Seeed XIAO ESP32-S3:** + +``` +INMP441 Pin โ†’ XIAO Pin + CLK โ†’ GPIO 2 + WS โ†’ GPIO 3 + SD โ†’ GPIO 9 + GND โ†’ GND + VCC โ†’ 3V3 +``` + +### Installation & Configuration + +1. **Clone the project** -1. **Clone or download the project** ```bash + git clone cd arduino-esp32 ``` -2. **Configure WiFi and Server** - Edit `src/config.h`: +2. **Edit `src/config.h`** with your settings: + ```cpp - #define WIFI_SSID "YourWiFiNetwork" + // WiFi + #define WIFI_SSID "YourNetwork" #define WIFI_PASSWORD "YourPassword" - #define SERVER_HOST "192.168.1.100" - #define SERVER_PORT 9000 - ``` -3. **Build the project** - ```bash - pio run + // Server + #define SERVER_HOST "192.168.1.50" // Your server IP + #define SERVER_PORT 9000 // TCP port ``` -4. **Upload to ESP32** +3. **Upload firmware** + ```bash - pio run --target upload + pio run --target upload --upload-port COM8 ``` -5. **Monitor output** +4. **Monitor serial output** ```bash - pio device monitor --baud 115200 + pio device monitor --port COM8 --baud 115200 ``` ---- - -## Hardware Setup - -### Pinout - ESP32-DevKit - -| Signal | Pin | Description | -|--------|-----|-------------| -| I2S_WS | GPIO15 | Word Select / LRCLK | -| I2S_SD | GPIO32 | Serial Data (microphone input) | -| I2S_SCK | GPIO14 | Serial Clock / BCLK | -| GND | GND | Ground | -| 3V3 | 3V3 | Power supply | - -### Pinout - Seeed XIAO ESP32-S3 - -| Signal | Pin | Description | -|--------|-----|-------------| -| I2S_WS | GPIO3 | Word Select / LRCLK | -| I2S_SD | GPIO9 | Serial Data (microphone input) | -| I2S_SCK | GPIO2 | Serial Clock / BCLK | -| GND | GND | Ground | -| 3V3 | 3V3 | Power supply | - ---- - -## Serial Commands - -Access the system via serial terminal (115200 baud): - -| Command | Function | -|---------|----------| -| `STATUS` | Show WiFi, TCP, memory, and system state | -| `STATS` | Display detailed system statistics | -| `HEALTH` | Perform system health check with indicators | -| `CONFIG SHOW` | Display all configuration parameters | -| `CONNECT` | Manually attempt to connect to server | -| `DISCONNECT` | Manually disconnect from server | -| `RESTART` | Restart the system | -| `HELP` | Show all available commands | +### Expected Output -Example output: ``` -> STATUS -WiFi: CONNECTED (192.168.1.100) -WiFi Signal: -65 dBm -TCP State: CONNECTED -Server: audio.server.com:9000 -System State: CONNECTED -Free Memory: 65536 bytes -WiFi Reconnects: 2 -Server Reconnects: 1 -TCP Errors: 0 +[INFO] ESP32 Audio Streamer Starting Up +[INFO] WiFi connected - IP: 192.168.1.19 +[INFO] Attempting to connect to server 192.168.1.50:9000 (attempt 1)... +[INFO] Server connection established +[INFO] Starting audio transmission: first chunk is 19200 bytes ``` --- -## Configuration +## ๐ŸŽฏ Core Features -### Essential Parameters (`src/config.h`) +### Streaming -```cpp -// WiFi Configuration -#define WIFI_SSID "" // Your WiFi network -#define WIFI_PASSWORD "" // Your WiFi password - -// Server Configuration -#define SERVER_HOST "" // Server IP or hostname -#define SERVER_PORT 0 // Server port (1-65535) +### Audio Format +- **Sample Rate**: 16 kHz +- **Bit Depth**: 16-bit +- **Channels**: Mono (1-channel) +- **Bitrate**: ~256 Kbps (~32 KB/sec) +- **Chunk Size**: 19200 bytes per TCP write (600ms of audio) -// Audio Configuration -#define I2S_SAMPLE_RATE 16000 // Audio sample rate (Hz) -#define I2S_BUFFER_SIZE 4096 // Buffer size (bytes) +### Reliability -// Memory Thresholds -#define MEMORY_WARN_THRESHOLD 40000 // Low memory warning (bytes) -#define MEMORY_CRITICAL_THRESHOLD 20000 // Critical level (bytes) +- โœ… WiFi auto-reconnect with exponential backoff +- โœ… TCP connection state machine +- โœ… Transient vs permanent error classification +- โœ… Automatic I2S reinitialization on failure +- โœ… Memory leak detection via heap trending +- โœ… Hardware watchdog timer (60 seconds) -// Debug Configuration -#define DEBUG_LEVEL 3 // 0=OFF, 3=INFO, 5=VERBOSE -``` +### Control & Monitoring -See `CONFIGURATION_GUIDE.md` for detailed parameter descriptions. +- โœ… 8 Serial commands for runtime control +- โœ… Real-time statistics every 5 minutes +- โœ… 6 configurable debug levels +- โœ… System health monitoring --- -## System Architecture +## ๐Ÿ› ๏ธ Common Tasks -### State Machine +### Check System Status ``` -INITIALIZING - โ†“ -CONNECTING_WIFI - โ†“ (success) -CONNECTING_SERVER - โ†“ (success) -CONNECTED โ† main streaming state - โ†“ (WiFi lost or connection error) -ERROR - โ†“ (recovery attempt) -CONNECTING_WIFI (retry) +Send serial command: STATS +Response: Current uptime, bytes sent, error counts, memory stats ``` -### Key Components - -| Component | Purpose | -|-----------|---------| -| `I2SAudio` | I2S audio acquisition with error classification | -| `NetworkManager` | WiFi and TCP with state machine | -| `ConfigValidator` | Startup configuration validation | -| `SerialCommandHandler` | Real-time serial commands | -| `AdaptiveBuffer` | Dynamic buffer sizing by signal strength | -| `RuntimeDebugContext` | Runtime-configurable debug output | +### Change Debug Level ---- +``` +Send serial command: DEBUG 4 +(0=OFF, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE) +``` -## Performance Metrics +### View WiFi Signal Strength -### Build Profile ``` -RAM: 15.0% (49,224 / 327,680 bytes) -Flash: 59.3% (777,461 / 1,310,720 bytes) -Build time: ~6 seconds -Warnings: 0 | Errors: 0 +Send serial command: SIGNAL +Response: Current RSSI in dBm ``` -### Audio Format -- **Sample Rate**: 16 kHz -- **Bit Depth**: 16-bit -- **Channels**: Mono (left channel) -- **Format**: Raw PCM, little-endian -- **Bitrate**: ~256 Kbps (32 KB/sec) +### Force Server Reconnect ---- +``` +Send serial command: RECONNECT +``` -## Documentation +### View All Commands -| Document | Purpose | -|----------|---------| -| `CONFIGURATION_GUIDE.md` | All 40+ parameters with recommended values | -| `TROUBLESHOOTING.md` | Solutions for 30+ common issues | -| `ERROR_HANDLING.md` | Error classification and recovery flows | -| `IMPLEMENTATION_SUMMARY.md` | Phase 1 implementation details | -| `PHASE2_IMPLEMENTATION_COMPLETE.md` | Phase 2: I2S error handling | -| `PHASE3_IMPLEMENTATION_COMPLETE.md` | Phase 3: TCP state machine, commands, debug, buffer, tests | -| `test_framework.md` | Unit testing architecture | +``` +Send serial command: HELP +``` --- -## Testing +## ๐Ÿ“Š System Architecture -### Build Verification -```bash -pio run # Build for ESP32-Dev -pio run -e seeed_xiao_esp32s3 # Build for XIAO S3 ``` - -### Unit Tests -```bash -pio test # Run all tests -pio test -f test_adaptive_buffer # Run specific test +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ I2S Audio โ”‚โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ Adaptive โ”‚โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ WiFi/TCP โ”‚ +โ”‚ Input โ”‚ โ”‚ Buffer โ”‚ โ”‚ Network โ”‚ +โ”‚ (16kHz) โ”‚ โ”‚ (adaptive) โ”‚ โ”‚ Manager โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†“ + INMP441 Server (TCP) + Microphone Port 9000 + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ State Machine (main loop) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ INITIALIZING โ†’ CONNECTING_WIFI โ†’ CONNECTING_SERVER โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ CONNECTED โ†’ (loops) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ (error?) โ†’ ERROR state โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- -## Troubleshooting +## ๐Ÿ“‹ Serial Command Reference -### Common Issues +``` +HELP - Show all available commands +STATS - Print system statistics (uptime, bytes sent, memory, errors) +STATUS - Print current system state +SIGNAL - Print WiFi RSSI (signal strength) in dBm +DEBUG [0-5] - Set debug level (0=OFF, 5=VERBOSE) +RECONNECT - Force server reconnection +REBOOT - Restart the ESP32 +``` -**"WiFi connection timeout"** -- Check WIFI_SSID and WIFI_PASSWORD -- Verify WiFi signal strength -- See `TROUBLESHOOTING.md` for detailed steps +--- -**"Server connection failed"** -- Verify SERVER_HOST and SERVER_PORT -- Check firewall settings -- Ensure server is running -- See `TROUBLESHOOTING.md` +## ๐Ÿ› Quick Troubleshooting -**"I2S read errors"** -- Verify microphone wiring -- Check I2S pins for conflicts -- Verify power supply stability -- See `TROUBLESHOOTING.md` +**ESP32 won't connect to WiFi?** -**"Memory low warnings"** -- Monitor via `STATS` command -- Check for leaks via `HEALTH` command -- See `TROUBLESHOOTING.md` +- Verify WiFi credentials in `config.h` +- Ensure network is 2.4 GHz (not 5 GHz) ---- +**Server connection timeout?** -## Project Structure +- Check `SERVER_HOST` matches actual server IP +- Verify server is listening: `ss -tuln | grep 9000` +- Check firewall allows port 9000 -``` -arduino-esp32/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ config.h # Configuration constants -โ”‚ โ”œโ”€โ”€ main.cpp # Main application -โ”‚ โ”œโ”€โ”€ i2s_audio.h/cpp # I2S + error classification -โ”‚ โ”œโ”€โ”€ network.h/cpp # WiFi + TCP + state machine -โ”‚ โ”œโ”€โ”€ serial_command.h/cpp # Serial commands -โ”‚ โ”œโ”€โ”€ debug_mode.h/cpp # Debug configuration -โ”‚ โ”œโ”€โ”€ adaptive_buffer.h/cpp # Buffer management -โ”‚ โ””โ”€โ”€ ... (other components) -โ”‚ -โ”œโ”€โ”€ test/ -โ”‚ โ””โ”€โ”€ test_adaptive_buffer.cpp # Unit tests -โ”‚ -โ”œโ”€โ”€ platformio.ini # Build configuration -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ CONFIGURATION_GUIDE.md # Configuration help -โ”œโ”€โ”€ TROUBLESHOOTING.md # Problem solving -โ””โ”€โ”€ ERROR_HANDLING.md # Error reference -``` +**No audio streaming?** ---- +- Verify I2S pins match your board +- Check microphone connections +- Send `STATS` command to see error count -## Production Deployment - -### Pre-deployment Checklist -- [ ] WiFi credentials configured -- [ ] Server host and port correct -- [ ] I2S pins verified for your hardware -- [ ] Debug level set to 0 or 1 -- [ ] All tests passing -- [ ] Build successful with zero warnings -- [ ] Serial commands responding -- [ ] Statistics printing every 5 minutes - -### Monitoring in Production -```bash -# Check every 5 minutes -STATS # View statistics -HEALTH # System health check -STATUS # Current state -``` +**For detailed help**, see `TROUBLESHOOTING.md`. --- -## Version History +## ๐Ÿ“ฆ Configuration Parameters -| Version | Date | Changes | -|---------|------|---------| -| 2.0 | Oct 20, 2025 | Phase 3: State machine, commands, debug, buffer, tests | -| 2.0 | Oct 20, 2025 | Phase 2: Enhanced I2S error handling | -| 2.0 | Oct 20, 2025 | Phase 1: Config validation, documentation, memory detection | +See `src/config.h` for complete reference: + +- WiFi: SSID, password, retry settings +- Server: Host, port, reconnect backoff +- I2S: Sample rate (16kHz), buffer sizes +- Safety: Memory thresholds, watchdog timeout +- Debug: Log level (0-5) --- -## Project Status +## ๐Ÿ”„ Recent Updates + +**October 21, 2025** - Connection Startup Bug Fix + +- Fixed 5-second startup delay before first server connection +- Added `startExpired()` method to NonBlockingTimer +- Server connections now attempt immediately after WiFi -**Status**: โœ… **PRODUCTION-READY** +**October 20, 2025** - Protocol Alignment Complete -- โœ… All 14 planned improvements implemented -- โœ… Comprehensive error handling -- โœ… Extensive documentation -- โœ… Unit test framework -- โœ… Zero build warnings/errors -- โœ… Tested on multiple boards +- TCP socket options verified and aligned +- Data format: 16kHz, 16-bit, mono โœ“ +- Chunk size: 19200 bytes โœ“ +- Full server/client compatibility โœ“ --- -## Support +## ๐Ÿ“– For More Information -1. Check `TROUBLESHOOTING.md` for common issues -2. Review `CONFIGURATION_GUIDE.md` for parameter help -3. See `ERROR_HANDLING.md` for error explanations -4. Use `HELP` serial command for available commands +- **Complete Technical Reference** โ†’ `DEVELOPMENT.md` +- **Troubleshooting & Diagnostics** โ†’ `TROUBLESHOOTING.md` +- **Source Code** โ†’ `src/` directory --- -**Last Updated**: October 20, 2025 -**Build Status**: SUCCESS โœ“ -**License**: MIT +**Status**: โœ… Production Ready | **Last Updated**: October 21, 2025 | **Version**: 2.0 diff --git a/platformio.ini b/platformio.ini index 2651ff6..d877583 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,10 +1,6 @@ [platformio] default_envs = esp32dev -; Unit test configuration -test_framework = unity -test_ignore = .gitignore - [env:esp32dev] platform = espressif32 board = esp32dev @@ -20,6 +16,9 @@ upload_speed = 921600 monitor_filters = esp32_exception_decoder +test_framework = unity +test_ignore = **/docs + [env:seeed_xiao_esp32s3] platform = espressif32 board = seeed_xiao_esp32s3 @@ -33,4 +32,7 @@ build_flags = upload_speed = 921600 -monitor_filters = esp32_exception_decoder \ No newline at end of file +monitor_filters = esp32_exception_decoder + +test_framework = unity +test_ignore = **/docs diff --git a/src/NonBlockingTimer.h b/src/NonBlockingTimer.h index 2b5988d..33d2cce 100644 --- a/src/NonBlockingTimer.h +++ b/src/NonBlockingTimer.h @@ -3,7 +3,8 @@ #include -class NonBlockingTimer { +class NonBlockingTimer +{ private: unsigned long previousMillis; unsigned long interval; @@ -11,34 +12,51 @@ class NonBlockingTimer { bool autoReset; public: - NonBlockingTimer(unsigned long intervalMs = 1000, bool autoResetEnabled = true) + NonBlockingTimer(unsigned long intervalMs = 1000, bool autoResetEnabled = true) : previousMillis(0), interval(intervalMs), isRunning(false), autoReset(autoResetEnabled) {} - void setInterval(unsigned long intervalMs) { + void setInterval(unsigned long intervalMs) + { interval = intervalMs; } - void start() { + void start() + { previousMillis = millis(); isRunning = true; } - void stop() { + void startExpired() + { + // Start timer in already-expired state for immediate first trigger + previousMillis = millis() - interval - 1; + isRunning = true; + } + + void stop() + { isRunning = false; } - void reset() { + void reset() + { previousMillis = millis(); } - bool check() { - if (!isRunning) return false; - + bool check() + { + if (!isRunning) + return false; + unsigned long currentMillis = millis(); - if (currentMillis - previousMillis >= interval) { - if (autoReset) { + if (currentMillis - previousMillis >= interval) + { + if (autoReset) + { previousMillis = currentMillis; - } else { + } + else + { isRunning = false; } return true; @@ -46,25 +64,31 @@ class NonBlockingTimer { return false; } - bool isExpired() { - if (!isRunning) return false; + bool isExpired() + { + if (!isRunning) + return false; return (millis() - previousMillis >= interval); } - unsigned long getElapsed() { + unsigned long getElapsed() + { return millis() - previousMillis; } - unsigned long getRemaining() { + unsigned long getRemaining() + { unsigned long elapsed = getElapsed(); return (elapsed >= interval) ? 0 : (interval - elapsed); } - bool getIsRunning() const { + bool getIsRunning() const + { return isRunning; } - unsigned long getInterval() const { + unsigned long getInterval() const + { return interval; } }; diff --git a/src/adaptive_buffer.cpp b/src/adaptive_buffer.cpp index bc33ec9..9b3f526 100644 --- a/src/adaptive_buffer.cpp +++ b/src/adaptive_buffer.cpp @@ -8,63 +8,85 @@ int32_t AdaptiveBuffer::last_rssi = -100; uint32_t AdaptiveBuffer::adjustment_count = 0; unsigned long AdaptiveBuffer::last_adjustment_time = 0; -void AdaptiveBuffer::initialize(size_t base_size) { +void AdaptiveBuffer::initialize(size_t base_size) +{ base_buffer_size = base_size; current_buffer_size = base_size; LOG_INFO("Adaptive Buffer initialized with base size: %u bytes", base_size); } -size_t AdaptiveBuffer::calculateBufferSize(int32_t rssi) { - // RSSI to buffer size mapping: - // Strong signal (-50 to -60): 100% = base_size - // Good signal (-60 to -70): 80% = base_size * 0.8 - // Acceptable (-70 to -80): 60% = base_size * 0.6 - // Weak (-80 to -90): 40% = base_size * 0.4 - // Very weak (<-90): 20% = base_size * 0.2 +size_t AdaptiveBuffer::calculateBufferSize(int32_t rssi) +{ + // RSSI-based buffer sizing for network reliability + // Design principle: Weak signal = more packet loss/retransmission = need LARGER buffers + // Strong signal = reliable transmission = can use smaller buffers to save RAM + // + // RSSI to buffer size mapping (inversely proportional to signal strength): + // Strong signal (-50 to -60): 50% = base_size * 0.5 (reliable, minimal buffering) + // Good signal (-60 to -70): 75% = base_size * 0.75 + // Acceptable (-70 to -80): 100% = base_size (normal operation) + // Weak (-80 to -90): 120% = base_size * 1.2 (needs extra buffering) + // Very weak (<-90): 150% = base_size * 1.5 (maximum buffering for reliability) + // + // Note: We cap at 150% to avoid excessive memory usage on long-term weak signals size_t new_size; - if (rssi >= -60) { - // Strong signal - full buffer + if (rssi >= -60) + { + // Strong signal - can use smaller buffer to save RAM + new_size = (base_buffer_size * 50) / 100; + } + else if (rssi >= -70) + { + // Good signal - 75% buffer + new_size = (base_buffer_size * 75) / 100; + } + else if (rssi >= -80) + { + // Acceptable signal - use full buffer new_size = base_buffer_size; - } else if (rssi >= -70) { - // Good signal - 80% buffer - new_size = (base_buffer_size * 80) / 100; - } else if (rssi >= -80) { - // Acceptable signal - 60% buffer - new_size = (base_buffer_size * 60) / 100; - } else if (rssi >= -90) { - // Weak signal - 40% buffer - new_size = (base_buffer_size * 40) / 100; - } else { - // Very weak signal - 20% buffer (minimum useful size) - new_size = (base_buffer_size * 20) / 100; + } + else if (rssi >= -90) + { + // Weak signal - INCREASE buffer to absorb jitter + new_size = (base_buffer_size * 120) / 100; + } + else + { + // Very weak signal - MAXIMIZE buffer for reliability + new_size = (base_buffer_size * 150) / 100; } // Ensure minimum size (256 bytes) - if (new_size < 256) { + if (new_size < 256) + { new_size = 256; } return new_size; } -void AdaptiveBuffer::updateBufferSize(int32_t rssi) { +void AdaptiveBuffer::updateBufferSize(int32_t rssi) +{ last_rssi = rssi; // Only adjust if minimum interval passed (5 seconds) unsigned long now = millis(); - if (now - last_adjustment_time < 5000) { + if (now - last_adjustment_time < 5000) + { return; } size_t new_size = calculateBufferSize(rssi); // Only log if size changed significantly (>10%) - if (new_size != current_buffer_size) { + if (new_size != current_buffer_size) + { int change_pct = ((int)new_size - (int)current_buffer_size) * 100 / (int)current_buffer_size; - if (abs(change_pct) >= 10) { + if (abs(change_pct) >= 10) + { LOG_DEBUG("Buffer size adjusted: %u โ†’ %u bytes (%d%%) for RSSI %d dBm", current_buffer_size, new_size, change_pct, rssi); @@ -75,31 +97,38 @@ void AdaptiveBuffer::updateBufferSize(int32_t rssi) { } } -size_t AdaptiveBuffer::getBufferSize() { +size_t AdaptiveBuffer::getBufferSize() +{ return current_buffer_size; } -uint8_t AdaptiveBuffer::getEfficiencyScore() { +uint8_t AdaptiveBuffer::getEfficiencyScore() +{ // Score based on how close buffer size is to optimal for current signal - // 100 = perfect match, lower = less optimal - - if (last_rssi >= -60) { - return 100; // Strong signal - using full buffer - } else if (last_rssi >= -70) { - return (current_buffer_size * 100) / (base_buffer_size * 80 / 100); - } else if (last_rssi >= -80) { - return (current_buffer_size * 100) / (base_buffer_size * 60 / 100); - } else if (last_rssi >= -90) { - return (current_buffer_size * 100) / (base_buffer_size * 40 / 100); - } else { - return (current_buffer_size * 100) / (base_buffer_size * 20 / 100); + // 100 = perfect match, capped at 100 to prevent overflow + // + // This calculates what percentage of the optimal buffer we're currently using. + // Higher is better - means we're well-matched to current network conditions. + + size_t optimal_size = calculateBufferSize(last_rssi); + + if (optimal_size == 0) + { + return 0; // Safety check } + + uint16_t raw_score = (current_buffer_size * 100) / optimal_size; + + // Cap at 100 to prevent overflow in uint8_t and to reflect perfection + return (raw_score > 100) ? 100 : (uint8_t)raw_score; } -int32_t AdaptiveBuffer::getLastRSSI() { +int32_t AdaptiveBuffer::getLastRSSI() +{ return last_rssi; } -uint32_t AdaptiveBuffer::getAdjustmentCount() { +uint32_t AdaptiveBuffer::getAdjustmentCount() +{ return adjustment_count; } diff --git a/src/config.h b/src/config.h index 3e4af9b..e0c2e55 100644 --- a/src/config.h +++ b/src/config.h @@ -2,11 +2,22 @@ #define CONFIG_H // ===== WiFi Configuration ===== -#define WIFI_SSID "YOUR_WIFI_SSID" -#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" -// Compile-time check: fail if placeholders are not replaced -#if (WIFI_SSID[0] == 'Y' && WIFI_SSID[1] == 'O' && WIFI_SSID[2] == 'U') || \ +// WARNING: Do NOT commit real WiFi credentials to version control! +// This file contains placeholder values. For real deployments, create a 'config_local.h' +// file with your actual credentials, and add 'config_local.h' to your .gitignore. +// Example: +// #define WIFI_SSID "your-ssid" +// #define WIFI_PASSWORD "your-password" +// +// To override these values, you can include 'config_local.h' below: +#ifdef __has_include +# if __has_include("config_local.h") +# include "config_local.h" +# endif +#endif +#define WIFI_SSID "SSID NAME" +#define WIFI_PASSWORD "WIFI PASSWORD" #define WIFI_RETRY_DELAY 500 // milliseconds #define WIFI_MAX_RETRIES 20 #define WIFI_TIMEOUT 30000 // milliseconds @@ -14,36 +25,47 @@ // ===== WiFi Static IP (Optional) ===== // Uncomment to use static IP instead of DHCP // #define USE_STATIC_IP -#define STATIC_IP 0, 0, 0, 0 -#define GATEWAY_IP 0, 0, 0, 0 -#define SUBNET_MASK 0, 0, 0, 0 -#define DNS_IP 0, 0, 0, 0 +// Example values below; update to match your network if using static IP +#define STATIC_IP 192, 168, 1, 100 // Device static IP address +#define GATEWAY_IP 192, 168, 1, 1 // Router/gateway IP address +#define SUBNET_MASK 255, 255, 255, 0 // Subnet mask +#define DNS_IP 192, 168, 1, 1 // DNS server IP address // ===== Server Configuration ===== -#define SERVER_HOST "" -#define SERVER_PORT 0 -#define SERVER_RECONNECT_MIN 5000 // milliseconds -#define SERVER_RECONNECT_MAX 60000 // milliseconds -#define TCP_WRITE_TIMEOUT 5000 // milliseconds +#define SERVER_HOST "192.168.x.x" +#define SERVER_PORT 9000 +#define SERVER_RECONNECT_MIN 5000 // milliseconds +#define SERVER_RECONNECT_MAX 60000 // milliseconds +#define SERVER_BACKOFF_JITTER_PCT 20 // percent jitter on backoff (0-100) +#define TCP_WRITE_TIMEOUT 5000 // milliseconds - timeout for send operations +#define TCP_RECEIVE_TIMEOUT 10000 // milliseconds - timeout for receive operations (primarily for protocol compliance) + +// TCP chunk size MUST match server's TCP_CHUNK_SIZE expectation for proper streaming +// Server (receiver.py) expects 19200 bytes per chunk: +// - 9600 samples ร— 2 bytes/sample = 19200 bytes +// - Duration: 9600 samples รท 16000 Hz = 0.6 seconds = 600ms of audio +// - Data rate: 19200 bytes รท 0.6 sec = 32000 bytes/sec = 32 KB/sec +// This aligns with server's SO_RCVBUF=65536 and socket receive loop optimization +#define TCP_CHUNK_SIZE 19200 // bytes per write() chunk - MUST match server receiver.py // ===== Board Detection ===== #ifdef ARDUINO_SEEED_XIAO_ESP32S3 - #define BOARD_XIAO_ESP32S3 - #define BOARD_NAME "Seeed XIAO ESP32-S3" +#define BOARD_XIAO_ESP32S3 +#define BOARD_NAME "Seeed XIAO ESP32-S3" #else - #define BOARD_ESP32DEV - #define BOARD_NAME "ESP32-DevKit" +#define BOARD_ESP32DEV +#define BOARD_NAME "ESP32-DevKit" #endif // ===== I2S Hardware Pins ===== #ifdef BOARD_XIAO_ESP32S3 - #define I2S_WS_PIN 3 - #define I2S_SD_PIN 9 - #define I2S_SCK_PIN 2 +#define I2S_WS_PIN 3 +#define I2S_SD_PIN 9 +#define I2S_SCK_PIN 2 #else - #define I2S_WS_PIN 15 - #define I2S_SD_PIN 32 - #define I2S_SCK_PIN 14 +#define I2S_WS_PIN 15 +#define I2S_SD_PIN 32 +#define I2S_SCK_PIN 14 #endif // ===== I2S Parameters ===== @@ -66,29 +88,31 @@ #define STATS_PRINT_INTERVAL 300000 // 5 minutes // ===== System Initialization & Timeouts ===== -#define SERIAL_INIT_DELAY 1000 // milliseconds - delay after serial init -#define GRACEFUL_SHUTDOWN_DELAY 100 // milliseconds - delay between shutdown steps -#define ERROR_RECOVERY_DELAY 5000 // milliseconds - delay before recovery attempt -#define TASK_YIELD_DELAY 1 // milliseconds - delay in main loop for background tasks +#define SERIAL_INIT_DELAY 1000 // milliseconds - delay after serial init +#define GRACEFUL_SHUTDOWN_DELAY 100 // milliseconds - delay between shutdown steps +#define ERROR_RECOVERY_DELAY 5000 // milliseconds - delay before recovery attempt +#define TASK_YIELD_DELAY 1 // milliseconds - delay in main loop for background tasks // ===== TCP Keepalive Configuration ===== -#define TCP_KEEPALIVE_IDLE 5 // seconds - idle time before keepalive probe -#define TCP_KEEPALIVE_INTERVAL 5 // seconds - interval between keepalive probes -#define TCP_KEEPALIVE_COUNT 3 // count - number of keepalive probes before disconnect +#define TCP_KEEPALIVE_IDLE 5 // seconds - idle time before keepalive probe +#define TCP_KEEPALIVE_INTERVAL 5 // seconds - interval between keepalive probes +#define TCP_KEEPALIVE_COUNT 3 // count - number of keepalive probes before disconnect // ===== Logger Configuration ===== -#define LOGGER_BUFFER_SIZE 256 // bytes - circular buffer for log messages +#define LOGGER_BUFFER_SIZE 256 // bytes - circular buffer for log messages +#define LOGGER_MAX_LINES_PER_SEC 20 // rate limit to avoid log storms +#define LOGGER_BURST_MAX 60 // maximum burst of logs allowed // ===== Watchdog Configuration ===== -#define WATCHDOG_TIMEOUT_SEC 10 // seconds - watchdog timeout (ESP32 feeds it in loop) +#define WATCHDOG_TIMEOUT_SEC 60 // seconds - watchdog timeout (aligned with connection operations) // ===== Task Priorities ===== -#define TASK_PRIORITY_HIGH 5 // reserved for critical tasks -#define TASK_PRIORITY_NORMAL 3 // default priority -#define TASK_PRIORITY_LOW 1 // background tasks +#define TASK_PRIORITY_HIGH 5 // reserved for critical tasks +#define TASK_PRIORITY_NORMAL 3 // default priority +#define TASK_PRIORITY_LOW 1 // background tasks // ===== State Machine Timeouts ===== -#define STATE_CHANGE_DEBOUNCE 100 // milliseconds - debounce state transitions +#define STATE_CHANGE_DEBOUNCE 100 // milliseconds - debounce state transitions // ===== Debug Configuration ===== // Compile-time debug level (0=OFF, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE) diff --git a/src/config_validator.h b/src/config_validator.h index 2439a52..700655b 100644 --- a/src/config_validator.h +++ b/src/config_validator.h @@ -128,11 +128,11 @@ class ConfigValidator { } // Check PORT - if (strlen(SERVER_PORT) == 0) { - LOG_ERROR("Server PORT is empty - must configure SERVER_PORT in config.h"); + if (SERVER_PORT <= 0 || SERVER_PORT > 65535) { + LOG_ERROR("Server PORT (%d) is invalid - must be 1-65535", SERVER_PORT); valid = false; } else { - LOG_INFO(" โœ“ Server PORT configured: %s", SERVER_PORT); + LOG_INFO(" โœ“ Server PORT configured: %d", SERVER_PORT); } // Validate reconnection timeouts @@ -309,10 +309,10 @@ class ConfigValidator { if (WATCHDOG_TIMEOUT_SEC <= 0) { LOG_ERROR("WATCHDOG_TIMEOUT_SEC must be > 0, got %u seconds", WATCHDOG_TIMEOUT_SEC); valid = false; - } else if (WATCHDOG_TIMEOUT_SEC < 5) { - LOG_WARN("WATCHDOG_TIMEOUT_SEC (%u sec) is very short - minimum recommended is 5 seconds", WATCHDOG_TIMEOUT_SEC); + } else if (WATCHDOG_TIMEOUT_SEC < 30) { + LOG_WARN("WATCHDOG_TIMEOUT_SEC (%u sec) is short - recommend >= 30 seconds", WATCHDOG_TIMEOUT_SEC); } else { - LOG_INFO(" โœ“ Watchdog timeout: %u seconds", WATCHDOG_TIMEOUT_SEC); + LOG_INFO(" \u2713 Watchdog timeout: %u seconds", WATCHDOG_TIMEOUT_SEC); } // Verify watchdog timeout doesn't conflict with WiFi timeout @@ -321,7 +321,7 @@ class ConfigValidator { LOG_WARN("WATCHDOG_TIMEOUT_SEC (%u) <= WIFI_TIMEOUT (%u sec) - watchdog may reset during WiFi connection", WATCHDOG_TIMEOUT_SEC, wifi_timeout_sec); } else { - LOG_INFO(" โœ“ Watchdog timeout compatible with WiFi timeout"); + LOG_INFO(" \u2713 Watchdog timeout compatible with WiFi timeout"); } // Verify watchdog timeout doesn't conflict with error recovery delay @@ -331,12 +331,12 @@ class ConfigValidator { WATCHDOG_TIMEOUT_SEC, error_delay_sec); valid = false; } else { - LOG_INFO(" โœ“ Watchdog timeout compatible with error recovery delay"); + LOG_INFO(" \u2713 Watchdog timeout compatible with error recovery delay"); } // Verify watchdog is long enough for state operations // Typical operations: WiFi ~25s, I2S read ~1ms, TCP write ~100ms - if (WATCHDOG_TIMEOUT_SEC < (wifi_timeout_sec + 2)) { + if (WATCHDOG_TIMEOUT_SEC < (wifi_timeout_sec + 5)) { LOG_WARN("WATCHDOG_TIMEOUT_SEC (%u) is close to WIFI_TIMEOUT (%u sec) - margin may be tight", WATCHDOG_TIMEOUT_SEC, wifi_timeout_sec); } diff --git a/src/i2s_audio.cpp b/src/i2s_audio.cpp index f7a9e43..9674825 100644 --- a/src/i2s_audio.cpp +++ b/src/i2s_audio.cpp @@ -36,8 +36,16 @@ bool I2SAudio::initialize() { // Install I2S driver esp_err_t result = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); if (result != ESP_OK) { - LOG_ERROR("I2S driver install failed: %d", result); - return false; + LOG_ERROR("I2S driver install failed (APLL on): %d", result); + // Retry without APLL as fallback for boards where APLL fails + i2s_config.use_apll = false; + result = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); + if (result != ESP_OK) { + LOG_ERROR("I2S driver install failed (APLL off): %d", result); + return false; + } else { + LOG_WARN("I2S initialized without APLL - clock stability reduced"); + } } // Set I2S pin configuration diff --git a/src/logger.cpp b/src/logger.cpp index e3f0f71..17b2fa7 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -1,24 +1,83 @@ #include "logger.h" +#include "config.h" #include LogLevel Logger::min_level = LOG_INFO; -const char* Logger::level_names[] = { +const char *Logger::level_names[] = { "DEBUG", "INFO", "WARN", "ERROR", - "CRITICAL" -}; + "CRITICAL"}; -void Logger::init(LogLevel level) { +static float _logger_tokens = 0.0f; +static uint32_t _logger_last_refill_ms = 0; +static uint32_t _logger_suppressed = 0; + +static inline void logger_refill_tokens() +{ + uint32_t now = millis(); + if (_logger_last_refill_ms == 0) + { + _logger_last_refill_ms = now; + _logger_tokens = LOGGER_BURST_MAX; + return; + } + uint32_t elapsed = now - _logger_last_refill_ms; + if (elapsed == 0) + return; + float rate_per_ms = (float)LOGGER_MAX_LINES_PER_SEC / 1000.0f; + _logger_tokens += elapsed * rate_per_ms; + if (_logger_tokens > (float)LOGGER_BURST_MAX) + _logger_tokens = (float)LOGGER_BURST_MAX; + _logger_last_refill_ms = now; +} + +void Logger::init(LogLevel level) +{ min_level = level; Serial.begin(115200); delay(1000); + _logger_tokens = LOGGER_BURST_MAX; + _logger_last_refill_ms = millis(); + _logger_suppressed = 0; } -void Logger::log(LogLevel level, const char* file, int line, const char* fmt, ...) { - if (level < min_level) return; +void Logger::log(LogLevel level, const char *file, int line, const char *fmt, ...) +{ + if (level < min_level) + return; + + logger_refill_tokens(); + if (_logger_tokens < 1.0f) + { + // Rate limited: drop message and count it + _logger_suppressed++; + return; + } + + // If there were suppressed messages and we have enough budget, report once + if (_logger_suppressed > 0 && _logger_tokens >= 2.0f) + { + _logger_tokens -= 1.0f; + Serial.printf("[%6lu] [%-8s] [Heap:%6u] %s (%s:%d)\n", + millis() / 1000, + "INFO", + ESP.getFreeHeap(), + "[logger] Suppressed messages due to rate limiting", + "logger", + 0); + _logger_tokens -= 1.0f; + Serial.printf("[%6lu] [%-8s] [Heap:%6u] Suppressed count: %u (%s:%d)\n", + millis() / 1000, + "INFO", + ESP.getFreeHeap(), + _logger_suppressed, + "logger", + 0); + _logger_suppressed = 0; + } char buffer[256]; va_list args; @@ -27,10 +86,12 @@ void Logger::log(LogLevel level, const char* file, int line, const char* fmt, .. va_end(args); // Extract filename from path - const char* filename = strrchr(file, '/'); - if (!filename) filename = strrchr(file, '\\'); + const char *filename = strrchr(file, '/'); + if (!filename) + filename = strrchr(file, '\\'); filename = filename ? filename + 1 : file; + _logger_tokens -= 1.0f; Serial.printf("[%6lu] [%-8s] [Heap:%6u] %s (%s:%d)\n", millis() / 1000, level_names[level], diff --git a/src/main.cpp b/src/main.cpp index 695b95d..77429d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -168,8 +168,20 @@ void gracefulShutdown() { // ===== Setup ===== void setup() { - // Initialize logger - Logger::init(LOG_INFO); + // Initialize logger (align with compile-time DEBUG_LEVEL) + LogLevel bootLogLevel = LOG_INFO; + #if DEBUG_LEVEL >= 4 + bootLogLevel = LOG_DEBUG; + #elif DEBUG_LEVEL == 3 + bootLogLevel = LOG_INFO; + #elif DEBUG_LEVEL == 2 + bootLogLevel = LOG_WARN; + #elif DEBUG_LEVEL == 1 + bootLogLevel = LOG_ERROR; + #else + bootLogLevel = LOG_CRITICAL; + #endif + Logger::init(bootLogLevel); LOG_INFO("========================================"); LOG_INFO("ESP32 Audio Streamer Starting Up"); LOG_INFO("Board: %s", BOARD_NAME); @@ -216,6 +228,11 @@ void setup() { // Move to WiFi connection state systemState.setState(SystemState::CONNECTING_WIFI); + // Initialize and configure watchdog to a safe timeout + // Ensure timeout comfortably exceeds WiFi timeouts and recovery delays + esp_task_wdt_init(WATCHDOG_TIMEOUT_SEC, true); + esp_task_wdt_add(NULL); + LOG_INFO("Setup complete - entering main loop"); } diff --git a/src/network.cpp b/src/network.cpp index 846ed26..345ca2d 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -3,20 +3,87 @@ #include "esp_task_wdt.h" #include #include +#include +#include + +// Helper macro for setsockopt with error checking +#define SET_SOCKOPT(fd, level, optname, value) \ + do \ + { \ + if (setsockopt(fd, level, optname, &(value), sizeof(value)) != 0) \ + { \ + LOG_WARN("Failed to set socket option %s: errno=%d", #optname, errno); \ + } \ + } while (0) + +// Helper macro for struct timeval setsockopt +#define SET_SOCKOPT_TIMEVAL(fd, level, optname, tv) \ + do \ + { \ + if (setsockopt(fd, level, optname, &(tv), sizeof(tv)) != 0) \ + { \ + LOG_WARN("Failed to set socket option %s: errno=%d", #optname, errno); \ + } \ + } while (0) + +// Simple LCG for jitter generation (no to keep footprint small) +static uint32_t _nb_rng = 2166136261u; +static inline uint32_t nb_rand() +{ + _nb_rng = _nb_rng * 1664525u + 1013904223u; + return _nb_rng; +} +static inline unsigned long apply_jitter(unsigned long base_ms) +{ +#if SERVER_BACKOFF_JITTER_PCT > 0 + uint32_t r = nb_rand(); + + // Calculate jitter range with safety check for negative values + int32_t jitter_range = (int32_t)(base_ms * SERVER_BACKOFF_JITTER_PCT / 100); + if (jitter_range < 0) + { + jitter_range = 0; // Safety: prevent negative range + } + + // Apply random jitter within [-jitter_range, +jitter_range] + // Use safe cast to prevent integer overflow in modulo operation + uint32_t jitter_span = (2u * (uint32_t)jitter_range) + 1u; + int32_t jitter = (int32_t)(r % jitter_span) - jitter_range; + + // Apply jitter and bounds-check the result + long with_jitter = (long)base_ms + jitter; + if (with_jitter < (long)SERVER_RECONNECT_MIN) + { + with_jitter = SERVER_RECONNECT_MIN; + } + if ((unsigned long)with_jitter > SERVER_RECONNECT_MAX) + { + with_jitter = SERVER_RECONNECT_MAX; + } + + return (unsigned long)with_jitter; +#else + return base_ms; +#endif +} // ExponentialBackoff implementation ExponentialBackoff::ExponentialBackoff(unsigned long min_ms, unsigned long max_ms) : min_delay(min_ms), max_delay(max_ms), current_delay(min_ms), consecutive_failures(0) {} -unsigned long ExponentialBackoff::getNextDelay() { - if (consecutive_failures > 0) { +unsigned long ExponentialBackoff::getNextDelay() +{ + if (consecutive_failures > 0) + { current_delay = min(current_delay * 2, max_delay); } consecutive_failures++; - return current_delay; + // Apply jitter to avoid sync storms + return apply_jitter(current_delay); } -void ExponentialBackoff::reset() { +void ExponentialBackoff::reset() +{ consecutive_failures = 0; current_delay = min_delay; } @@ -40,7 +107,8 @@ unsigned long NetworkManager::tcp_state_change_time = 0; unsigned long NetworkManager::tcp_connection_established_time = 0; uint32_t NetworkManager::tcp_state_changes = 0; -void NetworkManager::initialize() { +void NetworkManager::initialize() +{ LOG_INFO("Initializing network..."); // Initialize adaptive buffer management @@ -49,34 +117,44 @@ void NetworkManager::initialize() { // Configure WiFi for reliability WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); - WiFi.setSleep(false); // Prevent power-save disconnects + WiFi.setSleep(false); // Prevent power-save disconnects WiFi.persistent(false); // Reduce flash wear - // Configure static IP if enabled - #ifdef USE_STATIC_IP +// Configure static IP if enabled +#ifdef USE_STATIC_IP IPAddress local_IP(STATIC_IP); IPAddress gateway(GATEWAY_IP); IPAddress subnet(SUBNET_MASK); IPAddress dns(DNS_IP); - if (WiFi.config(local_IP, gateway, subnet, dns)) { + if (WiFi.config(local_IP, gateway, subnet, dns)) + { LOG_INFO("Static IP configured: %s", local_IP.toString().c_str()); - } else { + } + else + { LOG_ERROR("Static IP configuration failed - falling back to DHCP"); } - #endif +#endif // Start WiFi connection WiFi.begin(WIFI_SSID, WIFI_PASSWORD); wifi_retry_timer.start(); wifi_retry_count = 0; + // Start server retry timer in expired state for immediate first connection attempt + // This avoids unnecessary 5-second startup delay + server_retry_timer.startExpired(); + LOG_INFO("Network initialization started"); } -void NetworkManager::handleWiFiConnection() { +void NetworkManager::handleWiFiConnection() +{ // If already connected, just return - if (WiFi.status() == WL_CONNECTED) { - if (wifi_retry_count > 0) { + if (WiFi.status() == WL_CONNECTED) + { + if (wifi_retry_count > 0) + { // Just connected after retries LOG_INFO("WiFi connected after %d attempts", wifi_retry_count); wifi_reconnect_count++; @@ -86,14 +164,16 @@ void NetworkManager::handleWiFiConnection() { } // Not connected - handle reconnection with non-blocking timer - if (!wifi_retry_timer.check()) { + if (!wifi_retry_timer.check()) + { return; // Not time to retry yet } // Feed watchdog to prevent resets during connection esp_task_wdt_reset(); - if (wifi_retry_count == 0) { + if (wifi_retry_count == 0) + { LOG_WARN("WiFi disconnected - attempting reconnection..."); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); server_connected = false; @@ -102,42 +182,60 @@ void NetworkManager::handleWiFiConnection() { wifi_retry_count++; - if (wifi_retry_count > WIFI_MAX_RETRIES) { - LOG_CRITICAL("WiFi connection failed after %d attempts - rebooting", WIFI_MAX_RETRIES); - delay(1000); - ESP.restart(); + if (wifi_retry_count > WIFI_MAX_RETRIES) + { + // Enter safe backoff mode instead of rebooting; keep serial alive + unsigned long backoff = 1000UL * (wifi_retry_count - WIFI_MAX_RETRIES); + if (backoff > 30000UL) + backoff = 30000UL; + // Add small jitter to avoid herd reconnects + backoff = apply_jitter(backoff); + LOG_CRITICAL("WiFi connection failed after %d attempts - backing off %lu ms (no reboot)", WIFI_MAX_RETRIES, backoff); + wifi_retry_timer.setInterval(backoff); + wifi_retry_timer.start(); + wifi_retry_count = WIFI_MAX_RETRIES; // clamp to avoid overflow + return; } } -bool NetworkManager::isWiFiConnected() { +bool NetworkManager::isWiFiConnected() +{ return WiFi.status() == WL_CONNECTED; } -void NetworkManager::monitorWiFiQuality() { - if (!rssi_check_timer.check()) return; - if (!isWiFiConnected()) return; +void NetworkManager::monitorWiFiQuality() +{ + if (!rssi_check_timer.check()) + return; + if (!isWiFiConnected()) + return; int32_t rssi = WiFi.RSSI(); // Update adaptive buffer based on signal strength AdaptiveBuffer::updateBufferSize(rssi); - if (rssi < RSSI_WEAK_THRESHOLD) { - LOG_WARN("Weak WiFi signal: %d dBm - triggering preemptive reconnection", rssi); - WiFi.disconnect(); - WiFi.reconnect(); - } else if (rssi < -70) { + if (rssi < RSSI_WEAK_THRESHOLD) + { + LOG_WARN("Weak WiFi signal: %d dBm - increasing buffer, avoiding forced disconnect", rssi); + // No forced disconnect; rely on natural link loss and adaptive buffering + } + else if (rssi < -70) + { LOG_WARN("WiFi signal degraded: %d dBm", rssi); } } -bool NetworkManager::connectToServer() { - if (!isWiFiConnected()) { +bool NetworkManager::connectToServer() +{ + if (!isWiFiConnected()) + { return false; } // Check if it's time to retry (using exponential backoff) - if (!server_retry_timer.isExpired()) { + if (!server_retry_timer.isExpired()) + { return false; } @@ -150,7 +248,8 @@ bool NetworkManager::connectToServer() { // Feed watchdog during connection attempt esp_task_wdt_reset(); - if (client.connect(SERVER_HOST, SERVER_PORT)) { + if (client.connect(SERVER_HOST, SERVER_PORT)) + { LOG_INFO("Server connection established"); server_connected = true; last_successful_write = millis(); @@ -160,31 +259,48 @@ bool NetworkManager::connectToServer() { // Update state to CONNECTED updateTCPState(TCPConnectionState::CONNECTED); - // Configure TCP keepalive for dead connection detection + // Configure TCP socket options for low-latency audio streaming int sockfd = client.fd(); - if (sockfd >= 0) { + if (sockfd >= 0) + { + // TCP_NODELAY: Disable Nagle's algorithm for low-latency streaming + // Server expects immediate audio chunks without buffering delays + // This matches server's configuration in server/receiver.py (see https://github.com/example/audio-server/blob/main/server/receiver.py): conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + int nodelay = 1; + SET_SOCKOPT(sockfd, IPPROTO_TCP, TCP_NODELAY, nodelay); + + // TCP keepalive: Detect stale connections int keepAlive = 1; - int keepIdle = 5; // Start probing after 5s idle - int keepInterval = 5; // Probe every 5s - int keepCount = 3; // Drop after 3 failed probes - - setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive)); - setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle)); - setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(keepInterval)); - setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(keepCount)); - - LOG_DEBUG("TCP keepalive configured"); + int keepIdle = TCP_KEEPALIVE_IDLE; // seconds + int keepInterval = TCP_KEEPALIVE_INTERVAL; // seconds + int keepCount = TCP_KEEPALIVE_COUNT; // count + + SET_SOCKOPT(sockfd, SOL_SOCKET, SO_KEEPALIVE, keepAlive); + SET_SOCKOPT(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, keepIdle); + SET_SOCKOPT(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, keepInterval); + SET_SOCKOPT(sockfd, IPPROTO_TCP, TCP_KEEPCNT, keepCount); + + // Set send timeout to avoid indefinite blocking writes + struct timeval snd_to; + snd_to.tv_sec = TCP_WRITE_TIMEOUT / 1000; + snd_to.tv_usec = (TCP_WRITE_TIMEOUT % 1000) * 1000; + SET_SOCKOPT_TIMEVAL(sockfd, SOL_SOCKET, SO_SNDTIMEO, snd_to); + + LOG_DEBUG("TCP socket options configured: TCP_NODELAY=1, keepalive enabled, send timeout=%dms", TCP_WRITE_TIMEOUT); + LOG_INFO("Audio streaming configured: %d bytes per chunk (%.0fms at 16kHz)", TCP_CHUNK_SIZE, (float)TCP_CHUNK_SIZE / 32.0f); } return true; - } else { + } + else + { LOG_ERROR("Server connection failed"); server_connected = false; // Update state to ERROR handleTCPError("connectToServer"); - // Set next retry time with exponential backoff + // Set next retry time with exponential backoff + jitter unsigned long next_delay = server_backoff.getNextDelay(); server_retry_timer.setInterval(next_delay); server_retry_timer.start(); @@ -194,8 +310,10 @@ bool NetworkManager::connectToServer() { } } -void NetworkManager::disconnectFromServer() { - if (server_connected || client.connected()) { +void NetworkManager::disconnectFromServer() +{ + if (server_connected || client.connected()) + { // Update state to CLOSING updateTCPState(TCPConnectionState::CLOSING); @@ -208,9 +326,11 @@ void NetworkManager::disconnectFromServer() { } } -bool NetworkManager::isServerConnected() { +bool NetworkManager::isServerConnected() +{ // Double-check: our flag AND actual connection state - if (server_connected && !client.connected()) { + if (server_connected && !client.connected()) + { LOG_WARN("Server connection lost unexpectedly"); server_connected = false; server_retry_timer.setInterval(SERVER_RECONNECT_MIN); @@ -219,104 +339,138 @@ bool NetworkManager::isServerConnected() { return server_connected; } -WiFiClient& NetworkManager::getClient() { +WiFiClient &NetworkManager::getClient() +{ return client; } -bool NetworkManager::writeData(const uint8_t* data, size_t length) { - if (!isServerConnected()) { +bool NetworkManager::writeData(const uint8_t *data, size_t length) +{ + if (!isServerConnected()) + { return false; } - size_t bytes_sent = client.write(data, length); - - if (bytes_sent == length) { - last_successful_write = millis(); - return true; - } else { - LOG_ERROR("TCP write incomplete: sent %u of %u bytes", bytes_sent, length); - - // Handle write error - handleTCPError("writeData"); + // Diagnostic: Log first transmission to verify audio stream starts + static bool first_transmission = true; + if (first_transmission && length > 0) + { + LOG_INFO("Starting audio transmission: first chunk is %u bytes (%.0fms of audio)", (unsigned)length, (float)length / 32.0f); + first_transmission = false; + } - // Check for write timeout - if (millis() - last_successful_write > TCP_WRITE_TIMEOUT) { - LOG_ERROR("TCP write timeout - closing stale connection"); - disconnectFromServer(); + // Write in chunks to minimize long blocking writes and respect SO_SNDTIMEO + size_t total_sent = 0; + while (total_sent < length) + { + size_t chunk = min((size_t)TCP_CHUNK_SIZE, length - total_sent); + size_t sent = client.write(data + total_sent, chunk); + if (sent == 0) + { + LOG_ERROR("TCP write returned 0 (timeout or error) after %u/%u bytes", (unsigned)total_sent, (unsigned)length); + handleTCPError("writeData"); + if (millis() - last_successful_write > TCP_WRITE_TIMEOUT) + { + LOG_ERROR("TCP write timeout - closing stale connection"); + disconnectFromServer(); + } + return false; } - - return false; + total_sent += sent; } + + last_successful_write = millis(); + return true; } -uint32_t NetworkManager::getWiFiReconnectCount() { +uint32_t NetworkManager::getWiFiReconnectCount() +{ return wifi_reconnect_count; } -uint32_t NetworkManager::getServerReconnectCount() { +uint32_t NetworkManager::getServerReconnectCount() +{ return server_reconnect_count; } -uint32_t NetworkManager::getTCPErrorCount() { +uint32_t NetworkManager::getTCPErrorCount() +{ return tcp_error_count; } // ===== TCP Connection State Machine Implementation ===== -void NetworkManager::updateTCPState(TCPConnectionState new_state) { - if (tcp_state != new_state) { +// Helper function: Convert TCP connection state enum to human-readable string +static const char *getTCPStateName(TCPConnectionState state) +{ + switch (state) + { + case TCPConnectionState::DISCONNECTED: + return "DISCONNECTED"; + case TCPConnectionState::CONNECTING: + return "CONNECTING"; + case TCPConnectionState::CONNECTED: + return "CONNECTED"; + case TCPConnectionState::ERROR: + return "ERROR"; + case TCPConnectionState::CLOSING: + return "CLOSING"; + default: + return "UNKNOWN"; + } +} + +void NetworkManager::updateTCPState(TCPConnectionState new_state) +{ + if (tcp_state != new_state) + { TCPConnectionState old_state = tcp_state; tcp_state = new_state; tcp_state_change_time = millis(); tcp_state_changes++; - // Log state transitions - const char* old_name = "UNKNOWN"; - const char* new_name = "UNKNOWN"; - - switch (old_state) { - case TCPConnectionState::DISCONNECTED: old_name = "DISCONNECTED"; break; - case TCPConnectionState::CONNECTING: old_name = "CONNECTING"; break; - case TCPConnectionState::CONNECTED: old_name = "CONNECTED"; break; - case TCPConnectionState::ERROR: old_name = "ERROR"; break; - case TCPConnectionState::CLOSING: old_name = "CLOSING"; break; - } - - switch (new_state) { - case TCPConnectionState::DISCONNECTED: new_name = "DISCONNECTED"; break; - case TCPConnectionState::CONNECTING: new_name = "CONNECTING"; break; - case TCPConnectionState::CONNECTED: new_name = "CONNECTED"; break; - case TCPConnectionState::ERROR: new_name = "ERROR"; break; - case TCPConnectionState::CLOSING: new_name = "CLOSING"; break; - } - - LOG_INFO("TCP state transition: %s โ†’ %s", old_name, new_name); + // Log state transition using helper function + LOG_INFO("TCP state transition: %s โ†’ %s", getTCPStateName(old_state), getTCPStateName(new_state)); // Update connection established time when entering CONNECTED state - if (new_state == TCPConnectionState::CONNECTED) { + if (new_state == TCPConnectionState::CONNECTED) + { tcp_connection_established_time = millis(); } } } -void NetworkManager::handleTCPError(const char* error_source) { +void NetworkManager::handleTCPError(const char *error_source) +{ tcp_error_count++; LOG_ERROR("TCP error from %s", error_source); updateTCPState(TCPConnectionState::ERROR); + + // ERROR state recovery: The next call to connectToServer() will attempt reconnection + // with exponential backoff. The ERROR state is a transient state that leads to either: + // 1. DISCONNECTED (if connection is lost) โ†’ next connection attempt resets to CONNECTING + // 2. CONNECTED (if error is recovered) โ†’ normal operation resumes + // 3. Another ERROR (if connection remains problematic) โ†’ exponential backoff continues + // + // The system does NOT get stuck in ERROR state due to the polling-based reconnection + // logic in connectToServer() which continuously attempts to re-establish the connection. } -bool NetworkManager::validateConnection() { +bool NetworkManager::validateConnection() +{ // Validate that connection state matches actual TCP connection bool is_actually_connected = client.connected(); bool state_says_connected = (tcp_state == TCPConnectionState::CONNECTED); - if (state_says_connected && !is_actually_connected) { + if (state_says_connected && !is_actually_connected) + { LOG_WARN("TCP state mismatch: state=CONNECTED but client.connected()=false"); updateTCPState(TCPConnectionState::DISCONNECTED); return false; } - if (!state_says_connected && is_actually_connected) { + if (!state_says_connected && is_actually_connected) + { LOG_WARN("TCP state mismatch: state!= CONNECTED but client.connected()=true"); updateTCPState(TCPConnectionState::CONNECTED); return true; @@ -325,46 +479,57 @@ bool NetworkManager::validateConnection() { return is_actually_connected; } -TCPConnectionState NetworkManager::getTCPState() { - validateConnection(); // Synchronize state with actual connection +TCPConnectionState NetworkManager::getTCPState() +{ + validateConnection(); // Synchronize state with actual connection return tcp_state; } -bool NetworkManager::isTCPConnecting() { +bool NetworkManager::isTCPConnecting() +{ return tcp_state == TCPConnectionState::CONNECTING; } -bool NetworkManager::isTCPConnected() { - validateConnection(); // Synchronize before returning +bool NetworkManager::isTCPConnected() +{ + validateConnection(); // Synchronize before returning return tcp_state == TCPConnectionState::CONNECTED; } -bool NetworkManager::isTCPError() { +bool NetworkManager::isTCPError() +{ return tcp_state == TCPConnectionState::ERROR; } -unsigned long NetworkManager::getTimeSinceLastWrite() { +unsigned long NetworkManager::getTimeSinceLastWrite() +{ return millis() - last_successful_write; } -unsigned long NetworkManager::getConnectionUptime() { - if (tcp_state != TCPConnectionState::CONNECTED) { +unsigned long NetworkManager::getConnectionUptime() +{ + if (tcp_state != TCPConnectionState::CONNECTED) + { return 0; } return millis() - tcp_connection_established_time; } -uint32_t NetworkManager::getTCPStateChangeCount() { +uint32_t NetworkManager::getTCPStateChangeCount() +{ return tcp_state_changes; } // ===== Adaptive Buffer Management ===== -void NetworkManager::updateAdaptiveBuffer() { - if (!isWiFiConnected()) return; +void NetworkManager::updateAdaptiveBuffer() +{ + if (!isWiFiConnected()) + return; AdaptiveBuffer::updateBufferSize(WiFi.RSSI()); } -size_t NetworkManager::getAdaptiveBufferSize() { +size_t NetworkManager::getAdaptiveBufferSize() +{ return AdaptiveBuffer::getBufferSize(); }