diff --git a/bin/eth-ota-upload.py b/bin/eth-ota-upload.py new file mode 100644 index 00000000000..68bc1e2a7fd --- /dev/null +++ b/bin/eth-ota-upload.py @@ -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() 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..db5691bccab 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -6,7 +6,14 @@ #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 #include +#endif #include #if HAS_NETWORKING @@ -69,6 +76,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 +90,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) { @@ -136,6 +151,10 @@ static int32_t reconnectETH() } #endif +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + initEthOTA(); +#endif + ethStartupComplete = true; } } @@ -162,6 +181,10 @@ static int32_t reconnectETH() } #endif +#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA) + ethOTALoop(); +#endif + return 5000; // every 5 seconds } @@ -182,6 +205,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 +219,7 @@ bool initEthernet() ETH_SPI_PORT.begin(); #endif Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); +#endif uint8_t mac[6]; @@ -201,7 +232,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/mesh/eth/ethOTA.cpp b/src/mesh/eth/ethOTA.cpp new file mode 100644 index 00000000000..65d7324ce1b --- /dev/null +++ b/src/mesh/eth/ethOTA.cpp @@ -0,0 +1,301 @@ +#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_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(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 diff --git a/src/mesh/eth/ethOTA.h b/src/mesh/eth/ethOTA.h new file mode 100644 index 00000000000..32637ac06a6 --- /dev/null +++ b/src/mesh/eth/ethOTA.h @@ -0,0 +1,22 @@ +#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/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/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/README.md b/variants/rp2350/pico2_w5500_e22/README.md new file mode 100644 index 00000000000..259f06df9a6 --- /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**: + +```text +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: + +```text +.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=7`, 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..47bc3dc2648 --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/platformio.ini @@ -0,0 +1,33 @@ +[env:pico2_w5500_e22] +extends = rp2350_base +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 + -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 + -D HAS_ETHERNET_OTA + +# 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..8c80e798ec5 --- /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..4cdd7cfd1be --- /dev/null +++ b/variants/rp2350/pico2_w5500_e22/wiring.svg @@ -0,0 +1 @@ +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