mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-05-17 06:45:46 +02:00
remove sideload plugin
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 MeshEnvy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,117 +0,0 @@
|
||||
# meshforge-sideload
|
||||
|
||||
A PlatformIO plugin used by the [MeshForge](https://meshforge.org) web flasher to sideload data files to Meshtastic devices after flashing.
|
||||
|
||||
**This is not a C++ library.** It adds no code to device firmware. Instead it:
|
||||
1. Registers a `sideload` PlatformIO build target that transfers data files to a connected device via Meshtastic's built-in XModem file transfer protocol
|
||||
2. Ships the Meshtastic firmware patches that enable `/__ext__/` and `/__int__/` path routing in XModem (see `patches/`)
|
||||
|
||||
## Protocol
|
||||
|
||||
Frames are delimited by `0xBB` and coexist safely with Meshtastic's serial API (which uses `0x94C3`) and KISS framing (which uses `0xC0`).
|
||||
|
||||
```
|
||||
Frame: 0xBB <cmd:1> <len:2 LE> <payload:len> <crc8:1>
|
||||
Response: 0xBB 0x80 <status:1> (0x00 = OK, 0x01 = ERR)
|
||||
|
||||
Commands:
|
||||
0x00 PING payload=[] → OK + "MESHFORGE-SIDELOAD:0.1\n"
|
||||
0x01 OPEN payload=[pathLen:1][path][size:4 LE]
|
||||
0x02 DATA payload=[bytes, max 256]
|
||||
0x03 CLOSE payload=[]
|
||||
0x04 MKDIR payload=[path]
|
||||
```
|
||||
|
||||
## Path routing
|
||||
|
||||
| Destination prefix | nRF52 | ESP32 |
|
||||
|--------------------|-------|-------|
|
||||
| `/ext/foo` | External QSPI LittleFS | LittleFS (prefix stripped) |
|
||||
| `/int/foo` | InternalFS | LittleFS (prefix stripped) |
|
||||
| `/foo` (bare) | InternalFS | LittleFS |
|
||||
|
||||
On ESP32 both prefixes are equivalent — the device has one filesystem. The prefix convention lets `meshforge.yaml` use a single universal path (e.g. `/ext/bbs/kb`) that works correctly on both platforms.
|
||||
|
||||
## Installation
|
||||
|
||||
```ini
|
||||
; platformio.ini
|
||||
[env:your_target]
|
||||
lib_deps =
|
||||
meshenvy/meshforge-sideload@^0.1.0
|
||||
```
|
||||
|
||||
## Meshtastic integration
|
||||
|
||||
The library has no dependency on Meshtastic. Integrate it by calling `begin()` from your module's `setup()` and `poll()` from `runOnce()`.
|
||||
|
||||
### 1. Add the library
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
meshenvy/meshforge-sideload@^0.1.0
|
||||
```
|
||||
|
||||
### 2. Override the external FS (nRF52 only)
|
||||
|
||||
By default `/ext/` paths fall back to `InternalFS`. If your firmware has an external QSPI LittleFS, override the weak function anywhere in your source:
|
||||
|
||||
```cpp
|
||||
// In your module .cpp, after including your QSPI flash header:
|
||||
#if defined(MFSL_PLATFORM_NRF52)
|
||||
Adafruit_LittleFS_Namespace::Adafruit_LittleFS& meshforgeSideloadExtFS() {
|
||||
return myQspiFs; // your Adafruit_LittleFS instance, already mounted
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
The library never calls `begin()` on this FS — mount it before calling `MeshForgeSideload::begin()`.
|
||||
|
||||
### 3. Wire into your module
|
||||
|
||||
```cpp
|
||||
#include "MeshForgeSideload.h"
|
||||
|
||||
static MeshForgeSideload meshForgeSideload;
|
||||
|
||||
void YourModule::setup() {
|
||||
// ... your existing setup ...
|
||||
meshForgeSideload.begin();
|
||||
}
|
||||
|
||||
int32_t YourModule::runOnce() {
|
||||
meshForgeSideload.poll();
|
||||
// ... your existing logic ...
|
||||
return interval;
|
||||
}
|
||||
```
|
||||
|
||||
`poll()` is non-blocking and returns immediately when no `0xBB` frame is present on Serial.
|
||||
|
||||
### 4. Declare data files in meshforge.yaml
|
||||
|
||||
Add a `meshforge.yaml` to your firmware repo root:
|
||||
|
||||
```yaml
|
||||
meshforge:
|
||||
data:
|
||||
- my-data/*.bin:/ext/my/path
|
||||
```
|
||||
|
||||
The MeshForge CI bundler includes these files in the firmware `.tar.gz`. The MeshForge web flasher reads the yaml from the bundle, waits for the device to boot after flashing, and sideloads the files automatically.
|
||||
|
||||
## MeshCore
|
||||
|
||||
The library is firmware-agnostic — it depends only on the Arduino `Serial` object and standard Adafruit nRF52 BSP / ESP-IDF Arduino filesystem APIs. MeshCore runs on the same Adafruit nRF52 BSP, so the integration should work identically: override `meshforgeSideloadExtFS()` if needed and call `poll()` from your main loop. Formal MeshCore testing has not been done yet.
|
||||
|
||||
## Platforms
|
||||
|
||||
| Platform | Support |
|
||||
|----------|---------|
|
||||
| nRF52840 (Adafruit BSP) | Supported — InternalFS + optional QSPI ext FS |
|
||||
| ESP32 / ESP32-S3 | Supported — LittleFS |
|
||||
| Other Arduino targets | Compile-time no-op with `#pragma message` warning |
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -1,97 +0,0 @@
|
||||
"""
|
||||
register_sideload_target.py — PlatformIO extra_script that registers the
|
||||
`sideload` custom target for the meshforge-sideload library.
|
||||
|
||||
Transfers data files declared in meshforge.yaml to the device using
|
||||
Node.uploadFile() from the meshtastic Python library.
|
||||
|
||||
Usage:
|
||||
pio run -t sideload
|
||||
pio run -t upload -t sideload
|
||||
pio run -t sideload --upload-port /dev/cu.usbmodem101
|
||||
MESHFORGE_PORT=/dev/cu.usbmodemXXXX pio run -t sideload
|
||||
MESHFORGE_BOOT_WAIT=5 pio run -t sideload
|
||||
"""
|
||||
|
||||
Import('env') # noqa: F821
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def _ensure_vendor_meshtastic():
|
||||
"""Add vendor/meshtastic-python to sys.path so we can import it."""
|
||||
project_dir = env.subst('$PROJECT_DIR') # noqa: F821
|
||||
# firmware/ → TinyBBS/ → vendor/ → mesh-forge root
|
||||
candidate = os.path.normpath(os.path.join(project_dir, '..', '..', '..', 'vendor', 'meshtastic-python'))
|
||||
if os.path.isdir(os.path.join(candidate, 'meshtastic')) and candidate not in sys.path:
|
||||
sys.path.insert(0, candidate)
|
||||
# Also add tools/ dir of meshforge-sideload for mf_protocol
|
||||
libdeps = env.subst('$PROJECT_LIBDEPS_DIR') # noqa: F821
|
||||
pioenv = env.subst('$PIOENV') # noqa: F821
|
||||
tools_dir = os.path.join(libdeps, pioenv, 'meshforge-sideload', 'tools')
|
||||
if os.path.isdir(tools_dir) and tools_dir not in sys.path:
|
||||
sys.path.insert(0, tools_dir)
|
||||
|
||||
|
||||
def _autodetect_port():
|
||||
try:
|
||||
import serial.tools.list_ports
|
||||
KNOWN = ['RAK', 'nRF52', 'Adafruit', 'Nordic', 'Meshtastic', 'WisMesh',
|
||||
'T-Echo', 'CP210', 'CH340', 'FTDI', 'USB Serial', 'JTAG',
|
||||
'LilyGO', 'Espressif']
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for p in ports:
|
||||
desc = (p.description or '') + ' ' + (p.manufacturer or '')
|
||||
if any(k.lower() in desc.lower() for k in KNOWN):
|
||||
return p.device
|
||||
for p in ports:
|
||||
if 'usbmodem' in p.device or 'usbserial' in p.device:
|
||||
return p.device
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _sideload(source, target, env): # noqa: F821
|
||||
_ensure_vendor_meshtastic()
|
||||
|
||||
try:
|
||||
from mf_protocol import run_sideload
|
||||
except ImportError as e:
|
||||
print(f'meshforge-sideload: ERROR importing mf_protocol: {e}')
|
||||
raise SystemExit(1)
|
||||
|
||||
project_dir = env['PROJECT_DIR'] # noqa: F821
|
||||
port_name = (
|
||||
env.get('UPLOAD_PORT')
|
||||
or os.environ.get('MESHFORGE_PORT')
|
||||
or _autodetect_port()
|
||||
)
|
||||
if not port_name:
|
||||
print(
|
||||
'meshforge-sideload: ERROR — no serial port found.\n'
|
||||
' Pass one: pio run -t sideload --upload-port /dev/cu.usbmodemXXXX\n'
|
||||
' Or set: MESHFORGE_PORT=/dev/cu.usbmodemXXXX pio run -t sideload'
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
boot_wait = float(os.environ.get('MESHFORGE_BOOT_WAIT', '3'))
|
||||
baud = int(env.get('MONITOR_SPEED', 115200))
|
||||
|
||||
run_sideload(
|
||||
project_dir=project_dir,
|
||||
port_name=port_name,
|
||||
baud=baud,
|
||||
boot_wait_s=boot_wait,
|
||||
)
|
||||
|
||||
|
||||
env.AddCustomTarget( # noqa: F821
|
||||
name='sideload',
|
||||
dependencies=None,
|
||||
actions=_sideload,
|
||||
title='MeshForge Sideload',
|
||||
description='Upload meshforge.yaml data files to device via Meshtastic XModem',
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "meshforge-sideload",
|
||||
"version": "0.1.0",
|
||||
"description": "PlatformIO plugin that registers a 'sideload' build target for uploading data files to Meshtastic devices via XModem. Also ships the Meshtastic firmware patches required to enable /__ext__/ and /__int__/ path routing.",
|
||||
"keywords": ["meshtastic", "meshtastic-module", "sideload", "xmodem"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "MeshEnvy",
|
||||
"url": "https://github.com/meshenvy"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/meshenvy/mesh-forge"
|
||||
},
|
||||
"license": "MIT",
|
||||
"frameworks": ["arduino"],
|
||||
"platforms": ["nordicnrf52", "espressif32"],
|
||||
"build": {
|
||||
"extraScript": "extra_scripts/register_sideload_target.py"
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
From 488d40597572d54b19245e23282fcf2b1cd2140c Mon Sep 17 00:00:00 2001
|
||||
From: Ben Allfree <ben@benallfree.com>
|
||||
Date: Sun, 12 Apr 2026 13:33:44 -0700
|
||||
Subject: [PATCH] fix(xmodem): truncate file on open instead of appending
|
||||
|
||||
FILE_O_WRITE on nRF52 (Adafruit LittleFS) appends to existing files
|
||||
rather than truncating. Remove the file before opening for write so
|
||||
repeated XModem uploads always produce the correct file size.
|
||||
|
||||
Made-with: Cursor
|
||||
---
|
||||
src/xmodem.cpp | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/src/xmodem.cpp b/src/xmodem.cpp
|
||||
index 1d8c77760..cdbbaefe1 100644
|
||||
--- a/src/xmodem.cpp
|
||||
+++ b/src/xmodem.cpp
|
||||
@@ -123,6 +123,8 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
|
||||
if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash
|
||||
spiLock->lock();
|
||||
+ // Remove existing file first so we truncate rather than append
|
||||
+ if (FSCom.exists(filename)) FSCom.remove(filename);
|
||||
file = FSCom.open(filename, FILE_O_WRITE);
|
||||
spiLock->unlock();
|
||||
if (file) {
|
||||
--
|
||||
2.50.1
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
From a577f8b8774f3a517c72b526dc8dc2b67cd0aea5 Mon Sep 17 00:00:00 2001
|
||||
From: Ben Allfree <ben@benallfree.com>
|
||||
Date: Sun, 12 Apr 2026 11:29:40 -0700
|
||||
Subject: [PATCH] Add littleFS support for external flash on nRF
|
||||
|
||||
---
|
||||
src/FSCommon.cpp | 22 +++
|
||||
src/FSCommon.h | 18 ++
|
||||
src/platform/nrf52/extfs-nrf52.cpp | 198 +++++++++++++++++++++
|
||||
variants/nrf52840/nano-g2-ultra/variant.h | 1 +
|
||||
variants/nrf52840/rak_wismeshtap/variant.h | 1 +
|
||||
variants/nrf52840/t-echo-lite/variant.h | 1 +
|
||||
variants/nrf52840/t-echo-plus/variant.h | 1 +
|
||||
variants/nrf52840/t-echo/variant.h | 1 +
|
||||
8 files changed, 243 insertions(+)
|
||||
create mode 100644 src/platform/nrf52/extfs-nrf52.cpp
|
||||
|
||||
diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp
|
||||
index f215be80f..2c14f308c 100644
|
||||
--- a/src/FSCommon.cpp
|
||||
+++ b/src/FSCommon.cpp
|
||||
@@ -284,6 +284,22 @@ void rmDir(const char *dirname)
|
||||
*/
|
||||
__attribute__((weak, noinline)) void preFSBegin() {}
|
||||
|
||||
+#if defined(ARCH_NRF52)
|
||||
+// Default null; set by extFSInit() when external flash is available.
|
||||
+Adafruit_LittleFS *extFS = nullptr;
|
||||
+
|
||||
+/**
|
||||
+ * Default weak implementation — no external filesystem.
|
||||
+ * Override in your firmware to mount a QSPI/SPI flash chip and assign extFS:
|
||||
+ *
|
||||
+ * void extFSInit() {
|
||||
+ * static MyExternalFS myFS;
|
||||
+ * if (myFS.begin()) extFS = &myFS;
|
||||
+ * }
|
||||
+ */
|
||||
+__attribute__((weak, noinline)) void extFSInit() {}
|
||||
+#endif
|
||||
+
|
||||
void fsInit()
|
||||
{
|
||||
#ifdef FSCom
|
||||
@@ -293,6 +309,12 @@ void fsInit()
|
||||
LOG_ERROR("Filesystem mount failed");
|
||||
// assert(0); This auto-formats the partition, so no need to fail here.
|
||||
}
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ extFSInit();
|
||||
+ if (extFS) {
|
||||
+ LOG_DEBUG("External filesystem mounted OK");
|
||||
+ }
|
||||
+#endif
|
||||
#if defined(ARCH_ESP32)
|
||||
LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes());
|
||||
#else
|
||||
diff --git a/src/FSCommon.h b/src/FSCommon.h
|
||||
index fdc0b76ec..9b3de20c2 100644
|
||||
--- a/src/FSCommon.h
|
||||
+++ b/src/FSCommon.h
|
||||
@@ -45,7 +45,25 @@ using namespace STM32_LittleFS_Namespace;
|
||||
#include "InternalFileSystem.h"
|
||||
#define FSCom InternalFS
|
||||
#define FSBegin() FSCom.begin() // InternalFS formats on failure
|
||||
+#include "Adafruit_LittleFS.h"
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
+
|
||||
+/**
|
||||
+ * Optional external LittleFS instance (e.g. QSPI flash on boards that define
|
||||
+ * EXTERNAL_FLASH_USE_QSPI in their variant.h).
|
||||
+ *
|
||||
+ * Null by default. Set by extFSInit() when a board or firmware module
|
||||
+ * initialises an external flash filesystem. XModem uses this pointer to
|
||||
+ * route "/ext/" paths to external storage instead of InternalFS.
|
||||
+ */
|
||||
+extern Adafruit_LittleFS *extFS;
|
||||
+
|
||||
+/**
|
||||
+ * Called from fsInit() to initialise the external filesystem.
|
||||
+ * The default weak implementation is a no-op; override in a platform or
|
||||
+ * firmware module to mount the QSPI (or SPI) flash and assign extFS.
|
||||
+ */
|
||||
+void extFSInit();
|
||||
#endif
|
||||
|
||||
void fsInit();
|
||||
diff --git a/src/platform/nrf52/extfs-nrf52.cpp b/src/platform/nrf52/extfs-nrf52.cpp
|
||||
new file mode 100644
|
||||
index 000000000..f316da5fc
|
||||
--- /dev/null
|
||||
+++ b/src/platform/nrf52/extfs-nrf52.cpp
|
||||
@@ -0,0 +1,198 @@
|
||||
+/**
|
||||
+ * @file extfs-nrf52.cpp
|
||||
+ * @brief Optional external QSPI LittleFS for nRF52840 boards.
|
||||
+ *
|
||||
+ * Compiled only when BOTH of the following are defined in a board's variant.h:
|
||||
+ *
|
||||
+ * EXTERNAL_FLASH_USE_QSPI — chip is physically wired (hardware fact)
|
||||
+ * MESHTASTIC_EXTERNAL_FLASH_FS — board opts into LittleFS on that chip
|
||||
+ *
|
||||
+ * The two-define pattern lets boards that use external flash for other purposes
|
||||
+ * (raw storage, FAT, custom formats) declare the hardware without getting an
|
||||
+ * unwanted LittleFS mount. If either define is absent this file is a no-op
|
||||
+ * and extFS stays null.
|
||||
+ *
|
||||
+ * Required variant.h defines (already on QSPI-capable boards):
|
||||
+ * PIN_QSPI_SCK, PIN_QSPI_CS, PIN_QSPI_IO0..IO3
|
||||
+ * EXTERNAL_FLASH_USE_QSPI (hardware capability)
|
||||
+ * MESHTASTIC_EXTERNAL_FLASH_FS (intent to use as LittleFS — add this)
|
||||
+ *
|
||||
+ * Optional build flag:
|
||||
+ * EXTFLASH_SIZE_BYTES — total chip size in bytes (defaults to 2 MB)
|
||||
+ *
|
||||
+ * This file is intentionally self-contained so it can be proposed as a
|
||||
+ * pull request to Meshtastic firmware without touching other platform files
|
||||
+ * beyond the weak extFSInit() hook in FSCommon.cpp.
|
||||
+ */
|
||||
+
|
||||
+#if defined(ARCH_NRF52) && defined(EXTERNAL_FLASH_USE_QSPI) && defined(MESHTASTIC_EXTERNAL_FLASH_FS)
|
||||
+
|
||||
+#include "FSCommon.h"
|
||||
+#include "configuration.h"
|
||||
+#include <Arduino.h>
|
||||
+#include <nrfx_qspi.h>
|
||||
+#include <cstring>
|
||||
+
|
||||
+// ── Chip geometry ─────────────────────────────────────────────────────────────
|
||||
+
|
||||
+#ifndef EXTFLASH_SIZE_BYTES
|
||||
+#define EXTFLASH_SIZE_BYTES (2 * 1024 * 1024) // Default: 2 MB (MX25R1635F / GD25Q16C)
|
||||
+#endif
|
||||
+
|
||||
+#define EXTFLASH_PAGE_SIZE 256
|
||||
+#define EXTFLASH_SECTOR_SIZE 4096
|
||||
+#define EXTFLASH_BLOCK_COUNT (EXTFLASH_SIZE_BYTES / EXTFLASH_SECTOR_SIZE)
|
||||
+
|
||||
+// ── QSPI hardware init ────────────────────────────────────────────────────────
|
||||
+
|
||||
+static uint8_t _qspi_scratch[EXTFLASH_PAGE_SIZE] __attribute__((aligned(4)));
|
||||
+static bool _qspi_ready = false;
|
||||
+
|
||||
+static bool _qspi_init()
|
||||
+{
|
||||
+ if (_qspi_ready) return true;
|
||||
+
|
||||
+#ifndef NRFX_QSPI_DEFAULT_CONFIG_IRQ_PRIORITY
|
||||
+#define NRFX_QSPI_DEFAULT_CONFIG_IRQ_PRIORITY 6
|
||||
+#endif
|
||||
+
|
||||
+ nrfx_qspi_config_t cfg = NRFX_QSPI_DEFAULT_CONFIG(
|
||||
+ PIN_QSPI_SCK, PIN_QSPI_CS,
|
||||
+ PIN_QSPI_IO0, PIN_QSPI_IO1, PIN_QSPI_IO2, PIN_QSPI_IO3);
|
||||
+
|
||||
+ // Conservative 8 MHz clock; avoids needing the QE status-register bit set
|
||||
+ // for true quad I/O. Works reliably on all boards with QSPI flash.
|
||||
+ cfg.phy_if.sck_freq = NRF_QSPI_FREQ_DIV8;
|
||||
+
|
||||
+ nrfx_err_t err = nrfx_qspi_init(&cfg, NULL, NULL); // blocking mode
|
||||
+ if (err == NRFX_ERROR_INVALID_STATE) {
|
||||
+ // Already initialised (e.g. by bootloader hand-off) — that's fine.
|
||||
+ _qspi_ready = true;
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (err != NRFX_SUCCESS) {
|
||||
+ LOG_ERROR("QSPI init failed: %d", (int)err);
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ _qspi_ready = true;
|
||||
+ LOG_DEBUG("External QSPI flash ready (8 MHz)");
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+// ── LittleFS I/O callbacks ────────────────────────────────────────────────────
|
||||
+
|
||||
+static int _ef_read(const struct lfs_config *c, lfs_block_t block,
|
||||
+ lfs_off_t off, void *buf, lfs_size_t size)
|
||||
+{
|
||||
+ (void)c;
|
||||
+ uint32_t addr = block * EXTFLASH_SECTOR_SIZE + off;
|
||||
+
|
||||
+ if (((uintptr_t)buf & 3) == 0 && (size & 3) == 0) {
|
||||
+ return (nrfx_qspi_read(buf, size, addr) == NRFX_SUCCESS) ? LFS_ERR_OK : LFS_ERR_IO;
|
||||
+ }
|
||||
+
|
||||
+ // Unaligned: go through scratch
|
||||
+ uint8_t *dst = (uint8_t *)buf;
|
||||
+ while (size > 0) {
|
||||
+ uint32_t chunk = (size > sizeof(_qspi_scratch)) ? sizeof(_qspi_scratch) : size;
|
||||
+ uint32_t qchunk = (chunk + 3) & ~3u;
|
||||
+ if (nrfx_qspi_read(_qspi_scratch, qchunk, addr) != NRFX_SUCCESS) return LFS_ERR_IO;
|
||||
+ memcpy(dst, _qspi_scratch, chunk);
|
||||
+ dst += chunk; addr += chunk; size -= chunk;
|
||||
+ }
|
||||
+ return LFS_ERR_OK;
|
||||
+}
|
||||
+
|
||||
+static int _ef_prog(const struct lfs_config *c, lfs_block_t block,
|
||||
+ lfs_off_t off, const void *buf, lfs_size_t size)
|
||||
+{
|
||||
+ (void)c;
|
||||
+ uint32_t addr = block * EXTFLASH_SECTOR_SIZE + off;
|
||||
+
|
||||
+ if (((uintptr_t)buf & 3) == 0 && (size & 3) == 0) {
|
||||
+ if (nrfx_qspi_write(buf, size, addr) != NRFX_SUCCESS) return LFS_ERR_IO;
|
||||
+ } else {
|
||||
+ const uint8_t *src = (const uint8_t *)buf;
|
||||
+ while (size > 0) {
|
||||
+ uint32_t chunk = (size > sizeof(_qspi_scratch)) ? sizeof(_qspi_scratch) : size;
|
||||
+ uint32_t qchunk = (chunk + 3) & ~3u;
|
||||
+ memcpy(_qspi_scratch, src, chunk);
|
||||
+ for (uint32_t i = chunk; i < qchunk; i++) _qspi_scratch[i] = 0xFF;
|
||||
+ if (nrfx_qspi_write(_qspi_scratch, qchunk, addr) != NRFX_SUCCESS) return LFS_ERR_IO;
|
||||
+ src += chunk; addr += chunk; size -= chunk;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ while (nrfx_qspi_mem_busy_check() == NRFX_ERROR_BUSY) yield();
|
||||
+ return LFS_ERR_OK;
|
||||
+}
|
||||
+
|
||||
+static int _ef_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
+{
|
||||
+ (void)c;
|
||||
+ uint32_t addr = block * EXTFLASH_SECTOR_SIZE;
|
||||
+ if (nrfx_qspi_erase(NRF_QSPI_ERASE_LEN_4KB, addr) != NRFX_SUCCESS) return LFS_ERR_IO;
|
||||
+ while (nrfx_qspi_mem_busy_check() == NRFX_ERROR_BUSY) yield();
|
||||
+ return LFS_ERR_OK;
|
||||
+}
|
||||
+
|
||||
+static int _ef_sync(const struct lfs_config *c)
|
||||
+{
|
||||
+ (void)c;
|
||||
+ while (nrfx_qspi_mem_busy_check() == NRFX_ERROR_BUSY) yield();
|
||||
+ return LFS_ERR_OK;
|
||||
+}
|
||||
+
|
||||
+// ── LittleFS instance ─────────────────────────────────────────────────────────
|
||||
+
|
||||
+static uint8_t _read_buf [EXTFLASH_PAGE_SIZE] __attribute__((aligned(4)));
|
||||
+static uint8_t _prog_buf [EXTFLASH_PAGE_SIZE] __attribute__((aligned(4)));
|
||||
+static uint8_t _lookahead_buf[64] __attribute__((aligned(4)));
|
||||
+
|
||||
+static struct lfs_config _cfg = {
|
||||
+ .context = NULL,
|
||||
+ .read = _ef_read,
|
||||
+ .prog = _ef_prog,
|
||||
+ .erase = _ef_erase,
|
||||
+ .sync = _ef_sync,
|
||||
+
|
||||
+ .read_size = EXTFLASH_PAGE_SIZE,
|
||||
+ .prog_size = EXTFLASH_PAGE_SIZE,
|
||||
+ .block_size = EXTFLASH_SECTOR_SIZE,
|
||||
+ .block_count = EXTFLASH_BLOCK_COUNT,
|
||||
+ .lookahead = 512,
|
||||
+
|
||||
+ .read_buffer = _read_buf,
|
||||
+ .prog_buffer = _prog_buf,
|
||||
+ .lookahead_buffer = _lookahead_buf,
|
||||
+ .file_buffer = NULL,
|
||||
+};
|
||||
+
|
||||
+static Adafruit_LittleFS _extLittleFS(&_cfg);
|
||||
+
|
||||
+// ── extFSInit() — overrides the weak no-op in FSCommon.cpp ───────────────────
|
||||
+
|
||||
+void extFSInit()
|
||||
+{
|
||||
+ if (!_qspi_init()) return;
|
||||
+
|
||||
+ if (_extLittleFS.begin()) {
|
||||
+ LOG_INFO("External QSPI LittleFS mounted (%u blocks x %u B)",
|
||||
+ EXTFLASH_BLOCK_COUNT, EXTFLASH_SECTOR_SIZE);
|
||||
+ extFS = &_extLittleFS;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // First boot or corrupted — format then remount
|
||||
+ LOG_WARN("External QSPI LittleFS mount failed, formatting...");
|
||||
+ if (!_extLittleFS.format() || !_extLittleFS.begin()) {
|
||||
+ LOG_ERROR("External QSPI LittleFS format/mount failed");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ LOG_INFO("External QSPI LittleFS formatted and mounted");
|
||||
+ extFS = &_extLittleFS;
|
||||
+}
|
||||
+
|
||||
+#endif // ARCH_NRF52 && EXTERNAL_FLASH_USE_QSPI && MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h
|
||||
index 631af72d8..93f5e4011 100644
|
||||
--- a/variants/nrf52840/nano-g2-ultra/variant.h
|
||||
+++ b/variants/nrf52840/nano-g2-ultra/variant.h
|
||||
@@ -90,6 +90,7 @@ External serial flash W25Q16JV_IQ
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES W25Q16JV_IQ
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
+#define MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
|
||||
/*
|
||||
* Lora radio
|
||||
diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h
|
||||
index 358117cd5..106fce89d 100644
|
||||
--- a/variants/nrf52840/rak_wismeshtap/variant.h
|
||||
+++ b/variants/nrf52840/rak_wismeshtap/variant.h
|
||||
@@ -154,6 +154,7 @@ static const uint8_t SCK = PIN_SPI_SCK;
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES IS25LP080D
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
+#define MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
|
||||
/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports
|
||||
RAK5005-O <-> nRF52840
|
||||
diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h
|
||||
index 54c7bdfb5..01d26f2d4 100644
|
||||
--- a/variants/nrf52840/t-echo-lite/variant.h
|
||||
+++ b/variants/nrf52840/t-echo-lite/variant.h
|
||||
@@ -98,6 +98,7 @@ static const uint8_t A0 = PIN_A0;
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
+#define MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
|
||||
// Lora radio
|
||||
|
||||
diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h
|
||||
index 7ebdf48c0..0cca07efc 100644
|
||||
--- a/variants/nrf52840/t-echo-plus/variant.h
|
||||
+++ b/variants/nrf52840/t-echo-plus/variant.h
|
||||
@@ -77,6 +77,7 @@ static const uint8_t A0 = PIN_A0;
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES MX25R1635F
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
+#define MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
|
||||
// LoRa SX1262
|
||||
#define USE_SX1262
|
||||
diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h
|
||||
index f4644c6de..b13046121 100644
|
||||
--- a/variants/nrf52840/t-echo/variant.h
|
||||
+++ b/variants/nrf52840/t-echo/variant.h
|
||||
@@ -121,6 +121,7 @@ External serial flash WP25R1635FZUIL0
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES MX25R1635F
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
+#define MESHTASTIC_EXTERNAL_FLASH_FS
|
||||
|
||||
/*
|
||||
* Lora radio
|
||||
--
|
||||
2.50.1
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp
|
||||
index 2c14f308c..79e927e64 100644
|
||||
--- a/src/FSCommon.cpp
|
||||
+++ b/src/FSCommon.cpp
|
||||
@@ -324,6 +324,100 @@ void fsInit()
|
||||
#endif
|
||||
}
|
||||
|
||||
+// ── FSRoute virtual mount-point routing ──────────────────────────────────────
|
||||
+
|
||||
+#ifdef FSCom
|
||||
+
|
||||
+FSRoute fsRoute(const char *path)
|
||||
+{
|
||||
+ FSRoute r;
|
||||
+ if (strncmp(path, "/__ext__/", 9) == 0) {
|
||||
+ r.mount = FsMount::External;
|
||||
+ r.path[0] = '/';
|
||||
+ strlcpy(r.path + 1, path + 9, sizeof(r.path) - 1);
|
||||
+ } else if (strncmp(path, "/__int__/", 9) == 0) {
|
||||
+ r.mount = FsMount::Internal;
|
||||
+ r.path[0] = '/';
|
||||
+ strlcpy(r.path + 1, path + 9, sizeof(r.path) - 1);
|
||||
+ } else if (strncmp(path, "/__sd__/", 8) == 0) {
|
||||
+ r.mount = FsMount::SD;
|
||||
+ r.path[0] = '/';
|
||||
+ strlcpy(r.path + 1, path + 8, sizeof(r.path) - 1);
|
||||
+ } else {
|
||||
+ r.mount = FsMount::Internal;
|
||||
+ strlcpy(r.path, path, sizeof(r.path));
|
||||
+ }
|
||||
+ return r;
|
||||
+}
|
||||
+
|
||||
+// ── FS selection helper ───────────────────────────────────────────────────────
|
||||
+// Returns the correct FS object for the given mount, falling back to FSCom
|
||||
+// when the requested mount is unavailable.
|
||||
+
|
||||
+#if defined(ARCH_NRF52)
|
||||
+static Adafruit_LittleFS &_fsForMount(FsMount mount)
|
||||
+{
|
||||
+ if (mount == FsMount::External && extFS != nullptr)
|
||||
+ return *extFS;
|
||||
+ // SD: future
|
||||
+ return (Adafruit_LittleFS &)FSCom;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
+// ── Public helpers ────────────────────────────────────────────────────────────
|
||||
+
|
||||
+File fsOpenRead(const FSRoute &r)
|
||||
+{
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ return _fsForMount(r.mount).open(r.path, FILE_O_READ);
|
||||
+#else
|
||||
+ (void)r.mount;
|
||||
+ return FSCom.open(r.path, FILE_O_READ);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+File fsOpenWrite(const FSRoute &r)
|
||||
+{
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ return _fsForMount(r.mount).open(r.path, FILE_O_WRITE);
|
||||
+#else
|
||||
+ (void)r.mount;
|
||||
+ return FSCom.open(r.path, FILE_O_WRITE);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+bool fsRemove(const FSRoute &r)
|
||||
+{
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ return _fsForMount(r.mount).remove(r.path);
|
||||
+#else
|
||||
+ (void)r.mount;
|
||||
+ return FSCom.remove(r.path);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+bool fsMkdir(const FSRoute &r)
|
||||
+{
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ return _fsForMount(r.mount).mkdir(r.path);
|
||||
+#else
|
||||
+ (void)r.mount;
|
||||
+ return FSCom.mkdir(r.path);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+bool fsExists(const FSRoute &r)
|
||||
+{
|
||||
+#if defined(ARCH_NRF52)
|
||||
+ return _fsForMount(r.mount).exists(r.path);
|
||||
+#else
|
||||
+ (void)r.mount;
|
||||
+ return FSCom.exists(r.path);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+#endif // FSCom
|
||||
+
|
||||
/**
|
||||
* Initializes the SD card and mounts the file system.
|
||||
*/
|
||||
diff --git a/src/FSCommon.h b/src/FSCommon.h
|
||||
index 9b3de20c2..1ac0d04a1 100644
|
||||
--- a/src/FSCommon.h
|
||||
+++ b/src/FSCommon.h
|
||||
@@ -73,4 +73,44 @@ bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
-void setupSDCard();
|
||||
\ No newline at end of file
|
||||
+void setupSDCard();
|
||||
+
|
||||
+#ifdef FSCom
|
||||
+/**
|
||||
+ * Virtual filesystem mount-point routing.
|
||||
+ *
|
||||
+ * Path-prefix convention (double-underscore delimiters avoid collisions):
|
||||
+ * /__int__/foo → internal flash (InternalFS / LittleFS)
|
||||
+ * /__ext__/foo → external flash (QSPI LittleFS if mounted, else internal)
|
||||
+ * /__sd__/foo → SD card (if mounted, else internal)
|
||||
+ * /foo → internal flash (bare paths passed through unchanged)
|
||||
+ *
|
||||
+ * This provides a lightweight mount-point convention without a full VFS.
|
||||
+ * All consumers call fsRoute() + the helpers below; when Meshtastic gains a
|
||||
+ * proper VFS layer these functions become the adapter to it.
|
||||
+ */
|
||||
+enum class FsMount { Internal, External, SD };
|
||||
+
|
||||
+struct FSRoute {
|
||||
+ FsMount mount = FsMount::Internal;
|
||||
+ char path[128] = {}; // real path after prefix stripped
|
||||
+};
|
||||
+
|
||||
+/** Resolve a path string to an FSRoute (mount + stripped path). */
|
||||
+FSRoute fsRoute(const char *path);
|
||||
+
|
||||
+/** Open a file for reading on the routed filesystem. */
|
||||
+File fsOpenRead(const FSRoute &r);
|
||||
+
|
||||
+/** Open a file for writing on the routed filesystem. */
|
||||
+File fsOpenWrite(const FSRoute &r);
|
||||
+
|
||||
+/** Remove a file on the routed filesystem. Returns true on success. */
|
||||
+bool fsRemove(const FSRoute &r);
|
||||
+
|
||||
+/** Create a directory (and parents) on the routed filesystem. */
|
||||
+bool fsMkdir(const FSRoute &r);
|
||||
+
|
||||
+/** Return true if the path exists on the routed filesystem. */
|
||||
+bool fsExists(const FSRoute &r);
|
||||
+#endif // FSCom
|
||||
\ No newline at end of file
|
||||
diff --git a/src/xmodem.cpp b/src/xmodem.cpp
|
||||
index 1d8c77760..847fe49c8 100644
|
||||
--- a/src/xmodem.cpp
|
||||
+++ b/src/xmodem.cpp
|
||||
@@ -120,10 +120,13 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) {
|
||||
// NULL packet has the destination filename
|
||||
memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size);
|
||||
+ activeRoute_ = fsRoute(filename);
|
||||
|
||||
if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash
|
||||
spiLock->lock();
|
||||
- file = FSCom.open(filename, FILE_O_WRITE);
|
||||
+ // Remove existing file first so we truncate rather than append
|
||||
+ if (FSCom.exists(filename)) FSCom.remove(filename);
|
||||
+ file = fsOpenWrite(activeRoute_);
|
||||
spiLock->unlock();
|
||||
if (file) {
|
||||
sendControl(meshtastic_XModem_Control_ACK);
|
||||
@@ -137,7 +140,7 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
} else { // Transmit this file from Flash
|
||||
LOG_INFO("XModem: Transmit file %s", filename);
|
||||
spiLock->lock();
|
||||
- file = FSCom.open(filename, FILE_O_READ);
|
||||
+ file = fsOpenRead(activeRoute_);
|
||||
spiLock->unlock();
|
||||
if (file) {
|
||||
packetno = 1;
|
||||
@@ -200,8 +203,7 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
spiLock->lock();
|
||||
file.flush();
|
||||
file.close();
|
||||
-
|
||||
- FSCom.remove(filename);
|
||||
+ fsRemove(activeRoute_);
|
||||
spiLock->unlock();
|
||||
isReceiving = false;
|
||||
break;
|
||||
@@ -255,7 +257,6 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
xmodemStore.seq = packetno;
|
||||
spiLock->lock();
|
||||
file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes));
|
||||
-
|
||||
xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes));
|
||||
spiLock->unlock();
|
||||
xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size);
|
||||
diff --git a/src/xmodem.h b/src/xmodem.h
|
||||
index 4cfcb43e1..48ff25fbb 100644
|
||||
--- a/src/xmodem.h
|
||||
+++ b/src/xmodem.h
|
||||
@@ -67,7 +67,8 @@ class XModemAdapter
|
||||
File file;
|
||||
#endif
|
||||
|
||||
- char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0};
|
||||
+ char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0};
|
||||
+ FSRoute activeRoute_; // resolved once at SOH, reused for DATA/EOT/CAN
|
||||
|
||||
protected:
|
||||
meshtastic_XModem xmodemStore = meshtastic_XModem_init_zero;
|
||||
@@ -1,83 +0,0 @@
|
||||
commit b5de3323b3e106edb28efcda815d86a066ef34f4
|
||||
Author: Ben Allfree <ben@benallfree.com>
|
||||
Date: Sun Apr 12 21:31:37 2026 -0700
|
||||
|
||||
tdeck fixes
|
||||
|
||||
diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp
|
||||
index e24aa3c57..c81854a7b 100644
|
||||
--- a/src/SerialConsole.cpp
|
||||
+++ b/src/SerialConsole.cpp
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Throttle.h"
|
||||
#include "configuration.h"
|
||||
#include "time.h"
|
||||
+#include "xmodem.h"
|
||||
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
#define IS_USB_SERIAL
|
||||
@@ -92,7 +93,7 @@ int32_t SerialConsole::runOnce()
|
||||
#if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
return Port.available() ? delay : INT32_MAX;
|
||||
#elif defined(IS_USB_SERIAL)
|
||||
- return HWCDC::isPlugged() ? delay : (1000 * 20);
|
||||
+ return (HWCDC::isPlugged() || xModem.isActive() || delay < 250) ? delay : (1000 * 20);
|
||||
#else
|
||||
return delay;
|
||||
#endif
|
||||
diff --git a/src/xmodem.cpp b/src/xmodem.cpp
|
||||
index ccd6a1b95..3155a6526 100644
|
||||
--- a/src/xmodem.cpp
|
||||
+++ b/src/xmodem.cpp
|
||||
@@ -152,8 +152,12 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
case meshtastic_XModem_Control_SOH:
|
||||
case meshtastic_XModem_Control_STX:
|
||||
if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) {
|
||||
- // NULL packet has the destination filename
|
||||
- memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size);
|
||||
+ // NULL packet has the destination filename (protobuf bytes are not NUL-terminated)
|
||||
+ size_t n = xmodemPacket.buffer.size;
|
||||
+ if (n >= sizeof(filename))
|
||||
+ n = sizeof(filename) - 1;
|
||||
+ memcpy(filename, xmodemPacket.buffer.bytes, n);
|
||||
+ filename[n] = '\0';
|
||||
activeRoute_ = fsRoute(filename);
|
||||
|
||||
if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash
|
||||
@@ -201,6 +205,16 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
}
|
||||
} else {
|
||||
if (isReceiving) {
|
||||
+ if (xmodemPacket.seq == 0) {
|
||||
+ // Duplicate OPEN retry (client re-sent while already receiving) — re-ACK so Python proceeds.
|
||||
+ sendControl(meshtastic_XModem_Control_ACK);
|
||||
+ break;
|
||||
+ }
|
||||
+ if (xmodemPacket.seq + 1 == packetno) {
|
||||
+ // Already-delivered packet still in flight (stale serial buffer retry) — re-ACK.
|
||||
+ sendControl(meshtastic_XModem_Control_ACK);
|
||||
+ break;
|
||||
+ }
|
||||
// normal file data packet
|
||||
if ((xmodemPacket.seq == packetno) &&
|
||||
check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) {
|
||||
diff --git a/src/xmodem.h b/src/xmodem.h
|
||||
index 48ff25fbb..5b8c4930d 100644
|
||||
--- a/src/xmodem.h
|
||||
+++ b/src/xmodem.h
|
||||
@@ -51,6 +51,7 @@ class XModemAdapter
|
||||
void handlePacket(meshtastic_XModem xmodemPacket);
|
||||
meshtastic_XModem getForPhone();
|
||||
void resetForPhone();
|
||||
+ bool isActive() const { return isReceiving || isTransmitting; }
|
||||
|
||||
private:
|
||||
bool isReceiving = false;
|
||||
@@ -61,6 +62,7 @@ class XModemAdapter
|
||||
|
||||
uint16_t packetno = 0;
|
||||
|
||||
+// Adafruit nRF/STM32 File can be constructed bound to FSCom; Arduino-ESP32 fs::File cannot.
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_STM32WL)
|
||||
File file = File(FSCom);
|
||||
#else
|
||||
@@ -1,81 +0,0 @@
|
||||
# MeshForge Meshtastic patches
|
||||
|
||||
Two patches against Meshtastic firmware that enable MeshForge data sideloading.
|
||||
Both are pending upstream merge. Once merged, these patches and the apply script
|
||||
become unnecessary.
|
||||
|
||||
---
|
||||
|
||||
## 00-xmodem-truncate-fix.patch
|
||||
|
||||
**PR title:** `fix(xmodem): truncate file on open instead of appending`
|
||||
|
||||
Standalone 2-line fix. `FILE_O_WRITE` on nRF52 (Adafruit LittleFS) appends to
|
||||
existing files rather than truncating. Removes the file before opening for write
|
||||
so repeated XModem uploads always produce the correct file size. Independent of
|
||||
the other two patches — can be reviewed and merged on its own.
|
||||
|
||||
**Sentinel:** `Remove existing file first so we truncate`
|
||||
|
||||
---
|
||||
|
||||
## 01-nrf-external-flash.patch
|
||||
|
||||
**PR title:** `feat(nrf52): optional external QSPI LittleFS filesystem`
|
||||
|
||||
Adds infrastructure for mounting the external QSPI flash chip as a LittleFS
|
||||
volume on nRF52840 boards that have it wired:
|
||||
|
||||
- `src/FSCommon.h` — `extern Adafruit_LittleFS* extFS` pointer + `extFSInit()` declaration
|
||||
- `src/FSCommon.cpp` — `extFS = nullptr` global, weak `extFSInit()` no-op, called from `fsInit()`
|
||||
- `src/platform/nrf52/extfs-nrf52.cpp` — actual QSPI init + LittleFS mount using `nrfx_qspi`
|
||||
- `variants/nrf52840/{t-echo,t-echo-lite,t-echo-plus,nano-g2-ultra,rak_wismeshtap}/variant.h` — add `MESHTASTIC_EXTERNAL_FLASH_FS`
|
||||
|
||||
**Sentinel:** `extFSInit`
|
||||
|
||||
---
|
||||
|
||||
## 02-xmodem-vfs-routing.patch
|
||||
|
||||
**PR title:** `feat(nrf52): virtual filesystem mount points + XModem path routing`
|
||||
|
||||
Depends on patch 01. Adds a lightweight VFS routing layer and wires XModem into it:
|
||||
|
||||
- `src/FSCommon.h` — `FsMount` enum, `FSRoute` struct, `fsRoute()` + helpers
|
||||
- `src/FSCommon.cpp` — routing and helper implementations
|
||||
- `src/xmodem.h` / `src/xmodem.cpp` — XModem uses `fsRoute()` for `/__ext__/` and `/__int__/` paths
|
||||
|
||||
Path prefix convention:
|
||||
|
||||
| Prefix | Destination |
|
||||
|--------|-------------|
|
||||
| `/__ext__/foo` | `extFS` if mounted, else internal |
|
||||
| `/__int__/foo` | Internal flash (InternalFS / LittleFS) |
|
||||
| `/__sd__/foo` | SD card (reserved, falls back to internal) |
|
||||
| `/foo` | Internal flash — bare paths unchanged |
|
||||
|
||||
**Sentinel:** `fsRoute`
|
||||
|
||||
---
|
||||
|
||||
## Applying
|
||||
|
||||
```bash
|
||||
# From within a Meshtastic firmware checkout:
|
||||
git apply meshforge-sideload/patches/01-nrf-external-flash.patch
|
||||
git apply meshforge-sideload/patches/02-xmodem-vfs-routing.patch
|
||||
```
|
||||
|
||||
Or via PlatformIO extra_scripts (applied automatically at build time):
|
||||
|
||||
```ini
|
||||
[env]
|
||||
extra_scripts = pre:path/to/meshforge-sideload/patches/apply_patches.py
|
||||
```
|
||||
|
||||
## Reverting
|
||||
|
||||
```bash
|
||||
git apply -R meshforge-sideload/patches/02-xmodem-vfs-routing.patch
|
||||
git apply -R meshforge-sideload/patches/01-nrf-external-flash.patch
|
||||
```
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
apply_patches.py — PlatformIO pre: extra_script.
|
||||
|
||||
Applies the MeshForge Meshtastic patches to the firmware source tree in order:
|
||||
|
||||
01-nrf-external-flash.patch — extFSInit hook, extFS pointer, extfs-nrf52.cpp
|
||||
02-xmodem-vfs-routing.patch — FSRoute, fsRoute(), XModem /__ext__/ routing
|
||||
|
||||
Each patch is skipped if its sentinel string is already present (idempotent).
|
||||
Safe to run on every build.
|
||||
|
||||
Add to your firmware's platformio.ini:
|
||||
|
||||
extra_scripts =
|
||||
pre:path/to/apply_patches.py
|
||||
"""
|
||||
|
||||
Import('env') # noqa: F821
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
_PATCHES = [
|
||||
('00-xmodem-truncate-fix.patch', 'Remove existing file first so we truncate'), # standalone fix
|
||||
('01-nrf-external-flash.patch', 'extFSInit'), # PR1: external flash FS
|
||||
('02-xmodem-vfs-routing.patch', 'fsRoute'), # PR2: VFS routing + XModem
|
||||
]
|
||||
|
||||
_PATCH_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _project_dir():
|
||||
return env.subst('$PROJECT_DIR') # noqa: F821
|
||||
|
||||
|
||||
def _sentinel_present(sentinel: str) -> bool:
|
||||
"""Return True if the sentinel string already exists in the source tree."""
|
||||
for dirpath, _, filenames in os.walk(os.path.join(_project_dir(), 'src')):
|
||||
for fname in filenames:
|
||||
if not (fname.endswith('.h') or fname.endswith('.cpp')):
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(dirpath, fname), encoding='utf-8', errors='replace') as f:
|
||||
if sentinel in f.read():
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _apply(patch_file: str) -> None:
|
||||
patch_path = os.path.join(_PATCH_DIR, patch_file)
|
||||
if not os.path.isfile(patch_path):
|
||||
print(f'[meshforge-patches] WARNING: {patch_file} not found')
|
||||
return
|
||||
|
||||
# Dry-run first
|
||||
check = subprocess.run(
|
||||
['git', 'apply', '--check', '--whitespace=nowarn', patch_path],
|
||||
cwd=_project_dir(),
|
||||
capture_output=True,
|
||||
)
|
||||
if check.returncode != 0:
|
||||
print(f'[meshforge-patches] WARNING: {patch_file} cannot apply cleanly — skipping '
|
||||
f'(already applied or conflict)')
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['git', 'apply', '--whitespace=nowarn', patch_path],
|
||||
cwd=_project_dir(),
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f'[meshforge-patches] Applied {patch_file}')
|
||||
else:
|
||||
print(f'[meshforge-patches] ERROR applying {patch_file}:\n{result.stderr.decode()}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
for patch_file, sentinel in _PATCHES:
|
||||
if _sentinel_present(sentinel):
|
||||
print(f'[meshforge-patches] {patch_file} already applied — skipping')
|
||||
else:
|
||||
print(f'[meshforge-patches] Applying {patch_file}...')
|
||||
_apply(patch_file)
|
||||
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
mf_protocol.py — MeshForge sideload via meshtastic-python XModem.
|
||||
|
||||
Uses Node.uploadFile() from the meshtastic Python library (vendored at
|
||||
vendor/meshtastic-python) for all file transfers.
|
||||
|
||||
Requires: pip install pyserial (meshtastic is loaded from the vendor submodule)
|
||||
"""
|
||||
|
||||
import glob as globmod
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
# ── Resolve the vendored meshtastic-python library ────────────────────────────
|
||||
# Walk up from this file to find the mesh-forge repo root, then add the
|
||||
# vendored library to sys.path.
|
||||
|
||||
def _find_vendor_meshtastic():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
# tools/ → meshforge-sideload/ → mesh-forge root
|
||||
candidate = os.path.normpath(os.path.join(here, '..', '..', 'vendor', 'meshtastic-python'))
|
||||
if os.path.isdir(os.path.join(candidate, 'meshtastic')):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
_vendor_path = _find_vendor_meshtastic()
|
||||
if _vendor_path and _vendor_path not in sys.path:
|
||||
sys.path.insert(0, _vendor_path)
|
||||
|
||||
|
||||
# ── meshforge.yaml parser ─────────────────────────────────────────────────────
|
||||
|
||||
def parse_data_entries(yaml_text: str) -> list:
|
||||
"""Return list of (glob_pattern, device_dest) from meshforge.yaml data: section."""
|
||||
entries = []
|
||||
in_mf = in_data = False
|
||||
for raw in yaml_text.splitlines():
|
||||
line = re.sub(r'#.*$', '', raw).rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip())
|
||||
content = line.strip()
|
||||
if indent == 0:
|
||||
in_mf = (content == 'meshforge:')
|
||||
in_data = False
|
||||
elif in_mf and indent == 2:
|
||||
in_data = (content == 'data:')
|
||||
elif in_mf and in_data and indent == 4:
|
||||
m = re.match(r'^-\s+(.+)$', content)
|
||||
if m:
|
||||
entry = m.group(1).strip().strip('"\'')
|
||||
colon = entry.find(':')
|
||||
if colon > 0:
|
||||
entries.append((entry[:colon].strip(), entry[colon + 1:].strip()))
|
||||
return entries
|
||||
|
||||
|
||||
# ── Autodetect port ───────────────────────────────────────────────────────────
|
||||
|
||||
def autodetect_port():
|
||||
try:
|
||||
import serial.tools.list_ports
|
||||
KNOWN = ['RAK', 'nRF52', 'Adafruit', 'Nordic', 'Meshtastic', 'WisMesh',
|
||||
'T-Echo', 'CP210', 'CH340', 'FTDI', 'USB Serial', 'JTAG',
|
||||
'LilyGO', 'Espressif']
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for p in ports:
|
||||
desc = (p.description or '') + ' ' + (p.manufacturer or '')
|
||||
if any(k.lower() in desc.lower() for k in KNOWN):
|
||||
return p.device
|
||||
for p in ports:
|
||||
if 'usbmodem' in p.device or 'usbserial' in p.device:
|
||||
return p.device
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── Main sideload routine ─────────────────────────────────────────────────────
|
||||
|
||||
def run_sideload(project_dir: str, port_name: str, baud: int = 115200,
|
||||
boot_wait_s: float = 3.0) -> None:
|
||||
try:
|
||||
import meshtastic.serial_interface
|
||||
except ImportError:
|
||||
print('ERROR: meshtastic library not found.\n'
|
||||
'Install with: pip install meshtastic\n'
|
||||
'Or ensure vendor/meshtastic-python is in sys.path')
|
||||
sys.exit(1)
|
||||
|
||||
yaml_path = os.path.join(project_dir, 'meshforge.yaml')
|
||||
if not os.path.isfile(yaml_path):
|
||||
print('meshforge-sideload: no meshforge.yaml — nothing to sideload')
|
||||
return
|
||||
|
||||
with open(yaml_path, encoding='utf-8') as f:
|
||||
entries = parse_data_entries(f.read())
|
||||
|
||||
if not entries:
|
||||
print('meshforge-sideload: no data: entries — nothing to sideload')
|
||||
return
|
||||
|
||||
transfers = []
|
||||
for glob_pat, dest in entries:
|
||||
for src in sorted(globmod.glob(os.path.join(project_dir, glob_pat), recursive=True)):
|
||||
if os.path.isfile(src):
|
||||
transfers.append((src, dest.rstrip('/') + '/' + os.path.basename(src)))
|
||||
|
||||
if not transfers:
|
||||
print('meshforge-sideload: no files matched — nothing to sideload')
|
||||
return
|
||||
|
||||
print(f'meshforge-sideload: waiting {boot_wait_s}s for device to boot...')
|
||||
time.sleep(boot_wait_s)
|
||||
|
||||
print(f'meshforge-sideload: connecting to {port_name}...')
|
||||
iface = meshtastic.serial_interface.SerialInterface(port_name)
|
||||
|
||||
try:
|
||||
for i, (src, device_path) in enumerate(transfers):
|
||||
size_kb = os.path.getsize(src) / 1024
|
||||
name = os.path.basename(src)
|
||||
print(f' [{i + 1}/{len(transfers)}] {name} ({size_kb:.1f} KB) → {device_path}')
|
||||
|
||||
def progress(sent, total, _name=name):
|
||||
pct = 100 * sent // total
|
||||
bar = '#' * (pct // 5) + '.' * (20 - pct // 5)
|
||||
print(f'\r [{bar}] {pct}%', end='', flush=True)
|
||||
|
||||
ok = iface.localNode.uploadFile(src, device_path, on_progress=progress)
|
||||
if ok:
|
||||
print(f'\r [{"#" * 20}] 100% done')
|
||||
else:
|
||||
print(f'\r FAILED')
|
||||
raise RuntimeError(f'Upload failed: {src} → {device_path}')
|
||||
finally:
|
||||
iface.close()
|
||||
|
||||
print(f'meshforge-sideload: {len(transfers)} file(s) uploaded successfully')
|
||||
Reference in New Issue
Block a user