feat: add Ethernet OTA support for RP2350/W5500 boards (#10136)
* feat: add Raspberry Pi Pico 2 + W5500 + E22-900M30S variant Adds community variant for Raspberry Pi Pico 2 (RP2350, 4 MB flash) with external WIZnet W5500 Ethernet module and EBYTE E22-900M30S LoRa module (SX1262, 30 dBm PA, 868/915 MHz). Key details: - LoRa on SPI1: GP10/11/12/13 (SCK/MOSI/MISO/CS), RST=GP15, DIO1=GP14, BUSY=GP2, RXEN=GP3 (held HIGH via SX126X_ANT_SW) - W5500 on SPI0: GP16/17/18/19/20 (MISO/CS/SCK/MOSI/RST) - SX126X_DIO2_AS_RF_SWITCH: DIO2→TXEN bridge on module handles PA - SX126X_DIO3_TCXO_VOLTAGE 1.8: TCXO support via EBYTE_E22 flags - DHCP timeout reduced to 10 s to avoid blocking LoRa startup - GPS on UART1/Serial2: GP8 TX, GP9 RX - Reuses WIZNET_5500_EVB_PICO2 code paths for Ethernet init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * pico2_w5500_e22: rename define and address review feedback Rename WIZNET_5500_EVB_PICO2 to PICO2_W5500_E22 so the variant-specific define matches the variant directory name and isn't confused with an on-board EVB SKU. Review fixes from PR #10135: - Gate the 10 s Ethernet DHCP timeout behind PICO2_W5500_E22 so other Ethernet builds keep the default 60 s behavior; apply the same timeout to reconnectETH() for consistency. - Drop the unused -D EBYTE_E22 flag; EBYTE_E22_900M30S already selects TX_GAIN_LORA / SX126X_MAX_POWER in src/configuration.h. - Rewrite "on-board W5500" comments to describe the external module. - Correct README TX_GAIN_LORA value (7, not 10) and drop the EBYTE_E22 row. * fix(pico2_w5500_e22): drop DEBUG_RP2040_PORT=Serial The arduino-pico framework hooks _write() when DEBUG_RP2040_PORT=Serial is set and dumps raw debug bytes onto USB CDC, corrupting any binary protobuf stream sent through StreamAPI (e.g. `meshtastic --port COMx`). The variant excludes BT and WiFi, so the primary client transport is Ethernet TCP via ethServerAPI — unaffected — but users who configure the node over USB serial would see protobuf decode failures from debug-byte interleaving. Removing the flag restores clean USB CDC. Debug output can still be enabled per-build by adding -D DEBUG_RP2040_PORT=Serial1 to redirect to UART0 instead of USB CDC. * feat: add Ethernet OTA support for RP2350/W5500 boards Adds over-the-air firmware update capability for RP2350-based boards with a WIZnet W5500 Ethernet module (e.g. pico2_w5500_e22). Protocol (MOTA): - SHA256 challenge-response authentication with a configurable PSK (override via USERPREFS_OTA_PSK; default key ships in source) - 12-byte header: magic "MOTA" + firmware size + CRC32 - Firmware received in 1 KB chunks, verified with CRC32, written via Updater (picoOTA), then device reboots to apply - Constant-time hash comparison prevents timing attacks on auth - 30s inactivity timeout + 5s cooldown after failed auth - Response codes 0x00-0x08 map 1:1 to OTAResponse enum Firmware side: - ethOTA.cpp / ethOTA.h: OTA TCP server on port 4243 - ethClient.cpp: wire initEthOTA/ethOTALoop into reconnect loop - main-rp2xx0.cpp: hardware watchdog (8s, paused during debug) - pico2_w5500_e22/platformio.ini: HAS_ETHERNET_OTA flag, filesystem_size bumped to 0.75m for OTA staging Host side: - bin/eth-ota-upload.py: Python uploader with progress and full result-code mapping (matches OTAResponse 0x00-0x08) * style(eth): clang-format ethOTA.cpp per repo .clang-format Reformat to the repo trunk clang-format config (IndentWidth 4, ColumnLimit 130). Resolves the Trunk Check 'Incorrect formatting' failure on PR #10136. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
GitHub
Claude Sonnet 4.6
parent
68af6b277c
commit
4f0e2dde98
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Meshtastic Ethernet OTA Upload Tool
|
||||
|
||||
Uploads firmware to RP2350-based Meshtastic devices via Ethernet (W5500).
|
||||
Compresses firmware with GZIP and sends it over TCP using the MOTA protocol.
|
||||
Authenticates using SHA256 challenge-response with a pre-shared key (PSK).
|
||||
|
||||
Usage:
|
||||
python bin/eth-ota-upload.py --host 192.168.1.100 firmware.bin
|
||||
python bin/eth-ota-upload.py --host 192.168.1.100 --psk mySecretKey firmware.bin
|
||||
python bin/eth-ota-upload.py --host 192.168.1.100 --psk-hex 6d65736874... firmware.bin
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Default PSK matching the firmware default: "meshtastic_ota_default_psk_v1!!!"
|
||||
DEFAULT_PSK = b"meshtastic_ota_default_psk_v1!!!"
|
||||
|
||||
|
||||
def crc32(data: bytes) -> int:
|
||||
"""Compute CRC32 matching ErriezCRC32 (standard CRC32 with final XOR)."""
|
||||
import binascii
|
||||
|
||||
return binascii.crc32(data) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def load_firmware(path: str) -> bytes:
|
||||
"""Load firmware file, compressing with GZIP if not already compressed."""
|
||||
# Reject UF2 files — OTA requires raw .bin firmware
|
||||
if path.lower().endswith(".uf2"):
|
||||
bin_path = path.rsplit(".", 1)[0] + ".bin"
|
||||
print(f"ERROR: UF2 files cannot be used for OTA updates.")
|
||||
print(f" The Updater/picoOTA expects raw .bin firmware.")
|
||||
print(f" Try: {bin_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Check if already GZIP compressed (magic bytes 1f 8b)
|
||||
if data[:2] == b"\x1f\x8b":
|
||||
print(f"Firmware already GZIP compressed: {len(data):,} bytes")
|
||||
return data
|
||||
|
||||
print(f"Firmware raw size: {len(data):,} bytes")
|
||||
compressed = gzip.compress(data, compresslevel=9)
|
||||
ratio = len(compressed) / len(data) * 100
|
||||
print(f"GZIP compressed: {len(compressed):,} bytes ({ratio:.1f}%)")
|
||||
return compressed
|
||||
|
||||
|
||||
def authenticate(sock: socket.socket, psk: bytes) -> bool:
|
||||
"""Perform SHA256 challenge-response authentication with the device."""
|
||||
# Receive 32-byte nonce from server
|
||||
nonce = b""
|
||||
while len(nonce) < 32:
|
||||
chunk = sock.recv(32 - len(nonce))
|
||||
if not chunk:
|
||||
print("ERROR: Connection closed during authentication")
|
||||
return False
|
||||
nonce += chunk
|
||||
|
||||
# Compute SHA256(nonce || PSK)
|
||||
h = hashlib.sha256()
|
||||
h.update(nonce)
|
||||
h.update(psk)
|
||||
response = h.digest()
|
||||
|
||||
# Send 32-byte response
|
||||
sock.sendall(response)
|
||||
|
||||
# Wait for auth result (1 byte)
|
||||
result = sock.recv(1)
|
||||
if not result:
|
||||
print("ERROR: No authentication response")
|
||||
return False
|
||||
|
||||
if result[0] == 0x06: # ACK
|
||||
print("Authentication successful.")
|
||||
return True
|
||||
elif result[0] == 0x07: # OTA_ERR_AUTH
|
||||
print("ERROR: Authentication failed — wrong PSK")
|
||||
return False
|
||||
else:
|
||||
print(f"ERROR: Unexpected auth response 0x{result[0]:02X}")
|
||||
return False
|
||||
|
||||
|
||||
def upload_firmware(host: str, port: int, firmware: bytes, psk: bytes, timeout: float) -> bool:
|
||||
"""Upload firmware over TCP using the MOTA protocol with PSK authentication."""
|
||||
fw_crc = crc32(firmware)
|
||||
fw_size = len(firmware)
|
||||
|
||||
print(f"Connecting to {host}:{port}...")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
|
||||
try:
|
||||
sock.connect((host, port))
|
||||
print("Connected.")
|
||||
|
||||
# Step 1: Authenticate
|
||||
print("Authenticating...")
|
||||
if not authenticate(sock, psk):
|
||||
return False
|
||||
|
||||
# Step 2: Send 12-byte MOTA header: magic(4) + size(4) + crc32(4)
|
||||
header = struct.pack("<4sII", b"MOTA", fw_size, fw_crc)
|
||||
sock.sendall(header)
|
||||
print(f"Header sent: size={fw_size:,}, CRC32=0x{fw_crc:08X}")
|
||||
|
||||
# Wait for ACK (1 byte)
|
||||
ack = sock.recv(1)
|
||||
if not ack or ack[0] != 0x06:
|
||||
error_codes = {
|
||||
0x02: "Size error",
|
||||
0x04: "Invalid magic",
|
||||
0x05: "Update.begin() failed",
|
||||
}
|
||||
code = ack[0] if ack else 0xFF
|
||||
msg = error_codes.get(code, f"Unknown error 0x{code:02X}")
|
||||
print(f"ERROR: Server rejected header: {msg}")
|
||||
return False
|
||||
|
||||
print("Header accepted. Uploading firmware...")
|
||||
|
||||
# Send firmware in 1KB chunks
|
||||
chunk_size = 1024
|
||||
sent = 0
|
||||
start_time = time.time()
|
||||
|
||||
while sent < fw_size:
|
||||
end = min(sent + chunk_size, fw_size)
|
||||
chunk = firmware[sent:end]
|
||||
sock.sendall(chunk)
|
||||
sent = end
|
||||
|
||||
# Progress bar
|
||||
pct = sent * 100 // fw_size
|
||||
bar_len = 40
|
||||
filled = bar_len * sent // fw_size
|
||||
bar = "█" * filled + "░" * (bar_len - filled)
|
||||
elapsed = time.time() - start_time
|
||||
speed = sent / elapsed if elapsed > 0 else 0
|
||||
sys.stdout.write(f"\r [{bar}] {pct:3d}% {sent:,}/{fw_size:,} ({speed/1024:.1f} KB/s)")
|
||||
sys.stdout.flush()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n Transfer complete in {elapsed:.1f}s")
|
||||
|
||||
# Wait for final result (1 byte)
|
||||
print("Waiting for verification...")
|
||||
result = sock.recv(1)
|
||||
if not result:
|
||||
print("ERROR: No response from device")
|
||||
return False
|
||||
|
||||
result_codes = {
|
||||
0x00: "OK — Update staged, device rebooting",
|
||||
0x01: "CRC mismatch",
|
||||
0x02: "Size error",
|
||||
0x03: "Write error",
|
||||
0x04: "Magic mismatch",
|
||||
0x05: "Updater.begin() failed",
|
||||
0x07: "Auth failed",
|
||||
0x08: "Timeout",
|
||||
}
|
||||
code = result[0]
|
||||
msg = result_codes.get(code, f"Unknown result 0x{code:02X}")
|
||||
|
||||
if code == 0x00:
|
||||
print(f"SUCCESS: {msg}")
|
||||
return True
|
||||
else:
|
||||
print(f"ERROR: {msg}")
|
||||
return False
|
||||
|
||||
except socket.timeout:
|
||||
print("ERROR: Connection timed out")
|
||||
return False
|
||||
except ConnectionRefusedError:
|
||||
print(f"ERROR: Connection refused by {host}:{port}")
|
||||
return False
|
||||
except OSError as e:
|
||||
print(f"ERROR: {e}")
|
||||
return False
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Upload firmware to Meshtastic RP2350 devices via Ethernet OTA"
|
||||
)
|
||||
parser.add_argument("firmware", help="Path to firmware .bin or .bin.gz file")
|
||||
parser.add_argument("--host", required=True, help="Device IP address")
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=4243, help="OTA port (default: 4243)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=60.0,
|
||||
help="Socket timeout in seconds (default: 60)",
|
||||
)
|
||||
psk_group = parser.add_mutually_exclusive_group()
|
||||
psk_group.add_argument(
|
||||
"--psk",
|
||||
type=str,
|
||||
help="Pre-shared key as UTF-8 string (default: meshtastic_ota_default_psk_v1!!!)",
|
||||
)
|
||||
psk_group.add_argument(
|
||||
"--psk-hex",
|
||||
type=str,
|
||||
help="Pre-shared key as hex string (e.g., 6d65736874...)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Resolve PSK
|
||||
if args.psk:
|
||||
psk = args.psk.encode("utf-8")
|
||||
elif args.psk_hex:
|
||||
try:
|
||||
psk = bytes.fromhex(args.psk_hex)
|
||||
except ValueError:
|
||||
print("ERROR: Invalid hex string for --psk-hex")
|
||||
sys.exit(1)
|
||||
else:
|
||||
psk = DEFAULT_PSK
|
||||
|
||||
print("Meshtastic Ethernet OTA Upload")
|
||||
print("=" * 40)
|
||||
|
||||
firmware = load_firmware(args.firmware)
|
||||
|
||||
if upload_firmware(args.host, args.port, firmware, psk, args.timeout):
|
||||
print("\nDevice is rebooting with new firmware.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nUpload failed.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "main.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
#include "mesh/eth/ethOTA.h"
|
||||
#endif
|
||||
#ifdef USE_ARDUINO_ETHERNET
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5100/W5200/W5500
|
||||
// Shorter DHCP timeout so LoRa startup isn't blocked when no DHCP server is present.
|
||||
@@ -154,6 +157,10 @@ static int32_t reconnectETH()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
initEthOTA();
|
||||
#endif
|
||||
|
||||
ethStartupComplete = true;
|
||||
}
|
||||
}
|
||||
@@ -180,6 +187,10 @@ static int32_t reconnectETH()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
ethOTALoop();
|
||||
#endif
|
||||
|
||||
return 5000; // every 5 seconds
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
|
||||
#include "ethOTA.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <SHA256.h>
|
||||
#include <Updater.h>
|
||||
#ifdef ARCH_RP2040
|
||||
#include <hardware/watchdog.h>
|
||||
#define FEED_WATCHDOG() watchdog_update()
|
||||
#else
|
||||
#define FEED_WATCHDOG() ((void)0)
|
||||
#endif
|
||||
|
||||
/// Protocol header sent by the upload tool
|
||||
struct __attribute__((packed)) OTAHeader {
|
||||
uint8_t magic[4]; // "MOTA" (Meshtastic OTA)
|
||||
uint32_t firmwareSize; // Size of the firmware payload in bytes (little-endian)
|
||||
uint32_t crc32; // CRC32 of the entire firmware payload
|
||||
};
|
||||
|
||||
/// Response codes sent back to the client
|
||||
enum OTAResponse : uint8_t {
|
||||
OTA_OK = 0x00,
|
||||
OTA_ERR_CRC = 0x01,
|
||||
OTA_ERR_SIZE = 0x02,
|
||||
OTA_ERR_WRITE = 0x03,
|
||||
OTA_ERR_MAGIC = 0x04,
|
||||
OTA_ERR_BEGIN = 0x05,
|
||||
OTA_ACK = 0x06, // ACK uses ASCII ACK character
|
||||
OTA_ERR_AUTH = 0x07,
|
||||
OTA_ERR_TIMEOUT = 0x08,
|
||||
};
|
||||
|
||||
static const uint32_t OTA_TIMEOUT_MS = 30000; // 30s inactivity timeout
|
||||
static const size_t OTA_CHUNK_SIZE = 1024; // 1KB receive buffer
|
||||
static const uint32_t OTA_AUTH_COOLDOWN_MS = 5000; // 5s cooldown after failed auth
|
||||
static const size_t OTA_NONCE_SIZE = 32;
|
||||
static const size_t OTA_HASH_SIZE = 32;
|
||||
|
||||
// OTA PSK — override via USERPREFS_OTA_PSK in userPrefs.jsonc
|
||||
// USERPREFS_OTA_PSK is stringified by PlatformIO (wrapped in quotes), so we
|
||||
// use a char[] and sizeof-1 to exclude the trailing NUL byte from the hash.
|
||||
#ifdef USERPREFS_OTA_PSK
|
||||
static const char otaPSKString[] = USERPREFS_OTA_PSK;
|
||||
static const uint8_t *const otaPSK = reinterpret_cast<const uint8_t *>(otaPSKString);
|
||||
static const size_t otaPSKSize = sizeof(otaPSKString) - 1;
|
||||
#else
|
||||
// Default PSK (CHANGE THIS for production deployments)
|
||||
static const uint8_t otaPSK[] = {0x6d, 0x65, 0x73, 0x68, 0x74, 0x61, 0x73, 0x74, 0x69, 0x63, 0x5f, 0x6f, 0x74, 0x61, 0x5f, 0x64,
|
||||
0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x73, 0x6b, 0x5f, 0x76, 0x31, 0x21, 0x21, 0x21};
|
||||
// = "meshtastic_ota_default_psk_v1!!!"
|
||||
static const size_t otaPSKSize = sizeof(otaPSK);
|
||||
#endif
|
||||
|
||||
static EthernetServer *otaServer = nullptr;
|
||||
static uint32_t lastAuthFailure = 0;
|
||||
|
||||
static bool readExact(EthernetClient &client, uint8_t *buf, size_t len)
|
||||
{
|
||||
size_t received = 0;
|
||||
uint32_t lastActivity = millis();
|
||||
|
||||
while (received < len) {
|
||||
if (!client.connected()) {
|
||||
return false;
|
||||
}
|
||||
int avail = client.available();
|
||||
if (avail > 0) {
|
||||
size_t toRead = min((size_t)avail, len - received);
|
||||
size_t got = client.read(buf + received, toRead);
|
||||
received += got;
|
||||
lastActivity = millis();
|
||||
} else {
|
||||
if (millis() - lastActivity > OTA_TIMEOUT_MS) {
|
||||
return false;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
FEED_WATCHDOG();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Compute SHA256(nonce || psk) for challenge-response authentication
|
||||
static void computeAuthHash(const uint8_t *nonce, size_t nonceLen, const uint8_t *psk, size_t pskLen, uint8_t *hashOut)
|
||||
{
|
||||
SHA256 sha;
|
||||
sha.reset();
|
||||
sha.update(nonce, nonceLen);
|
||||
sha.update(psk, pskLen);
|
||||
sha.finalize(hashOut, OTA_HASH_SIZE);
|
||||
}
|
||||
|
||||
/// Challenge-response authentication. Returns true if client is authenticated.
|
||||
static bool authenticateClient(EthernetClient &client)
|
||||
{
|
||||
// Rate-limit after failed auth — close silently so the error byte is not
|
||||
// misinterpreted as part of the nonce by a re-trying client.
|
||||
if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) {
|
||||
LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection");
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate random nonce
|
||||
uint8_t nonce[OTA_NONCE_SIZE];
|
||||
for (size_t i = 0; i < OTA_NONCE_SIZE; i += 4) {
|
||||
uint32_t r = random();
|
||||
size_t remaining = OTA_NONCE_SIZE - i;
|
||||
memcpy(nonce + i, &r, min((size_t)4, remaining));
|
||||
}
|
||||
|
||||
// Send nonce to client
|
||||
client.write(nonce, OTA_NONCE_SIZE);
|
||||
|
||||
// Read client's response: SHA256(nonce || PSK)
|
||||
uint8_t clientHash[OTA_HASH_SIZE];
|
||||
if (!readExact(client, clientHash, OTA_HASH_SIZE)) {
|
||||
LOG_WARN("ETH OTA: Timeout reading auth response");
|
||||
lastAuthFailure = millis();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute expected hash
|
||||
uint8_t expectedHash[OTA_HASH_SIZE];
|
||||
computeAuthHash(nonce, OTA_NONCE_SIZE, otaPSK, otaPSKSize, expectedHash);
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
uint8_t diff = 0;
|
||||
for (size_t i = 0; i < OTA_HASH_SIZE; i++) {
|
||||
diff |= clientHash[i] ^ expectedHash[i];
|
||||
}
|
||||
|
||||
if (diff != 0) {
|
||||
LOG_WARN("ETH OTA: Authentication failed");
|
||||
client.write(OTA_ERR_AUTH);
|
||||
lastAuthFailure = millis();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auth success — send ACK
|
||||
client.write(OTA_ACK);
|
||||
LOG_INFO("ETH OTA: Authentication successful");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handleOTAClient(EthernetClient &client)
|
||||
{
|
||||
LOG_INFO("ETH OTA: Client connected from %u.%u.%u.%u", client.remoteIP()[0], client.remoteIP()[1], client.remoteIP()[2],
|
||||
client.remoteIP()[3]);
|
||||
|
||||
// Step 1: Challenge-response authentication
|
||||
if (!authenticateClient(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Read 12-byte header
|
||||
OTAHeader hdr;
|
||||
if (!readExact(client, (uint8_t *)&hdr, sizeof(hdr))) {
|
||||
LOG_WARN("ETH OTA: Timeout reading header");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate magic
|
||||
if (memcmp(hdr.magic, "MOTA", 4) != 0) {
|
||||
LOG_WARN("ETH OTA: Invalid magic");
|
||||
client.write(OTA_ERR_MAGIC);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("ETH OTA: Firmware size=%u, CRC32=0x%08X", hdr.firmwareSize, hdr.crc32);
|
||||
|
||||
// Sanity check on size (must be > 0 and fit in LittleFS)
|
||||
if (hdr.firmwareSize == 0 || hdr.firmwareSize > 1024 * 1024) {
|
||||
LOG_WARN("ETH OTA: Invalid firmware size");
|
||||
client.write(OTA_ERR_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Begin the update — this opens firmware.bin on LittleFS
|
||||
if (!Update.begin(hdr.firmwareSize)) {
|
||||
LOG_ERROR("ETH OTA: Update.begin() failed, error=%u", Update.getError());
|
||||
client.write(OTA_ERR_BEGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
// ACK the header — client can start sending firmware data
|
||||
client.write(OTA_ACK);
|
||||
|
||||
// Receive firmware in chunks
|
||||
uint8_t buf[OTA_CHUNK_SIZE];
|
||||
size_t remaining = hdr.firmwareSize;
|
||||
uint32_t crc = CRC32_INITIAL;
|
||||
uint32_t lastActivity = millis();
|
||||
size_t totalReceived = 0;
|
||||
|
||||
while (remaining > 0) {
|
||||
if (!client.connected()) {
|
||||
LOG_WARN("ETH OTA: Client disconnected during transfer");
|
||||
Update.end(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int avail = client.available();
|
||||
if (avail <= 0) {
|
||||
if (millis() - lastActivity > OTA_TIMEOUT_MS) {
|
||||
LOG_WARN("ETH OTA: Timeout during transfer (%u/%u bytes)", totalReceived, hdr.firmwareSize);
|
||||
client.write(OTA_ERR_TIMEOUT);
|
||||
Update.end(false);
|
||||
return;
|
||||
}
|
||||
delay(1);
|
||||
FEED_WATCHDOG();
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t toRead = min((size_t)avail, min(remaining, sizeof(buf)));
|
||||
size_t got = client.read(buf, toRead);
|
||||
if (got == 0)
|
||||
continue;
|
||||
|
||||
// Write to Updater (LittleFS firmware.bin)
|
||||
size_t written = Update.write(buf, got);
|
||||
if (written != got) {
|
||||
LOG_ERROR("ETH OTA: Write failed (wrote %u of %u), error=%u", written, got, Update.getError());
|
||||
client.write(OTA_ERR_WRITE);
|
||||
Update.end(false);
|
||||
return;
|
||||
}
|
||||
|
||||
crc = crc32Update(buf, got, crc);
|
||||
remaining -= got;
|
||||
totalReceived += got;
|
||||
lastActivity = millis();
|
||||
FEED_WATCHDOG();
|
||||
|
||||
// Progress log every ~10%
|
||||
if (totalReceived % (hdr.firmwareSize / 10 + 1) < got) {
|
||||
LOG_INFO("ETH OTA: %u%% (%u/%u bytes)", (uint32_t)(100ULL * totalReceived / hdr.firmwareSize), totalReceived,
|
||||
hdr.firmwareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify CRC32
|
||||
uint32_t computedCRC = crc32Final(crc);
|
||||
if (computedCRC != hdr.crc32) {
|
||||
LOG_ERROR("ETH OTA: CRC mismatch (expected=0x%08X, computed=0x%08X)", hdr.crc32, computedCRC);
|
||||
client.write(OTA_ERR_CRC);
|
||||
Update.end(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Finalize — this calls picoOTA.commit() which stages the update for the
|
||||
// bootloader
|
||||
if (!Update.end(true)) {
|
||||
LOG_ERROR("ETH OTA: Update.end() failed, error=%u", Update.getError());
|
||||
client.write(OTA_ERR_WRITE);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("ETH OTA: Update staged successfully (%u bytes). Rebooting...", hdr.firmwareSize);
|
||||
client.write(OTA_OK);
|
||||
client.flush();
|
||||
delay(500);
|
||||
|
||||
// Reboot — the built-in bootloader will apply the update from LittleFS
|
||||
rp2040.reboot();
|
||||
}
|
||||
|
||||
void initEthOTA()
|
||||
{
|
||||
if (!otaServer) {
|
||||
otaServer = new EthernetServer(ETH_OTA_PORT);
|
||||
otaServer->begin();
|
||||
LOG_INFO("ETH OTA: Server listening on TCP port %d", ETH_OTA_PORT);
|
||||
}
|
||||
}
|
||||
|
||||
void ethOTALoop()
|
||||
{
|
||||
if (!otaServer)
|
||||
return;
|
||||
|
||||
EthernetClient client = otaServer->accept();
|
||||
if (client) {
|
||||
handleOTAClient(client);
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // HAS_ETHERNET && HAS_ETHERNET_OTA
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
|
||||
#ifdef USE_ARDUINO_ETHERNET
|
||||
#include <Ethernet.h>
|
||||
#else
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif
|
||||
|
||||
#define ETH_OTA_PORT 4243
|
||||
|
||||
/// Initialize the Ethernet OTA server (call after Ethernet is connected)
|
||||
void initEthOTA();
|
||||
|
||||
/// Poll for incoming OTA connections (call periodically from ethClient
|
||||
/// reconnect loop)
|
||||
void ethOTALoop();
|
||||
|
||||
#endif // HAS_ETHERNET && HAS_ETHERNET_OTA
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "hardware/xosc.h"
|
||||
#include <hardware/clocks.h>
|
||||
#include <hardware/pll.h>
|
||||
#include <hardware/watchdog.h>
|
||||
#include <pico/stdlib.h>
|
||||
#include <pico/unique_id.h>
|
||||
|
||||
@@ -99,6 +100,10 @@ void getMacAddr(uint8_t *dmac)
|
||||
|
||||
void rp2040Setup()
|
||||
{
|
||||
if (watchdog_caused_reboot()) {
|
||||
LOG_WARN("Rebooted by watchdog!");
|
||||
}
|
||||
|
||||
/* Sets a random seed to make sure we get different random numbers on each boot. */
|
||||
uint32_t seed = 0;
|
||||
if (!HardwareRNG::seed(seed)) {
|
||||
@@ -128,6 +133,16 @@ void rp2040Setup()
|
||||
#endif
|
||||
}
|
||||
|
||||
void rp2040Loop()
|
||||
{
|
||||
static bool watchdog_running = false;
|
||||
if (!watchdog_running) {
|
||||
watchdog_enable(8000, true); // 8s timeout; pauses during debug
|
||||
watchdog_running = true;
|
||||
}
|
||||
watchdog_update();
|
||||
}
|
||||
|
||||
void enterDfuMode()
|
||||
{
|
||||
reset_usb_boot(0, 0);
|
||||
|
||||
@@ -4,12 +4,16 @@ board = rpipico2
|
||||
board_level = community
|
||||
upload_protocol = picotool
|
||||
|
||||
# Increase LittleFS from 0.5m to 0.75m so GZIP firmware (~614KB) fits for OTA staging
|
||||
board_build.filesystem_size = 0.75m
|
||||
|
||||
build_flags =
|
||||
${rp2350_base.build_flags}
|
||||
-ULED_BUILTIN # avoid "LED_BUILTIN redefined" warnings from framework common.h
|
||||
-I variants/rp2350/diy/pico2_w5500_e22
|
||||
-D HW_SPI1_DEVICE
|
||||
-D EBYTE_E22_900M30S # selects the EBYTE E22-900M30S module config, including TCXO voltage support and TX gain / max power settings
|
||||
-D HAS_ETHERNET_OTA # enable Ethernet OTA firmware update server on port 4243
|
||||
|
||||
# Re-enable Ethernet and API source paths excluded in rp2350_base
|
||||
build_src_filter = ${rp2350_base.build_src_filter} +<mesh/eth/> +<mesh/api/> +<mqtt/>
|
||||
|
||||
Reference in New Issue
Block a user