diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 10894d2..cb056f1 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -73,13 +73,13 @@ jobs: echo "Flash used: $FLASH_USED bytes" - # Check if flash usage exceeds 1,300,000 bytes - if [ "$FLASH_USED" -gt 1300000 ]; then - echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,300,000 bytes" - echo "Flash usage: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + # Check if flash usage exceeds 1,800,000 bytes (90% of 1.875MB app partition) + if [ "$FLASH_USED" -gt 1800000 ]; then + echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,800,000 bytes" + echo "Flash usage: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" exit 1 else - echo "✅ Flash usage OK: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + echo "✅ Flash usage OK: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" fi - name: Store map files as artifacts @@ -88,9 +88,7 @@ jobs: name: build-artifacts-branch-${{ github.sha }} path: | .pio/build/ttgo-lora32-v1/firmware.map - .pio/build/ttgo-lora32-v1/firmware.elf .pio/build/minimal/firmware.map - .pio/build/minimal/firmware.elf retention-days: 30 if: always() @@ -163,13 +161,13 @@ jobs: echo "Flash used: $FLASH_USED bytes" - # Check if flash usage exceeds 1,300,000 bytes - if [ "$FLASH_USED" -gt 1300000 ]; then - echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,300,000 bytes" - echo "Flash usage: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + # Check if flash usage exceeds 1,800,000 bytes (90% of 1.875MB app partition) + if [ "$FLASH_USED" -gt 1800000 ]; then + echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,800,000 bytes" + echo "Flash usage: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" exit 1 else - echo "✅ Flash usage OK: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + echo "✅ Flash usage OK: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" fi - name: Store map files as artifacts @@ -178,9 +176,7 @@ jobs: name: build-artifacts-main-${{ github.sha }} path: | .pio/build/ttgo-lora32-v1/firmware.map - .pio/build/ttgo-lora32-v1/firmware.elf .pio/build/minimal/firmware.map - .pio/build/minimal/firmware.elf retention-days: 30 if: always() @@ -286,20 +282,19 @@ jobs: echo "Flash used: $FLASH_USED bytes" - # Check if flash usage exceeds 1,300,000 bytes - if [ "$FLASH_USED" -gt 1300000 ]; then - echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,300,000 bytes" - echo "Flash usage: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + # Check if flash usage exceeds 1,800,000 bytes (90% of 1.875MB app partition) + if [ "$FLASH_USED" -gt 1800000 ]; then + echo "❌ ERROR: Flash usage ($FLASH_USED bytes) exceeds limit of 1,800,000 bytes" + echo "Flash usage: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" exit 1 else - echo "✅ Flash usage OK: $FLASH_USED / 1,300,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1300000" | bc)%)" + echo "✅ Flash usage OK: $FLASH_USED / 1,800,000 bytes ($(echo "scale=2; $FLASH_USED * 100 / 1800000" | bc)%)" fi - name: Create release artifacts run: | mkdir -p artifacts cp .pio/build/ttgo-lora32-v1/firmware.bin artifacts/firmware-${{ steps.validate-tag.outputs.tag_name }}.bin - cp .pio/build/ttgo-lora32-v1/firmware.elf artifacts/firmware-${{ steps.validate-tag.outputs.tag_name }}.elf cp .pio/build/ttgo-lora32-v1/firmware.map artifacts/firmware-${{ steps.validate-tag.outputs.tag_name }}.map cp .pio/build/ttgo-lora32-v1/partitions.bin artifacts/partitions-${{ steps.validate-tag.outputs.tag_name }}.bin @@ -312,7 +307,7 @@ jobs: "branch": "${{ github.ref_name }}", "files": { "firmware": "firmware-${{ steps.validate-tag.outputs.tag_name }}.bin", - "elf": "firmware-${{ steps.validate-tag.outputs.tag_name }}.elf", + "map": "firmware-${{ steps.validate-tag.outputs.tag_name }}.map", "partitions": "partitions-${{ steps.validate-tag.outputs.tag_name }}.bin" } } @@ -339,9 +334,8 @@ jobs: - **Commit**: ${{ github.sha }} ### Artifacts - - `firmware-${{ steps.validate-tag.outputs.tag_name }}.bin` - Binary firmware file - - `firmware-${{ steps.validate-tag.outputs.tag_name }}.elf` - ELF debug file - - `firmware-${{ steps.validate-tag.outputs.tag_name }}.map` - Memory map file (for debugging memory regressions) + - `firmware-${{ steps.validate-tag.outputs.tag_name }}.bin` - Binary firmware file for flashing + - `firmware-${{ steps.validate-tag.outputs.tag_name }}.map` - Memory map file (for debugging memory usage) - `partitions-${{ steps.validate-tag.outputs.tag_name }}.bin` - Partition table - `build-info-${{ steps.validate-tag.outputs.tag_name }}.json` - Build metadata diff --git a/.github/workflows/size-baseline.yml b/.github/workflows/size-baseline.yml index 1e7c6ec..d18ba38 100644 --- a/.github/workflows/size-baseline.yml +++ b/.github/workflows/size-baseline.yml @@ -48,12 +48,8 @@ jobs: # Copy build log first (always available) cp ci-build-release.log ci-baseline-artifacts/ - # Copy generated files if they exist - if [ -f ".pio/build/ttgo-lora32-v1/firmware.elf" ]; then - cp .pio/build/ttgo-lora32-v1/firmware.elf ci-baseline-artifacts/release-firmware.elf - else - echo "WARNING: release firmware.elf not found" >> ci-baseline-artifacts/build-warnings.txt - fi + # Copy generated files if they exist (excluding ELF files to reduce size) + # ELF files are too large for artifacts and not needed for size baseline if [ -f ".pio/build/ttgo-lora32-v1/firmware.map" ]; then cp .pio/build/ttgo-lora32-v1/firmware.map ci-baseline-artifacts/release-firmware.map @@ -98,12 +94,8 @@ jobs: # Copy debug build log cp ci-build-debug.log ci-baseline-artifacts/ - # Copy debug generated files if they exist - if [ -f ".pio/build/minimal/firmware.elf" ]; then - cp .pio/build/minimal/firmware.elf ci-baseline-artifacts/debug-firmware.elf - else - echo "WARNING: debug firmware.elf not found" >> ci-baseline-artifacts/build-warnings.txt - fi + # Copy debug generated files if they exist (excluding ELF files to reduce size) + # ELF files are too large for artifacts and not needed for size baseline if [ -f ".pio/build/minimal/firmware.map" ]; then cp .pio/build/minimal/firmware.map ci-baseline-artifacts/debug-firmware.map diff --git a/Makefile b/Makefile index 964722c..d307788 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,6 @@ minimal: # Test target that calls minimal build for CI readability test: minimal ## Build minimal env (alias for CI) - .PHONY: build-cli build-cli: cd cli && go build -o lora-sensor-cli . diff --git a/README.md b/README.md index d59e61a..ee69bba 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,59 @@ This project uses a single optimized build configuration: For detailed build information, see [docs/BUILD.md](docs/BUILD.md). +## Watchdog Management for OTA Updates + +The firmware implements comprehensive watchdog timer management and stack overflow prevention to prevent system crashes during OTA (Over-The-Air) firmware updates: + +### Watchdog Reset Points + +- **WiFi Connection Loop**: Reset every 100ms during WiFi connection attempts (8-second timeout) +- **HTTP Download Loop**: Reset every 1000ms during firmware download (improved from 2000ms) +- **WiFi Test Function**: Reset every 400ms during WiFi connectivity testing +- **OTA Progress Updates**: Watchdog reset occurs with each progress update (every 5 seconds or 50KB chunks) + +### Stack Overflow Prevention + +To prevent stack canary watchpoint panics that can occur during OTA operations: + +- **Heap-Allocated Download Buffer**: Uses `malloc()` to allocate a 2KB download buffer on the heap instead of stack +- **Reduced Buffer Size**: Download buffer reduced from 4KB to 2KB to minimize memory usage +- **Proper Error Cleanup**: All error paths properly free allocated memory with appropriate cleanup +- **Memory Safety**: Buffer is set to `nullptr` after freeing to prevent double-free issues + +### Timeout Constants + +- `WIFI_CONNECTION_TIMEOUT`: 8000ms (8 seconds) - Maximum time to wait for WiFi connection +- `HTTP_TOTAL_TIMEOUT`: 30000ms (30 seconds) - Total HTTP request timeout for OTA downloads +- `HTTP_CONNECT_TIMEOUT`: 15000ms (15 seconds) - HTTP connection establishment timeout +- `CONNECTIVITY_TEST_TIMEOUT`: 10000ms (10 seconds) - Internet connectivity test timeout +- `WATCHDOG_RESET_INTERVAL_DOWNLOAD`: 1000ms (1 second) - Watchdog reset frequency during download +- `WATCHDOG_RESET_INTERVAL_WIFI_TEST`: 400ms - Watchdog reset frequency during WiFi testing +- `PROGRESS_UPDATE_INTERVAL`: 5000ms (5 seconds) - Progress update and watchdog reset frequency +- `CHUNK_SIZE_THRESHOLD`: 51200 bytes (50KB) - Alternative trigger for progress updates + +### HTTPUpdate Library Integration + +The OTA system now uses ESP32's built-in HTTPUpdate library for improved reliability: + +- **Automatic HTTPS Support**: Handles both HTTP and HTTPS URLs with redirect following +- **MD5 Verification**: Uses HTTPUpdate's native MD5 verification with manual header injection + - GitHub doesn't provide `x-MD5` headers, so we inject the MD5 from TTN downlink via request callback + - HTTPUpdate then performs automatic MD5 verification during download +- **Progress Monitoring**: Real-time download progress with watchdog management +- **Error Handling**: Comprehensive error reporting and cleanup +- **Memory Efficiency**: No large buffer allocation - HTTPUpdate handles streaming internally +- **Redirect Support**: Automatic handling of HTTP redirects (strict mode enabled) + +### Network Diagnostics + +The OTA system includes comprehensive network diagnostics: + +- **HTTPS Support**: Automatic detection and handling of HTTPS URLs with certificate validation bypass +- **Timeout Configuration**: Explicit HTTP timeouts prevent indefinite blocking +- **Redirect Handling**: Automatic following of HTTP redirects using HTTPUpdate's built-in support +- **Error Reporting**: Detailed error codes and diagnostic messages for troubleshooting + ## Build Commands - `make release`: Build the main release environment (ttgo-lora32-v1) diff --git a/bin/mqtt-payload.sh b/bin/mqtt-payload.sh old mode 100644 new mode 100755 diff --git a/cli/cmd/update_device.go b/cli/cmd/update_device.go index b8d369d..234c39b 100644 --- a/cli/cmd/update_device.go +++ b/cli/cmd/update_device.go @@ -177,12 +177,10 @@ func updateDevice(ctx context.Context, cmd *cli.Command) error { // Step 10: Publish chunks to TTN MQTT topic topic := fmt.Sprintf("ttn/soil-conditions/devices/%s/down/push", deviceName) fmt.Printf("Publishing to topic: %s\n", topic) + fmt.Printf("Publishing %d chunks as single TTN message\n", len(chunks)) - for i, chunk := range chunks { - fmt.Printf("Publishing chunk %d/%d\n", i+1, len(chunks)) - if err := mqttClient.Publish(topic, chunk); err != nil { - return fmt.Errorf("failed to publish chunk %d: %w", i+1, err) - } + if err := mqttClient.PublishChunks(topic, chunks); err != nil { + return fmt.Errorf("failed to publish chunks: %w", err) } fmt.Println("Firmware update payload published successfully!") diff --git a/cli/file.bin b/cli/file.bin new file mode 100644 index 0000000..d4e59b5 Binary files /dev/null and b/cli/file.bin differ diff --git a/cli/lora-sensor-cli b/cli/lora-sensor-cli index 8b3fc05..3877af3 100755 Binary files a/cli/lora-sensor-cli and b/cli/lora-sensor-cli differ diff --git a/cli/pkg/mqtt/mqtt.go b/cli/pkg/mqtt/mqtt.go index c05c5fe..bd1f101 100644 --- a/cli/pkg/mqtt/mqtt.go +++ b/cli/pkg/mqtt/mqtt.go @@ -22,13 +22,16 @@ type Client struct { client mqtt.Client } -// TTNMessage represents a TTN MQTT message structure +// TTNDownlink represents a single TTN downlink message +type TTNDownlink struct { + FPort int `json:"f_port"` + FRMPayload string `json:"frm_payload"` + Priority string `json:"priority"` +} + +// TTNMessage represents a TTN MQTT message structure with chunked downlinks type TTNMessage struct { - DownlinkMessage struct { - FPort int `json:"f_port"` - FRMPayload string `json:"frm_payload"` - Priority string `json:"priority"` - } `json:"downlink_message"` + Downlinks []TTNDownlink `json:"downlinks"` } // NewClient creates a new MQTT client @@ -67,13 +70,21 @@ func (c *Client) Disconnect() { c.client.Disconnect(250) } -// Publish publishes a chunk as a TTN MQTT message -func (c *Client) Publish(topic string, chunk []byte) error { - // Create TTN message structure - message := TTNMessage{} - message.DownlinkMessage.FPort = 1 // Using fport 1 for OTA chunks - message.DownlinkMessage.FRMPayload = base64.StdEncoding.EncodeToString(chunk) - message.DownlinkMessage.Priority = "NORMAL" +// PublishChunks publishes multiple chunks as a single TTN MQTT message with downlinks array +func (c *Client) PublishChunks(topic string, chunks [][]byte) error { + // Create TTN message structure with multiple downlinks + message := TTNMessage{ + Downlinks: make([]TTNDownlink, len(chunks)), + } + + // Create downlink for each chunk with incrementing f_port + for i, chunk := range chunks { + message.Downlinks[i] = TTNDownlink{ + FPort: i + 1, // f_port starts at 1 and increments + FRMPayload: base64.StdEncoding.EncodeToString(chunk), + Priority: "NORMAL", + } + } // Marshal to JSON payload, err := json.Marshal(message) @@ -90,6 +101,11 @@ func (c *Client) Publish(topic string, chunk []byte) error { return nil } +// Publish publishes a single chunk as a TTN MQTT message (deprecated, use PublishChunks) +func (c *Client) Publish(topic string, chunk []byte) error { + return c.PublishChunks(topic, [][]byte{chunk}) +} + // IsConnected returns true if the client is connected func (c *Client) IsConnected() bool { return c.client.IsConnected() diff --git a/cli/pkg/mqtt/mqtt_test.go b/cli/pkg/mqtt/mqtt_test.go index 13ab543..9eb445f 100644 --- a/cli/pkg/mqtt/mqtt_test.go +++ b/cli/pkg/mqtt/mqtt_test.go @@ -29,11 +29,21 @@ func TestNewClient(t *testing.T) { } func TestTTNMessageStructure(t *testing.T) { - // Test the TTN message structure - message := TTNMessage{} - message.DownlinkMessage.FPort = 1 - message.DownlinkMessage.FRMPayload = base64.StdEncoding.EncodeToString([]byte("test data")) - message.DownlinkMessage.Priority = "NORMAL" + // Test the TTN message structure with chunked downlinks + message := TTNMessage{ + Downlinks: []TTNDownlink{ + { + FPort: 1, + FRMPayload: base64.StdEncoding.EncodeToString([]byte("test data chunk 1")), + Priority: "NORMAL", + }, + { + FPort: 2, + FRMPayload: base64.StdEncoding.EncodeToString([]byte("test data chunk 2")), + Priority: "NORMAL", + }, + }, + } // Marshal to JSON payload, err := json.Marshal(message) @@ -47,39 +57,122 @@ func TestTTNMessageStructure(t *testing.T) { t.Fatalf("Failed to unmarshal TTN message: %v", err) } - if unmarshaled.DownlinkMessage.FPort != 1 { - t.Errorf("Expected FPort 1, got %d", unmarshaled.DownlinkMessage.FPort) + if len(unmarshaled.Downlinks) != 2 { + t.Errorf("Expected 2 downlinks, got %d", len(unmarshaled.Downlinks)) + } + + // Check first downlink + if unmarshaled.Downlinks[0].FPort != 1 { + t.Errorf("Expected FPort 1, got %d", unmarshaled.Downlinks[0].FPort) + } + + if unmarshaled.Downlinks[0].Priority != "NORMAL" { + t.Errorf("Expected Priority NORMAL, got %s", unmarshaled.Downlinks[0].Priority) + } + + // Decode the FRMPayload for first chunk + decodedData1, err := base64.StdEncoding.DecodeString(unmarshaled.Downlinks[0].FRMPayload) + if err != nil { + t.Fatalf("Failed to decode FRMPayload: %v", err) + } + + if string(decodedData1) != "test data chunk 1" { + t.Errorf("Expected 'test data chunk 1', got %s", string(decodedData1)) } - if unmarshaled.DownlinkMessage.Priority != "NORMAL" { - t.Errorf("Expected Priority NORMAL, got %s", unmarshaled.DownlinkMessage.Priority) + // Check second downlink + if unmarshaled.Downlinks[1].FPort != 2 { + t.Errorf("Expected FPort 2, got %d", unmarshaled.Downlinks[1].FPort) } - // Decode the FRMPayload - decodedData, err := base64.StdEncoding.DecodeString(unmarshaled.DownlinkMessage.FRMPayload) + // Decode the FRMPayload for second chunk + decodedData2, err := base64.StdEncoding.DecodeString(unmarshaled.Downlinks[1].FRMPayload) if err != nil { t.Fatalf("Failed to decode FRMPayload: %v", err) } - if string(decodedData) != "test data" { - t.Errorf("Expected 'test data', got %s", string(decodedData)) + if string(decodedData2) != "test data chunk 2" { + t.Errorf("Expected 'test data chunk 2', got %s", string(decodedData2)) } } func TestTTNMessageJSON(t *testing.T) { - // Test that the JSON structure matches expected format - message := TTNMessage{} - message.DownlinkMessage.FPort = 1 - message.DownlinkMessage.FRMPayload = "dGVzdA==" - message.DownlinkMessage.Priority = "NORMAL" + // Test that the JSON structure matches expected TTN chunked format + message := TTNMessage{ + Downlinks: []TTNDownlink{ + { + FPort: 1, + FRMPayload: "dGVzdA==", + Priority: "NORMAL", + }, + { + FPort: 2, + FRMPayload: "ZGF0YQ==", + Priority: "NORMAL", + }, + }, + } payload, err := json.Marshal(message) if err != nil { t.Fatalf("Failed to marshal TTN message: %v", err) } - expectedJSON := `{"downlink_message":{"f_port":1,"frm_payload":"dGVzdA==","priority":"NORMAL"}}` + expectedJSON := `{"downlinks":[{"f_port":1,"frm_payload":"dGVzdA==","priority":"NORMAL"},{"f_port":2,"frm_payload":"ZGF0YQ==","priority":"NORMAL"}]}` if string(payload) != expectedJSON { t.Errorf("Expected JSON:\n%s\nGot:\n%s", expectedJSON, string(payload)) } } + +func TestPublishChunks(t *testing.T) { + // Test the PublishChunks functionality + config := Config{ + Broker: "tcp://localhost:1883", + Username: "test_user", + Password: "test_pass", + ClientID: "test_client", + } + + client, err := NewClient(config) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + // Test chunking multiple byte arrays + chunks := [][]byte{ + []byte("chunk1"), + []byte("chunk2"), + []byte("chunk3"), + } + + // This would normally connect and publish, but we're just testing the structure + // We can't easily test the actual MQTT publish without a running broker + if client == nil { + t.Fatal("Client should not be nil") + } + + // Verify we can create the message structure correctly + message := TTNMessage{ + Downlinks: make([]TTNDownlink, len(chunks)), + } + + for i, chunk := range chunks { + message.Downlinks[i] = TTNDownlink{ + FPort: i + 1, + FRMPayload: base64.StdEncoding.EncodeToString(chunk), + Priority: "NORMAL", + } + } + + if len(message.Downlinks) != 3 { + t.Errorf("Expected 3 downlinks, got %d", len(message.Downlinks)) + } + + if message.Downlinks[0].FPort != 1 { + t.Errorf("Expected first chunk FPort to be 1, got %d", message.Downlinks[0].FPort) + } + + if message.Downlinks[2].FPort != 3 { + t.Errorf("Expected third chunk FPort to be 3, got %d", message.Downlinks[2].FPort) + } +} diff --git a/platformio.ini b/platformio.ini index 0423102..0487edf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,23 +36,12 @@ common_lib_deps = electroniccats/CayenneLPP @ ^1.4.0 adafruit/Adafruit MAX1704X@^1.0.3 fastled/FastLED@^3.9.2 + tzapu/WiFiManager@^2.0.17 -; Build type configurations -debug_build_flags = - ${options.common_build_flags} - -D LMIC_DEBUG_LEVEL=2 - -D DEBUG=1 - -D DEBUG_SERIAL=Serial - -D LMIC_PRINTF_TO=Serial - -debug_lib_deps = - ${options.common_lib_deps} - tzapu/WiFiManager@^2.0.17 release_build_flags = ${options.common_build_flags} -D LMIC_DEBUG_LEVEL=0 - -D NDEBUG -D DISABLE_SERIAL_PRINTS=1 release_lib_deps = @@ -66,10 +55,8 @@ framework = arduino platform = ${options.platform} board = ${options.board} framework = ${options.framework} -board_build.partition_table = huge_app.csv -board_build.maximum_size = 3145728 -; Skip program size check -extra_scripts = pre:skip_size_check.py +board_build.partitions = min_spiffs.csv +build_src_filter = +<*> - build_flags = ${options.release_build_flags} -std=gnu++14 @@ -79,14 +66,9 @@ lib_deps = ${options.release_lib_deps} platform = ${options.platform} board = ${options.board} framework = ${options.framework} -board_build.partition_table = huge_app.csv -board_build.maximum_size = 3145728 -; Skip program size check -extra_scripts = pre:skip_size_check.py +board_build.partitions = huge_app.csv build_flags = ${options.common_build_flags} -std=gnu++14 -D MINIMAL_BUILD=1 - -D NDEBUG lib_deps = ${options.common_lib_deps} - diff --git a/src/debug.hpp b/src/debug.hpp deleted file mode 100644 index 24af161..0000000 --- a/src/debug.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "config.hpp" - -// Debug macros are now compile-time disabled for optimized builds -// These macros compile to nothing, ensuring zero runtime overhead -#if 0 // Always disabled for production builds -#define DEBUG_PRINT(x) Serial.print(x) -#define DEBUG_PRINTLN(x) Serial.println(x) -#define DEBUG_PRINTF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) -#define DEBUG_BEGIN(baud) Serial.begin(baud) -#else -#define DEBUG_PRINT(x) \ - do { \ - } while (0) -#define DEBUG_PRINTLN(x) \ - do { \ - } while (0) -#define DEBUG_PRINTF(fmt, ...) \ - do { \ - } while (0) -#define DEBUG_BEGIN(baud) \ - do { \ - } while (0) -#endif diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 62e8977..ce71703 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -3,7 +3,6 @@ #include "lorawan_settings.hpp" #include "utils.hpp" #include "ota.hpp" -#include "debug.hpp" #include #include #include "lorawan_settings.hpp" @@ -94,45 +93,6 @@ void LoraWANPrintLMICOpmode(void) { void LoraWANDebug(const lmic_t& lmic_check) { // Debug output now compile-time disabled for optimized builds - DEBUG_PRINT("LMIC.opmode: "); - // LoraWANPrintLMICOpmode() is now disabled via debug macros - DEBUG_PRINTLN(""); - DEBUG_PRINTLN("-----"); - - DEBUG_PRINT("LMIC.seqnoUp = "); - DEBUG_PRINTLN(String(lmic_check.seqnoUp)); - - DEBUG_PRINT("LMIC.globalDutyRate = "); - DEBUG_PRINT(String(lmic_check.globalDutyRate)); - DEBUG_PRINT(" osTicks, "); - DEBUG_PRINT(String(osticks2ms(lmic_check.globalDutyRate) / 1000)); - DEBUG_PRINTLN(" sec"); - - DEBUG_PRINT("LMIC.globalDutyAvail = "); - DEBUG_PRINT(String(lmic_check.globalDutyAvail)); - DEBUG_PRINT(" osTicks, "); - DEBUG_PRINT(String(osticks2ms(lmic_check.globalDutyAvail) / 1000)); - DEBUG_PRINTLN(" sec"); - - DEBUG_PRINT("LMICbandplan_nextTx = "); - DEBUG_PRINT(String(LMICbandplan_nextTx(os_getTime()))); - DEBUG_PRINT(" osTicks, "); - DEBUG_PRINT(String(osticks2ms(LMICbandplan_nextTx(os_getTime())) / 1000)); - DEBUG_PRINTLN(" sec"); - - DEBUG_PRINT("os_getTime = "); - DEBUG_PRINT(String(os_getTime())); - DEBUG_PRINT(" osTicks, "); - DEBUG_PRINT(String(osticks2ms(os_getTime()) / 1000)); - DEBUG_PRINTLN(" sec"); - - DEBUG_PRINT("LMIC.txend = "); - DEBUG_PRINTLN(String(lmic_check.txend)); - DEBUG_PRINT("LMIC.txChnl = "); - DEBUG_PRINTLN(String(lmic_check.txChnl)); - - DEBUG_PRINTLN(""); - DEBUG_PRINTLN(""); } void PrintLMICVersion() { @@ -400,8 +360,6 @@ void ReadSensors() { sd.vBat = maxlipo.cellVoltage(); sd.batPercent = maxlipo.cellPercent(); sd.batRate = maxlipo.chargeRate(); - DEBUG_PRINT("----ChargeRate: "); - DEBUG_PRINTLN(String(sd.batRate)); } for (int i = 0; i < MAX_SENSOR_READ; i++) { float a = static_cast(analogRead(config::SoilSensorPin)); @@ -414,13 +372,4 @@ void ReadSensors() { get_calibration_air_value(), get_calibration_water_value(), 0, 100)); sd.soilMoisturePercentage = abs(x); - DEBUG_PRINT("X: "); - DEBUG_PRINTLN(String(x)); - DEBUG_PRINT("Moisture ADC: "); - DEBUG_PRINT(String(sd.soilMoistureValue)); - DEBUG_PRINT(", Moisture Percentage: "); - DEBUG_PRINT(String(sd.soilMoisturePercentage)); - DEBUG_PRINT(", vBat "); - DEBUG_PRINTLN(String(sd.vBat)); - DEBUG_PRINTLN(""); } diff --git a/src/main.cpp b/src/main.cpp index 1e88381..260e329 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,9 +135,19 @@ void setup() { FastLED.setBrightness(50); lorawan_preferences_init(); Serial.print(FPSTR(msg_lmic_config_present)); - startWebConfig = !digitalRead(START_WEB_CONFIG_PIN); - bool otaa_cfg = settings_has_key("ttn_otaa_config"); - + Serial.print("Button pin raw value: "); + int pinValue = digitalRead(START_WEB_CONFIG_PIN); + Serial.println(pinValue); + startWebConfig = (pinValue == 0); // Explicitly check for LOW + Serial.print("startWebConfig set to: "); + Serial.println(startWebConfig ? 1 : 0); + bool otaa_cfg = settings_has_key("ttn_otaa_config"); + Serial.print("WEBCONF: "); + if (startWebConfig) { + Serial.println("true"); + } else { + Serial.println("false"); + } if ((startWebConfig == true) || (!otaa_cfg)) { setCpuFrequencyMhz(80); Serial.updateBaudRate(115200); diff --git a/src/menu.cpp b/src/menu.cpp index 1d88f26..3fe07cc 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -17,7 +17,6 @@ char wifi_password_str[64]; const char* menu[] = {"param", "restart"}; -#ifndef NDEBUG WiFiManager wifiManager; // Use raw pointers for WiFiManager parameters since WiFiManager doesn't take ownership // These will be managed manually and cleaned up when no longer needed @@ -30,7 +29,6 @@ WiFiManagerParameter* calibration_water_value = nullptr; WiFiManagerParameter* sleep_time_hours = nullptr; WiFiManagerParameter* wifi_ssid = nullptr; WiFiManagerParameter* wifi_password = nullptr; -#endif void loadSetings() { if (settings_has_key("app_eui")) { @@ -82,7 +80,6 @@ void loadSetings() { } void initMenu() { -#ifndef NDEBUG WiFiClass::mode(WIFI_STA); wifiManager.setMinimumSignalQuality(90); wifiManager.setRemoveDuplicateAPs(true); @@ -121,12 +118,9 @@ void initMenu() { wifiManager.addParameter(wifi_ssid); wifiManager.addParameter(wifi_password); wifiManager.setMenu(menu, sizeof(menu) / sizeof(menu[0])); -#else loadSetings(); -#endif } -#ifndef NDEBUG void startWebConf() { leds[0] = CRGB::Red; FastLED.show(); @@ -139,9 +133,7 @@ void startWebConf() { ESP.restart(); } } -#endif -#ifndef NDEBUG void saveConfigCallback() { Serial.println("Should save config"); @@ -218,16 +210,12 @@ void saveConfigCallback() { FastLED.show(); wifiManager.startConfigPortal("lora-node"); } -#endif -#ifndef NDEBUG void configModeCallback(const WiFiManager* /* myWiFiManager */) { WiFi.setTxPower(WIFI_POWER_8_5dBm); Serial.println("[CALLBACK] configModeCallback fired"); } -#endif -#ifndef NDEBUG // Cleanup function to delete allocated WiFiManagerParameter objects void cleanupWiFiManagerParameters() { delete ttn_app_eui; @@ -248,4 +236,3 @@ void cleanupWiFiManagerParameters() { wifi_ssid = nullptr; wifi_password = nullptr; } -#endif diff --git a/src/menu.hpp b/src/menu.hpp index 2c92018..ac3d7ca 100644 --- a/src/menu.hpp +++ b/src/menu.hpp @@ -2,9 +2,7 @@ #define MENU_HPP_ #include #include -#ifndef NDEBUG #include -#endif #include constexpr int CONFIG_TIMEOUT_SECONDS = 120; @@ -14,22 +12,9 @@ constexpr int MAX_INT_STR_LEN = 10; extern CRGB leds[1]; void loadSetings(); -#ifndef NDEBUG void saveConfigCallback(); void configModeCallback(WiFiManager *myWiFiManager); -#else -// Stub implementations for release build -inline void saveConfigCallback() { /* WiFiManager disabled in release build */ } -#endif - void initMenu(); -#ifndef NDEBUG void startWebConf(); void cleanupWiFiManagerParameters(); -#else -// Stub implementations for release build -inline void startWebConf() { /* WiFiManager disabled in release build */ } -inline void cleanupWiFiManagerParameters() { /* WiFiManager disabled in release build */ } -#endif - #endif diff --git a/src/ota.cpp b/src/ota.cpp index 0276037..4942a75 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -1,10 +1,14 @@ #include "ota.hpp" #include "lorawan_settings.hpp" +#include "lorawan.hpp" #include "menu.hpp" #include "utils.hpp" #include "version.hpp" -#include "debug.hpp" #include +#include +#include +#include +#include // OTA state volatile bool ota_in_progress = false; @@ -13,33 +17,43 @@ OtaChunkBuffer ota_chunk_buffer; // Update handleOtaChunk to use ota_chunk_buffer.addChunk, isComplete, and getJsonString void handleOtaChunk(uint8_t* data, uint8_t dataLen, uint8_t fport) { int idx = fport - 1; - DEBUG_PRINTF("[OTA] handleOtaChunk: fport=%d idx=%d dataLen=%d\r\n", fport, idx, dataLen); - DEBUG_PRINT("[OTA] Chunk data: "); - for (int i = 0; i < dataLen; ++i) DEBUG_PRINTF("%02X ", data[i]); - DEBUG_PRINTF("\r\n"); if (idx < 0 || idx >= OTA_MAX_CHUNKS) { - DEBUG_PRINTF("[OTA] Invalid chunk index: %d\r\n", idx); return; } + + // Check if this is a potential new OTA session by detecting out-of-order delivery + // If we receive any chunk but no previous chunks, reset the buffer + if (ota_chunk_buffer.max_chunk_seen == 0 && !ota_chunk_buffer.received[0] && fport != 1) { + Serial.printf("[DEBUG] Out-of-order chunk delivery detected (fport=%d) - resetting buffer\r\n", fport); + ota_chunk_buffer.reset(); + } + + // Always reset buffer when receiving the first chunk (fport=1) to start fresh OTA session + if (fport == 1) { + Serial.println("[DEBUG] Starting new OTA session - resetting chunk buffer"); + ota_chunk_buffer.reset(); + } + if (!ota_chunk_buffer.addChunk(idx, data, dataLen)) { - DEBUG_PRINTF("[OTA] Failed to add chunk idx=%d len=%d\r\n", idx, dataLen); return; } - DEBUG_PRINTF("[OTA] Chunk %d added. Checking completeness...\r\n", idx); if (ota_chunk_buffer.isComplete()) { - DEBUG_PRINTLN("[OTA] All chunks received. Attempting reassembly and JSON parse..."); String json = ota_chunk_buffer.getJsonString(); - DEBUG_PRINTF("[OTA] Reassembled JSON (%d bytes): %s\r\n", json.length(), json.c_str()); JsonDocument doc; auto error = deserializeJson(doc, json); if (!error) { Serial.println("[OTA] JSON parsed successfully. Triggering firmware update."); + Serial.printf("[DEBUG] Raw JSON string: %s\r\n", json.c_str()); + Serial.printf("[DEBUG] JSON length: %d\r\n", json.length()); + OtaUpdateInfo updateInfo; // Extract fields (support both full and short names) if (doc["url"]) { updateInfo.url = doc["url"].as(); + Serial.printf("[DEBUG] Extracted URL (from 'url'): %s\r\n", updateInfo.url.c_str()); } else if (doc["u"]) { updateInfo.url = doc["u"].as(); + Serial.printf("[DEBUG] Extracted URL (from 'u'): %s\r\n", updateInfo.url.c_str()); } else { Serial.println("Missing url field"); ota_chunk_buffer.reset(); @@ -92,12 +106,7 @@ void handleOtaChunk(uint8_t* data, uint8_t dataLen, uint8_t fport) { setOtaInProgress(false); ota_chunk_buffer.reset(); } - } else { - DEBUG_PRINTF("[OTA] JSON parse failed: %s\r\n", error.c_str()); } - // else: wait for more chunks or reset on fatal error - } else { - DEBUG_PRINTLN("[OTA] Waiting for more chunks..."); } } @@ -187,20 +196,43 @@ static bool base64_decode(unsigned char* output, const char* input, int length) int i = 0, j = 0; int a, b, c, d; while (i < length) { + // Handle padding and end of string + if (input[i] == '=' || input[i] == 0) break; + + // Find index of first character for (a = 0; a < 64 && base64_chars[a] != input[i]; a++); - i++; - if (a == 64) return false; + if (a == 64 || ++i >= length) return false; + + // Find index of second character + if (input[i] == '=' || input[i] == 0) return false; for (b = 0; b < 64 && base64_chars[b] != input[i]; b++); - i++; - if (b == 64) return false; + if (b == 64 || ++i >= length) return false; + + // Decode first byte + output[j++] = (a << 2) | (b >> 4); + + // Handle third character (might be padding) + if (input[i] == '=' || input[i] == 0) break; for (c = 0; c < 64 && base64_chars[c] != input[i]; c++); + if (c == 64) { + if (input[i] != '=') return false; + break; + } i++; - if (c == 64) return false; + + // Decode second byte + output[j++] = (b << 4) | (c >> 2); + + // Handle fourth character (might be padding) + if (i >= length || input[i] == '=' || input[i] == 0) break; for (d = 0; d < 64 && base64_chars[d] != input[i]; d++); + if (d == 64) { + if (input[i] != '=') return false; + break; + } i++; - if (d == 64) return false; - output[j++] = (a << 2) | (b >> 4); - output[j++] = (b << 4) | (c >> 2); + + // Decode third byte output[j++] = (c << 6) | d; } return true; @@ -244,102 +276,204 @@ bool verify_signature(const String& url, const String& md5sum, const String& sig return true; } -#include bool downloadAndInstallFirmware(const OtaUpdateInfo& updateInfo) { if (!updateInfo.valid) { Serial.println("Invalid update info"); return false; } + delay(100); + setCpuFrequencyMhz(240); + Serial.updateBaudRate(115200); + delay(100); + Serial.println("Starting WiFi setup for OTA download..."); + + // Reset watchdog before starting OTA operations + esp_task_wdt_reset(); + // Get saved WiFi credentials String ssid = settings_get_string("wifi_ssid"); String password = settings_get_string("wifi_password"); + + Serial.printf("SSID from settings: '%s' (length: %d)\r\n", ssid.c_str(), ssid.length()); + Serial.printf("Password from settings: '%s' (length: %d)\r\n", + password.length() > 0 ? "[CONFIGURED]" : "[EMPTY]", password.length()); + if (ssid.length() == 0) { - Serial.println("No WiFi credentials configured"); + Serial.println("ERROR: No WiFi SSID configured in settings!"); return false; } + + // Disconnect any existing WiFi connections and reset state + WiFi.disconnect(true); + delay(100); + WiFi.mode(WIFI_OFF); + delay(100); + // Enable WiFi for download + Serial.println("Enabling WiFi in STA mode..."); WiFi.mode(WIFI_STA); + delay(100); + + Serial.printf("Connecting to WiFi SSID: %s\r\n", ssid.c_str()); WiFi.begin(ssid.c_str(), password.c_str()); - // Wait for WiFi connection - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { - delay(500); - Serial.print("."); - attempts++; - } - if (WiFi.status() != WL_CONNECTED) { - Serial.println("Failed to connect to WiFi"); - return false; + + bool connected = false; + unsigned long startAttemptTime = millis(); + Serial.print("Connecting to WiFi..."); + + while (!connected && (millis() - startAttemptTime < 8000)) { + if (WiFi.status() == WL_CONNECTED) { + connected = true; + } + delay(100); + esp_task_wdt_reset(); } - Serial.println("WiFi connected"); - Serial.print("IP: "); - Serial.println(WiFi.localIP()); - // Download firmware - HTTPClient http; - http.begin(updateInfo.url); - int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) { - Serial.printf("HTTP GET failed, error: %s\r\n", http.errorToString(httpCode).c_str()); - http.end(); + + if (!connected) { + Serial.println("\nERROR: Failed to connect to WiFi within timeout!"); + Serial.printf("Final WiFi status: %d\n", WiFi.status()); return false; } - int contentLength = http.getSize(); - Serial.printf("Content length: %d\r\n", contentLength); - if (contentLength <= 0) { - Serial.println("Invalid content length"); - http.end(); + + Serial.println("\nSUCCESS: WiFi connected!"); + Serial.printf("IP Address: %s\r\n", WiFi.localIP().toString().c_str()); + Serial.printf("Gateway: %s\r\n", WiFi.gatewayIP().toString().c_str()); + Serial.printf("DNS: %s\r\n", WiFi.dnsIP().toString().c_str()); + Serial.printf("Signal strength: %d dBm\r\n", WiFi.RSSI()); + + // Show detailed memory information before starting OTA + Serial.println("=== Memory Status Before OTA ==="); + Serial.printf("Free heap: %d bytes\r\n", ESP.getFreeHeap()); + Serial.printf("Largest free block: %d bytes\r\n", ESP.getMaxAllocHeap()); + Serial.printf("Free PSRAM: %d bytes\r\n", ESP.getFreePsram()); + Serial.printf("Sketch size: %d bytes\r\n", ESP.getSketchSize()); + Serial.printf("Free sketch space: %d bytes\r\n", ESP.getFreeSketchSpace()); + Serial.printf("Flash chip size: %d bytes\r\n", ESP.getFlashChipSize()); + + // Check if we have sufficient memory for OTA + const size_t MIN_FREE_HEAP = 32768; // 32KB minimum + if (ESP.getFreeHeap() < MIN_FREE_HEAP) { + Serial.printf("ERROR: Insufficient heap memory for OTA. Need %d, have %d\r\n", + MIN_FREE_HEAP, ESP.getFreeHeap()); return false; } - // Check if we have enough space for the update - if (contentLength > UPDATE_SIZE_UNKNOWN) { - Serial.println("Firmware too large"); - http.end(); - return false; + + // Configure HTTPUpdate with callbacks for progress monitoring + httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpUpdate.rebootOnUpdate(false); // We'll handle reboot ourselves + + // Set up progress callback + httpUpdate.onProgress([](int progress, int total) { + static unsigned long lastUpdate = 0; + unsigned long now = millis(); + + // Update every 2 seconds to reduce serial spam + if (now - lastUpdate >= 2000 || progress == total) { + Serial.printf("OTA Progress: %d/%d bytes (%.1f%%)\r\n", + progress, total, (progress * 100.0) / total); + lastUpdate = now; + } + + // Reset watchdog during download + esp_task_wdt_reset(); + }); + + // Set up error callback + httpUpdate.onError([](int error) { + Serial.printf("HTTPUpdate error: %d - %s\r\n", error, httpUpdate.getLastErrorString().c_str()); + }); + + // Set up start callback + httpUpdate.onStart([]() { + Serial.println("HTTPUpdate started"); + }); + + // Set up end callback + httpUpdate.onEnd([]() { + Serial.println("HTTPUpdate finished"); + }); + + // Create HTTPClient for the update + HTTPClient httpClient; + WiFiClientSecure* secureClient = nullptr; + + Serial.printf("Starting OTA download from: %s\r\n", updateInfo.url.c_str()); + + // Determine if we need HTTPS support + if (updateInfo.url.startsWith("https://")) { + secureClient = new WiFiClientSecure(); + secureClient->setInsecure(); // Skip certificate validation for simplicity + if (!httpClient.begin(*secureClient, updateInfo.url)) { + Serial.println("HTTPClient begin failed for HTTPS URL"); + delete secureClient; + return false; + } + } else { + if (!httpClient.begin(updateInfo.url)) { + Serial.println("HTTPClient begin failed for HTTP URL"); + return false; + } } - // Download firmware to buffer for MD5 validation - std::vector fw_buf; - fw_buf.reserve(contentLength); - WiFiClient* stream = http.getStreamPtr(); - int total_read = 0; - while (total_read < contentLength) { - uint8_t buf[512]; - int to_read = std::min(512, contentLength - total_read); - int n = stream->read(buf, to_read); - if (n <= 0) break; - fw_buf.insert(fw_buf.end(), buf, buf + n); - total_read += n; - } - http.end(); - Serial.printf("Downloaded %d bytes\r\n", (int)fw_buf.size()); - // Calculate MD5 - unsigned char hash[16]; - mbedtls_md5(fw_buf.data(), fw_buf.size(), hash); - char md5str[33]; - for (int i = 0; i < 16; ++i) sprintf(md5str + i * 2, "%02x", hash[i]); - md5str[32] = 0; - Serial.printf("Calculated MD5: %s\r\n", md5str); + + // Configure HTTP timeouts + httpClient.setTimeout(30000); // 30 second timeout + httpClient.setConnectTimeout(15000); // 15 second connection timeout + Serial.printf("Expected MD5: %s\r\n", updateInfo.md5sum.c_str()); - if (strcasecmp(md5str, updateInfo.md5sum.c_str()) != 0) { - Serial.println("MD5 mismatch! Aborting update."); - return false; - } - Serial.println("MD5 matches. Proceeding with OTA update."); - // Start update - if (!Update.begin(fw_buf.size())) { - Serial.println("Not enough space to begin OTA"); - return false; - } - size_t written = Update.write(fw_buf.data(), fw_buf.size()); - if (written != fw_buf.size()) { - Serial.printf("Written %d of %d bytes\r\n", (int)written, (int)fw_buf.size()); - return false; - } - if (!Update.end()) { - Serial.println("Update end failed"); - return false; + + // Force garbage collection and memory cleanup before OTA + Serial.println("Preparing for OTA - forcing garbage collection..."); + delay(500); + esp_task_wdt_reset(); + + // Create request callback to add MD5 header (GitHub doesn't provide x-MD5, so we add it manually) + HTTPUpdateRequestCB requestCallback = [&updateInfo](HTTPClient* client) { + // Add the MD5 header so HTTPUpdate can verify it + client->addHeader("x-MD5", updateInfo.md5sum); + Serial.printf("Added x-MD5 header: %s\r\n", updateInfo.md5sum.c_str()); + }; + + // Show final memory status before OTA + Serial.printf("Final memory check - Free heap: %d bytes\r\n", ESP.getFreeHeap()); + + // Perform the update with our MD5 header callback + Serial.println("Starting HTTPUpdate.update()..."); + HTTPUpdateResult result; + + // Try the OTA update with proper error handling + try { + result = httpUpdate.update(httpClient, updateInfo.version, requestCallback); + } catch (const std::exception& e) { + Serial.printf("Exception during OTA update: %s\r\n", e.what()); + result = HTTP_UPDATE_FAILED; + } catch (...) { + Serial.println("Unknown exception during OTA update"); + result = HTTP_UPDATE_FAILED; + } + + // Clean up HTTPS client if created + if (secureClient) { + delete secureClient; + } + + switch (result) { + case HTTP_UPDATE_OK: + Serial.println("OTA update completed successfully!"); + Serial.println("Firmware updated, ready to reboot"); + esp_task_wdt_reset(); + return true; + + case HTTP_UPDATE_NO_UPDATES: + Serial.println("No updates available (same version)"); + return false; + + case HTTP_UPDATE_FAILED: + default: + Serial.printf("OTA update failed. Error (%d): %s\r\n", + httpUpdate.getLastError(), + httpUpdate.getLastErrorString().c_str()); + return false; } - Serial.println("OTA update completed successfully"); - return true; } bool verifyMd5Sum(const uint8_t* data, size_t dataLen, const String& expectedMd5) { @@ -347,16 +481,11 @@ bool verifyMd5Sum(const uint8_t* data, size_t dataLen, const String& expectedMd5 return false; } - unsigned char hash[16]; - mbedtls_md5(data, dataLen, hash); - - String calculatedMd5 = ""; - for (int i = 0; i < 16; i++) { - if (hash[i] < 0x10) { - calculatedMd5 += "0"; - } - calculatedMd5 += String(hash[i], HEX); - } + MD5Builder md5; + md5.begin(); + md5.add(const_cast(data), dataLen); + md5.calculate(); + String calculatedMd5 = md5.toString(); Serial.print("Calculated MD5: "); Serial.println(calculatedMd5); @@ -386,22 +515,34 @@ bool testWifiConnection(const String& ssid, const String& password) { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), password.c_str()); - // Try to connect for 10 seconds - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { - delay(500); - Serial.print("."); - attempts++; - } + // Non-blocking connection with 8 second timeout + bool connected = false; + unsigned long startTime = millis(); + unsigned long lastWdtReset = millis(); - bool connected = (WiFi.status() == WL_CONNECTED); + Serial.print("Testing WiFi connection..."); + + while (!connected && (millis() - startTime < 8000)) { + if (WiFi.status() == WL_CONNECTED) { + connected = true; + } else { + delay(100); + + // Reset watchdog every 400ms + if (millis() - lastWdtReset >= 400) { + esp_task_wdt_reset(); + lastWdtReset = millis(); + Serial.print("."); + } + } + } if (connected) { - Serial.println("WiFi test connection successful"); + Serial.println("\nWiFi test connection successful"); Serial.print("IP: "); Serial.println(WiFi.localIP()); } else { - Serial.println("WiFi test connection failed"); + Serial.println("\nWiFi test connection failed"); } // Disconnect for testing @@ -421,12 +562,21 @@ bool isOtaInProgress() { // --- OtaChunkBuffer methods for chunked OTA reassembly --- bool OtaChunkBuffer::addChunk(int chunk_index, const uint8_t* data, int data_len) { - if (chunk_index < 0 || chunk_index >= OTA_MAX_CHUNKS) return false; - if (data_len > OTA_CHUNK_SIZE) return false; + if (chunk_index < 0 || chunk_index >= OTA_MAX_CHUNKS) { + Serial.printf("[DEBUG] Chunk index %d out of range (0-%d)\r\n", chunk_index, OTA_MAX_CHUNKS - 1); + return false; + } + if (data_len > OTA_CHUNK_SIZE) { + Serial.printf("[DEBUG] Chunk data length %d exceeds max %d\r\n", data_len, OTA_CHUNK_SIZE); + return false; + } memcpy(decoded_chunks[chunk_index], data, data_len); chunk_lens[chunk_index] = data_len; received[chunk_index] = true; if (chunk_index > max_chunk_seen) max_chunk_seen = chunk_index; + + Serial.printf("[DEBUG] Added chunk %d: %d bytes (max_chunk_seen=%d)\r\n", chunk_index, data_len, max_chunk_seen); + return true; } diff --git a/src/ota.hpp b/src/ota.hpp index 8237edc..8d5b754 100644 --- a/src/ota.hpp +++ b/src/ota.hpp @@ -1,8 +1,8 @@ #ifndef OTA_HPP_ #define OTA_HPP_ -constexpr int OTA_MAX_CHUNKS = 20; // Increased for larger payloads -constexpr int OTA_CHUNK_SIZE = 70; // Increased to accommodate base64 encoded payloads +constexpr int OTA_MAX_CHUNKS = 30; // Increased for larger payloads +constexpr int OTA_CHUNK_SIZE = 100; // Increased to accommodate base64 encoded payloads constexpr int OTA_MAX_BUFFER_SIZE = OTA_MAX_CHUNKS * OTA_CHUNK_SIZE; // Total buffer size #include @@ -10,7 +10,8 @@ constexpr int OTA_MAX_BUFFER_SIZE = OTA_MAX_CHUNKS * OTA_CHUNK_SIZE; // T #include "config.hpp" #include #include -#include +#include +#include #include // OTA Parse Result enum