mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-28 13:54:39 +00:00
Add MeshTower V2 SD-backed LoRa OTA
This commit is contained in:
@@ -6,6 +6,7 @@ declare -A PIO_ENV_PLATFORM_BY_NAME=()
|
||||
declare -A PIO_ENV_BOARD_BY_NAME=()
|
||||
declare -A PIO_ENV_MQTT_BY_NAME=()
|
||||
declare -A PIO_ENV_OTA_BY_NAME=()
|
||||
declare -A PIO_ENV_SD_OTA_BY_NAME=()
|
||||
declare -A PIO_ENV_BUILD_BASE_BY_NAME=()
|
||||
declare -A PIO_ENV_FULL_BUILD_BY_NAME=()
|
||||
declare -A PIO_ENV_FULL_WIFI_OTA_BY_NAME=()
|
||||
@@ -166,7 +167,7 @@ init_project_context() {
|
||||
fi
|
||||
|
||||
if [ ${#SUPPORTED_PIO_ENVS[@]} -eq 0 ]; then
|
||||
while IFS=$'\t' read -r env_name env_platform env_mqtt env_ota env_full env_full_wifi env_board; do
|
||||
while IFS=$'\t' read -r env_name env_platform env_mqtt env_ota env_sd_ota env_full env_full_wifi env_board; do
|
||||
if [ -z "$env_name" ] || [ -z "$env_platform" ]; then
|
||||
continue
|
||||
fi
|
||||
@@ -175,6 +176,7 @@ init_project_context() {
|
||||
PIO_ENV_BOARD_BY_NAME["$env_name"]=$env_board
|
||||
PIO_ENV_MQTT_BY_NAME["$env_name"]=$env_mqtt
|
||||
PIO_ENV_OTA_BY_NAME["$env_name"]=$env_ota
|
||||
PIO_ENV_SD_OTA_BY_NAME["$env_name"]=$env_sd_ota
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$env_name"]=$env_full
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$env_name"]=$env_full_wifi
|
||||
done < <(
|
||||
@@ -192,6 +194,7 @@ for section, options in data:
|
||||
mqtt_enabled = False
|
||||
ota_enabled = False
|
||||
ota_disabled = False
|
||||
sd_ota = False
|
||||
admin_enabled = False
|
||||
espnow_enabled = "bridge_espnow" in env_name.lower()
|
||||
full_wifi_ota = False
|
||||
@@ -217,6 +220,8 @@ for section, options in data:
|
||||
admin_enabled = True
|
||||
if "DISABLE_LORA_OTA" in str(flag):
|
||||
ota_disabled = True
|
||||
if "OTA_SD_STORE" in str(flag):
|
||||
sd_ota = True
|
||||
match = pattern.search(str(flag))
|
||||
if match and platform is None:
|
||||
platform = match.group(0)
|
||||
@@ -228,6 +233,7 @@ for section, options in data:
|
||||
print(
|
||||
f"{env_name}\t{platform}\t{1 if mqtt_enabled else 0}"
|
||||
f"\t{1 if ota_enabled and not ota_disabled else 0}"
|
||||
f"\t{1 if sd_ota else 0}"
|
||||
f"\t{1 if full_enabled else 0}\t{1 if full_wifi_ota else 0}"
|
||||
f"\t{board_value}"
|
||||
)
|
||||
@@ -261,6 +267,7 @@ for section, options in data:
|
||||
PIO_ENV_BOARD_BY_NAME["$ota_env"]="${PIO_ENV_BOARD_BY_NAME[$env_name]}"
|
||||
PIO_ENV_MQTT_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$ota_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$ota_env"]="${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}"
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$ota_env"]="$env_name"
|
||||
@@ -1938,8 +1945,13 @@ apply_lora_ota_override() {
|
||||
local env_name=$1
|
||||
|
||||
if is_lora_ota_build "$env_name"; then
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_FOLDER_SERIAL"
|
||||
if [ "${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1 -DOTA_FLASH_STORE=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -DOTA_SD_STORE=1 -DOTA_FOLDER_SERIAL"
|
||||
else
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_FOLDER_SERIAL"
|
||||
fi
|
||||
else
|
||||
append_platformio_build_unflags "-DENABLE_OTA=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UENABLE_OTA"
|
||||
|
||||
@@ -8,6 +8,7 @@ Below are a few quick start guides.
|
||||
- [CLI Commands](./cli_commands.md)
|
||||
- [CLI Availability by Firmware Build](./cli_build_matrix.md)
|
||||
- [Easy LoRa OTA: ESP32 full images and nRF52 deltas](./ota_easy.md)
|
||||
- [MeshTower V2 microSD self-updates](./ota_meshtower_v2_sdcard.md)
|
||||
- [GPS Tracking](./gps_tracking.md)
|
||||
- [Companion Protocol](./companion_protocol.md)
|
||||
- [Packet Format](./packet_format.md)
|
||||
|
||||
+6
-3
@@ -7,9 +7,11 @@ LoRa. Choose the package type for the **destination** node:
|
||||
| --- | --- | --- | --- |
|
||||
| ESP32 | Full firmware | New non-merged application `.bin` | ESP32 A/B firmware slots |
|
||||
| nRF52 | In-place delta | Exact running `firmware.hex` and new `firmware.hex` | Exact-board OTAFIX bootloader |
|
||||
| MeshTower V2 SD target | Full firmware or in-place delta | New `firmware.hex`; a delta also needs the exact running `firmware.hex` | Matching SD-aware OTAFIX bootloader |
|
||||
|
||||
An nRF52 node cannot install an ESP32-style full-image container. It deliberately accepts only an in-place
|
||||
delta built against its exact running firmware.
|
||||
A normal nRF52 target cannot install a full-image container. It deliberately accepts only an in-place
|
||||
delta built against its exact running firmware. The MeshTower V2 microSD target is the exception because
|
||||
it stages the complete container off-chip; see [MeshTower V2 microSD LoRa OTA](ota_meshtower_v2_sdcard.md).
|
||||
|
||||
## Temporary OTA channel used in this guide
|
||||
|
||||
@@ -298,7 +300,8 @@ ota status
|
||||
If it is the source or destination, install a supported `-ota-` build over WiFi or USB first. An intermediate
|
||||
repeater does not need the OTA CLI and can relay opaquely while its matching `tempradio` window is active.
|
||||
- **The update is marked `[other hw]`:** it is for a different board or firmware role. Do not install it.
|
||||
- **An nRF52 node does not list a full update:** this is intentional. nRF52 accepts only an in-place delta.
|
||||
- **An nRF52 node does not list a full update:** this is intentional for internal-flash targets. The
|
||||
MeshTower V2 microSD target accepts full images with its matching SD-aware bootloader.
|
||||
- **nRF52 reports no bootloader apply support:** install the exact-board in-place-delta OTAFIX bootloader
|
||||
before trying LoRa OTA.
|
||||
- **nRF52 reports a base mismatch:** the file passed to `--base` is not the exact application running on
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# MeshTower V2 microSD LoRa OTA
|
||||
|
||||
The `Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors` target uses the MeshTower V2 onboard
|
||||
microSD socket as persistent storage for its own LoRa OTA downloads. It accepts
|
||||
both full `.mota` images and in-place delta `.mota` images. After verification,
|
||||
the matching SD-aware OTAFIX bootloader reads the staged file from the card and
|
||||
programs the nRF52840 application region.
|
||||
|
||||
The pin assignment follows the
|
||||
[Heltec MeshTower V2 partial reference circuit](https://resource.heltec.cn/download/MeshTower-V2/schematic/MeshTower_V2_Partial_Reference_Circuit.pdf):
|
||||
|
||||
| Signal | nRF52840 pin | Arduino pin number |
|
||||
|---|---:|---:|
|
||||
| SD CS | P1.00 | 32 |
|
||||
| SD MOSI | P1.01 | 33 |
|
||||
| SD SCK | P0.06 | 6 |
|
||||
| SD MISO | P0.26 | 26 |
|
||||
|
||||
The SD socket uses its own SPI peripheral, so card traffic does not change the
|
||||
LoRa radio pinout.
|
||||
|
||||
## Card requirements
|
||||
|
||||
Use a FAT16, FAT32, or exFAT card with an MBR partition table whose first
|
||||
partition starts after sector 1. This is the normal layout produced by most SD
|
||||
formatters. GPT and unpartitioned "super-floppy" layouts are rejected.
|
||||
|
||||
MeshCore creates `/meshcore-ota.mota` as a contiguous file. Sector 1, which is
|
||||
outside the partition, holds a checksummed bootloader handoff record. The
|
||||
firmware validates this gap before writing it; an incompatible card layout
|
||||
fails safely without modifying sector 1.
|
||||
|
||||
## Capacity and update types
|
||||
|
||||
The card removes the internal-flash staging limit. The `.mota` container may be
|
||||
much larger than the old internal staging gap, and either a full image or an
|
||||
in-place delta may be downloaded. The installed firmware itself must still fit
|
||||
the nRF52840 application region below InternalFS (ending at `0xED000`); SD
|
||||
storage does not increase the MCU's executable flash.
|
||||
|
||||
For this S140 v6 target, the maximum application image including its `EndF`
|
||||
trailer is `0xC7000` bytes (815,104 bytes). To package a full self-update:
|
||||
|
||||
```bash
|
||||
motatool build --fw ./Heltec_tower_v2_sdcard-new.hex --out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
```
|
||||
|
||||
A delta uses the exact installed image as its base. The normal `0x98000`
|
||||
workspace remains compatible. If either image is larger than that legacy
|
||||
limit, build the patch with the SD target's larger workspace:
|
||||
|
||||
```bash
|
||||
motatool build \
|
||||
--base ./Heltec_tower_v2_sdcard-running.hex \
|
||||
--fw ./Heltec_tower_v2_sdcard-new.hex \
|
||||
--patch-type in-place \
|
||||
--inplace-memory 0xC7000 \
|
||||
--out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
```
|
||||
|
||||
The download is resumable because the partial `.mota` stays on the card.
|
||||
Once it reaches `ready`, `ota install` performs the final verification,
|
||||
publishes the bootloader handoff, and reboots. Keep the card inserted through
|
||||
the reboot and installation.
|
||||
|
||||
The SD-aware bootloader is mandatory. `ota install` refuses to reboot if the
|
||||
bootloader capability marker does not advertise SD staging and the selected
|
||||
codec. Existing `Heltec_tower_v2_repeater` firmware continues to use the
|
||||
internal-flash delta path and is unchanged.
|
||||
+10
-5
@@ -254,7 +254,7 @@ cover `approval` or `leaves[]`:
|
||||
|
||||
| `codec_id` | Meaning | Used by |
|
||||
|---|---|---|
|
||||
| 0 | full / raw | PAYLOAD = reconstructed image (`BODY||EndF`). ESP32 A/B only. |
|
||||
| 0 | full / raw | PAYLOAD = reconstructed image (`BODY||EndF`). ESP32 A/B or the SD-backed MeshTower V2 target. |
|
||||
| 1 | detools **sequential** | random read of base + sequential write of result -> ESP32 A->B inactive slot. |
|
||||
| 2 | detools **in-place** | bounded scratch; rewrites the app region in place -> nRF52 single-slot. |
|
||||
|
||||
@@ -263,10 +263,11 @@ delta only if `base_hash` matches its own `EndF.body_hash`. After applying, the
|
||||
(sha2-256:32) to `image_hash` before it is booted - the hard security gate.
|
||||
|
||||
**A fetcher only requests firmware it can apply.** Each node declares the codec(s) it can apply
|
||||
(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`), while nRF52
|
||||
accepts only `in-place` because its single slot cannot stage a full application image. A `.mota` with an
|
||||
unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to an
|
||||
external folder may accept other codecs because that path captures bytes and never installs them.
|
||||
(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`). Normal nRF52
|
||||
targets accept only `in-place` because internal flash cannot stage a full application image. The
|
||||
MeshTower V2 SD target accepts `full` + `in-place` because the card holds the container. A `.mota` with
|
||||
an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to
|
||||
an external folder may accept other codecs because that path captures bytes and never installs them.
|
||||
|
||||
Compression is internal to the detools patch and must be supported by the applier. Patches are produced by
|
||||
**detools 0.53.0** (`tools/mota` -> `detools.create_patch`) and decoded on-device by detools' embeddable C
|
||||
@@ -670,6 +671,10 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the
|
||||
running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` - never trust `bank_0_size`),
|
||||
3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`.
|
||||
- **MeshTower V2 SD nRF52:** the application stores a contiguous `/meshcore-ota.mota` on microSD and
|
||||
publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching
|
||||
bootloader reads the card without mounting FAT, supports either a full image or an in-place delta,
|
||||
verifies the staged/full result hash, and never writes through `0xED000` where InternalFS begins.
|
||||
|
||||
The signature proves author authenticity; `approval` proves local owner consent - both required to apply.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "OtaFormat.h"
|
||||
#include "MotaContainer.h"
|
||||
#include "Identity.h"
|
||||
#include "OtaByteIO.h"
|
||||
#include <string.h>
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -25,6 +26,9 @@
|
||||
#include "nrf.h"
|
||||
#include "nrf_soc.h"
|
||||
#include "nrf_sdm.h"
|
||||
#if defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
@@ -494,6 +498,86 @@ bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlis
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
st = ApplyState();
|
||||
uint8_t hdr[8], manifest[MOTA_MFL];
|
||||
uint32_t total = store.staged_size();
|
||||
if (total < 8 + MOTA_MFL + 5 || !store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest))) {
|
||||
strcpy(msg, "SD container parse failed");
|
||||
return false;
|
||||
}
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
|
||||
strcpy(msg, "SD manifest parse failed");
|
||||
return false;
|
||||
}
|
||||
const bool full = m.is_full() && m.codec_id == CODEC_FULL &&
|
||||
m.payload_size == m.image_size;
|
||||
const bool delta = !m.is_full() && m.codec_id == CODEC_DETOOLS_INPLACE;
|
||||
if (!full && !delta) {
|
||||
strcpy(msg, "nRF52 SD bootloader accepts full or in-place delta only");
|
||||
return false;
|
||||
}
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (m.image_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
||||
m.image_size > MOTA_NRF52_APP_END - app_base) {
|
||||
strcpy(msg, "image exceeds nRF52 application region");
|
||||
return false;
|
||||
}
|
||||
st.image_size = m.image_size;
|
||||
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
|
||||
st.manifest_ok = true;
|
||||
|
||||
OtaBlCaps bl = ota_bootloader_caps();
|
||||
if (!bl.present || !(bl.storage_flags & OTA_BL_STORAGE_SD)) {
|
||||
strcpy(msg, "this bootloader has no SD OTA support - update the bootloader first");
|
||||
return false;
|
||||
}
|
||||
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
|
||||
snprintf(msg, 159,
|
||||
"bootloader cannot apply this SD update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
|
||||
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
VerifyResult vr = ota_verify(static_cast<const OtaStore&>(store), allow);
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
|
||||
strcpy(msg, "payload hash mismatch (incomplete or corrupt SD .mota)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta) {
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) {
|
||||
strcpy(msg, "cannot read running firmware (no EndF)");
|
||||
return false;
|
||||
}
|
||||
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) {
|
||||
strcpy(msg, "not built for the running firmware (base mismatch)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
st.slot_ok = true;
|
||||
if (vr.is_signed) {
|
||||
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
|
||||
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
|
||||
}
|
||||
if (!store.approve_for_bootloader()) {
|
||||
snprintf(msg, 159, "SD handoff failed: %s", store.last_error());
|
||||
return false;
|
||||
}
|
||||
sprintf(msg, "verified%s %s image on SD; rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#else // native / other platforms
|
||||
|
||||
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
|
||||
|
||||
@@ -50,6 +50,11 @@ bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len,
|
||||
// Returns true (msg = "verified...") when approved, false (msg = the first failing gate) otherwise.
|
||||
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
class OtaStoreSdNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#endif
|
||||
|
||||
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
|
||||
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
|
||||
|
||||
@@ -18,15 +18,18 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// 16-byte marker: magic[8] "MOTABLDR" + apply_abi(2) + codec_mask(2) + reserved(4).
|
||||
// 16-byte marker: magic[8] "MOTABLDR" + apply_abi(2) + codec_mask(2) + storage flags/reserved(4).
|
||||
static const uint8_t OTA_BL_MAGIC[8] = { 'M','O','T','A','B','L','D','R' };
|
||||
|
||||
struct OtaBlCaps {
|
||||
bool present = false;
|
||||
uint16_t apply_abi = 0; // max .mota format_ver the bootloader can apply
|
||||
uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2)
|
||||
uint8_t storage_flags = 0; // bit 0 => raw-SD handoff/apply is supported
|
||||
};
|
||||
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
|
||||
// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52.
|
||||
inline OtaBlCaps ota_bootloader_caps() {
|
||||
OtaBlCaps c;
|
||||
@@ -38,6 +41,7 @@ inline OtaBlCaps ota_bootloader_caps() {
|
||||
c.present = true;
|
||||
c.apply_abi = (uint16_t)(p[8] | ((uint16_t)p[9] << 8));
|
||||
c.codec_mask = (uint16_t)(p[10] | ((uint16_t)p[11] << 8));
|
||||
c.storage_flags = p[12];
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -136,9 +136,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
(unsigned)c.allow.count(), (unsigned)c.manager.target(), tenv ? tenv : "?");
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise.
|
||||
// blrc = the bootloader's last in-place-apply code (diagnostic; 0xB8=success, see ota_delta.c).
|
||||
// blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c).
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
#if defined(OTA_SD_STORE)
|
||||
const char* bl_state = !bl.present ? "NONE" :
|
||||
(bl.storage_flags & OTA_BL_STORAGE_SD) ? "SD" : "NO-SD";
|
||||
#else
|
||||
const char* bl_state = bl.present ? "apply" : "NONE";
|
||||
#endif
|
||||
if (n < 146) n += snprintf(reply + n, 160 - n, " | bl:%s blrc:%02X",
|
||||
c.bootloaderCaps().present ? "apply" : "NONE", ota_bootloader_last_rc());
|
||||
bl_state, ota_bootloader_last_rc());
|
||||
#endif
|
||||
|
||||
// ---- admin OTA stats: crypto identities (our fw's content-id + body_hash), serving set, live fetch,
|
||||
@@ -261,7 +268,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
OtaStore* store; const char* dname;
|
||||
if (strncmp(dst, "flash", 5) == 0) {
|
||||
store = &c.fetch_store; c.fetch_store.clear(); dname = "flash"; validate = false; // seed lives in the folder
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
c.manager.set_accept_full(false); // nRF52 flash can install only in-place deltas
|
||||
#endif
|
||||
} else if (strncmp(dst, "folder", 6) == 0) {
|
||||
@@ -284,7 +291,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
||||
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
c.manager.set_accept_full(false);
|
||||
#endif
|
||||
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
|
||||
@@ -304,10 +311,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
char hx[17]; mesh::Utils::toHex(hx, fi.body_hash, 8);
|
||||
int n = snprintf(reply, 160, "self body=%u image=%u base_hash=%s", (unsigned)fi.body_len, (unsigned)fi.image_len, hx);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can (delta install gate)
|
||||
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
|
||||
const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once)
|
||||
#if defined(OTA_SD_STORE)
|
||||
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_SD))
|
||||
snprintf(reply + n, 160 - n, " | bootloader: SD apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
|
||||
else
|
||||
snprintf(reply + n, 160 - n, " | bootloader: NO SD mota-apply support (install will refuse)");
|
||||
#else
|
||||
if (bl.present) snprintf(reply + n, 160 - n, " | bootloader: apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
|
||||
else snprintf(reply + n, 160 - n, " | bootloader: NO mota-apply support (delta install will refuse)");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} else if (is_cmd(a, "install|apply|applydelta", &rest)) {
|
||||
@@ -470,9 +484,22 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "OK announced");
|
||||
|
||||
} else if (strncmp(d, "verify", 6) == 0) {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
VerifyResult r = ota_verify(static_cast<const OtaStore&>(c.fetch_store), c.allow);
|
||||
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
||||
r.parsed, r.root_ok, r.payload_ok, r.image_ok, r.is_signed, r.sig_ok, r.trusted,
|
||||
r.integrity_ok(), r.auto_appliable());
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
const uint8_t* buf; uint32_t len;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0;
|
||||
#else
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); }
|
||||
else { buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0; }
|
||||
#endif
|
||||
if (len == 0 || !buf) { strcpy(reply, "ERR nothing to verify (flash-staged: applydelta verifies)"); return true; }
|
||||
VerifyResult r = ota_verify(buf, len, c.allow);
|
||||
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include "OtaFormat.h"
|
||||
#include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin()
|
||||
#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#include "OtaStoreFlashNrf52.h"
|
||||
#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#include "OtaStoreFlashEsp32.h"
|
||||
@@ -43,7 +45,7 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
|
||||
#ifndef OTA_SERVE_BUF_SIZE
|
||||
// nRF52 self-serving streams from flash; this buffer is only for the manual `ota dev stage` helper.
|
||||
// Keep it to one flash page so the OTA singleton does not consume another 16 KB of scarce SRAM.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE))
|
||||
#define OTA_SERVE_BUF_SIZE 4096
|
||||
#else
|
||||
#define OTA_SERVE_BUF_SIZE 16384
|
||||
@@ -55,7 +57,9 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
|
||||
|
||||
struct OtaContext {
|
||||
OtaManager manager;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
OtaStoreSdNrf52 fetch_store; // MeshTower V2: persistent SD staging, full + delta
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
OtaStoreFlashNrf52 fetch_store; // persistent flash staging (survives reboot; large deltas)
|
||||
#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
OtaStoreFlashEsp32 fetch_store; // stages in the inactive A/B slot (delta + full, RX-safe)
|
||||
@@ -141,7 +145,9 @@ struct OtaContext {
|
||||
}
|
||||
}
|
||||
bool ok;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg);
|
||||
#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
ok = ota_apply_detools_mota(fetch_store, allow, apply_st, msg);
|
||||
@@ -245,7 +251,10 @@ struct OtaContext {
|
||||
manager.begin(target_id, send, ctx);
|
||||
if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; }
|
||||
// a node only fetches firmware it can apply: ESP32 A/B -> sequential, nRF52 single-slot -> in-place
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
manager.set_accept_full(true);
|
||||
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
manager.set_accept_full(false); // single-slot bootloader applies deltas only
|
||||
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
#elif defined(ESP32_PLATFORM)
|
||||
|
||||
@@ -28,6 +28,10 @@ inline uint32_t mota_nrf52_app_base() { return MOTA_NRF52_APP_BASE_S140_V6; }
|
||||
// ExtraFS at 0xD4000..0xED000 (and InternalFS at 0xED000), while default scripts leave that range free.
|
||||
// Staging below 0xD4000 therefore never touches either filesystem.
|
||||
static const uint32_t MOTA_NRF52_FS_START = 0x000D4000u; // ExtraFS start (universal staging ceiling)
|
||||
// End of the normal nRF52840 application region. The SD-backed MeshTower V2
|
||||
// target can use this whole range because its staged container is off-chip.
|
||||
// InternalFS begins here and must never be erased by the bootloader.
|
||||
static const uint32_t MOTA_NRF52_APP_END = 0x000ED000u;
|
||||
static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// On-card handoff shared with Adafruit_nRF52_Bootloader_OTAFIX. The staged
|
||||
// .mota is a normal, contiguous filesystem file. Sector 1 is in the unused
|
||||
// gap between the MBR and the first partition and tells the bootloader where
|
||||
// that file's sectors live. Cards without such a gap are rejected.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static const uint32_t MOTA_SD_SECTOR_SIZE = 512u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_SECTOR = 1u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_VERSION = 1u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_LEN = 36u;
|
||||
static const uint8_t MOTA_SD_HANDOFF_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'S', 'D', '0', '1'
|
||||
};
|
||||
|
||||
inline uint32_t mota_sd_rd32(const uint8_t* p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
inline void mota_sd_wr32(uint8_t* p, uint32_t v) {
|
||||
p[0] = (uint8_t)v;
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
p[2] = (uint8_t)(v >> 16);
|
||||
p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
inline uint32_t mota_sd_crc32(const uint8_t* data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & (uint32_t)-(int32_t)(crc & 1u));
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
inline void mota_sd_encode_handoff(uint8_t sector[MOTA_SD_SECTOR_SIZE],
|
||||
uint32_t first_sector,
|
||||
uint32_t sector_count,
|
||||
uint32_t total_size,
|
||||
uint32_t card_sectors) {
|
||||
// Only own the record bytes. The caller preserves the rest of sector 1 in
|
||||
// case a card formatter placed non-partition metadata there.
|
||||
for (uint32_t i = 0; i < MOTA_SD_HANDOFF_LEN; i++) sector[i] = 0xFF;
|
||||
for (uint8_t i = 0; i < 8; i++) sector[i] = MOTA_SD_HANDOFF_MAGIC[i];
|
||||
mota_sd_wr32(sector + 8, MOTA_SD_HANDOFF_VERSION);
|
||||
mota_sd_wr32(sector + 12, first_sector);
|
||||
mota_sd_wr32(sector + 16, sector_count);
|
||||
mota_sd_wr32(sector + 20, total_size);
|
||||
mota_sd_wr32(sector + 24, ~total_size);
|
||||
mota_sd_wr32(sector + 28, card_sectors);
|
||||
mota_sd_wr32(sector + 32, mota_sd_crc32(sector, 32));
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -70,7 +70,12 @@ bool ota_self_firmware(SelfFwInfo& out) {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) { out = SelfFwInfo(); return false; }
|
||||
const uint8_t* region = (const uint8_t*)(uintptr_t)app_base;
|
||||
uint32_t region_len = MOTA_NRF52_FS_START - app_base;
|
||||
uint32_t region_len =
|
||||
#if defined(OTA_SD_STORE)
|
||||
MOTA_NRF52_APP_END - app_base;
|
||||
#else
|
||||
MOTA_NRF52_FS_START - app_base;
|
||||
#endif
|
||||
return find_self_firmware(region, region_len, out, /*verify_body=*/true);
|
||||
}
|
||||
#else
|
||||
@@ -89,7 +94,11 @@ bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
#if defined(OTA_SD_STORE)
|
||||
if (app_base >= MOTA_NRF52_APP_END || (uint64_t)app_base + off + len > MOTA_NRF52_APP_END) return false;
|
||||
#else
|
||||
if (!mota_nrf52_layout_valid(app_base) || (uint64_t)app_base + off + len > MOTA_NRF52_FS_START) return false;
|
||||
#endif
|
||||
memcpy(buf, (const uint8_t*)(uintptr_t)(app_base + off), len);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SdFat.h>
|
||||
#include <SPI.h>
|
||||
#include <new>
|
||||
#include <string.h>
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "OtaSdHandoff.h"
|
||||
|
||||
#ifndef OTA_SD_CS_PIN
|
||||
#define OTA_SD_CS_PIN PIN_SPI1_NSS
|
||||
#endif
|
||||
#ifndef OTA_SD_SCK_MHZ
|
||||
#define OTA_SD_SCK_MHZ 8
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
const char* const OtaStoreSdNrf52::PATH = "/meshcore-ota.mota";
|
||||
|
||||
OtaStoreSdNrf52::OtaStoreSdNrf52()
|
||||
: _sd(new (std::nothrow) SdFs()), _file(new (std::nothrow) FsFile()) {}
|
||||
|
||||
OtaStoreSdNrf52::~OtaStoreSdNrf52() {
|
||||
if (_file) { _file->close(); delete _file; }
|
||||
if (_sd) { _sd->end(); delete _sd; }
|
||||
}
|
||||
|
||||
void OtaStoreSdNrf52::fail(const char* message) {
|
||||
strncpy(_error, message ? message : "SD error", sizeof(_error) - 1);
|
||||
_error[sizeof(_error) - 1] = 0;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::mount() {
|
||||
if (_mounted) return true;
|
||||
_error[0] = 0;
|
||||
if (!_sd || !_file || !_sd->begin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI,
|
||||
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &SPI1))) {
|
||||
fail("SD mount failed");
|
||||
return false;
|
||||
}
|
||||
_mounted = true;
|
||||
if (!inspect_mbr()) {
|
||||
_sd->end();
|
||||
_mounted = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::inspect_mbr() {
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card() || !_sd->card()->readSector(0, sector)) {
|
||||
fail("SD MBR read failed");
|
||||
return false;
|
||||
}
|
||||
if (sector[510] != 0x55 || sector[511] != 0xAA) {
|
||||
fail("SD must use an MBR partition table");
|
||||
return false;
|
||||
}
|
||||
|
||||
// SdFs mounts the first usable MBR partition. Require its start to leave
|
||||
// sector 1 unused; a protective GPT entry (type 0xEE) is not safe here.
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
const uint8_t* p = sector + 446 + (uint32_t)i * 16;
|
||||
uint8_t type = p[4];
|
||||
uint32_t start = mota_sd_rd32(p + 8);
|
||||
uint32_t count = mota_sd_rd32(p + 12);
|
||||
if (type == 0 || count == 0) continue;
|
||||
if (type == 0xEE || start <= MOTA_SD_HANDOFF_SECTOR ||
|
||||
start >= _sd->card()->sectorCount() ||
|
||||
count > _sd->card()->sectorCount() - start) {
|
||||
fail("SD needs an MBR partition starting after sector 1");
|
||||
return false;
|
||||
}
|
||||
_partition_start = start;
|
||||
_partition_end = start + count;
|
||||
return true;
|
||||
}
|
||||
fail("SD has no usable MBR partition");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::invalidate_handoff() {
|
||||
if (!_mounted || !_sd->card()) return false;
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) return false;
|
||||
memset(sector, 0xFF, MOTA_SD_HANDOFF_LEN);
|
||||
return _sd->card()->writeSector(MOTA_SD_HANDOFF_SECTOR, sector) &&
|
||||
_sd->card()->syncDevice();
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::locate_file() {
|
||||
Sector_t first = 0, last = 0;
|
||||
if (!_file || !_file->contiguousRange(&first, &last) || last < first) {
|
||||
fail("OTA file is not contiguous");
|
||||
return false;
|
||||
}
|
||||
uint64_t need = ((uint64_t)_total + MOTA_SD_SECTOR_SIZE - 1) / MOTA_SD_SECTOR_SIZE;
|
||||
uint64_t available = (uint64_t)last - first + 1;
|
||||
if (need == 0 || need > available || first < _partition_start ||
|
||||
(uint64_t)first + need > _partition_end) {
|
||||
fail("OTA file sector range is invalid");
|
||||
return false;
|
||||
}
|
||||
_first_sector = (uint32_t)first;
|
||||
_allocated_sectors = (uint32_t)need;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t image_size,
|
||||
uint32_t, uint32_t payload_size) {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
||||
image_size > MOTA_NRF52_APP_END - app_base) {
|
||||
fail("image exceeds nRF52 application region");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::begin(uint32_t total_size) {
|
||||
_total = 0;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
if (total_size < 13 || !mount()) return false;
|
||||
if (!invalidate_handoff()) {
|
||||
fail("SD handoff clear failed");
|
||||
return false;
|
||||
}
|
||||
if (*_file) _file->close();
|
||||
_sd->remove(PATH);
|
||||
if (!_file->open(PATH, O_RDWR | O_CREAT | O_EXCL)) {
|
||||
fail("SD OTA file create failed");
|
||||
return false;
|
||||
}
|
||||
if (!_file->preAllocate(total_size)) {
|
||||
fail("SD lacks contiguous space for update");
|
||||
_file->close();
|
||||
_sd->remove(PATH);
|
||||
return false;
|
||||
}
|
||||
_total = total_size;
|
||||
if (!locate_file()) {
|
||||
_total = 0;
|
||||
_file->close();
|
||||
_sd->remove(PATH);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::set_meta_size(uint32_t meta_bytes) {
|
||||
if (!_total || meta_bytes > _total - 5) return false;
|
||||
uint8_t erased[512];
|
||||
memset(erased, 0xFF, sizeof(erased));
|
||||
for (uint32_t off = 0; off < meta_bytes; ) {
|
||||
uint32_t n = meta_bytes - off;
|
||||
if (n > sizeof(erased)) n = sizeof(erased);
|
||||
if (!write(off, erased, n)) return false;
|
||||
off += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::write(uint32_t offset, const uint8_t* data, uint32_t len) {
|
||||
if (!_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) ||
|
||||
_file->write(data, len) != len) {
|
||||
fail("SD OTA write failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const {
|
||||
if (!_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) ||
|
||||
_file->read(buf, len) != (int)len) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t OtaStoreSdNrf52::capacity() const {
|
||||
if (!_mounted || !_sd || !_sd->card()) return 0;
|
||||
uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_SECTOR_SIZE;
|
||||
return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::finalize() {
|
||||
if (!_file || !*_file || !_file->sync() || !_sd->card()->syncDevice()) {
|
||||
fail("SD OTA sync failed");
|
||||
return false;
|
||||
}
|
||||
return locate_file();
|
||||
}
|
||||
|
||||
void OtaStoreSdNrf52::checkpoint() {
|
||||
if (_file && *_file) {
|
||||
_file->sync();
|
||||
if (_sd->card()) _sd->card()->syncDevice();
|
||||
}
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::reopen() {
|
||||
_total = 0;
|
||||
if (!mount()) return false;
|
||||
if (*_file) _file->close();
|
||||
if (!_file->open(PATH, O_RDWR)) return false;
|
||||
uint8_t hdr[8];
|
||||
if (_file->fileSize() < 13 || !_file->seekSet(0) ||
|
||||
_file->read(hdr, sizeof(hdr)) != (int)sizeof(hdr) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0) {
|
||||
_file->close();
|
||||
return false;
|
||||
}
|
||||
uint32_t total = rd_u32le(hdr + 4);
|
||||
if (total < 13 || total != _file->fileSize()) {
|
||||
_file->close();
|
||||
return false;
|
||||
}
|
||||
_total = total;
|
||||
if (!locate_file()) {
|
||||
_total = 0;
|
||||
_file->close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreSdNrf52::clear() {
|
||||
_total = 0;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
if (!mount()) return;
|
||||
invalidate_handoff();
|
||||
if (_file && *_file) _file->close();
|
||||
_sd->remove(PATH);
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::approve_for_bootloader() {
|
||||
if (!_total || !finalize()) return false;
|
||||
if (!write(8 + MOTA_OFF_APPROVAL, APPROVAL_YES, sizeof(APPROVAL_YES)) ||
|
||||
!_file->sync() || !_sd->card()->syncDevice()) {
|
||||
fail("SD approval write failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) {
|
||||
fail("SD handoff sector read failed");
|
||||
return false;
|
||||
}
|
||||
mota_sd_encode_handoff(sector, _first_sector, _allocated_sectors,
|
||||
_total, _sd->card()->sectorCount());
|
||||
if (!_sd->card()->writeSector(MOTA_SD_HANDOFF_SECTOR, sector) ||
|
||||
!_sd->card()->syncDevice()) {
|
||||
fail("SD bootloader handoff write failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
|
||||
#include "OtaStore.h"
|
||||
|
||||
class SdFs;
|
||||
class FsFile;
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Persistent nRF52840 OTA store backed by the MeshTower V2 microSD socket.
|
||||
// The .mota remains a normal file, but is preallocated contiguously so the
|
||||
// bootloader can read it by raw sectors without embedding a FAT implementation.
|
||||
class OtaStoreSdNrf52 : public OtaStore {
|
||||
public:
|
||||
OtaStoreSdNrf52();
|
||||
~OtaStoreSdNrf52() override;
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t* data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override;
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override;
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
bool reopen() override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size,
|
||||
uint32_t payload_off, uint32_t payload_size) override;
|
||||
|
||||
// Called only after the app has verified payload, base, signature and trust.
|
||||
// Writes APRV into the file, then publishes the raw-sector handoff record.
|
||||
bool approve_for_bootloader();
|
||||
const char* last_error() const { return _error; }
|
||||
|
||||
private:
|
||||
static const char* const PATH;
|
||||
|
||||
bool mount();
|
||||
bool inspect_mbr();
|
||||
bool locate_file();
|
||||
bool invalidate_handoff();
|
||||
void fail(const char* message);
|
||||
|
||||
SdFs* _sd = nullptr;
|
||||
mutable FsFile* _file = nullptr;
|
||||
bool _mounted = false;
|
||||
uint32_t _total = 0;
|
||||
uint32_t _first_sector = 0;
|
||||
uint32_t _allocated_sectors = 0;
|
||||
uint32_t _partition_start = 0;
|
||||
uint32_t _partition_end = 0;
|
||||
char _error[80] = {0};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
|
||||
// 542 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// 540 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT
|
||||
// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.
|
||||
|
||||
@@ -105,6 +105,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x57172fcc, "Heltec_T190_room_server_observer_mqtt" },
|
||||
{ 0xa529b4f5, "Heltec_t1_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x85a8c944, "Heltec_tower_v2_repeater" },
|
||||
{ 0x0a9dbbf0, "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xb519a89f, "heltec_tracker_v1_1_repeater_observer_mqtt" },
|
||||
{ 0xa3e8d8ce, "heltec_tracker_v1_1_room_server_observer_mqtt" },
|
||||
{ 0xb16009a0, "heltec_tracker_v2_companion_radio_ble_femoff" },
|
||||
{ 0xdc780e80, "heltec_tracker_v2_companion_radio_ble_femon" },
|
||||
{ 0x4aa63180, "heltec_tracker_v2_companion_radio_usb_femoff" },
|
||||
@@ -114,7 +117,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xb62a1e75, "heltec_tracker_v2_kiss_modem" },
|
||||
{ 0x9e64845d, "heltec_tracker_v2_repeater" },
|
||||
{ 0x19232e8b, "heltec_tracker_v2_repeater_bridge_espnow" },
|
||||
{ 0xfaf164f4, "heltec_tracker_v2_repeater_observer_mqtt" },
|
||||
{ 0x38b948b4, "heltec_tracker_v2_room_server" },
|
||||
{ 0x462ddb9b, "heltec_tracker_v2_room_server_observer_mqtt" },
|
||||
{ 0xed21ca4f, "heltec_tracker_v2_sensor" },
|
||||
{ 0x9f5f39a7, "heltec_tracker_v2_terminal_chat" },
|
||||
{ 0x081d2219, "Heltec_v2_companion_radio_ble" },
|
||||
@@ -135,6 +140,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x644bb68d, "Heltec_v3_repeater_bridge_espnow" },
|
||||
{ 0xd19a9759, "Heltec_v3_repeater_bridge_rs232" },
|
||||
{ 0xde40f56f, "Heltec_v3_repeater_observer_mqtt" },
|
||||
{ 0x6dc15ab3, "Heltec_v3_repeater_observer_mqtt_sim" },
|
||||
{ 0xc59d294e, "Heltec_v3_room_server" },
|
||||
{ 0xceeddfaf, "Heltec_v3_room_server_observer_mqtt" },
|
||||
{ 0x21519537, "Heltec_v3_sensor" },
|
||||
@@ -311,9 +317,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xf7c5d584, "LilyGo_TLora_V2_1_1_6_companion_radio_wifi" },
|
||||
{ 0x81960cc0, "LilyGo_TLora_V2_1_1_6_kiss_modem" },
|
||||
{ 0x504020ea, "LilyGo_TLora_V2_1_1_6_repeater" },
|
||||
{ 0x9c652703, "LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt" },
|
||||
{ 0x9d8b60ae, "LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_" },
|
||||
{ 0xa2ce002f, "LilyGo_TLora_V2_1_1_6_room_server" },
|
||||
{ 0xdb780283, "LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt" },
|
||||
{ 0xba4f47f3, "LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt_" },
|
||||
{ 0x6f16479e, "LilyGo_TLora_V2_1_1_6_terminal_chat" },
|
||||
{ 0x1018e5d1, "M5Stack_Unit_C6L_companion_radio_ble" },
|
||||
{ 0xc0ab5040, "M5Stack_Unit_C6L_companion_radio_usb" },
|
||||
@@ -387,7 +393,6 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x76f3c984, "RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors" },
|
||||
{ 0x41e33a97, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors" },
|
||||
{ 0x29a0da19, "RAK_4631_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x8a6dc63b, "RAK_4631_room_server_lora_ota_no_external_sensors" },
|
||||
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
|
||||
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
|
||||
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
|
||||
@@ -409,17 +414,10 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x3caf966e, "solarxiao_33S_repeater" },
|
||||
{ 0xcb59ea4e, "solarxiao_33S_repeater_bridge_rs232" },
|
||||
{ 0xe3a28284, "solarxiao_33S_room_server" },
|
||||
{ 0xd0df8303, "Station_G2_companion_radio_ble" },
|
||||
{ 0x5c1a54f8, "Station_G2_companion_radio_usb" },
|
||||
{ 0x79cc029e, "Station_G2_companion_radio_wifi" },
|
||||
{ 0x41197b92, "Station_G2_kiss_modem" },
|
||||
{ 0x05747eb6, "Station_G2_logging_repeater" },
|
||||
{ 0x75587f24, "Station_G2_logging_repeater_bridge_espnow" },
|
||||
{ 0xf2128c81, "Station_G2_repeater" },
|
||||
{ 0x1b34e7b4, "Station_G2_repeater_bridge_espnow" },
|
||||
{ 0xdbb6a855, "Station_G2_repeater_observer_mqtt" },
|
||||
{ 0x73c4a019, "Station_G2_room_server" },
|
||||
{ 0xe4042b21, "Station_G2_room_server_observer_mqtt" },
|
||||
{ 0x8e549407, "Station_G3_ESP32_companion_radio_ble" },
|
||||
{ 0x1deac038, "Station_G3_ESP32_companion_radio_usb" },
|
||||
{ 0x2a2f303b, "Station_G3_ESP32_companion_radio_wifi" },
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include "MerkleTree.h"
|
||||
#include "Multihash.h"
|
||||
#include "Identity.h"
|
||||
#include "OtaByteIO.h"
|
||||
#include <SHA256.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
@@ -24,5 +28,76 @@ VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist&
|
||||
return r;
|
||||
}
|
||||
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
|
||||
VerifyResult r;
|
||||
const uint32_t total = store.staged_size();
|
||||
uint8_t hdr[8], manifest[MOTA_MFL], trailer[5];
|
||||
if (total < 8 + MOTA_MFL + 5 ||
|
||||
!store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest)) ||
|
||||
!store.read(total - 5, trailer, sizeof(trailer)) ||
|
||||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) return r;
|
||||
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) return r;
|
||||
const uint32_t leaves_off = 8 + MOTA_MFL;
|
||||
const uint64_t payload_off64 = (uint64_t)leaves_off + (uint64_t)m.block_count * 4;
|
||||
if (m.block_size() > OTA_DEFAULT_BLOCK_SIZE ||
|
||||
payload_off64 > UINT32_MAX ||
|
||||
payload_off64 + m.payload_size + 5 != total) return r;
|
||||
const uint32_t payload_off = (uint32_t)payload_off64;
|
||||
|
||||
const size_t leaves_len = (size_t)m.block_count * 4;
|
||||
uint8_t* leaves = static_cast<uint8_t*>(malloc(leaves_len));
|
||||
uint8_t* block = static_cast<uint8_t*>(malloc(m.block_size()));
|
||||
if (!leaves || !block || !store.read(leaves_off, leaves, leaves_len)) {
|
||||
free(leaves); free(block); return r;
|
||||
}
|
||||
|
||||
r.parsed = true;
|
||||
uint8_t root[4];
|
||||
merkle_root(root, leaves, m.block_count);
|
||||
r.root_ok = memcmp(root, m.merkle_root, sizeof(root)) == 0;
|
||||
|
||||
SHA256 image_sha;
|
||||
r.payload_ok = true;
|
||||
uint32_t off = 0;
|
||||
for (uint32_t i = 0; i < m.block_count; i++) {
|
||||
uint32_t n = m.payload_size - off;
|
||||
if (n > m.block_size()) n = m.block_size();
|
||||
if (!store.read(payload_off + off, block, n)) {
|
||||
r.payload_ok = false;
|
||||
break;
|
||||
}
|
||||
uint8_t leaf[4];
|
||||
merkle_leaf(leaf, block, n);
|
||||
if (memcmp(leaf, leaves + (size_t)i * 4, 4) != 0) {
|
||||
r.payload_ok = false;
|
||||
break;
|
||||
}
|
||||
if (m.is_full()) image_sha.update(block, n);
|
||||
off += n;
|
||||
}
|
||||
if (off != m.payload_size) r.payload_ok = false;
|
||||
if (m.is_full() && r.payload_ok) {
|
||||
uint8_t image_hash[32];
|
||||
image_sha.finalize(image_hash, sizeof(image_hash));
|
||||
r.image_ok = memcmp(image_hash, m.image_hash, sizeof(image_hash)) == 0;
|
||||
} else {
|
||||
r.image_ok = !m.is_full();
|
||||
}
|
||||
|
||||
r.is_signed = m.is_signed();
|
||||
if (r.is_signed) {
|
||||
mesh::Identity signer(m.signer_pubkey);
|
||||
r.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
|
||||
r.trusted = r.sig_ok && allow.contains(m.signer_pubkey);
|
||||
}
|
||||
free(leaves);
|
||||
free(block);
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "MotaContainer.h"
|
||||
#include "SignerAllowlist.h"
|
||||
#include "OtaStore.h"
|
||||
|
||||
// Full verification of a staged `.mota` (device-side: uses Ed25519 via mesh::Identity, so NOT compiled
|
||||
// on the native host - the portable integrity checks live in MotaContainer and are unit-tested there).
|
||||
@@ -26,5 +27,9 @@ struct VerifyResult {
|
||||
|
||||
VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow);
|
||||
|
||||
// Streaming verification for a persistent store that cannot expose one
|
||||
// contiguous memory view (notably the MeshTower V2 SD-backed nRF52 store).
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow);
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
||||
#include "helpers/ota/OtaSdHandoff.h"
|
||||
|
||||
using namespace mesh::ota;
|
||||
|
||||
@@ -95,3 +97,22 @@ TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
TEST(OtaSdHandoff, EncodesChecksummedRecordAndPreservesSectorTail) {
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
std::memset(sector, 0xA5, sizeof(sector));
|
||||
mota_sd_encode_handoff(sector, 2048, 1234, 630000, 8000000);
|
||||
|
||||
EXPECT_EQ(0, std::memcmp(sector, MOTA_SD_HANDOFF_MAGIC, 8));
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 8), MOTA_SD_HANDOFF_VERSION);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 12), 2048u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 16), 1234u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 20), 630000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 24), ~630000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 28), 8000000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 32), mota_sd_crc32(sector, 32));
|
||||
EXPECT_EQ(sector[MOTA_SD_HANDOFF_LEN], 0xA5); // bytes outside our record are untouched
|
||||
|
||||
sector[20] ^= 1u;
|
||||
EXPECT_NE(mota_sd_rd32(sector + 32), mota_sd_crc32(sector, 32));
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ DEFAULT_BLOCK_SIZE = 1024
|
||||
# the staged .mota must begin above it. Keep in sync with OtaFlashLayout_nrf52.h / the OTAFIX bootloader.
|
||||
NRF52_INPLACE_MEMORY = 0x00098000
|
||||
|
||||
# MeshTower V2's SD-backed OTA target keeps the staged .mota off-chip, so the application may use the
|
||||
# complete S140 v6 application region up to InternalFS instead of leaving room for internal staging.
|
||||
# This is deliberately target-specific: other nRF52 OTA builds still need NRF52_INPLACE_MEMORY above.
|
||||
NRF52_SD_APP_MEMORY = 0x000C7000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multihash helpers (sha2-256 truncations)
|
||||
|
||||
@@ -148,9 +148,12 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
body = bytes(ih.tobinarray(start=app_start, size=app_end - app_start))
|
||||
ident = _firmware_ident()
|
||||
out, h8 = ml.ensure_endf(body, ident)
|
||||
if len(out) > ml.NRF52_INPLACE_MEMORY:
|
||||
raise RuntimeError(f"nRF52 OTA image is {len(out)} bytes; in-place limit is "
|
||||
f"{ml.NRF52_INPLACE_MEMORY} bytes")
|
||||
sd_backed = _cppdef("OTA_SD_STORE") is not None
|
||||
image_limit = ml.NRF52_SD_APP_MEMORY if sd_backed else ml.NRF52_INPLACE_MEMORY
|
||||
limit_name = "SD application" if sd_backed else "in-place"
|
||||
if len(out) > image_limit:
|
||||
raise RuntimeError(f"nRF52 OTA image is {len(out)} bytes; {limit_name} limit is "
|
||||
f"{image_limit} bytes")
|
||||
if len(out) == len(body):
|
||||
print(f"EndF: already present in {os.path.basename(path)} (no change)"); return
|
||||
trailer = out[len(body):] # the EndF trailer (56 bytes with identity)
|
||||
|
||||
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||
-I variants/heltec_tower_v2
|
||||
-D HELTEC_TOWER_V2
|
||||
-D MOTA_HW_ID='"Heltec_tower_v2"'
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
@@ -44,6 +45,38 @@ build_flags =
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
; MeshTower V2 self-update target using the onboard microSD socket as the
|
||||
; persistent LoRa-OTA store. Requires the matching SD-aware OTAFIX bootloader.
|
||||
; Unlike the internal-flash target above, this accepts both complete images
|
||||
; and in-place deltas and does not reserve application flash for the container.
|
||||
[Heltec_tower_v2_sdcard]
|
||||
extends = Heltec_tower_v2
|
||||
build_flags =
|
||||
${Heltec_tower_v2.build_flags}
|
||||
-D HELTEC_TOWER_V2_SDCARD=1
|
||||
lib_deps =
|
||||
${Heltec_tower_v2.lib_deps}
|
||||
; SdFat 2.3.1 plus the tested full-exFAT mount fix from upstream beta.
|
||||
https://github.com/greiman/SdFat-beta.git#a249cfa121e25a96ee14d8813abf01755f1f6a25
|
||||
|
||||
[env:Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors]
|
||||
extends = Heltec_tower_v2_sdcard
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_src_filter = ${Heltec_tower_v2_sdcard.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${Heltec_tower_v2_sdcard.build_flags}
|
||||
-D ADVERT_NAME='"Heltec_Tower_V2 SD Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=254
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_SD_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
|
||||
|
||||
[env:Heltec_tower_v2_room_server]
|
||||
extends = Heltec_tower_v2
|
||||
build_src_filter = ${Heltec_tower_v2.build_src_filter}
|
||||
|
||||
@@ -36,6 +36,12 @@ void variant_shutdown()
|
||||
nrf_gpio_cfg_default(PIN_SPI_MISO);
|
||||
nrf_gpio_cfg_default(PIN_SPI_MOSI);
|
||||
nrf_gpio_cfg_default(PIN_SPI_SCK);
|
||||
#if defined(HELTEC_TOWER_V2_SDCARD)
|
||||
nrf_gpio_cfg_default(PIN_SPI1_NSS);
|
||||
nrf_gpio_cfg_default(PIN_SPI1_MISO);
|
||||
nrf_gpio_cfg_default(PIN_SPI1_MOSI);
|
||||
nrf_gpio_cfg_default(PIN_SPI1_SCK);
|
||||
#endif
|
||||
nrf_gpio_cfg_default(PIN_LED);
|
||||
detachInterrupt(PIN_GPS_PPS);
|
||||
detachInterrupt(PIN_BUTTON1);
|
||||
|
||||
@@ -16,12 +16,26 @@
|
||||
#define PIN_BOARD_SDA PIN_WIRE_SDA
|
||||
#define PIN_BOARD_SCL PIN_WIRE_SCL
|
||||
|
||||
#if defined(HELTEC_TOWER_V2_SDCARD)
|
||||
#define SPI_INTERFACES_COUNT (2)
|
||||
#else
|
||||
#define SPI_INTERFACES_COUNT (1)
|
||||
#endif
|
||||
#define PIN_SPI_MISO (0 + 23)
|
||||
#define PIN_SPI_MOSI (0 + 22)
|
||||
#define PIN_SPI_SCK (0 + 19)
|
||||
#define PIN_SPI_NSS LORA_CS
|
||||
|
||||
// MeshTower V2 microSD socket (separate SPIM2 bus), from the Heltec partial
|
||||
// reference circuit: CS=P1.00, MOSI=P1.01, SCK=P0.06, MISO=P0.26.
|
||||
#if defined(HELTEC_TOWER_V2_SDCARD)
|
||||
#define PIN_SPI1_MISO (0 + 26)
|
||||
#define PIN_SPI1_MOSI (32 + 1)
|
||||
#define PIN_SPI1_SCK (0 + 6)
|
||||
#define PIN_SPI1_NSS (32 + 0)
|
||||
#define OTA_SD_CS_PIN PIN_SPI1_NSS
|
||||
#endif
|
||||
|
||||
#define LED_BUILTIN (32 + 15)
|
||||
#define PIN_LED LED_BUILTIN
|
||||
#define LED_RED (-1)
|
||||
@@ -52,6 +66,12 @@
|
||||
#define P_LORA_MOSI PIN_SPI_MOSI
|
||||
#define P_LORA_SCLK PIN_SPI_SCK
|
||||
|
||||
#if defined(HELTEC_TOWER_V2_SDCARD)
|
||||
static const uint8_t SS = PIN_SPI1_NSS;
|
||||
#else
|
||||
static const uint8_t SS = PIN_SPI_NSS;
|
||||
#endif
|
||||
|
||||
#define USE_KCT8103L_PA_ONLY
|
||||
#define LORA_KCT8103L_EN (0 + 15)
|
||||
#define LORA_KCT8103L_TX_RX (0 + 16)
|
||||
|
||||
Reference in New Issue
Block a user