Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4368688
Fix MQTT payload generation for TTN chunking
jescarri Jul 22, 2025
b65a29a
Fix base64 decoder in OTA signature verification
jescarri Jul 22, 2025
f52d4f6
Add watchdog resets during firmware download to prevent timeout
jescarri Jul 22, 2025
a1060c4
Enable HTTP redirects for URL shortener support
jescarri Jul 22, 2025
5ffdd09
Add comprehensive WiFi debugging and improved connection handling
jescarri Jul 22, 2025
e422524
Add LMIC suspension during WiFi operations to prevent interference
jescarri Jul 23, 2025
975b4e5
WIP
jescarri Jul 23, 2025
55b903c
Fix OTA watchdog handling and update documentation
jescarri Jul 23, 2025
404bbb5
Fix OTA stack overflow and watchdog timeout issues
jescarri Jul 23, 2025
165c84e
Improve OTA HTTP timeout handling and connectivity diagnostics
jescarri Jul 23, 2025
97571df
Update documentation with HTTP timeout and network diagnostic improve…
jescarri Jul 23, 2025
6f92a69
Add debug output to diagnose OTA URL parsing issues
jescarri Jul 23, 2025
cc9aa9a
Increase OTA buffer sizes and add debug output for chunked messages
jescarri Jul 23, 2025
99d041f
WIP-Crash
jescarri Jul 24, 2025
1bd5297
feat: integrate ESP32 HTTPUpdate library for improved OTA reliability
jescarri Jul 24, 2025
b4272cf
fix: improve OTA robustness with memory diagnostics and error handling
jescarri Jul 24, 2025
d8defaa
fix: change to OTA-compatible partition table (min_spiffs.csv)
jescarri Jul 24, 2025
2712f17
fix: update GitHub Actions flash size limits for OTA partition table
jescarri Jul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 18 additions & 24 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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

Expand All @@ -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"
}
}
Expand All @@ -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

Expand Down
16 changes: 4 additions & 12 deletions .github/workflows/size-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Empty file modified bin/mqtt-payload.sh
100644 → 100755
Empty file.
8 changes: 3 additions & 5 deletions cli/cmd/update_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand Down
Binary file added cli/file.bin
Binary file not shown.
Binary file modified cli/lora-sensor-cli
Binary file not shown.
42 changes: 29 additions & 13 deletions cli/pkg/mqtt/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down
Loading