From f41f58ac81662a1c5bf48d15eeeea84b6a7426ab Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 12 Apr 2026 03:16:13 +0200 Subject: [PATCH 1/4] feat: add Raspberry Pi Pico 2 + W5500 + E22-900M30S variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/DebugConfiguration.h | 4 +- src/mesh/api/ethServerAPI.h | 6 +- src/mesh/eth/ethClient.cpp | 22 +- src/platform/rp2xx0/architecture.h | 2 + variants/rp2350/pico2_w5500_e22/README.md | 151 ++++++++++++ .../rp2350/pico2_w5500_e22/platformio.ini | 28 +++ variants/rp2350/pico2_w5500_e22/variant.h | 83 +++++++ variants/rp2350/pico2_w5500_e22/wiring.svg | 230 ++++++++++++++++++ 8 files changed, 523 insertions(+), 3 deletions(-) create mode 100644 variants/rp2350/pico2_w5500_e22/README.md create mode 100644 variants/rp2350/pico2_w5500_e22/platformio.ini create mode 100644 variants/rp2350/pico2_w5500_e22/variant.h create mode 100644 variants/rp2350/pico2_w5500_e22/wiring.svg diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index eac6260fcee..e20d8f4824c 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -147,7 +147,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); // Default Bluetooth PIN #define defaultBLEPin 123456 -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && defined(WIZNET_5500_EVB_PICO2) +#include // arduino-libraries/Ethernet — supports W5500 auto-detect +#elif HAS_ETHERNET && !defined(USE_WS5500) #include #endif // HAS_ETHERNET diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index 8f81ee6ffff..f4ce1946904 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -1,8 +1,12 @@ #pragma once #include "ServerAPI.h" -#ifndef USE_WS5500 +#if !defined(USE_WS5500) +#if defined(WIZNET_5500_EVB_PICO2) +#include +#else #include +#endif /** * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 440f7b76a88..85e776bb136 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -6,7 +6,11 @@ #include "main.h" #include "mesh/api/ethServerAPI.h" #include "target_specific.h" +#ifdef WIZNET_5500_EVB_PICO2 +#include // arduino-libraries/Ethernet — supports W5100/W5200/W5500 +#else #include +#endif #include #if HAS_NETWORKING @@ -69,6 +73,13 @@ static int32_t reconnectETH() delay(100); #endif +#ifdef WIZNET_5500_EVB_PICO2 // Re-configure SPI0 for the on-board W5500 + SPI.setRX(ETH_SPI0_MISO); + SPI.setSCK(ETH_SPI0_SCK); + SPI.setTX(ETH_SPI0_MOSI); + SPI.begin(); + Ethernet.init(PIN_ETHERNET_SS); +#else #ifdef RAK11310 ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); @@ -76,6 +87,7 @@ static int32_t reconnectETH() ETH_SPI_PORT.begin(); #endif Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); +#endif int status = 0; if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { @@ -182,6 +194,13 @@ bool initEthernet() digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. #endif +#ifdef WIZNET_5500_EVB_PICO2 // Configure SPI0 for the on-board W5500 + SPI.setRX(ETH_SPI0_MISO); + SPI.setSCK(ETH_SPI0_SCK); + SPI.setTX(ETH_SPI0_MOSI); + SPI.begin(); + Ethernet.init(PIN_ETHERNET_SS); +#else #ifdef RAK11310 // Initialize the SPI port ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); @@ -189,6 +208,7 @@ bool initEthernet() ETH_SPI_PORT.begin(); #endif Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); +#endif uint8_t mac[6]; @@ -201,7 +221,7 @@ bool initEthernet() if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { LOG_INFO("Start Ethernet DHCP"); - status = Ethernet.begin(mac); + status = Ethernet.begin(mac, 10000); // 10s timeout instead of default 60s } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { LOG_INFO("Start Ethernet Static"); Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, diff --git a/src/platform/rp2xx0/architecture.h b/src/platform/rp2xx0/architecture.h index 0c168ceee6e..be9ba02cd9d 100644 --- a/src/platform/rp2xx0/architecture.h +++ b/src/platform/rp2xx0/architecture.h @@ -33,6 +33,8 @@ #define HW_VENDOR meshtastic_HardwareModel_RP2040_LORA #elif defined(RP2040_FEATHER_RFM95) #define HW_VENDOR meshtastic_HardwareModel_RP2040_FEATHER_RFM95 +#elif defined(WIZNET_5500_EVB_PICO2) +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #elif defined(PRIVATE_HW) #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/variants/rp2350/pico2_w5500_e22/README.md b/variants/rp2350/pico2_w5500_e22/README.md new file mode 100644 index 00000000000..daeeff15cc7 --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/README.md @@ -0,0 +1,151 @@ +# Raspberry Pi Pico 2 + W5500 + E22-900M30S — Meshtastic Variant + +Meshtastic support for a **Raspberry Pi Pico 2** (RP2350, 4 MB flash) with an external **W5500** Ethernet module and an **EBYTE E22-900M30S** LoRa module. + +--- + +## Required Hardware + +| Component | Model | Notes | +|-----------|-------------------------------|------------------------------------------| +| MCU | Raspberry Pi Pico 2 | RP2350 @ 150 MHz, 512 KB RAM, 4 MB flash | +| Ethernet | W5500 module | Any WIZnet W5500 breakout board | +| LoRa | EBYTE E22-900M30S | SX1262 + 30 dBm PA, 868/915 MHz | + +--- + +## Pinout + +### System pins (Pico 2, fixed) + +| GPIO | Function | +|------|-----------------------------------------------| +| GP24 | VBUS sense — HIGH when USB is connected | +| GP25 | User LED (heartbeat) | +| GP29 | ADC3 — VSYS/3, measures supply voltage | + +### W5500 Ethernet (SPI0) + +| W5500 signal | Pico 2 GPIO | +|--------------|-------------| +| MISO | GP16 | +| CS / SCS | GP17 | +| SCK | GP18 | +| MOSI | GP19 | +| RST | GP20 | +| INT | — (nc) | +| VCC | 3.3V | +| GND | GND | + +> SPI0 is reserved for the W5500. + +### E22-900M30S LoRa (SPI1) + +| E22 signal | Pico 2 GPIO | Notes | +|------------|-------------|------------------------------------------------| +| SCK | GP10 | SPI1 clock | +| MOSI | GP11 | SPI1 TX | +| MISO | GP12 | SPI1 RX | +| NSS / CS | GP13 | Chip select | +| RESET | GP15 | Active LOW reset | +| DIO1 | GP14 | IRQ interrupt | +| BUSY | GP2 | Module busy indicator | +| RXEN | GP3 | LNA enable — held HIGH permanently | +| TXEN | ← DIO2 | See wiring note below | +| VCC | 3.3V | Add a 100 µF capacitor close to the module | +| GND | GND | — | + +> See `wiring.svg` in this directory for the full connection diagram. + +--- + +## Special wiring: DIO2 → TXEN bridge on the E22 module + +The E22-900M30S does **not** connect DIO2 to the TXEN pin of its PA internally. They must be bridged with a short wire or solder bridge **on the module itself**: + +``` +E22 DIO2 pin ──┐ + ├── wire / solder bridge on the module +E22 TXEN pin ──┘ +``` + +With this bridge in place, `SX126X_DIO2_AS_RF_SWITCH` causes the SX1262 to drive DIO2 HIGH automatically during TX, enabling the PA without needing an RP2350 GPIO for TXEN. + +**Without this bridge the module will not transmit.** + +--- + +## Build + +```bash +pio run -e pico2_w5500_e22 +``` + +### Flash — BOOTSEL mode + +1. Hold the **BOOTSEL** button on the Pico 2. +2. Connect USB to the PC — it appears as a `RPI-RP2` storage drive. +3. Copy the `.uf2` file: + +``` +.pio/build/pico2_w5500_e22/firmware-pico2_w5500_e22-*.uf2 +``` + +Or directly with picotool: + +```bash +pio run -e pico2_w5500_e22 -t upload +``` + +--- + +## Network usage + +This board uses Ethernet (no Wi-Fi). From the Meshtastic app: + +- **Enable Ethernet** under `Config → Network → Ethernet Enabled` +- **DHCP** by default; static IP can also be configured + +Services available once connected: + +| Service | Details | +|---------|-----------------------------| +| NTP | Time synchronization | +| MQTT | Messages to external broker | +| API | TCP socket on port 4403 | +| Syslog | Remote logging (optional) | + +--- + +## Technical notes + +### LoRa — RF control + +| Define | Effect | +|--------------------------------|---------------------------------------------------------------| +| `SX126X_ANT_SW 3` | GP3 (RXEN) driven HIGH at init and never toggled again | +| `SX126X_DIO2_AS_RF_SWITCH` | SX1262 drives DIO2 HIGH during TX → enables TXEN via bridge | +| `SX126X_DIO3_TCXO_VOLTAGE 1.8` | E22 TCXO controlled by DIO3 | +| `-D EBYTE_E22` | Enables TCXO support in firmware | +| `-D EBYTE_E22_900M30S` | Sets `TX_GAIN_LORA=10`, max power 22 dBm | + +> RXEN and TXEN may both be HIGH simultaneously during TX — this is safe for the E22 RF switch. + +### Ethernet + +- Library: `arduino-libraries/Ethernet@^2.0.2` (supports W5100/W5200/W5500 auto-detection). +- SPI0 is explicitly initialized with pins GP16/18/19 before `Ethernet.init()`. +- DHCP timeout is set to 10 s (instead of the default 60 s) to avoid blocking LoRa startup. + +### HW_VENDOR + +Mapped to `meshtastic_HardwareModel_PRIVATE_HW` — no dedicated model exists in the Meshtastic protobuf for this hardware combination yet. + +--- + +## Memory usage (reference build) + +| Resource | Used | Total | % | +|----------|---------|----------|-------| +| RAM | 94 KB | 512 KB | 18% | +| Flash | 964 KB | 3.58 MB | 26.3% | diff --git a/variants/rp2350/pico2_w5500_e22/platformio.ini b/variants/rp2350/pico2_w5500_e22/platformio.ini new file mode 100644 index 00000000000..adf388381e5 --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/platformio.ini @@ -0,0 +1,28 @@ +[env:pico2_w5500_e22] +extends = rp2350_base +board = rpipico2 +board_level = community +upload_protocol = picotool + +build_flags = + ${rp2350_base.build_flags} + -ULED_BUILTIN # avoid "LED_BUILTIN redefined" warnings from framework common.h + -D WIZNET_5500_EVB_PICO2 # reuse same code paths as EVB variant + -I variants/rp2350/pico2_w5500_e22 + -D DEBUG_RP2040_PORT=Serial + -D HW_SPI1_DEVICE + -D EBYTE_E22 # activates TCXO support (SX126X_DIO3_TCXO_VOLTAGE) + -D EBYTE_E22_900M30S # activates TX_GAIN_LORA=7 / SX126X_MAX_POWER=22 + +# Re-enable Ethernet and API source paths excluded in rp2350_base +build_src_filter = ${rp2350_base.build_src_filter} + + + + +lib_deps = + ${rp2350_base.lib_deps} + ${networking_base.lib_deps} + ${networking_extra.lib_deps} + # Standard WIZnet Ethernet library — supports W5100/W5200/W5500 auto-detect + arduino-libraries/Ethernet@^2.0.2 + +debug_build_flags = ${rp2350_base.build_flags}, -g +debug_tool = cmsis-dap diff --git a/variants/rp2350/pico2_w5500_e22/variant.h b/variants/rp2350/pico2_w5500_e22/variant.h new file mode 100644 index 00000000000..027e8ff4582 --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/variant.h @@ -0,0 +1,83 @@ +// Raspberry Pi Pico 2 + external W5500 Ethernet module + EBYTE E22-900M30S +// RP2350 (4 MB flash) — wire modules to the GPIO pins listed below +// +// LoRa (SX1262 / E22-900M30S) on SPI1: +// SCK=GP10 MOSI=GP11 MISO=GP12 CS=GP13 +// RST=GP15 DIO1/IRQ=GP14 BUSY=GP2 RXEN=GP3 +// TXEN: bridge E22_DIO2 → E22_TXEN on the module (no RP2350 GPIO needed) +// +// W5500 Ethernet on SPI0: +// MISO=GP16 CS=GP17 SCK=GP18 MOSI=GP19 RST=GP20 +// +// See wiring.svg in this directory for a complete connection diagram. + +#define ARDUINO_ARCH_AVR + +// Onboard LED (GP25 on Pico 2) +#define LED_POWER PIN_LED + +// Power monitoring +// GP24: VBUS sense – HIGH when USB is present (digital read) +// GP29: ADC3 measures VSYS/3 (200 kΩ / 100 kΩ divider, same as standard Pico 2) +#define EXT_PWR_DETECT 24 +#define BATTERY_PIN 29 +#define ADC_MULTIPLIER 3.0 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +// No real battery — suppress false "battery at 100%" while USB powers VSYS +#define NO_BATTERY_LEVEL_ON_CHARGE + +// Optional user button — connect a button between GP6 and GND +// #define BUTTON_PIN 6 +// #define BUTTON_NEED_PULLUP + +// GPS on UART1 (Serial2) — GP8 TX, GP9 RX +// GP8/GP9 belong to UART1, so we must use Serial2 (not the default Serial1/UART0). +// GP0/GP1 (UART0 defaults) are free but the firmware treats pin 0 as "not configured". +// GP4/GP5 occupied by I2C (SCL/SDA for BMP-280). +#define HAS_GPS 1 +#define GPS_TX_PIN 8 +#define GPS_RX_PIN 9 +#define GPS_BAUDRATE 38400 +#define GPS_SERIAL_PORT Serial2 + +// ---- EBYTE E22-900M30S on SPI1 ----------------------------------------- +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MOSI 11 +#define LORA_MISO 12 +#define LORA_CS 13 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 14 // IRQ +#define LORA_DIO2 2 // BUSY +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// GP3 = RXEN: driven HIGH at init and held there (LNA always enabled). +// SX1262 drives DIO2 HIGH during TX → TXEN via bridge on E22 module. +#define SX126X_ANT_SW 3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +// ---- W5500 Ethernet on SPI0 -------------------------------------------- +#define HAS_ETHERNET 1 + +#define ETH_SPI0_MISO 16 +#define ETH_SPI0_SCK 18 +#define ETH_SPI0_MOSI 19 + +#define PIN_ETHERNET_RESET 20 +#define PIN_ETHERNET_SS 17 +#define ETH_SPI_PORT SPI diff --git a/variants/rp2350/pico2_w5500_e22/wiring.svg b/variants/rp2350/pico2_w5500_e22/wiring.svg new file mode 100644 index 00000000000..28448f04bf9 --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/wiring.svg @@ -0,0 +1,230 @@ + + + + + Raspberry Pi Pico 2 — W5500 + E22-900M30S Wiring + env:pico2_w5500_e22 | SPI0=Ethernet SPI1=LoRa + + + + Raspberry Pi Pico 2 + RP2350 · 4 MB Flash + + + + + GP0 + GP1 + GP2 BUSY + GP3 RXEN + GP4 SDA + GP5 SCL + GP6 (BTN) + GP7 + GP8 + GP9 + GP10 SCK + GP11 MOSI + GP12 MISO + GP13 CS + GP14 IRQ + GP15 RST + GP16 MISO + GP17 CS + GP18 SCK + GP19 MOSI + GP20 RST + GP21 + GP22 + + + VBUS + VSYS + GND + 3V3_EN + 3V3 + ADC_VREF + GP28 + GP27 SCL + GP26 SDA + RUN + GP22 + GP21 + GP20 → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E22-900M30S + EBYTE SX1262 LoRa + + + GND + VCC 3.3V + RXEN + TXEN + DIO2 + DIO1 + BUSY + NSS/CS + SCK + MOSI + MISO + NRST + + + + + + + + + + + + + + + + + + + + ⚠ bridge DIO2→TXEN + + + + W5500 module + SPI0 Ethernet + + + GND + 3.3V + MISO + MOSI + SCLK + SCS/CS + RST + INT (nc) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Conexiones + + + + SPI1 — LoRa (E22-900M30S) + GP2 → BUSY GP10 → SCK + GP3 → RXEN GP11 → MOSI + GP13 → CS GP12 → MISO + GP14 → DIO1 GP15 → RST + + + + SPI0 — Ethernet (W5500) + GP16 → MISO GP18 → SCK + GP17 → CS GP19 → MOSI + GP20 → RST + ⚠ Bridge DIO2→TXEN en módulo E22 + + + + Alimentación + Pico 2 VBUS → USB 5V + Pico 2 3V3 → VCC E22 + VCC W5500 + GND común entre los 3 módulos + E22 consume hasta 1 A en TX — usar cap 100µF + + + + Build: + pio run -e pico2_w5500_e22 + + + + board = rpipico2 → 4 MB flash + (W5500-EVB-Pico2 solo tiene 2 MB) + + From 4ff08332195e76353f40834d86bfb8450f63fee0 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 12 Apr 2026 03:21:31 +0200 Subject: [PATCH 2/4] 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 Firmware side: - ethOTA.cpp / ethOTA.h: OTA TCP server on port 4243 - ethClient.cpp: calls initEthOTA() after Ethernet connects, ethOTALoop() polled every 5s - main-rp2xx0.cpp: enable watchdog (8s) so device recovers if OTA write stalls; log watchdog-caused reboots - pico2_w5500_e22: filesystem_size=0.75m (GZIP ~614KB fits), HAS_ETHERNET_OTA build flag Upload tool: - bin/eth-ota-upload.py: Python 3, stdlib-only, GZIP-compresses firmware before sending, shows progress bar + transfer speed Co-Authored-By: Claude Sonnet 4.6 --- bin/eth-ota-upload.py | 249 +++++++++++++++ src/mesh/eth/ethClient.cpp | 11 + src/mesh/eth/ethOTA.cpp | 288 ++++++++++++++++++ src/mesh/eth/ethOTA.h | 21 ++ src/platform/rp2xx0/main-rp2xx0.cpp | 15 + .../rp2350/pico2_w5500_e22/platformio.ini | 5 + 6 files changed, 589 insertions(+) create mode 100644 bin/eth-ota-upload.py create mode 100644 src/mesh/eth/ethOTA.cpp create mode 100644 src/mesh/eth/ethOTA.h diff --git a/bin/eth-ota-upload.py b/bin/eth-ota-upload.py new file mode 100644 index 00000000000..1b8631a79e4 --- /dev/null +++ b/bin/eth-ota-upload.py @@ -0,0 +1,249 @@ +#!/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", + 0x06: "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() diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 85e776bb136..db5691bccab 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -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 WIZNET_5500_EVB_PICO2 #include // arduino-libraries/Ethernet — supports W5100/W5200/W5500 #else @@ -148,6 +151,10 @@ static int32_t reconnectETH() } #endif +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + initEthOTA(); +#endif + ethStartupComplete = true; } } @@ -174,6 +181,10 @@ static int32_t reconnectETH() } #endif +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + ethOTALoop(); +#endif + return 5000; // every 5 seconds } diff --git a/src/mesh/eth/ethOTA.cpp b/src/mesh/eth/ethOTA.cpp new file mode 100644 index 00000000000..b9a1f189b00 --- /dev/null +++ b/src/mesh/eth/ethOTA.cpp @@ -0,0 +1,288 @@ +#include "configuration.h" + +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + +#include "ethOTA.h" +#include +#include +#include +#ifdef ARCH_RP2040 +#include +#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_ERR_TIMEOUT = 0x06, + OTA_ACK = 0x06, // ACK uses ASCII ACK character + OTA_ERR_AUTH = 0x07, +}; + +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 +#ifdef USERPREFS_OTA_PSK +static const uint8_t otaPSK[] = USERPREFS_OTA_PSK; +#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!!!" +#endif +static const size_t otaPSKSize = sizeof(otaPSK); + +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 + if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) { + LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection"); + client.write(OTA_ERR_AUTH); + 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 diff --git a/src/mesh/eth/ethOTA.h b/src/mesh/eth/ethOTA.h new file mode 100644 index 00000000000..29cbb4aac28 --- /dev/null +++ b/src/mesh/eth/ethOTA.h @@ -0,0 +1,21 @@ +#pragma once + +#include "configuration.h" + +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + +#ifdef WIZNET_5500_EVB_PICO2 +#include +#else +#include +#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 diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp index e59b0a9cda2..ee50f4fb1c7 100644 --- a/src/platform/rp2xx0/main-rp2xx0.cpp +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -3,6 +3,7 @@ #include "hardware/xosc.h" #include #include +#include #include #include @@ -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); diff --git a/variants/rp2350/pico2_w5500_e22/platformio.ini b/variants/rp2350/pico2_w5500_e22/platformio.ini index adf388381e5..47bc3dc2648 100644 --- a/variants/rp2350/pico2_w5500_e22/platformio.ini +++ b/variants/rp2350/pico2_w5500_e22/platformio.ini @@ -4,6 +4,10 @@ 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 + +# Add our variant files to include/src paths build_flags = ${rp2350_base.build_flags} -ULED_BUILTIN # avoid "LED_BUILTIN redefined" warnings from framework common.h @@ -13,6 +17,7 @@ build_flags = -D HW_SPI1_DEVICE -D EBYTE_E22 # activates TCXO support (SX126X_DIO3_TCXO_VOLTAGE) -D EBYTE_E22_900M30S # activates TX_GAIN_LORA=7 / SX126X_MAX_POWER=22 + -D HAS_ETHERNET_OTA # Re-enable Ethernet and API source paths excluded in rp2350_base build_src_filter = ${rp2350_base.build_src_filter} + + + From c77bfd9c80e6f2a7ecfcb4e627f10d5e83effaf1 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Thu, 16 Apr 2026 01:00:11 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?protocol,=20PSK,=20auth=20and=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix OTA_ERR_TIMEOUT/OTA_ACK collision (0x06→0x08 for timeout) - Fix USERPREFS_OTA_PSK trailing NUL byte via char[]+sizeof-1 - Fix auth cooldown: close connection silently instead of sending OTA_ERR_AUTH before the nonce (prevents byte being mis-consumed) - Fix README TX_GAIN_LORA value: 10→7 for EBYTE_E22_900M30S - Fix Trunk Check: clang-format ethOTA.cpp/h + variant.h, prettier README.md (MD040 code fence langs, MD060 table style), svgo wiring.svg Co-Authored-By: Claude Sonnet 4.6 --- src/mesh/eth/ethOTA.cpp | 467 +++++++++++---------- src/mesh/eth/ethOTA.h | 3 +- variants/rp2350/pico2_w5500_e22/README.md | 76 ++-- variants/rp2350/pico2_w5500_e22/variant.h | 32 +- variants/rp2350/pico2_w5500_e22/wiring.svg | 231 +--------- 5 files changed, 297 insertions(+), 512 deletions(-) diff --git a/src/mesh/eth/ethOTA.cpp b/src/mesh/eth/ethOTA.cpp index b9a1f189b00..65d7324ce1b 100644 --- a/src/mesh/eth/ethOTA.cpp +++ b/src/mesh/eth/ethOTA.cpp @@ -15,274 +15,287 @@ /// 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 + 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_ERR_TIMEOUT = 0x06, - OTA_ACK = 0x06, // ACK uses ASCII ACK character - OTA_ERR_AUTH = 0x07, + 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 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 uint8_t otaPSK[] = USERPREFS_OTA_PSK; +static const char otaPSKString[] = USERPREFS_OTA_PSK; +static const uint8_t *const otaPSK = + reinterpret_cast(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}; +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!!!" -#endif 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(); +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; } - return true; + 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); +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 - if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) { - LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection"); - client.write(OTA_ERR_AUTH); - 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 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; +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; } - // 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); + 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; } - // 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; + 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; } - 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(); + 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 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; +void ethOTALoop() { + if (!otaServer) + return; - EthernetClient client = otaServer->accept(); - if (client) { - handleOTAClient(client); - client.stop(); - } + EthernetClient client = otaServer->accept(); + if (client) { + handleOTAClient(client); + client.stop(); + } } #endif // HAS_ETHERNET && HAS_ETHERNET_OTA diff --git a/src/mesh/eth/ethOTA.h b/src/mesh/eth/ethOTA.h index 29cbb4aac28..32637ac06a6 100644 --- a/src/mesh/eth/ethOTA.h +++ b/src/mesh/eth/ethOTA.h @@ -15,7 +15,8 @@ /// Initialize the Ethernet OTA server (call after Ethernet is connected) void initEthOTA(); -/// Poll for incoming OTA connections (call periodically from ethClient reconnect loop) +/// Poll for incoming OTA connections (call periodically from ethClient +/// reconnect loop) void ethOTALoop(); #endif // HAS_ETHERNET && HAS_ETHERNET_OTA diff --git a/variants/rp2350/pico2_w5500_e22/README.md b/variants/rp2350/pico2_w5500_e22/README.md index daeeff15cc7..259f06df9a6 100644 --- a/variants/rp2350/pico2_w5500_e22/README.md +++ b/variants/rp2350/pico2_w5500_e22/README.md @@ -6,11 +6,11 @@ Meshtastic support for a **Raspberry Pi Pico 2** (RP2350, 4 MB flash) with an ex ## Required Hardware -| Component | Model | Notes | -|-----------|-------------------------------|------------------------------------------| -| MCU | Raspberry Pi Pico 2 | RP2350 @ 150 MHz, 512 KB RAM, 4 MB flash | -| Ethernet | W5500 module | Any WIZnet W5500 breakout board | -| LoRa | EBYTE E22-900M30S | SX1262 + 30 dBm PA, 868/915 MHz | +| Component | Model | Notes | +| --------- | ------------------- | ---------------------------------------- | +| MCU | Raspberry Pi Pico 2 | RP2350 @ 150 MHz, 512 KB RAM, 4 MB flash | +| Ethernet | W5500 module | Any WIZnet W5500 breakout board | +| LoRa | EBYTE E22-900M30S | SX1262 + 30 dBm PA, 868/915 MHz | --- @@ -18,16 +18,16 @@ Meshtastic support for a **Raspberry Pi Pico 2** (RP2350, 4 MB flash) with an ex ### System pins (Pico 2, fixed) -| GPIO | Function | -|------|-----------------------------------------------| -| GP24 | VBUS sense — HIGH when USB is connected | -| GP25 | User LED (heartbeat) | -| GP29 | ADC3 — VSYS/3, measures supply voltage | +| GPIO | Function | +| ---- | --------------------------------------- | +| GP24 | VBUS sense — HIGH when USB is connected | +| GP25 | User LED (heartbeat) | +| GP29 | ADC3 — VSYS/3, measures supply voltage | ### W5500 Ethernet (SPI0) | W5500 signal | Pico 2 GPIO | -|--------------|-------------| +| ------------ | ----------- | | MISO | GP16 | | CS / SCS | GP17 | | SCK | GP18 | @@ -41,19 +41,19 @@ Meshtastic support for a **Raspberry Pi Pico 2** (RP2350, 4 MB flash) with an ex ### E22-900M30S LoRa (SPI1) -| E22 signal | Pico 2 GPIO | Notes | -|------------|-------------|------------------------------------------------| -| SCK | GP10 | SPI1 clock | -| MOSI | GP11 | SPI1 TX | -| MISO | GP12 | SPI1 RX | -| NSS / CS | GP13 | Chip select | -| RESET | GP15 | Active LOW reset | -| DIO1 | GP14 | IRQ interrupt | -| BUSY | GP2 | Module busy indicator | -| RXEN | GP3 | LNA enable — held HIGH permanently | -| TXEN | ← DIO2 | See wiring note below | -| VCC | 3.3V | Add a 100 µF capacitor close to the module | -| GND | GND | — | +| E22 signal | Pico 2 GPIO | Notes | +| ---------- | ----------- | ------------------------------------------ | +| SCK | GP10 | SPI1 clock | +| MOSI | GP11 | SPI1 TX | +| MISO | GP12 | SPI1 RX | +| NSS / CS | GP13 | Chip select | +| RESET | GP15 | Active LOW reset | +| DIO1 | GP14 | IRQ interrupt | +| BUSY | GP2 | Module busy indicator | +| RXEN | GP3 | LNA enable — held HIGH permanently | +| TXEN | ← DIO2 | See wiring note below | +| VCC | 3.3V | Add a 100 µF capacitor close to the module | +| GND | GND | — | > See `wiring.svg` in this directory for the full connection diagram. @@ -63,7 +63,7 @@ Meshtastic support for a **Raspberry Pi Pico 2** (RP2350, 4 MB flash) with an ex The E22-900M30S does **not** connect DIO2 to the TXEN pin of its PA internally. They must be bridged with a short wire or solder bridge **on the module itself**: -``` +```text E22 DIO2 pin ──┐ ├── wire / solder bridge on the module E22 TXEN pin ──┘ @@ -87,7 +87,7 @@ pio run -e pico2_w5500_e22 2. Connect USB to the PC — it appears as a `RPI-RP2` storage drive. 3. Copy the `.uf2` file: -``` +```text .pio/build/pico2_w5500_e22/firmware-pico2_w5500_e22-*.uf2 ``` @@ -109,7 +109,7 @@ This board uses Ethernet (no Wi-Fi). From the Meshtastic app: Services available once connected: | Service | Details | -|---------|-----------------------------| +| ------- | --------------------------- | | NTP | Time synchronization | | MQTT | Messages to external broker | | API | TCP socket on port 4403 | @@ -121,13 +121,13 @@ Services available once connected: ### LoRa — RF control -| Define | Effect | -|--------------------------------|---------------------------------------------------------------| -| `SX126X_ANT_SW 3` | GP3 (RXEN) driven HIGH at init and never toggled again | -| `SX126X_DIO2_AS_RF_SWITCH` | SX1262 drives DIO2 HIGH during TX → enables TXEN via bridge | -| `SX126X_DIO3_TCXO_VOLTAGE 1.8` | E22 TCXO controlled by DIO3 | -| `-D EBYTE_E22` | Enables TCXO support in firmware | -| `-D EBYTE_E22_900M30S` | Sets `TX_GAIN_LORA=10`, max power 22 dBm | +| Define | Effect | +| ------------------------------ | ----------------------------------------------------------- | +| `SX126X_ANT_SW 3` | GP3 (RXEN) driven HIGH at init and never toggled again | +| `SX126X_DIO2_AS_RF_SWITCH` | SX1262 drives DIO2 HIGH during TX → enables TXEN via bridge | +| `SX126X_DIO3_TCXO_VOLTAGE 1.8` | E22 TCXO controlled by DIO3 | +| `-D EBYTE_E22` | Enables TCXO support in firmware | +| `-D EBYTE_E22_900M30S` | Sets `TX_GAIN_LORA=7`, max power 22 dBm | > RXEN and TXEN may both be HIGH simultaneously during TX — this is safe for the E22 RF switch. @@ -145,7 +145,7 @@ Mapped to `meshtastic_HardwareModel_PRIVATE_HW` — no dedicated model exists in ## Memory usage (reference build) -| Resource | Used | Total | % | -|----------|---------|----------|-------| -| RAM | 94 KB | 512 KB | 18% | -| Flash | 964 KB | 3.58 MB | 26.3% | +| Resource | Used | Total | % | +| -------- | ------ | ------- | ----- | +| RAM | 94 KB | 512 KB | 18% | +| Flash | 964 KB | 3.58 MB | 26.3% | diff --git a/variants/rp2350/pico2_w5500_e22/variant.h b/variants/rp2350/pico2_w5500_e22/variant.h index 027e8ff4582..8c80e798ec5 100644 --- a/variants/rp2350/pico2_w5500_e22/variant.h +++ b/variants/rp2350/pico2_w5500_e22/variant.h @@ -20,7 +20,7 @@ // GP24: VBUS sense – HIGH when USB is present (digital read) // GP29: ADC3 measures VSYS/3 (200 kΩ / 100 kΩ divider, same as standard Pico 2) #define EXT_PWR_DETECT 24 -#define BATTERY_PIN 29 +#define BATTERY_PIN 29 #define ADC_MULTIPLIER 3.0 #define BATTERY_SENSE_RESOLUTION_BITS 12 // No real battery — suppress false "battery at 100%" while USB powers VSYS @@ -31,9 +31,9 @@ // #define BUTTON_NEED_PULLUP // GPS on UART1 (Serial2) — GP8 TX, GP9 RX -// GP8/GP9 belong to UART1, so we must use Serial2 (not the default Serial1/UART0). -// GP0/GP1 (UART0 defaults) are free but the firmware treats pin 0 as "not configured". -// GP4/GP5 occupied by I2C (SCL/SDA for BMP-280). +// GP8/GP9 belong to UART1, so we must use Serial2 (not the default +// Serial1/UART0). GP0/GP1 (UART0 defaults) are free but the firmware treats pin +// 0 as "not configured". GP4/GP5 occupied by I2C (SCL/SDA for BMP-280). #define HAS_GPS 1 #define GPS_TX_PIN 8 #define GPS_RX_PIN 9 @@ -48,21 +48,21 @@ #undef LORA_MOSI #undef LORA_CS -#define LORA_SCK 10 +#define LORA_SCK 10 #define LORA_MOSI 11 #define LORA_MISO 12 -#define LORA_CS 13 +#define LORA_CS 13 -#define LORA_DIO0 RADIOLIB_NC +#define LORA_DIO0 RADIOLIB_NC #define LORA_RESET 15 -#define LORA_DIO1 14 // IRQ -#define LORA_DIO2 2 // BUSY -#define LORA_DIO3 RADIOLIB_NC +#define LORA_DIO1 14 // IRQ +#define LORA_DIO2 2 // BUSY +#define LORA_DIO3 RADIOLIB_NC #ifdef USE_SX1262 -#define SX126X_CS LORA_CS -#define SX126X_DIO1 LORA_DIO1 -#define SX126X_BUSY LORA_DIO2 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET // GP3 = RXEN: driven HIGH at init and held there (LNA always enabled). // SX1262 drives DIO2 HIGH during TX → TXEN via bridge on E22 module. @@ -75,9 +75,9 @@ #define HAS_ETHERNET 1 #define ETH_SPI0_MISO 16 -#define ETH_SPI0_SCK 18 +#define ETH_SPI0_SCK 18 #define ETH_SPI0_MOSI 19 #define PIN_ETHERNET_RESET 20 -#define PIN_ETHERNET_SS 17 -#define ETH_SPI_PORT SPI +#define PIN_ETHERNET_SS 17 +#define ETH_SPI_PORT SPI diff --git a/variants/rp2350/pico2_w5500_e22/wiring.svg b/variants/rp2350/pico2_w5500_e22/wiring.svg index 28448f04bf9..4cdd7cfd1be 100644 --- a/variants/rp2350/pico2_w5500_e22/wiring.svg +++ b/variants/rp2350/pico2_w5500_e22/wiring.svg @@ -1,230 +1 @@ - - - - - Raspberry Pi Pico 2 — W5500 + E22-900M30S Wiring - env:pico2_w5500_e22 | SPI0=Ethernet SPI1=LoRa - - - - Raspberry Pi Pico 2 - RP2350 · 4 MB Flash - - - - - GP0 - GP1 - GP2 BUSY - GP3 RXEN - GP4 SDA - GP5 SCL - GP6 (BTN) - GP7 - GP8 - GP9 - GP10 SCK - GP11 MOSI - GP12 MISO - GP13 CS - GP14 IRQ - GP15 RST - GP16 MISO - GP17 CS - GP18 SCK - GP19 MOSI - GP20 RST - GP21 - GP22 - - - VBUS - VSYS - GND - 3V3_EN - 3V3 - ADC_VREF - GP28 - GP27 SCL - GP26 SDA - RUN - GP22 - GP21 - GP20 → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - E22-900M30S - EBYTE SX1262 LoRa - - - GND - VCC 3.3V - RXEN - TXEN - DIO2 - DIO1 - BUSY - NSS/CS - SCK - MOSI - MISO - NRST - - - - - - - - - - - - - - - - - - - - ⚠ bridge DIO2→TXEN - - - - W5500 module - SPI0 Ethernet - - - GND - 3.3V - MISO - MOSI - SCLK - SCS/CS - RST - INT (nc) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Conexiones - - - - SPI1 — LoRa (E22-900M30S) - GP2 → BUSY GP10 → SCK - GP3 → RXEN GP11 → MOSI - GP13 → CS GP12 → MISO - GP14 → DIO1 GP15 → RST - - - - SPI0 — Ethernet (W5500) - GP16 → MISO GP18 → SCK - GP17 → CS GP19 → MOSI - GP20 → RST - ⚠ Bridge DIO2→TXEN en módulo E22 - - - - Alimentación - Pico 2 VBUS → USB 5V - Pico 2 3V3 → VCC E22 + VCC W5500 - GND común entre los 3 módulos - E22 consume hasta 1 A en TX — usar cap 100µF - - - - Build: - pio run -e pico2_w5500_e22 - - - - board = rpipico2 → 4 MB flash - (W5500-EVB-Pico2 solo tiene 2 MB) - - +Raspberry Pi Pico 2 — W5500 + E22-900M30S Wiringenv:pico2_w5500_e22 | SPI0=Ethernet SPI1=LoRaRaspberry Pi Pico 2RP2350 · 4 MB FlashGP0GP1GP2 BUSYGP3 RXENGP4 SDAGP5 SCLGP6 (BTN)GP7GP8GP9GP10 SCKGP11 MOSIGP12 MISOGP13 CSGP14 IRQGP15 RSTGP16 MISOGP17 CSGP18 SCKGP19 MOSIGP20 RSTGP21GP22VBUSVSYSGND3V3_EN3V3ADC_VREFGP28GP27 SCLGP26 SDARUNGP22GP21GP20 →E22-900M30SEBYTE SX1262 LoRaGNDVCC 3.3VRXENTXENDIO2DIO1BUSYNSS/CSSCKMOSIMISONRST⚠ bridge DIO2→TXENW5500 moduleSPI0 EthernetGND3.3VMISOMOSISCLKSCS/CSRSTINT (nc)ConexionesSPI1 — LoRa (E22-900M30S)GP2 → BUSY GP10 → SCKGP3 → RXEN GP11 → MOSIGP13 → CS GP12 → MISOGP14 → DIO1 GP15 → RSTSPI0 — Ethernet (W5500)GP16 → MISO GP18 → SCKGP17 → CS GP19 → MOSIGP20 → RST⚠ Bridge DIO2→TXEN en módulo E22AlimentaciónPico 2 VBUS → USB 5VPico 2 3V3 → VCC E22 + VCC W5500GND común entre los 3 módulosE22 consume hasta 1 A en TX — usar cap 100µFBuild:pio run -e pico2_w5500_e22board = rpipico2 → 4 MB flash(W5500-EVB-Pico2 solo tiene 2 MB) \ No newline at end of file From 417cec2faffdf288549b1f1729458dcd693e3bf3 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Fri, 17 Apr 2026 07:40:52 +0200 Subject: [PATCH 4/4] eth-ota: sync uploader result codes with firmware enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit moved OTA_ERR_TIMEOUT from 0x06 to 0x08 to free 0x06 for OTA_ACK, but bin/eth-ota-upload.py still mapped 0x06 to "Timeout" — so a real timeout from the device (0x08) was printed as "Unknown result 0x08" instead of being identified. Align the mapping with the full OTAResponse enum in ethOTA.cpp: keep 0x00–0x03, remove the stale 0x06, and add 0x04 (magic), 0x05 (begin), 0x07 (auth), 0x08 (timeout) so every firmware response prints a human-readable message. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/eth-ota-upload.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/eth-ota-upload.py b/bin/eth-ota-upload.py index 1b8631a79e4..68bc1e2a7fd 100644 --- a/bin/eth-ota-upload.py +++ b/bin/eth-ota-upload.py @@ -167,7 +167,10 @@ def upload_firmware(host: str, port: int, firmware: bytes, psk: bytes, timeout: 0x01: "CRC mismatch", 0x02: "Size error", 0x03: "Write error", - 0x06: "Timeout", + 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}")