mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 04:06:56 +00:00
3630 lines
129 KiB
HTML
3630 lines
129 KiB
HTML
|
|
<!doctype html>
|
|
<html lang="en" class="no-js">
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
<meta name="description" content="Documentation for the open source MeshCore firmware">
|
|
|
|
|
|
|
|
<link rel="canonical" href="https://mikecarper.github.io/MeshCore/ota_protocol/">
|
|
|
|
|
|
<link rel="prev" href="../ota_nrf52_qspi/">
|
|
|
|
|
|
<link rel="next" href="../ota_user_guide/">
|
|
|
|
|
|
|
|
|
|
|
|
<link rel="icon" href="../assets/images/favicon.png">
|
|
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
|
|
|
|
|
|
|
|
<title>MeshCore OTA - .mota container & LoRa protocol - MeshCore Docs</title>
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="../assets/stylesheets/main.ec1eaa64.min.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
|
|
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="../_stylesheets/extra.css">
|
|
|
|
<link rel="stylesheet" href="../_stylesheets/firmware_picker.css">
|
|
|
|
<link rel="stylesheet" href="../_stylesheets/telemetry_decoder.css">
|
|
|
|
<link rel="stylesheet" href="../_stylesheets/filter_tool.css">
|
|
|
|
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
|
|
|
|
|
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body dir="ltr">
|
|
|
|
|
|
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
|
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
|
<label class="md-overlay" for="__drawer"></label>
|
|
<div data-md-component="skip">
|
|
|
|
|
|
<a href="#meshcore-ota-mota-container-lora-protocol" class="md-skip">
|
|
Skip to content
|
|
</a>
|
|
|
|
</div>
|
|
<div data-md-component="announce">
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<header class="md-header md-header--shadow" data-md-component="header">
|
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
|
<a href=".." title="MeshCore Docs" class="md-header__button md-logo" aria-label="MeshCore Docs" data-md-component="logo">
|
|
|
|
<img src="../_assets/meshcore.svg" alt="logo">
|
|
|
|
</a>
|
|
<label class="md-header__button md-icon" for="__drawer">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
|
</label>
|
|
<div class="md-header__title" data-md-component="header-title">
|
|
<div class="md-header__ellipsis">
|
|
<div class="md-header__topic">
|
|
<span class="md-ellipsis">
|
|
MeshCore Docs
|
|
</span>
|
|
</div>
|
|
<div class="md-header__topic" data-md-component="header-topic">
|
|
<span class="md-ellipsis">
|
|
|
|
MeshCore OTA - .mota container & LoRa protocol
|
|
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="md-header__button md-icon" for="__search">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
|
</label>
|
|
<div class="md-search" data-md-component="search" role="dialog">
|
|
<label class="md-search__overlay" for="__search"></label>
|
|
<div class="md-search__inner" role="search">
|
|
<form class="md-search__form" name="search">
|
|
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
|
<label class="md-search__icon md-icon" for="__search">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
|
</label>
|
|
<nav class="md-search__options" aria-label="Search">
|
|
|
|
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
</button>
|
|
</nav>
|
|
|
|
<div class="md-search__suggest" data-md-component="search-suggest"></div>
|
|
|
|
</form>
|
|
<div class="md-search__output">
|
|
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
|
<div class="md-search-result" data-md-component="search-result">
|
|
<div class="md-search-result__meta">
|
|
Initializing search
|
|
</div>
|
|
<ol class="md-search-result__list" role="presentation"></ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="md-header__source">
|
|
<a href="https://github.com/mikecarper/MeshCore/" title="Go to repository" class="md-source" data-md-component="source">
|
|
<div class="md-source__icon md-icon">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
|
</div>
|
|
<div class="md-source__repository">
|
|
mikecarper/MeshCore
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
</nav>
|
|
|
|
</header>
|
|
|
|
<div class="md-container" data-md-component="container">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<main class="md-main" data-md-component="main">
|
|
<div class="md-main__inner md-grid">
|
|
|
|
|
|
|
|
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
|
<div class="md-sidebar__scrollwrap">
|
|
<div class="md-sidebar__inner">
|
|
|
|
|
|
|
|
|
|
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
|
<label class="md-nav__title" for="__drawer">
|
|
<a href=".." title="MeshCore Docs" class="md-nav__button md-logo" aria-label="MeshCore Docs" data-md-component="logo">
|
|
|
|
<img src="../_assets/meshcore.svg" alt="logo">
|
|
|
|
</a>
|
|
MeshCore Docs
|
|
</label>
|
|
|
|
<div class="md-nav__source">
|
|
<a href="https://github.com/mikecarper/MeshCore/" title="Go to repository" class="md-source" data-md-component="source">
|
|
<div class="md-source__icon md-icon">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
|
</div>
|
|
<div class="md-source__repository">
|
|
mikecarper/MeshCore
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<ul class="md-nav__list" data-md-scrollfix>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href=".." class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Introduction
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../WiFi/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
WiFi and MQTT by Firmware Type
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../cli_build_matrix/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
CLI Availability by Firmware Build
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../cli_command_availability/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
CLI Command Availability Matrix
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../cli_commands/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
CLI Commands
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../companion_contact_cache/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Full Companion contact caches
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../companion_contact_cache_results/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Contact-cache and NimBLE RAM qualification
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../companion_offline_queue/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Companion Offline Message Queue
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../companion_protocol/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Companion Protocol
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../companion_radio_full/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Full Companion
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../docs/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Local Documentation
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../esp32_memory_budget/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Classic ESP32 image memory budget
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../faq/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Frequently Asked Questions
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../filter_tool/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Filter policy playground
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../firmware_memory_budget/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Firmware memory checks
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../firmware_picker/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Firmware picker
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../flood_filtering/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Flood Filtering and Moderation
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../full_companion_features/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Full Companion: turn features on and off
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../full_companion_usb_switcher/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Full Companion USB CLI and binary switcher
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../gps_tracking/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
GPS Tracking
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../halo_keymind_settings/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Halo and Keymind Branch Settings
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../hardware_validation_bluetooth_stealth_2026-09-07/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
XIAO Bluetooth stealth hardware validation - 2026-09-07
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../hardware_validation_checklist/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Hardware validation checklist
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../host_cli_service/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
LoRa CLI Host Service
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../kiss_modem_protocol/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
MeshCore KISS Modem Protocol
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../lora_ota_automation/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Scripted LoRa OTA from start to finish
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../mbedtls-tls-footprint/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Shrinking the per-connection TLS footprint on non-PSRAM observers
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../nimble_companion_trial/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
ESP32-S3 NimBLE Full Companion trial
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../nimble_companion_trial_results/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
ESP32-S3 NimBLE trial results, 2026-09-08
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../nrf52_companion_storage_recovery/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
nRF52 Companion automatic ExtraFS recovery
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../nrf52_power_management/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
nRF52 Power Management
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../number_allocations/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Number Allocations
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../ota_easy/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Easy firmware updates over LoRa
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../ota_meshtower_v2_sdcard/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
MeshTower V2 microSD LoRa OTA
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../ota_nrf52_bootloader_update/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
nRF52 bootloader updates over LoRa
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../ota_nrf52_qspi/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
nRF52 repeater LoRa OTA with external QSPI
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item md-nav__item--active">
|
|
|
|
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
|
|
|
|
|
|
|
|
|
|
|
|
<label class="md-nav__link md-nav__link--active" for="__toc">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
MeshCore OTA - .mota container & LoRa protocol
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
<span class="md-nav__icon md-icon"></span>
|
|
</label>
|
|
|
|
<a href="./" class="md-nav__link md-nav__link--active">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
MeshCore OTA - .mota container & LoRa protocol
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="md-nav__title" for="__toc">
|
|
<span class="md-nav__icon md-icon"></span>
|
|
Table of contents
|
|
</label>
|
|
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#1-conventions" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
1. Conventions
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#2-firmware-image-the-endf-trailer" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
2. Firmware image & the EndF trailer
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="2. Firmware image & the EndF trailer">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#esp32-application-slot-profiles" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
ESP32 application-slot profiles
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#3-the-mota-container" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
3. The .mota container
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#4-the-manifest" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4. The manifest
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="4. The manifest">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#41-signed-region" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.1 Signed region
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#42-the-approval-field" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.2 The approval field
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#43-privileged-nrf52-bootloader-package-profile" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.3 Privileged nRF52 bootloader package profile
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#5-payload-codecs-delta-base" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
5. Payload, codecs & delta base
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#6-merkle-tree-sha2-2564" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
6. Merkle tree (sha2-256:4)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="6. Merkle tree (sha2-256:4)">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#61-proofs" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
6.1 Proofs
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#7-block-availability-staging-resume" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
7. Block availability, staging & resume
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#8-lora-ota-protocol" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8. LoRa OTA protocol
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="8. LoRa OTA protocol">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#81-two-tier-discovery" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.1 Two-tier discovery
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#82-anti-storm-mandatory-at-mesh-scale" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.2 Anti-storm (mandatory at mesh scale)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#83-fetch-handshake" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.3 Fetch handshake
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#84-message-bodies-transfer" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.4 Message bodies (transfer)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#85-sizing-against-max_packet_payload-184" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.5 Sizing against MAX_PACKET_PAYLOAD = 184
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#86-temporary-radio-and-transfer-boundary" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.6 Temporary-radio and transfer boundary
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#9-identity-trust-versioning" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
9. Identity, trust & versioning
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#10-multi-mota-serve-the-external-folder-relay" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10. Multi-mota serve & the external "folder" relay
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="10. Multi-mota serve & the external "folder" relay">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#101-the-motasource-abstraction-otasourceh" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10.1 The MotaSource abstraction (OtaSource.h)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#102-the-mota-seeder-transport-motaseederprotoh" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10.2 The mota-seeder transport (MotaSeederProto.h)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#11-cli-surface-otaclicpp" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
11. CLI surface (OtaCli.cpp)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#12-apply-bootloader-contract" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
12. Apply & bootloader contract
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#13-versioning-of-this-spec" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
13. Versioning of this spec
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../ota_user_guide/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Updating your node over the air (OTA) - user guide
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../packet_format/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Packet Format
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../payloads/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Payload Format
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../qr_codes/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
QR Codes
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../rak3401_mota_chain/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
RAK3401 1W repeater compact LoRa update chain
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../role_feature_switches/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Feature switches by role — 1.17.1.5 USA Cascade
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../spiffs_regular_file_reads/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
SPIFFS regular-file reads and login replay state
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../stats_binary_frames/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Stats Binary Frame Structures
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../telemetry_decoder/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Telemetry decoder
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../terminal_chat_cli/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Terminal Chat CLI
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../usb_serial_backpressure/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Native USB backpressure and radio liveness
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../v4_pixel5_font_trial/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Small-screen Picopixel message font
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item md-nav__item--nested">
|
|
|
|
|
|
|
|
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_50" >
|
|
|
|
|
|
<label class="md-nav__link" for="__nav_50" id="__nav_50_label" tabindex="0">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Releases
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
<span class="md-nav__icon md-icon"></span>
|
|
</label>
|
|
|
|
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_50_label" aria-expanded="false">
|
|
<label class="md-nav__title" for="__nav_50">
|
|
<span class="md-nav__icon md-icon"></span>
|
|
|
|
|
|
Releases
|
|
|
|
|
|
</label>
|
|
<ul class="md-nav__list" data-md-scrollfix>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../releases/1.17.1.5-doc-audit/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
Markdown audit — 1.17.1.5 USA Cascade
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="md-nav__item">
|
|
<a href="../releases/1.17.1.5/" class="md-nav__link">
|
|
|
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
MeshCore 1.17.1.5 Dev - USA Cascade
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</a>
|
|
</li>
|
|
|
|
|
|
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
|
|
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
|
<div class="md-sidebar__scrollwrap">
|
|
<div class="md-sidebar__inner">
|
|
|
|
|
|
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="md-nav__title" for="__toc">
|
|
<span class="md-nav__icon md-icon"></span>
|
|
Table of contents
|
|
</label>
|
|
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#1-conventions" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
1. Conventions
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#2-firmware-image-the-endf-trailer" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
2. Firmware image & the EndF trailer
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="2. Firmware image & the EndF trailer">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#esp32-application-slot-profiles" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
ESP32 application-slot profiles
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#3-the-mota-container" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
3. The .mota container
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#4-the-manifest" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4. The manifest
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="4. The manifest">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#41-signed-region" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.1 Signed region
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#42-the-approval-field" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.2 The approval field
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#43-privileged-nrf52-bootloader-package-profile" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
4.3 Privileged nRF52 bootloader package profile
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#5-payload-codecs-delta-base" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
5. Payload, codecs & delta base
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#6-merkle-tree-sha2-2564" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
6. Merkle tree (sha2-256:4)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="6. Merkle tree (sha2-256:4)">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#61-proofs" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
6.1 Proofs
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#7-block-availability-staging-resume" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
7. Block availability, staging & resume
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#8-lora-ota-protocol" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8. LoRa OTA protocol
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="8. LoRa OTA protocol">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#81-two-tier-discovery" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.1 Two-tier discovery
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#82-anti-storm-mandatory-at-mesh-scale" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.2 Anti-storm (mandatory at mesh scale)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#83-fetch-handshake" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.3 Fetch handshake
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#84-message-bodies-transfer" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.4 Message bodies (transfer)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#85-sizing-against-max_packet_payload-184" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.5 Sizing against MAX_PACKET_PAYLOAD = 184
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#86-temporary-radio-and-transfer-boundary" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
8.6 Temporary-radio and transfer boundary
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#9-identity-trust-versioning" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
9. Identity, trust & versioning
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#10-multi-mota-serve-the-external-folder-relay" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10. Multi-mota serve & the external "folder" relay
|
|
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="md-nav" aria-label="10. Multi-mota serve & the external "folder" relay">
|
|
<ul class="md-nav__list">
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#101-the-motasource-abstraction-otasourceh" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10.1 The MotaSource abstraction (OtaSource.h)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#102-the-mota-seeder-transport-motaseederprotoh" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
10.2 The mota-seeder transport (MotaSeederProto.h)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
</nav>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#11-cli-surface-otaclicpp" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
11. CLI surface (OtaCli.cpp)
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#12-apply-bootloader-contract" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
12. Apply & bootloader contract
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li class="md-nav__item">
|
|
<a href="#13-versioning-of-this-spec" class="md-nav__link">
|
|
<span class="md-ellipsis">
|
|
|
|
13. Versioning of this spec
|
|
|
|
</span>
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="md-content" data-md-component="content">
|
|
|
|
<article class="md-content__inner md-typeset">
|
|
|
|
|
|
|
|
|
|
|
|
<a href="https://github.com/mikecarper/MeshCore/edit/keymindCascade/docs/ota_protocol.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<h1 id="meshcore-ota-mota-container-lora-protocol">MeshCore OTA - <code>.mota</code> container & LoRa protocol</h1>
|
|
<p>This is the <strong>single source of truth</strong> for MeshCore's over-the-air firmware update system ("mOTA"). It is
|
|
written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool)
|
|
in another codebase or project. Everything below is implemented in this repository and covered by host,
|
|
simulation, build, or hardware tests as noted in the relevant section. Hardware qualification is target- and
|
|
chain-specific; do not infer it from implementation alone. Where a section names a source file, that file is
|
|
the authoritative reference for byte-level details.</p>
|
|
<blockquote>
|
|
<p><strong>Just want to update your node?</strong> See the plain-language <a href="../ota_user_guide/">OTA user guide</a> - this
|
|
document is the technical/wire specification.</p>
|
|
</blockquote>
|
|
<p><strong>Design goals</strong></p>
|
|
<ul>
|
|
<li>Distribute firmware over LoRa as a <strong>self-verifying, resumable, single-source block transfer</strong> that
|
|
survives reboots and never auto-applies without explicit consent.</li>
|
|
<li><strong>Trustless mesh relay:</strong> repeaters may forward packets while the source alone serves firmware data;
|
|
integrity is content-addressed
|
|
against a signed merkle root, so a relay need not be trusted and never needs the signing keys.</li>
|
|
<li><strong>Primary while transferring:</strong> periodic discovery stays at background priority, but manifest, block,
|
|
data, and proof packets for an active fetch use primary queue priority at every relay hop.</li>
|
|
<li><strong>Portable:</strong> the engine (<code>src/helpers/ota/OtaManager</code>) is Arduino/radio/crypto-free and host-testable,
|
|
so the same logic drives a device, a simulation, or a third-party implementation.</li>
|
|
</ul>
|
|
<p><strong>Source map</strong> (all under <code>src/helpers/ota/</code> unless noted)</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Concern</th>
|
|
<th>File</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Constants, enums, flags</td>
|
|
<td><code>OtaFormat.h</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Container/manifest parse</td>
|
|
<td><code>MotaContainer.{h,cpp}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Merkle tree + proofs</td>
|
|
<td><code>MerkleTree.{h,cpp}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>EndF self-identity</td>
|
|
<td><code>FirmwareInfo.{h,cpp}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Wire message codec</td>
|
|
<td><code>OtaProtocol.{h,cpp}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Session engine (serve+fetch+discovery)</td>
|
|
<td><code>OtaManager.{h,cpp}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Multi-mota / folder relay</td>
|
|
<td><code>OtaSource.h</code>, <code>MotaSourceSerial.{h,cpp}</code>, <code>MotaSeederProto.h</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Staging stores</td>
|
|
<td><code>OtaStore.h</code>, <code>OtaStoreFlashNrf52.*</code>, <code>OtaStoreFlashEsp32.*</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Apply</td>
|
|
<td><code>OtaApply.*</code>, bootloader <code>Adafruit_nRF52_Bootloader_OTAFIX</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Device glue (CLI/context)</td>
|
|
<td><code>OtaCli.cpp</code>, <code>OtaContext.h</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Host tooling</td>
|
|
<td><a href="https://github.com/vk496/motatool"><code>motatool</code></a> (standalone Rust CLI: build/verify/inspect/serve); <code>tools/mota/</code> (Python reference lib <code>motalib.py</code> + build/test glue)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<hr />
|
|
<h2 id="1-conventions">1. Conventions</h2>
|
|
<ul>
|
|
<li><strong>Endianness:</strong> all multi-byte integers are little-endian unless stated.</li>
|
|
<li><strong>Hashes (multihash):</strong> the hash family is declared once per manifest via <code>hash_algo</code> =
|
|
<code>0x12</code> = <strong>SHA-256</strong> (the <a href="https://github.com/multiformats/multihash">multihash</a> code for sha2-256).
|
|
Truncations used:</li>
|
|
<li><code>sha2-256:4</code> - first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs,
|
|
<code>manifest_id</code>, and the discovery <code>set_digest</code>.</li>
|
|
<li><code>sha2-256:8</code> - first 8 bytes. Base-firmware identity (<code>base_hash</code>, <code>EndF.body_hash</code>).</li>
|
|
<li><code>sha2-256:32</code> - full digest. The image security anchor (<code>image_hash</code>).
|
|
Digests are stored <strong>bare</strong> (just the truncated bytes); the family is implied by <code>hash_algo</code>.</li>
|
|
<li><strong>Signatures:</strong> Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key.</li>
|
|
</ul>
|
|
<p><strong>Reference constants</strong> (<code>OtaFormat.h</code>):</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Value</th>
|
|
<th>ASCII / note</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Container <code>MAGIC</code></td>
|
|
<td><code>6D 4F 54 41</code></td>
|
|
<td><code>mOTA</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Container <code>TRAILER</code></td>
|
|
<td><code>76 6B 34 39 36</code></td>
|
|
<td><code>vk496</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>EndF</code> marker</td>
|
|
<td><code>45 6E 64 46</code></td>
|
|
<td><code>EndF</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>hash_algo</code> (sha2-256)</td>
|
|
<td><code>0x12</code></td>
|
|
<td>multihash code</td>
|
|
</tr>
|
|
<tr>
|
|
<td>application <code>format_ver</code></td>
|
|
<td><code>0x02</code></td>
|
|
<td>ordinary full/delta application package</td>
|
|
</tr>
|
|
<tr>
|
|
<td>bootloader <code>format_ver</code></td>
|
|
<td><code>0x03</code></td>
|
|
<td>privileged exact-identity nRF52 bootloader package only</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>approval</code> = not approved</td>
|
|
<td><code>FF FF FF FF</code></td>
|
|
<td>erased NOR word</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>approval</code> = approved</td>
|
|
<td><code>41 50 52 56</code></td>
|
|
<td><code>APRV</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MFLAG_FULL</code></td>
|
|
<td><code>0x01</code></td>
|
|
<td>flags bit0</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MFLAG_SIGNED</code></td>
|
|
<td><code>0x02</code></td>
|
|
<td>flags bit1</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MFLAG_BOOTLOADER</code></td>
|
|
<td><code>0x04</code></td>
|
|
<td>flags bit2; valid only in the exact v3 bootloader profile</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>CODEC_FULL</code> / <code>_SEQUENTIAL</code> / <code>_INPLACE</code></td>
|
|
<td><code>0</code> / <code>1</code> / <code>2</code></td>
|
|
<td>Section 5</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>PAYLOAD_TYPE_OTA</code></td>
|
|
<td><code>0x0C</code></td>
|
|
<td>MeshCore packet type (<code>src/Packet.h</code>)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MAX_PACKET_PAYLOAD</code></td>
|
|
<td><code>184</code></td>
|
|
<td>usable bytes per packet (<code>src/MeshCore.h</code>)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Default application block size</td>
|
|
<td><code>2048</code></td>
|
|
<td><code>block_size_log2 = 0x0B</code>; deployed 1 KiB application packages remain accepted</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Bootloader-package block size</td>
|
|
<td><code>1024</code></td>
|
|
<td>fixed <code>block_size_log2 = 0x0A</code> for format 3</td>
|
|
</tr>
|
|
<tr>
|
|
<td>OTA discovery TX priority</td>
|
|
<td><code>250</code></td>
|
|
<td>background (<code>OTA_TX_PRIORITY</code>, <code>src/Mesh.h</code>)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>OTA active-transfer TX priority</td>
|
|
<td><code>0</code></td>
|
|
<td>primary (<code>OTA_TRANSFER_TX_PRIORITY</code>, <code>src/Mesh.h</code>)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p>Current application firmware publishes its receive/reassembly ceiling as the compact <code>maxblk:2048</code> field
|
|
near the front of both <code>ota status</code> and <code>ota self</code>. Keeping the field ahead of variable diagnostics ensures
|
|
it survives the 160-byte remote-admin reply limit. Host updaters must treat an absent field as the deployed
|
|
legacy capability (<code>1024</code>), reject a ready package whose manifest block size exceeds that capability, and use
|
|
the reported size when turning a raw firmware image into a new application package. This capability does not
|
|
change the format-3 bootloader package's fixed 1 KiB geometry.</p>
|
|
<hr />
|
|
<h2 id="2-firmware-image-the-endf-trailer">2. Firmware image & the <code>EndF</code> trailer</h2>
|
|
<p>Every OTA-capable build appends a fixed <strong>56-byte</strong> <code>EndF</code> trailer to its flashed image so a running node
|
|
can discover its own size <strong>and self-describing identity</strong> on any MCU (no linker symbols needed). Every
|
|
field is always present at a constant offset. Implemented by <code>FirmwareInfo.cpp</code>; appended at build time by
|
|
<code>tools/mota/pio_endf.py</code> (post-build hook).</p>
|
|
<pre><code>flashed image = BODY (image bytes) || EndF trailer
|
|
EndF trailer (fixed 56 bytes):
|
|
off 0 4 "EndF" 45 6E 64 46
|
|
off 4 4 body_len uint32 LE - length of BODY (excludes the whole trailer)
|
|
off 8 8 body_hash sha2-256:8 of BODY
|
|
off 16 4 fw_version uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre (0 = unknown)
|
|
off 20 4 target_id uint32 LE - sha2-256:4(pio_env): hardware + role + partition (fetch routing)
|
|
off 24 32 hw_id NUL-padded ASCII hardware tag (brick-safety), e.g. "RAK4631" ("" = unknown)
|
|
</code></pre>
|
|
<ul>
|
|
<li><strong>Self-describing identity.</strong> <code>pio_endf.py</code> uses <code>build.sh</code>'s <code>MOTA_TARGET_ID</code> when present (required for
|
|
virtual LoRa-OTA build names), otherwise it computes <code>target_id</code> from the PlatformIO env name. It reads
|
|
<code>hw_id</code> from <code>MOTA_HW_ID</code> and <code>fw_version</code>
|
|
from <code>FIRMWARE_VERSION</code>. The device reads them back (<code>ota_self_firmware()</code>), so a node's advertised
|
|
identity is correct regardless of how it was built - and the packaging tool reads them straight from a raw
|
|
<code>.bin</code> (no <code>--target-env</code>/<code>--fw-version</code> flags, no reliance on filenames; Section 9, Section 13). A dev build with no
|
|
dotted version simply carries <code>fw_version = 0</code> / empty <code>hw_id</code> (= unknown) - still a full 56-byte trailer.</li>
|
|
<li><strong>Size discovery:</strong> scan flash from the partition top downward for the <code>EndF</code> marker; the byte before it
|
|
is the last BODY byte (the trailer is always 56 bytes). See <code>ota_self_firmware()</code>.</li>
|
|
<li><strong>Delta base matching:</strong> a node's <code>body_hash</code> is read directly from its own <code>EndF</code>; a delta's <code>base_hash</code>
|
|
(Section 5) must equal it. <code>body_hash</code> is over BODY only.</li>
|
|
<li><strong>No circularity:</strong> <code>EndF</code> hashes only the BODY, never itself.</li>
|
|
</ul>
|
|
<p>The "reconstructed image" referenced by the manifest is the full <code>BODY || EndF</code> (what gets flashed).</p>
|
|
<h3 id="esp32-application-slot-profiles">ESP32 application-slot profiles</h3>
|
|
<p>ESP32 Companion firmware is exempt from the portable-slot limit. When an exact
|
|
Full recipe exists, one expanded-partition image supplies USB, BLE, WiFi,
|
|
source-only LoRa OTA, and persistent USB logging instead of separate transport
|
|
artifacts. A small set of high-capacity, non-PSRAM classic ESP32 companions use
|
|
100 contacts, 8 group channels, and a 16-frame offline queue in that combined
|
|
image to preserve internal-DRAM headroom. MQTT
|
|
observers and ESP-NOW bridges always use FULL builds because fitting them into the legacy slot would require
|
|
removing CLI and role features. Except for those FULL roles and the ESP32-C6 case below, non-companion ESP32
|
|
artifacts, including room, sensor, and repeater roles, must fit the legacy slot from <code>0x10000</code> up to
|
|
<code>0x150000</code> (<code>0x140000</code>, 1,310,720 bytes), including the 56-byte <code>EndF</code> trailer. The build checks both that
|
|
limit and the target's actual app partition. The ESP32-C6 <code>no_external_sensors</code> OTA siblings are the narrow
|
|
exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their
|
|
established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app
|
|
partition. For standalone ESP32 and nRF52 repeaters that need a lean staging
|
|
profile, <code>build.sh</code> also exposes an explicit <code>*_lora_ota_no_external_sensors</code>
|
|
artifact: the ordinary repeater remains sensor-enabled, while that sibling
|
|
trims selected optional environmental/ranging drivers for LoRa distribution. SolarXiao 30S and 33S use
|
|
matched external QSPI staging, so their ordinary full-sensor repeater is already install-capable and no
|
|
redundant lean sibling is generated. Integrated GPS and other
|
|
board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The legacy
|
|
suffix describes a driver trim, not removal of the generic I2C bus. Reduced RAK3401 and RAK4631 profiles
|
|
retain INA219, INA226, INA260, and INA3221 voltage/current monitors. These are retained optional-sensor-table
|
|
drivers, not the only I2C consumers: SSD1306 display, supported autodiscovered RTCs, and RAK12500 GPS remain
|
|
separate I2C peripherals where selected by the board recipe. The RAK3401 OTA repeater also retains RAK12500
|
|
I2C and RAK12501/L76K UART GPS support; install either GPS module in sensor slot A because slot D conflicts
|
|
with the RAK13302 radio's BUSY/DIO1 lines. The plain RAK4631 OTA repeater and its Serial2 bridge retain GPS.
|
|
RAK12501 uses Serial1. Its explicitly compiled Serial1 bridge therefore omits the combined GPS provider,
|
|
including RAK12500, even though RAK12500 itself does not use the UART.</p>
|
|
<p>The firmware-configured INA3221 address and RAK12500 address are both <code>0x42</code>, so those devices cannot share
|
|
one bus at those addresses. Keep RAK12500 at <code>0x42</code>, strap INA3221 A0 to SCL for <code>0x43</code>, and use firmware
|
|
built with <code>-DTELEM_INA3221_ADDRESS=0x43</code> when both are installed.</p>
|
|
<p>ESP32 siblings retain the compact browser WiFi updater and use up to
|
|
254 neighbors. Internal-DRAM-constrained targets use 50 as recorded in the
|
|
artifact capability manifest; see <a href="../esp32_memory_budget/">ESP32 memory budget</a>. RP2040 and STM32 targets are not offered because
|
|
those platforms do not yet have a safe bootloader/apply path.</p>
|
|
<p>nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default <code>-Ofast</code>. This
|
|
keeps the runtime software Ed25519 fallback from being expanded into tens of kilobytes of repeated curve
|
|
arithmetic while retaining CC310 hardware crypto, hardware RNG mixing, telemetry history, and board-native
|
|
features.</p>
|
|
<p>WiFi-heavy non-companion roles are not reduced to fit the legacy application slot. <code>build.sh</code> automatically
|
|
promotes every ESP32 MQTT observer and ESP-NOW bridge to the expanded FULL partition profile. These artifacts
|
|
retain the complete role CLI, WebConfig where supported, display and optional sensor support, full timezone
|
|
and TLS behavior, and the board's normal power-management implementation. The compact CLI is not compiled
|
|
into any build. Ordinary repeater builds remain sensor-enabled; only explicitly named
|
|
<code>*_lora_ota_no_external_sensors</code> siblings trim selected optional environmental/ranging drivers for LoRa
|
|
distribution, and those siblings retain the complete CLI and target-declared I2C peripherals.</p>
|
|
<p>MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset
|
|
during a settings save restores the last committed common preference image or publishes the completed new
|
|
image; it does not leave a partially written <code>/com_prefs</code> file to fail on the next boot. A truncated legacy
|
|
image is rejected before any partial radio or string fields are applied, then rewritten from safe defaults.</p>
|
|
<p>Option 3 in <code>build.sh</code> emits one <code>*-full-usb-wifi-ota-*</code> ESP32 artifact for each
|
|
FULL-capable non-companion hardware/role that has a matching MQTT environment.
|
|
It compiles USB packet logging and direct WiFi MQTT together. A
|
|
<code>*-full-logging-ota-*</code> fallback is emitted only when there is no MQTT sibling;
|
|
ordinary non-OTA roles compile runtime USB logging into their canonical image,
|
|
so separate standard-logging artifacts are not emitted. Non-MQTT FULL twins
|
|
are also skipped for covered ESP32 roles. MQTT observers and ESP-NOW bridges are emitted only with
|
|
expanded FULL partitions. Menu option 8, or <code>build-full-esp32-firmwares</code>,
|
|
builds the unified profiles plus necessary fallbacks. Menu option 9, or
|
|
<code>build-full-esp32-logging-firmwares</code>, builds only those fallbacks.
|
|
FULL builds restore WebConfig, display support, optional external sensors, and the full role CLI and feature
|
|
set,
|
|
full ElegantOTA where that target declares the required library, and LoRa OTA for every included role,
|
|
including room servers, sensors, observers, and bridges. They use expanded A/B partition
|
|
tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB
|
|
and 16 MiB boards. Explicit <code>*_lora_ota_no_external_sensors</code> targets are not duplicated; their ordinary
|
|
repeater build is the FULL, sensor-enabled counterpart. The
|
|
<code>*-full-usb-wifi-ota-*</code> profile enables USB packet logging and MQTT, with a
|
|
persistent <code>logging.output</code> selector; its verbose internal debug remains off.
|
|
The fallback <code>*-full-logging-ota-*</code> profile enables USB debug and packet
|
|
logging and has no MQTT target. Install a matching
|
|
<code>*-full-usb-wifi-ota-*-merged.bin</code> or <code>*-full-logging-ota-*-merged.bin</code> over USB once to write the expanded partition
|
|
table. After that, its matching non-merged FULL application image can be installed through USB, WiFi OTA,
|
|
or LoRa OTA. Do not install a non-merged FULL image onto a node that still has its old partition table.</p>
|
|
<blockquote>
|
|
<p><strong>Implementer note:</strong> the bootloader (and any non-Arduino consumer) MUST locate the body extent by
|
|
scanning for <code>EndF</code>, never by trusting a stored size - see the bootloader contract in Section 12.</p>
|
|
</blockquote>
|
|
<hr />
|
|
<h2 id="3-the-mota-container">3. The <code>.mota</code> container</h2>
|
|
<p>The distributed form (host-built, wire-transferred). Parsed by <code>mota_parse()</code> in <code>MotaContainer.cpp</code>.</p>
|
|
<pre><code>off size field
|
|
0 4 MAGIC = 6D 4F 54 41
|
|
4 4 MOTA_TOTAL_SIZE uint32 LE - total container bytes (incl. manifest, leaves[],
|
|
payload, trailer). Lets a node pre-reserve staging and compute
|
|
write_start = staging_region_end - MOTA_TOTAL_SIZE.
|
|
8 M MANIFEST (Section 4; M = 197 fixed + leaves[], 4*BC; no length field - BC from payload_size)
|
|
8 + M P PAYLOAD (payload_size bytes; delta or full image)
|
|
8 + M + P 5 TRAILER = 76 6B 34 39 36
|
|
</code></pre>
|
|
<p><code>MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5</code>. The manifest <code>M</code> <strong>includes</strong> <code>leaves[]</code>; the manifest-minus-leaves
|
|
prefix (<code>mfl</code>, sent over the wire as <code>OTA_MANIFEST</code>) is <code>[8, leaves_off)</code>.</p>
|
|
<p><strong>Staged (in-flash) form.</strong> Written bottom-aligned so <code>TRAILER</code> ends at <code>staging_region_end</code>. Identical
|
|
bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the <code>leaves[]</code> slots
|
|
(filled as blocks arrive - Section 7) and the 4-byte <code>approval</code> field (on owner consent - Section 4.2). Everything else
|
|
is immutable.</p>
|
|
<hr />
|
|
<h2 id="4-the-manifest">4. The manifest</h2>
|
|
<p><strong>Fixed layout.</strong> Every field sits at a constant offset and is always present - <code>base_hash</code>,
|
|
<code>signer_pubkey</code> and <code>signature</code> are zero-filled when not applicable (a full image / an unsigned container).
|
|
Only <code>leaves[]</code> is variable (one 4-byte hash per block). So the manifest-minus-leaves (<code>mfl</code>) is <strong>always
|
|
197 bytes</strong> and the parser is plain offset reads - no conditionals. Parsed by <code>mota_parse_manifest()</code>.</p>
|
|
<pre><code>off size field notes
|
|
0 1 format_ver = 0x02 application, or 0x03 privileged bootloader package
|
|
1 1 flags bit0 FULL; bit1 SIGNED; bit2 BOOTLOADER; bits3-7 reserved 0
|
|
2 1 hash_algo 0x12 = sha2-256
|
|
3 4 target_id device/arch/role discriminator (Section 9)
|
|
7 4 fw_version MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre (comparable uint32)
|
|
11 4 image_size size of the reconstructed image (BODY||EndF)
|
|
15 4 payload_size PAYLOAD bytes in this container
|
|
19 1 block_size_log2 e.g. 0x0B = 2048 (new application default); 0x0A = deployed 1024
|
|
20 4 merkle_root sha2-256:4 over PAYLOAD blocks (Section 6) - also the manifest_id
|
|
24 32 image_hash sha2-256:32 of the reconstructed image - SECURITY anchor
|
|
56 1 codec_id 0=full/raw, 1=detools-sequential, 2=detools-in-place
|
|
57 32 hw_id NUL-padded ASCII hardware tag (e.g. "RAK4631"); same tag => bootable-compatible.
|
|
SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip.
|
|
89 8 base_hash sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL.
|
|
97 32 signer_pubkey Ed25519 public key. 0 if not SIGNED.
|
|
129 64 signature Ed25519 over manifest[0, 129). 0 if not SIGNED.
|
|
193 4 approval FF FF FF FF = not approved; 41 50 52 56 ("APRV") = approved
|
|
--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container ---
|
|
197 4*BC leaves[] BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field)
|
|
</code></pre>
|
|
<p>The signature always covers <code>manifest[0, 129)</code> (the head + <code>base_hash</code> + <code>signer_pubkey</code>). <code>approval</code> is
|
|
outside the signed region so it can be flipped in place on consent without breaking the signature.</p>
|
|
<p>Manifest-minus-leaves size (<code>mfl</code>) is a constant <strong>197 bytes</strong> for every container (full or delta, signed
|
|
or unsigned). At 197 bytes the manifest exceeds one packet, so <code>OTA_MANIFEST</code> is always sent multi-fragment
|
|
(Section 8.4, 2 fragments) and reassembled by the fetcher.</p>
|
|
<p>The two versions are deliberately disjoint. Version 2 accepts application packages only and rejects the
|
|
BOOTLOADER bit. Version 3 accepts only flags exactly <code>FULL|SIGNED|BOOTLOADER</code>; a non-bootloader v3 package
|
|
is invalid. Consequently, deployed v2-only application parsers reject a bootloader package before they can
|
|
mistake its raw 40 KiB payload for an application image.</p>
|
|
<h3 id="41-signed-region">4.1 Signed region</h3>
|
|
<p><code>signature</code> covers manifest bytes <code>[0, 129)</code> - the head + <code>base_hash</code> + <code>signer_pubkey</code>. It does <strong>not</strong>
|
|
cover <code>approval</code> or <code>leaves[]</code>:</p>
|
|
<ul>
|
|
<li><code>leaves[]</code> are verified against the signed <code>merkle_root</code> (Section 6), so they need no separate signature.</li>
|
|
<li><code>approval</code> is device-local consent (Section 4.2), deliberately outside the signature.</li>
|
|
</ul>
|
|
<h3 id="42-the-approval-field">4.2 The <code>approval</code> field</h3>
|
|
<ul>
|
|
<li>Distributed and <strong>forced on ingest</strong> to <code>FF FF FF FF</code> (a peer can never pre-approve).</li>
|
|
<li>The local owner's <code>ota applydelta</code> writes <code>41 50 52 56</code> (<code>"APRV"</code>) - a single NOR-safe write (only clears
|
|
bits from the erased word). Any partial/other value reads as not-approved (fail-safe).</li>
|
|
<li>Bound to this image (lives in this <code>.mota</code>'s manifest, re-erased when a new <code>.mota</code> is staged).</li>
|
|
<li>A <strong>consent</strong> marker, not a security primitive. Authenticity = <code>signature</code> + <code>image_hash</code> + <code>hw_id</code>.</li>
|
|
</ul>
|
|
<h3 id="43-privileged-nrf52-bootloader-package-profile">4.3 Privileged nRF52 bootloader package profile</h3>
|
|
<p>A v3 bootloader package has a deliberately narrow, non-extensible profile:</p>
|
|
<ul>
|
|
<li>flags exactly <code>FULL|SIGNED|BOOTLOADER</code>, <code>CODEC_FULL</code>, nonzero <code>fw_version</code>, zero <code>base_hash</code>;</li>
|
|
<li>a raw payload and <code>image_size</code> of exactly <code>0xA000</code> (40 KiB), split into exactly forty 1024-byte blocks;</li>
|
|
<li>a target derived from the installed CRC-valid embedded manifest identity. Deployed XIAO identities keep
|
|
raw board IDs <code>0x28860044</code>/<code>0x28860045</code>; generic identities use LE32(SHA-256(canonical padded hw32));</li>
|
|
<li>signed <code>hw_id</code> exactly <code>XIAO_BL_28860044</code>/<code>XIAO_BL_28860045</code> for deployed XIAO, or the zero-padded
|
|
32-byte <code>NRF_BL_<BOARD_ID>_<DEVICE_NAME></code> for a generic target;</li>
|
|
<li>a sane nRF52840 vector table, exactly one CRC-valid embedded manifest v1 with the exact board/name pair,
|
|
followed by the required CRC-covered <code>BLM2</code>/<code>SOFT</code> continuity extension (embedded boot version,
|
|
SoftDevice family/FWID, application base, and layout ABI), with that complete 76-byte envelope at the
|
|
canonical final-image offset <code>0x9FB4</code>,
|
|
and exactly one <code>MOTABLDR</code> marker advertising ABI >= 3, both application codecs (<code>FULL|INPLACE</code>, mask
|
|
<code>0x0005</code>), boot-update continuity, and the exact storage flags for the application layout (<code>0x09</code>
|
|
MeshTower V2 SD, <code>0x0E</code> XIAO QSPI, or <code>0x0A</code> shared internal staging).</li>
|
|
</ul>
|
|
<p>The incoming embedded identity must exactly match the installed CRC-valid bootloader identity. Both scans
|
|
consider every aligned structurally valid candidate so magic bytes in a literal pool cannot shadow the real
|
|
manifest. Duplicate accounting counts each CRC-valid 44-byte base record before interpreting adjacent
|
|
continuity metadata, so a corrupt or half-present <code>BLM2</code> extension cannot hide a second identity; after
|
|
exactly one base record is selected, malformed claimed continuity fails closed. A package must be signed by
|
|
a key already in the device's trusted allowlist; unlike ordinary
|
|
application packages, there is no unsigned manual-install exception.
|
|
The signed outer <code>fw_version</code> must equal the embedded boot version. Qualified internal/QSPI targets may
|
|
bootstrap a CRC-valid legacy-v1 installed bootloader once; MeshTower SD instead requires local BLM2
|
|
provisioning because it has no safe legacy media handoff. After bootstrap every remote successor must be strictly newer and match the live
|
|
SoftDevice/application layout. Low-byte zero and all-ones boot versions are invalid. Remote rollback has no
|
|
override and must use local DFU/SWD.</p>
|
|
<hr />
|
|
<h2 id="5-payload-codecs-delta-base">5. Payload, codecs & delta base</h2>
|
|
<p><code>PAYLOAD</code> is either the full reconstructed image (<code>FULL</code>) or a delta (<code>!FULL</code>).</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><code>codec_id</code></th>
|
|
<th>Meaning</th>
|
|
<th>Used by</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>0</td>
|
|
<td>full / raw</td>
|
|
<td>PAYLOAD = reconstructed image (<code>BODY||EndF</code>). ESP32 A/B or an external SD/QSPI nRF52 target.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>1</td>
|
|
<td>detools <strong>sequential</strong></td>
|
|
<td>random read of base + sequential write of result -> ESP32 A->B inactive slot.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>2</td>
|
|
<td>detools <strong>in-place</strong></td>
|
|
<td>bounded scratch; rewrites the app region in place -> nRF52 single-slot.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p>For deltas, <code>base_hash</code> = the base build's <code>EndF.body_hash</code> (sha2-256:8 of its BODY). A node applies a
|
|
delta only if <code>base_hash</code> matches its own <code>EndF.body_hash</code>. After applying, the result MUST hash
|
|
(sha2-256:32) to <code>image_hash</code> before it is booted - the hard security gate.</p>
|
|
<p><strong>A fetcher only requests firmware it can apply.</strong> Each node declares the codec(s) it can apply
|
|
(<code>set_apply_codec</code>/<code>set_apply_codec2</code>): ESP32 accepts <code>full</code> + <code>sequential</code> (+ <code>in-place</code>). Internal-staging
|
|
nRF52 targets accept only <code>in-place</code> because internal flash cannot hold a second full application image.
|
|
Matched SD and raw-QSPI nRF52 targets accept <code>full</code> + <code>in-place</code> because external media holds the container. A <code>.mota</code> 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.</p>
|
|
<p>Compression is internal to the detools patch and must be supported by the applier. Patches are produced by
|
|
<strong>detools 0.53.0</strong> (<code>tools/mota</code> -> <code>detools.create_patch</code>) and decoded on-device by detools' embeddable C
|
|
decoder, vendored verbatim at <code>src/helpers/ota/detools/</code> (see its <code>README.meshcore.txt</code>). That build
|
|
enables only the self-contained <code>NONE</code> + <code>CRLE</code> compressions (no malloc/liblzma/heatshrink), so MeshCore
|
|
deltas use <code>--compression crle</code>. <strong>Do not reimplement the codec</strong> - use the vendored decoder.</p>
|
|
<hr />
|
|
<h2 id="6-merkle-tree-sha2-2564">6. Merkle tree (sha2-256:4)</h2>
|
|
<p>Verifies each PAYLOAD block against the signed <code>merkle_root</code> <strong>before</strong> the whole payload exists, so
|
|
corruption/forgery is localized to a block. Implemented in <code>MerkleTree.cpp</code>.</p>
|
|
<ul>
|
|
<li><strong>Blocks:</strong> PAYLOAD splits into <code>BC = ceil(payload_size / B)</code> blocks, <code>B = 2^block_size_log2</code> (new
|
|
application default 2048; deployed 1024-byte application packages remain valid). The last block is its
|
|
real length (<strong>no zero padding</strong>). Format-3 bootloader packages remain fixed at 1024 bytes.</li>
|
|
<li><strong>Leaf:</strong> <code>leaves[i] = sha2-256:4( block_i_bytes )</code>.</li>
|
|
<li><strong>Internal node:</strong> <code>node = sha2-256:4( left || right )</code> (4+4 input bytes).</li>
|
|
<li><strong>Odd level:</strong> an odd count promotes the <strong>last node unchanged</strong> to the next level (no duplication).</li>
|
|
<li><strong>Root:</strong> reduce until one node remains. <code>BC == 1</code> -> root = <code>leaves[0]</code>. <code>BC == 0</code> is invalid.</li>
|
|
</ul>
|
|
<h3 id="61-proofs">6.1 Proofs</h3>
|
|
<p>A proof for block <code>i</code> is the ordered list of sibling digests from leaf to root. Promoted levels contribute
|
|
<strong>no</strong> element. Verification (needs <code>BC</code> to know the tree shape):</p>
|
|
<pre><code>h = leaf_i ; idx = i ; n = BC ; p = 0
|
|
while n > 1:
|
|
if (n is odd) and (idx == n-1): # this node was promoted
|
|
pass
|
|
else:
|
|
sib, side = proof[p] ; p += 1
|
|
h = sha2-256:4( sib || h ) if side==left else sha2-256:4( h || sib )
|
|
idx //= 2 ; n = (n + 1) // 2
|
|
accept iff h == merkle_root and p == len(proof)
|
|
</code></pre>
|
|
<p>Over LoRa, <code>leaves[]</code> are <strong>omitted</strong> from the manifest transfer. A serving node computes a block's proof
|
|
on demand from its stored <code>leaves[]</code> and normally sends <code>OTA_PROOF</code> immediately after that block's paced
|
|
<code>OTA_DATA</code>. <code>OTA_REQ_PROOF</code> remains the fallback for an older source or a lost proactive proof. The fetcher
|
|
fills its own <code>leaves[i]</code> as each verified block lands.</p>
|
|
<hr />
|
|
<h2 id="7-block-availability-staging-resume">7. Block availability, staging & resume</h2>
|
|
<p>There is no separate availability structure. <strong>Block <code>i</code> is present <=> <code>leaves[i]</code> is non-erased</strong>
|
|
(<code>!= FF FF FF FF</code>). Because <code>leaves[]</code> live in the staged flash region, availability <strong>survives reboot</strong>.</p>
|
|
<p><strong>Commit order per block (crash-safe):</strong> (1) verify proof, (2) write block payload to its offset, (3) write
|
|
<code>leaves[i]</code> <strong>last</strong>. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched
|
|
(idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning <code>leaves[]</code> for a persistent,
|
|
reopenable store. A hybrid nRF52 transfer is the deliberate exception: its SRAM-backed payload suffix is
|
|
volatile, so the application refuses to adopt that staged header instead of rebuilding partial progress.</p>
|
|
<p><strong>Resume (<code>OtaManager::resumeStaged</code> + <code>OtaStore::checkpoint</code>/<code>reopen</code>):</strong> an interrupted fetch resumes from
|
|
the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present
|
|
blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress
|
|
every N committed blocks) is runtime-tunable (<code>ota config checkpoint <N></code>, 0 = only finalized containers
|
|
resume). Boot-time adoption is an automatic fetch decision: current <code>autofetch</code> must be enabled, the stored
|
|
target must equal the node target, policy <code>signed</code> requires the signed bit, and an enabled running-version
|
|
floor requires a strictly newer manifest. An explicit MID pull may deliberately resume an older or unsigned
|
|
package and keeps target <code>0</code> as a MID-only wildcard. Stores keep <code>leaves[]</code> in RAM until flush and never
|
|
auto-GC, preserving resumable progress. The debug/operator equivalent is <code>ota dev resume <MID8></code>; after a
|
|
reboot the MID is mandatory, while a no-argument form may only reuse a still-active session MID. It never
|
|
uses the <code>nullptr</code> automatic-adoption path, so a malformed MID or no active MID fails closed.
|
|
Hybrid nRF52 staging cannot enter this resume path after an application restart, even if its flash prefix
|
|
still contains metadata; the complete logical container must be fetched again.</p>
|
|
<p><strong>Flash-store note (RX-safe writes):</strong> a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa
|
|
RX, so the flash stores (<code>OtaStoreFlashNrf52</code>/<code>OtaStoreFlashEsp32</code>) <strong>coalesce writes to the erase unit</strong>
|
|
(4 KB page / sector) and commit each once off the per-packet path. Ordinary stores keep RAM at O(one page),
|
|
not O(image); a qualified hybrid nRF52 profile additionally reserves one fixed 64 KiB staging arena. A small
|
|
delta that fits page 0 does zero flash I/O until COMPLETE.</p>
|
|
<hr />
|
|
<h2 id="8-lora-ota-protocol">8. LoRa OTA protocol</h2>
|
|
<p>Carried in MeshCore packets with <strong><code>PAYLOAD_TYPE_OTA = 0x0C</code></strong>. Every OTA packet payload is:</p>
|
|
<pre><code>[0] ota_msg_type (OtaMsgType, OtaFormat.h)
|
|
[1..] body (fixed per type; encode/decode in OtaProtocol.cpp)
|
|
</code></pre>
|
|
<p>Message types:</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><code>ota_msg_type</code></th>
|
|
<th>val</th>
|
|
<th>routing</th>
|
|
<th>purpose</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>OTA_ADV</code></td>
|
|
<td>0x01</td>
|
|
<td>discovery</td>
|
|
<td>tiny per-node beacon (discovery tier 1)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_QUERY</code></td>
|
|
<td>0x02</td>
|
|
<td>discovery</td>
|
|
<td>ask a source for its catalog (discovery tier 2)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_HAVE</code></td>
|
|
<td>0x03</td>
|
|
<td>discovery</td>
|
|
<td>the catalog reply (fragmented, digest-tagged)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_GET_MANIFEST</code></td>
|
|
<td>0x04</td>
|
|
<td>transfer</td>
|
|
<td>request a manifest's fragments (<code>want_mask</code>) by <code>manifest_id</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_MANIFEST</code></td>
|
|
<td>0x05</td>
|
|
<td>transfer</td>
|
|
<td>the manifest-minus-leaves, fragmented</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_REQ</code></td>
|
|
<td>0x06</td>
|
|
<td>transfer</td>
|
|
<td>request fragments from an adaptive flight of 1-4 blocks (<code>want_mask</code> per block)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_DATA</code></td>
|
|
<td>0x07</td>
|
|
<td>transfer</td>
|
|
<td>one self-describing fragment of a block's data</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_REQ_PROOF</code></td>
|
|
<td>0x08</td>
|
|
<td>transfer</td>
|
|
<td>request/re-request a missing proof</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_PROOF</code></td>
|
|
<td>0x09</td>
|
|
<td>transfer</td>
|
|
<td>the merkle proof for one block</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_GET_LEAVES</code></td>
|
|
<td>0x0A</td>
|
|
<td>transfer</td>
|
|
<td>request the target's <code>leaves[]</code> fragments (<code>want_mask</code>) - warm-start only</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_LEAVES</code></td>
|
|
<td>0x0B</td>
|
|
<td>transfer</td>
|
|
<td>a fragment of the <code>leaves[]</code> array (for host-side seed leaf-diff)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<ul>
|
|
<li><strong><code>manifest_id</code></strong> = the manifest's <code>merkle_root</code> (4 bytes) - a compact content id present in every
|
|
transfer message, so a multi-mota server dispatches each request to the right image.</li>
|
|
<li><strong>Priority:</strong> <code>OTA_ADV</code>, <code>OTA_QUERY</code>, and <code>OTA_HAVE</code> enqueue at background priority 250. Once a fetch is
|
|
active, manifest, block request, data, and proof messages enqueue at primary priority 0. Relay-only
|
|
nodes classify the wire message identically, so a transfer stays primary across the complete path.</li>
|
|
<li><strong>Reliability is <em>eventual</em>:</strong> the fetcher re-requests only missing fragments after an adaptive deadline
|
|
derived from packet airtime, outstanding response packets, duty pacing, and path length. The manager may
|
|
still run a one-second maintenance tick, but that tick is not itself a retry timer. No hard ACKs or global
|
|
ordering are required.</li>
|
|
<li><strong>Relay envelope:</strong> OTA still uses a bounded flood-shaped mesh header so the same packets can cross the
|
|
configured number of hops without first discovering an addressed return path. During TempRadio each node
|
|
forwards one copy; active OTA packets do not use the generic flood-retry subsystem. The fetcher verifies
|
|
every block against the signed root, and a repeater without <code>ENABLE_OTA</code> can transport
|
|
<code>PAYLOAD_TYPE_OTA</code> opaquely without the manager, staging store, installer, or destination bootloader.</li>
|
|
<li><strong>Hop limit + duty cycle:</strong> OTA floods accumulate one path-hash per relay (the mesh's flood routing). A
|
|
node with the OTA manager <em>accepts</em> a packet only if it arrived within <code>ota config hops</code> hops (default 3;
|
|
<code>0</code> = direct only) and <em>relays</em> it only while still under that limit, appending its own hash. Relay-only
|
|
repeaters instead use their ordinary flood limits and forwarding filters. Discovery relays remain
|
|
lowest-priority and may be skipped when the packet pool runs low. Active-transfer relays bypass that
|
|
background pool gate and use priority 0; operators should therefore treat TempRadio as a dedicated OTA
|
|
maintenance window because the transfer can delay unrelated mesh traffic.</li>
|
|
</ul>
|
|
<h3 id="81-two-tier-discovery">8.1 Two-tier discovery</h3>
|
|
<p>Because a node may serve <strong>many</strong> mOTAs (its own firmware plus an external folder - Section 10), discovery is split
|
|
so the periodic beacon stays tiny regardless of catalog size:</p>
|
|
<p><strong>Tier 1 - <code>OTA_ADV</code> beacon</strong> (10 bytes, constant). Flooded as a short burst at boot, then every
|
|
<code>advert_mins</code> minutes (default 24h; runtime-tunable via <code>ota config advert</code>, <code>0</code> disables the periodic
|
|
re-advertise). It is also emitted immediately whenever the served set changes (e.g. a <code>motatool</code> folder is
|
|
attached/detached), so peers learn about newly-available firmware without waiting for the next interval:</p>
|
|
<pre><code>seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id
|
|
n_motas uint8 - count of complete servable mOTAs (saturates at 255)
|
|
set_digest[4] sha2-256:4 over the SORTED set of served manifest_ids (see below)
|
|
</code></pre>
|
|
<p><code>set_digest</code> is a <strong>content hash of the offering</strong>, not a counter: canonical across nodes, and it changes
|
|
iff the set of served mids changes. A peer that has already catalogued this <code>{seeder, set_digest}</code> ignores
|
|
the beacon (steady state is query-free). For a single served mota, <code>set_digest = sha2-256:4(mid)</code>.</p>
|
|
<p><strong>Tier 2 - <code>OTA_QUERY</code> -> <code>OTA_HAVE</code></strong> (on interest only):</p>
|
|
<pre><code>OTA_QUERY (flood): seeder_id[4] set_digest[4] filter_target(uint32) want_fragments(uint32)
|
|
# filter_target 0 = everything; want_fragments 0 = every fragment
|
|
OTA_HAVE (flood): seeder_id[4] set_digest[4] frag_idx(1) frag_total(1) n_rows(1) rows[]
|
|
HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2)
|
|
</code></pre>
|
|
<p><code>have_count</code> is the number of blocks the source holds (<code>== block_count</code> for a complete offered image).
|
|
Receivers do not advertise partial or completed downloads as new sources.</p>
|
|
<p>A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as
|
|
<code>OTA_HAVE</code> rows (fragmented if they exceed one packet - 10 rows per fragment). A receiver marks the catalog
|
|
complete only after all <code>frag_total</code> fragments arrive. If any are missing after the recovery timeout, it sends
|
|
another QUERY whose <code>want_fragments</code> bitmap names only the holes. <code>want_fragments</code> is an append-only extension:
|
|
an original 13-byte QUERY is still accepted and means "send every fragment." The heavy manifest is
|
|
fetched per-mid only on commit (Section 8.3).</p>
|
|
<p>Fragment numbers are canonical pages of the complete catalog sorted by <code>manifest_id</code>. <code>filter_target</code> may
|
|
remove rows from a requested page (and can therefore produce an empty fragment), but it never renumbers pages
|
|
or changes <code>frag_total</code>. This keeps missing-fragment recovery unambiguous when filtered and unfiltered queries
|
|
for the same <code>{seeder, set_digest}</code> are overheard together.</p>
|
|
<h3 id="82-anti-storm-mandatory-at-mesh-scale">8.2 Anti-storm (mandatory at mesh scale)</h3>
|
|
<p>If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS
|
|
pattern), all in <code>OtaManager</code>:</p>
|
|
<ul>
|
|
<li><strong><code>OTA_HAVE</code> is flooded and digest-tagged.</strong> EVERY node that overhears it caches the rows <strong>passively</strong>
|
|
(keyed by <code>{seeder, set_digest}</code>) - no query of its own needed.</li>
|
|
<li><strong>Jittered query:</strong> a peer needing a catalog schedules its <code>OTA_QUERY</code> after a random delay
|
|
<code>OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000))</code>, derived from <code>id +/ digest +/ self</code>.</li>
|
|
<li><strong>Overhear suppression:</strong> during the jitter window, overhearing another QUERY that covers the same scope,
|
|
or completing the HAVE fragment set for the same <code>{seeder, set_digest}</code>, cancels the pending query.</li>
|
|
<li><strong>Per-source recovery:</strong> each seeder has independent query/retry state. One source cannot overwrite another
|
|
source's timer, and a partial reply requests only missing fragments after 15 seconds (five bounded retries,
|
|
then another source ADV or explicit <code>ota ls</code> can start a fresh series).</li>
|
|
</ul>
|
|
<p>Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free.</p>
|
|
<h3 id="83-fetch-handshake">8.3 Fetch handshake</h3>
|
|
<pre><code>fetcher server (any node that has the mid)
|
|
OTA_GET_MANIFEST(mid, want_mask) > (want_mask=0xFFFF first; only missing fragments on retry)
|
|
<------- OTA_MANIFEST(mid, frag_idx, frag_total, bytes) x requested frags
|
|
(reassemble manifest, verify, compute geometry: BC, block_size, payload_size)
|
|
for each adaptive flight of missing blocks (starts at 1, grows on clean flights):
|
|
OTA_REQ(mid, {block_idx, want_mask}[]) > (one packet; all fragments first, only holes on recovery)
|
|
<------- OTA_DATA(mid, block_idx, frag_off, data) x requested frags/block
|
|
<------- OTA_PROOF(mid, block_idx, n_proof, proof) x requested blocks
|
|
(independently reassemble + verify each block, but remain RX-silent until the flight drains)
|
|
[after adaptive deadline: recover one block's holes, or OTA_REQ_PROOF for a missing proof]
|
|
(clean flight grows by one block; recovered flight halves the next width)
|
|
when all blocks present: verify full merkle_root + image_hash -> COMPLETE
|
|
</code></pre>
|
|
<p>Before allocating or writing the selected store, the receiver parses the reassembled manifest and requires
|
|
its <code>merkle_root</code> to equal the requested/wire <code>manifest_id</code> and its <code>target_id</code> to equal the catalog or
|
|
explicit-pull target that opened the receive slot. The wire envelope and HAVE row are advisory; they cannot
|
|
label and stage a different manifest.</p>
|
|
<h3 id="84-message-bodies-transfer">8.4 Message bodies (transfer)</h3>
|
|
<p>All offsets after the 1-byte type. Encoders/decoders in <code>OtaProtocol.cpp</code>; constants in <code>OtaManager.h</code>.</p>
|
|
<pre><code>OTA_GET_MANIFEST: manifest_id[4] want_mask(uint16) # bit k = send manifest fragment k; 0xFFFF = all
|
|
OTA_MANIFEST: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_MF_FRAG=176 B/frag
|
|
OTA_REQ: manifest_id[4] { block_idx(uint16) want_mask(uint16) }[1..4]
|
|
# legacy: bit k requests its 160-byte fragment
|
|
# v2: bit15=marker, bit14=allow transport DEFLATE,
|
|
# bit13=2 KiB descriptor, bits0..12=fragment bitmap;
|
|
# bit12 must be zero when bit13 is set (0xFFFF stays legacy)
|
|
OTA_DATA legacy: manifest_id[4] block_idx(uint16) frag_off(uint16) data[<=160]
|
|
OTA_DATA v2: manifest_id[4] block_idx(uint16) descriptor(uint16)
|
|
stream_id[4] data[<=171]
|
|
# deployed 1 KiB descriptor: bit15=marker, bit14=DEFLATE,
|
|
# bits13..10=fragment, bits9..0=complete encoded length minus one
|
|
# negotiated 2 KiB descriptor: bit15=marker, bits14..11=fragment,
|
|
# bits10..0=complete encoded length minus one; DEFLATE iff that
|
|
# length is strictly less than the manifest-derived raw block length
|
|
OTA_REQ_PROOF: manifest_id[4] block_idx(uint16)
|
|
OTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof x 4 bytes
|
|
OTA_GET_LEAVES: manifest_id[4] want_mask(uint16) # bit k = send leaves fragment k; 0xFFFF = all
|
|
OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_LEAVES_FRAG=176 leaf bytes
|
|
</code></pre>
|
|
<ul>
|
|
<li>
|
|
<p><strong>Warm-start / leaf-diff (<code>OTA_GET_LEAVES</code>/<code>OTA_LEAVES</code>) - motatool folder-capture only.</strong> Capturing a
|
|
device's firmware into a <code>motatool serve</code> folder is slow (a full image is hundreds of blocks). Because
|
|
builds here are non-deterministic, you cannot reproduce the exact target on the host - but a <em>similar</em>
|
|
build (e.g. a fresh recompile) is ~99% identical. So <code>motatool serve --seed <similar.mota></code> stages that
|
|
build's payload into the destination <code>.part</code>, and <code>ota pull <mid8> folder validate</code> makes the fetcher (1) bulk-
|
|
fetch the target's <code>leaves[]</code> via <code>OTA_GET_LEAVES</code>/<code>OTA_LEAVES</code> (bitmap-fragmented with a <code>want_mask</code>, same
|
|
anti-burst rule as <code>OTA_MANIFEST</code>), (2) recompute the merkle root from them and check it equals the
|
|
manifest root (authenticate), then (3) keep every seeded block whose leaf matches and pull full <code>OTA_DATA</code>
|
|
only for the blocks that differ. The <code>want_mask</code> is a fixed <strong>uint16</strong>, so <code>leaves[]</code> is capped at
|
|
<code>OTA_LEAVES_MAXFRAG=16</code> fragments (<code>OTA_DIFF_MAX_BLOCKS=704</code> blocks); larger images just fall back to a full
|
|
fetch. <strong>Normal P2P nodes never use this</strong> - they target only the blocks they want; the only always-on part
|
|
is answering <code>OTA_GET_LEAVES</code> with leaves the node already holds, so any node's firmware can be captured.</p>
|
|
</li>
|
|
<li>
|
|
<p><strong>Negotiated 171-byte fragments:</strong> the deployed profile remains unchanged: <code>frag_off</code> is a byte offset and
|
|
<code>data[]</code> carries at most 160 bytes. A new fetcher marks an <code>OTA_REQ</code> row as v2. For a deployed 1 KiB block,
|
|
its first request also includes every fragment bit needed by the legacy representation, so an old source can
|
|
answer it completely. The extended 2 KiB profile reserves bit 12 as zero; this both fits its twelve v2
|
|
fragments and ensures the deployed <code>0xFFFF</code> legacy all-fragments request is never misclassified. Receipt of
|
|
untagged <code>OTA_DATA</code> switches the fetch session to the old geometry. If no v2 data appears by the first
|
|
adaptive deadline, the fetcher retries with an ordinary legacy mask. New sources answer v2 with a packed descriptor, a repeated 4-byte representation
|
|
ID, and exactly 171 data bytes except the final fragment. A raw 1 KiB block falls from seven data packets to
|
|
six; a raw 2 KiB block falls from thirteen to twelve. Message type IDs do not change, so multi-hop relays
|
|
continue to forward request/data/proof packets opaquely with the same priority.</p>
|
|
</li>
|
|
<li><strong>Transport-only DEFLATE:</strong> a fetcher sets the v2 DEFLATE-permission bit only when its application includes
|
|
and configures the decoder. A source may encode each logical block as an independent raw RFC 1951 stream
|
|
with full stored, fixed-Huffman, and dynamic-Huffman support (<code>BTYPE=0/1/2</code>). Compression is used only when
|
|
strictly smaller, otherwise the source returns raw v2 data. The
|
|
receiver inflates to the manifest-derived block length, then performs the unchanged Merkle proof and writes
|
|
the original bytes to staging. Thus staging and bootloader apply remain uncompressed. Deployed 1 KiB
|
|
<code>.mota</code> containers remain compatible; new 2 KiB containers require a receiver advertising the extended
|
|
descriptor. Format-3 bootloader packages retain their strict 1 KiB geometry. The 4-byte <code>stream_id</code> is
|
|
SHA-256:4 of the complete raw or compressed
|
|
representation and appears in every fragment. A receiver locks <code>{encoding, encoded length, stream_id}</code> for
|
|
a block, preventing fragments from independently encoded seeders from being mixed. After one sparse retry,
|
|
a second stalled interval clears the full in-flight window and switches the session to legacy geometry, so
|
|
a vanished v2 seeder cannot prevent an older source from taking over; proof verification remains unchanged.
|
|
Every <code>ENABLE_OTA</code> MeshCore application registers the vendored tinf 1.2.1 full raw decoder; its wrapper
|
|
bounds output to the exact logical block length and rejects truncation, malformed streams, and trailing
|
|
whole bytes. Builds without OTA do not link the decoder. This application capability is independent of the
|
|
bootloader because transport data is inflated before the unchanged staged container is written.</li>
|
|
<li><strong>Adaptive flight size is not signed block size.</strong> Each slot holds one manifest-defined block: 1 KiB for
|
|
deployed application containers or 2 KiB for new ones. A clean link changes how many complete blocks one
|
|
<code>OTA_REQ</code> names: 1, then 2, then 3, then the compiled cap. It never changes block geometry. Two KiB halves
|
|
the leaf, block-request, and proof count for a payload while giving each independent DEFLATE stream a larger
|
|
history window.</li>
|
|
<li><strong>Append-only request-window compatibility:</strong> the first four-byte request row is exactly the original
|
|
<code>block_idx + want_mask</code> body. Old sources decode that row and ignore appended bytes. New sources queue all
|
|
rows. If a new fetcher meets an old source, the unserved tail rows eventually time out and are recovered as
|
|
ordinary single-row requests; the dirty flight then contracts. Old fetchers continue sending nine-byte
|
|
single-row requests, which new sources accept normally.</li>
|
|
<li><strong>Fragment-level requests (anti-deadlock + anti-congestion):</strong> <code>OTA_REQ</code>, <code>OTA_GET_MANIFEST</code>, and <code>OTA_QUERY</code>
|
|
carry fragment masks. For catalog discovery, <code>want_fragments</code> is a 32-bit bitmap and covers the protocol
|
|
maximum 255-row catalog (26 fragments at the current packet size). For block and manifest transfer, the
|
|
<code>want_mask</code> is 16 bits. A fetcher requests the full set on the first ask
|
|
(<code>(1<<nf)-1</code>, or <code>0xFFFF</code> before <code>frag_total</code> is known) and <strong>only the still-missing bits</strong> on any retry,
|
|
so recovering one lost fragment re-sends <em>one</em> fragment, not the whole block/manifest. This is essential on
|
|
half-duplex radios: re-requesting a whole multi-fragment burst let the periodic retry (a transmit) collide
|
|
with the tail of the in-flight burst and drop the same fragment forever - a hang. Requesting only the hole
|
|
removes the burst, so there is nothing to collide with. The block/manifest mask matches the 16-bit
|
|
reassembly bitmap (<=16 fragments/block; 1 KiB is 7 legacy or 6 raw-v2 fragments; 2 KiB is 13 legacy or 12
|
|
raw-v2 fragments). <code>OTA_PROOF</code> is a single packet and needs no mask.</li>
|
|
<li><strong>Data and proof remain separate packets, without a normal extra round trip.</strong> A server retains requested
|
|
blocks in a bounded descriptor queue, admits at most one response per main-loop pass, and sends one
|
|
<code>OTA_PROOF</code> after each block's requested fragments. Before admitting that proactive proof, the source leaves
|
|
an airtime/duty-aware 100-3000 ms RX turnaround gap. It accounts for one active transmission plus the two
|
|
paced-response queue credits. A legacy receiver uses the gap to send its immediate
|
|
<code>OTA_REQ_PROOF</code>; receiving that explicit request bypasses the remaining gap, avoiding a proof/request
|
|
collision and an otherwise multi-second legacy retry. A newer receiver uses an airtime/path-aware proof
|
|
grace (never less than 500 ms) and defers <code>OTA_REQ_PROOF</code> while any flight slot still expects DATA. Thus a
|
|
missing early proof cannot make the receiver transmit into the rest of a legitimate half-duplex response
|
|
train. After the complete-flight deadline, only one slot's missing fragments/proof is requested at a time.</li>
|
|
</ul>
|
|
<h3 id="85-sizing-against-max_packet_payload-184">8.5 Sizing against <code>MAX_PACKET_PAYLOAD = 184</code></h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>message</th>
|
|
<th>fixed overhead</th>
|
|
<th>payload/packet</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>OTA_DATA</code> legacy</td>
|
|
<td>9 B (type+mid4+idx2+off2)</td>
|
|
<td><code>OTA_FRAG_DATA = 160</code> -> 7 raw frags/1 KiB or 13/2 KiB</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_DATA</code> v2</td>
|
|
<td>13 B (legacy header + stream ID)</td>
|
|
<td><code>OTA_FRAG_DATA_V2 = 171</code> -> 6 raw frags/1 KiB or 12/2 KiB; fewer when compressed</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_MANIFEST</code></td>
|
|
<td>7 B</td>
|
|
<td><code>OTA_MF_FRAG = 176</code> -> signed manifest ~ 2 frags</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_HAVE</code></td>
|
|
<td>12 B</td>
|
|
<td>10 rows x 16 B per fragment</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>OTA_PROOF</code></td>
|
|
<td>8 B</td>
|
|
<td>up to ~44 sibling digests (>> any real tree)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p>A served mota supports up to 1024 leaves in the default 4 KiB proof scratch (about 2 MiB of payload at the
|
|
new 2 KiB default, or 1 MiB for a deployed 1 KiB container); larger self-images pass a bigger scratch buffer.</p>
|
|
<h3 id="86-temporary-radio-and-transfer-boundary">8.6 Temporary-radio and transfer boundary</h3>
|
|
<p>OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while
|
|
its <code>tempradio</code> window is actually running. A receiver selects missing blocks in serial order into a bounded
|
|
request flight. Every session starts with one block. A clean completed flight increases the next request by
|
|
one block; a flight requiring fragment/proof recovery halves the next width (<code>4 -> 2</code>, <code>3 -> 2</code>, <code>2 -> 1</code>).
|
|
The default compiled cap is two blocks; the RAK3401 LoRa-OTA target caps at four, so it probes
|
|
<code>1 -> 2 -> 3 -> 4</code>. All rows are sent in one backward-compatible <code>OTA_REQ</code>, and no freed slot is refilled
|
|
until the current flight is finished. It never serves
|
|
partial blocks. A normal install receiver never re-advertises its completed
|
|
download. An SD archive node is the deliberate exception: after a fully proof-verified container is published
|
|
to its persistent archive, it registers that complete file as a MotaSource and advertises it as a new seeder.
|
|
This keeps each active transfer as one transmitter and one receiver while still allowing active temporary-radio
|
|
repeaters between them and persistent archive nodes to improve future availability.</p>
|
|
<p>TempRadio is treated as a private maintenance network. Active transfer packets use priority 0, bypass the
|
|
public-flood receive holdoff, use the full transmit budget without overwriting the saved normal-radio airtime
|
|
factor, retain the relay role's airtime-scaled transmit collision window, and do not schedule generic flood
|
|
retries. Deployed firmware predating that TempRadio budget override can be accelerated manually with a saved
|
|
<code>get af</code> / temporary <code>set af 0</code> / restore sequence. The bounded serving
|
|
queue admits at most two DATA/PROOF packets ahead of the radio while preserving at least four free packet-pool
|
|
entries. CAD remains enabled to arbitrate the half-duplex channel, but its busy retry is scaled to one-quarter
|
|
of a packet airtime and clamped to 5-50 ms instead of the ordinary 120-360 ms cadence. Discovery traffic keeps
|
|
collision jitter and background priority. The fetch deadline uses the active radio's measured maximum-packet
|
|
airtime, remaining DATA/PROOF packet count, dispatcher airtime factor, and longest observed path (falling back
|
|
to the configured hop horizon before one is observed), with bounded guard time. Faster SF/BW settings
|
|
therefore recover loss sooner; slower or multi-hop settings do not spuriously re-request a response still on
|
|
air. These changes remove software waits and duplicate bursts; they do not remove the one required forwarding
|
|
transmission per hop.</p>
|
|
<hr />
|
|
<h2 id="9-identity-trust-versioning">9. Identity, trust & versioning</h2>
|
|
<ul>
|
|
<li><strong><code>target_id</code></strong> (4 B): <code>sha2-256:4(pio_env_name)</code> (little-endian uint32). The env name uniquely captures
|
|
hardware <strong>and</strong> role/partition, so a node auto-fetches only matching firmware (a companion image is not
|
|
fetched onto a repeater even though it shares <code>hw_id</code>). It is <strong>self-described in the firmware's EndF</strong>
|
|
(Section 2, written by <code>pio_endf.py</code>) and read via <code>ota_self_firmware()</code>, so it is correct on any build;
|
|
<code>-D MOTA_TARGET_ID</code> / <code>MainBoard::getOtaTargetId()</code> is the fallback when no EndF identity is present.
|
|
<code>tools/mota</code> reads it from the firmware's EndF (or <code>--target-env</code>). A manual <code>ota pull</code>/<code>want</code> can override
|
|
target (deliberate role switch); the <code>hw_id</code> brick-safety gate (Section 4) still applies at apply time.</li>
|
|
<li><strong><code>target_id</code> vs <code>hw_id</code></strong> - complementary, not redundant: <code>target_id</code> is the fetch-routing key
|
|
(hw + role + partition); <code>hw_id</code> is the human-readable brick-safety key (hardware only). Same board, two
|
|
roles => same <code>hw_id</code>, different <code>target_id</code>.</li>
|
|
<li><strong>Naming a <code>target_id</code> locally:</strong> only the 4-byte <code>target_id</code> ever travels on the wire. To show <em>which</em>
|
|
board/role a target is, a node (and <code>motatool</code>) reverse-looks-it-up in <code>src/helpers/ota/OtaTargets.h</code> -
|
|
a generated <code>target_id -> env-name</code> table covering every <code>ENABLE_OTA</code> env (<code>tools/mota/gen_targets.py</code>,
|
|
resolved from <code>pio project config</code>). So <code>ota ls</code> can render <code>[Heltec_v3_repeater]</code> for a neighbour's
|
|
beacon without the string being transmitted. Unknown IDs show as raw <code>hw XXXXXXXX</code> / <code>N/A</code> values.</li>
|
|
<li><strong><code>fw_version</code>:</strong> packed comparable uint32 (<code>MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre</code>); also self-described
|
|
in EndF. <code>ota ls</code> prints the stable eight-hex manifest ID and uses <code>[same target]</code>, <code>[unsupported]</code>, or
|
|
<code>[rescue]</code> after combining target equality with the local codec, bootloader, and EndF preflight. A known
|
|
different target is rendered by environment name; an unknown/unset target remains raw or <code>?</code>. Target
|
|
equality is routing information, not by itself an assertion that an image is safe to install.</li>
|
|
<li><strong><code>hw_id</code>:</strong> 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a <code>.mota</code>
|
|
whose <code>hw_id</code> differs from the device's own tag (empty on either side = permissive). Brick-safety
|
|
independent of signature.</li>
|
|
<li><strong>Signing & allowlist:</strong> a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded
|
|
in firmware; <code>ota key add/list/rm</code>). A <code>.mota</code> is eligible for <strong>auto-install</strong> only if signed by an
|
|
allowlisted key, the signature verifies, <code>image_hash</code> matches, and its nonzero signed <code>fw_version</code> is
|
|
strictly greater than the running hash-valid EndF version. Both catalog admission and final automatic
|
|
apply enforce the version floor, so a lying HAVE row cannot bypass it. Manual <code>ota install</code> is the explicit
|
|
equal-version/rollback override and generally permits unsigned packages,
|
|
but a package that claims to be signed must have a valid signature from an allowlisted key or it is rejected.
|
|
The removable-SD target is stricter: every application install needs a valid allowlisted signature because
|
|
the app mints an authenticated one-reset media authorization for OTAFIX.
|
|
<strong>Transfer needs no trust</strong> - blocks are content-addressed against the manifest's merkle root.</li>
|
|
<li><strong>Policies (persisted):</strong> <code>autofetch</code> in {off, any, signed} (default off) gates automatic block fetching of
|
|
own-target adverts; <code>autoinstall</code> in {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
|
|
allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs
|
|
without operator intent.</li>
|
|
<li><strong>Supersession:</strong> a newer version announced mid-download does not abort the in-progress transfer
|
|
(finish-current).</li>
|
|
</ul>
|
|
<hr />
|
|
<h2 id="10-multi-mota-serve-the-external-folder-relay">10. Multi-mota serve & the external "folder" relay</h2>
|
|
<p>A node serves a <strong>set</strong> of mOTAs: its own firmware plus, optionally, an external folder of <code>.mota</code> files it
|
|
relays without holding them in flash. To peers it simply "has N mOTAs"; the relay is trustless (fetchers
|
|
verify everything). The serve side (<code>OtaManager</code>) keeps a lightweight registry of what it advertises and two
|
|
resident "views": <code>view0</code> (its own firmware) and one on-demand view loaded from a source when a request
|
|
targets an external mota. Every fetch message carries <code>manifest_id</code>, so dispatch is a registry lookup.</p>
|
|
<p>The USB/TCP host-folder link can also be a <strong>pull destination</strong> (the reverse direction): <code>ota pull <mid8> folder</code>
|
|
fetches a <code>.mota</code> off the mesh and streams it onto the host as <code><mid>.mota</code> via the seeder STORAGE ops
|
|
(<code>OP_STAT/BEGIN/WRITE/SREAD/FIN</code>, see <code>MotaSeederProto.h</code>), using a <code>FolderMotaStore</code> as the fetch's
|
|
<code>OtaStore</code> instead of RAM/flash. This captures an exact copy of a device's firmware - e.g. to build a delta
|
|
against firmware you don't have. Resume is bookkeeping-free: <code>BEGIN</code> 0xFF-fills the file and, on reconnect
|
|
after a link drop (the fetch PAUSES, holding progress on the host - no RAM/flash fallback), <code>STAT</code>+<code>SREAD</code>
|
|
let the fetcher recompute and refill only the missing blocks.
|
|
The phone-oriented BLE link is deliberately source-only and does not register
|
|
a folder destination.</p>
|
|
<h3 id="101-the-motasource-abstraction-otasourceh">10.1 The <code>MotaSource</code> abstraction (<code>OtaSource.h</code>)</h3>
|
|
<p>Transport-agnostic provider of one or more complete <code>.mota</code> as random-access bytes. The same serve code
|
|
drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. - only <code>read()</code> differs.</p>
|
|
<pre><code class="language-cpp">struct MotaDesc { // catalog metadata + region offsets (no whole image in RAM)
|
|
uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags;
|
|
uint8_t block_size_log2, source_caps;
|
|
uint32_t total_size, leaves_off, block_count, payload_off, payload_size;
|
|
};
|
|
class MotaSource {
|
|
virtual uint8_t count(); // # mOTAs offered
|
|
virtual bool describe(uint8_t idx, MotaDesc& out); // metadata + offsets
|
|
virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes
|
|
virtual bool read_deflated_block(uint8_t idx, uint16_t block,
|
|
uint8_t* buf, uint16_t cap, uint16_t* len); // optional raw RFC 1951
|
|
};
|
|
</code></pre>
|
|
<p>To serve an external mota the node reads its manifest-minus-leaves + <code>leaves[]</code> into RAM (<=4 KiB for <=1024
|
|
blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.</p>
|
|
<h3 id="102-the-mota-seeder-transport-motaseederprotoh">10.2 The <code>mota-seeder</code> transport (<code>MotaSeederProto.h</code>)</h3>
|
|
<p>A <code>MotaSource</code> is fed by a host that serves a folder over the device's <strong>USB serial</strong> (the same console the
|
|
CLI uses - no extra hardware), on an ESP32 WiFi companion or FULL ESP32 role over <strong>WiFi (TCP)</strong>, or on an
|
|
nRF52 Full Companion over an encrypted <strong>BLE GATT</strong> service. The
|
|
host is the
|
|
standalone Rust tool <a href="https://github.com/vk496/motatool"><code>motatool</code></a> (<code>motatool serve --serial <port></code> /
|
|
<code>--tcp <host[:port]></code>, which also builds + verifies + inspects <code>.mota</code>). The device only emits request frames <em>while
|
|
actively serving a fetch</em>, and reads the reply synchronously, so over the shared USB console binary frames
|
|
coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:</p>
|
|
<pre><code>request (device -> host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)
|
|
response (host -> device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior)
|
|
|
|
OP_COUNT 0x01 args: - -> payload: count(1)
|
|
OP_DESCRIBE 0x02 args: idx(1) -> payload: MotaDesc wire (38 B)
|
|
OP_READ 0x03 args: idx(1) off(4) len(2) -> payload: len bytes
|
|
OP_DEFLATE_BLOCK 0x09 args: idx(1) block(2) off(2) len(2)
|
|
-> payload: total_encoded_len(2) + requested bytes
|
|
MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)
|
|
total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4)
|
|
block_size_log2(1) source_caps(1) reserved(2)
|
|
status: 0 = OK, non-zero = error (out of range / past EOF).
|
|
</code></pre>
|
|
<p><code>SerialMotaSource</code> splits logical reads into replies of at most 192 payload
|
|
bytes. A manifest's leaf table can exceed 256 bytes and new payload blocks are
|
|
normally 2 KiB; requesting either in one transaction can overrun common USB
|
|
CDC/UART receive rings even though the host successfully wrote the complete
|
|
reply. Chunking is internal to the transport and does not change <code>OP_READ</code> or
|
|
the <code>MotaSource</code> random-access contract.</p>
|
|
<p><code>OP_DEFLATE_BLOCK</code> lets a host-folder source perform the optional transport compression without linking an
|
|
encoder into the embedded seeder. <code>len=0, off=0</code> queries the exact encoded length; subsequent chunks are at
|
|
most 190 bytes, keeping the total response payload at 192 bytes. The host independently encodes each manifest
|
|
payload block as ordinary raw RFC 1951 at level 9. It returns an error for an invalid range, an unsupported
|
|
operation, or a result that is not smaller than the raw block; <code>SerialMotaSource</code> then serves the ordinary raw
|
|
v2 representation. A supporting host sets <code>source_caps bit 0</code> in every descriptor. Older hosts leave that
|
|
formerly-reserved byte zero, so upgraded firmware skips the optional request instead of waiting for an old
|
|
daemon that silently ignores unknown operations.</p>
|
|
<p>Manifest fragments are retained as bounded response jobs and admitted one at
|
|
a time. Their source-side gap follows the active maximum packet airtime and
|
|
dispatcher duty spacing, clamped to 100-1000 ms. The 100 ms floor protects
|
|
fast radios' TX-to-RX turnaround; the cap keeps the receiver's one-second
|
|
manifest progress/retry observation responsive. The source uses the same
|
|
radio-aware 100-3000 ms drain/turnaround gap before an unsolicited block proof,
|
|
but immediately serves a
|
|
legacy receiver's explicit <code>OTA_REQ_PROOF</code>. Relay collision delay is a separate
|
|
setting: active OTA floods honor the relay role's configured <code>txdelay</code>, and the
|
|
deployment runner temporarily uses <code>0.3</code> on managed relays.</p>
|
|
<p><strong>What to plug into <code>--serial</code>.</strong> Use the USB serial console of an OTA-enabled MeshCore node built with
|
|
<code>OTA_FOLDER_SERIAL</code>. The node must have a working LoRa radio plus an
|
|
<code>ota folder on</code> command; that command confirms it can host and advertise the folder. A <strong>KISS modem will not
|
|
work</strong>: KISS firmware exposes a TNC/KISS frame interface, not the MeshCore CLI and <code>mota-seeder</code>
|
|
request/response transport. An ESP32 WiFi companion or FULL ESP32 role with active WiFi is the alternative
|
|
source connection: use its dedicated seeder port with <code>motatool serve --tcp <host>:5001</code>.
|
|
An nRF52 Full Companion can instead pair with a phone or Linux host, subscribe
|
|
to its mOTA request characteristic, and use protocol-v14
|
|
<code>CMD_BLE_MOTA_SOURCE</code>. That BLE path is source-only; it does not expose the
|
|
reverse <code>FolderMotaStore</code> capture operations.</p>
|
|
<p>Device CLI: <code>ota folder on</code> (attach + announce), <code>ota folder</code> (list), <code>ota folder off</code>. Build flag
|
|
<code>OTA_FOLDER_SERIAL</code> (default stream = console <code>Serial</code>; override <code>OTA_FOLDER_SERIAL_STREAM</code> + define
|
|
<code>OTA_FOLDER_SERIAL_BEGIN</code> for a dedicated UART). ESP32 WiFi companions and FULL ESP32 roles run a
|
|
<code>WiFiServer</code> on the <strong>dedicated seeder port</strong> (<code>OTA_SEEDER_TCP_PORT</code>, default <code>5001</code>) while WiFi is usable.
|
|
On a companion it is separate from the app port (<code>TCP_PORT</code>, default <code>5000</code>); on infrastructure roles it is
|
|
separate from WebConfig and browser OTA on port 80. The node auto-attaches the source when a seeder client
|
|
connects and detaches when it closes (no <code>ota folder on</code> needed over TCP). An already-active serial folder
|
|
causes a TCP client to be rejected instead of silently replacing it. Verified on hardware: a RAK4631
|
|
relays a host folder to a
|
|
Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (<code>:5001</code>) while the companion serves a
|
|
phone on <code>:5000</code> - every block merkle-checked.</p>
|
|
<p>The attach reply and bare <code>ota folder</code> report <code>host=advertised/offered</code>. The registry is RAM-bounded
|
|
(<code>OTA_MAX_SERVE</code>, with the node's own firmware consuming one slot), so a host may correctly index more valid
|
|
files than this particular firmware can advertise. Omitted entries are now reported instead of silently
|
|
disappearing. Operators should split a large chain or use a higher-capacity/SD seeder when the two counts differ.</p>
|
|
<p><strong>Transport-agnostic by design.</strong> The request/response <em>semantics</em> (<code>COUNT</code> / <code>DESCRIBE(idx)</code> /
|
|
<code>READ(idx, off, len)</code> over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum +
|
|
resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a
|
|
reliable stream and the <strong>WiFi (TCP)</strong> transport reuses it as-is - both ends just treat the socket as a
|
|
byte stream (on-device, <code>SerialMotaSource</code> runs verbatim over an Arduino <code>Stream</code>-compatible <code>WiFiClient</code>;
|
|
<code>motatool</code>'s <code>TcpTransport</code> mirrors its <code>SerialTransport</code>). The nRF52 Full
|
|
Companion's <strong>BLE GATT</strong> path also reuses the exact frame and checksum. Device
|
|
requests are notifications on a dedicated characteristic and host responses
|
|
are ordered write-with-response fragments on a second characteristic. Keeping
|
|
the same framing makes retries and corruption handling identical across USB,
|
|
TCP, and GATT. The Linux reference implementation is
|
|
<code>tools/ble_mota/ble_mota_seeder.py</code>; a phone app can implement the same
|
|
transport-free catalog operations.</p>
|
|
<hr />
|
|
<h2 id="11-cli-surface-otaclicpp">11. CLI surface (<code>OtaCli.cpp</code>)</h2>
|
|
<p>User-facing OTA data should travel via <code>CMD_OTA_*</code> companion binary frames; the text CLI below is
|
|
debug/operator oriented and replies are <code>snprintf</code>-bounded into a 160-byte buffer.</p>
|
|
<p>Commands take intuitive aliases (matched by the first word; see <code>is_cmd</code> in <code>OtaCli.cpp</code>) so they're easy
|
|
to type and read - <code>status</code>/<code>neighbors</code>/<code>pull</code>/<code>drop</code>/<code>applydelta</code> are the canonical names, the aliases are
|
|
the recommended user-facing forms. Output is plain-language (a user-facing guide lives at
|
|
<a href="../ota_user_guide/">ota_user_guide.md</a>).</p>
|
|
<pre><code>ota help | ? | h list the commands
|
|
ota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys
|
|
ota ls | neighbors | nbrs | updates | n [page] paged updates (queries sources; rows arrive async via OTA_HAVE)
|
|
ota get | pull | download <mid8|#index> flash [rescue] | folder [validate]
|
|
fetch by stable mid8 (preferred) or current page index
|
|
ota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader
|
|
ota rescue install <base_hash16> internal-flash nRF52 only: recover from failed app-side EndF validation
|
|
ota bootloader [status] capable allowlisted nRF52 repeater: installed BL identity/caps + staged confirmation
|
|
ota bootloader install <MID8> <HASH16>
|
|
explicitly verify/arm one complete trusted v3 package; never automatic
|
|
ota cancel | drop | stop drop the fetch; durably invalidate device staging, or retain a folder partial for resume
|
|
ota announce | adv serve self + send a beacon now
|
|
ota self | id print this firmware's EndF (body/image size, base_hash)
|
|
ota qspi | storage QSPI nRF52 only: JEDEC/SR1/stage/latched storage error (read-only)
|
|
ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list
|
|
ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy
|
|
ota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list
|
|
ota dev ... bring-up helpers (stage/recv/serve/resume <MID8>/verify)
|
|
</code></pre>
|
|
<p>For a device-backed pull, current firmware returns success only after the
|
|
persistent store can no longer be reopened; flash/SD/QSPI I/O or readback
|
|
failure is reported as an error even though the in-memory manager session was
|
|
dropped. For a <code>folder</code> pull, cancellation detaches the live transfer but
|
|
deliberately leaves the host <code>.part</code> file available for a later resume.
|
|
If the shared receive engine is currently performing the MeshTower SD
|
|
auto-archive capture, cancellation likewise detaches that archive transfer and
|
|
retains its <code>.part</code> file; it does not erase the unrelated manual-install store.</p>
|
|
<hr />
|
|
<p><a id="12-apply-bootloader-contract"></a></p>
|
|
<h2 id="12-apply-bootloader-contract">12. Apply & bootloader contract</h2>
|
|
<ul>
|
|
<li><strong>ESP32 (A/B):</strong> applied in-firmware via the detools decoder into the inactive OTA slot
|
|
(<code>OtaApply.cpp::ota_apply_detools_mota</code> + <code>OtaStoreFlashEsp32</code>), then set-boot + reboot (power-safe,
|
|
rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096).</li>
|
|
<li><strong>nRF52 (single-slot):</strong> the running firmware <strong>never</strong> flashes the app. <code>ota install</code> verifies the
|
|
container fully (<code>image_hash</code>, codec, signature/allowlist, <code>hw_id</code>, and <code>base_hash</code> for a delta), writes
|
|
<code>approval = "APRV"</code>, then reboots into the modified bootloader
|
|
(<code>Adafruit_nRF52_Bootloader_OTAFIX</code>). The bootloader:</li>
|
|
<li>locates the staged <code>.mota</code> in the approved internal, raw-SD, or raw-QSPI store without trusting an
|
|
unchecked stored size,</li>
|
|
<li>re-checks <code>TRAILER</code>, <code>image_hash</code>, and <code>approval == "APRV"</code>; for a delta it also checks that
|
|
<code>base_hash</code> equals the running firmware's <code>EndF.body_hash</code> (recomputed by scanning for <code>EndF</code> - never
|
|
trust <code>bank_0_size</code>),</li>
|
|
<li>writes a full external-media payload or applies the in-place codec over the app region, then boots only
|
|
if the result hashes to <code>image_hash</code>.</li>
|
|
<li><strong>nRF52 internal staging ceiling:</strong> an internal-store application derives the ceiling from facts available
|
|
in every build, not a board-name list. A companion that actually links the internal ExtraFS datastore stays
|
|
below <code>0xD4000</code>; a default linker region or a role that does not mount ExtraFS can reclaim the unused
|
|
100 KiB through <code>0xED000</code>. A qualified internal bootloader-self-update target keeps that normal flash
|
|
ceiling and does not reserve a second boot-package or flash-scratch region, but it uses a dedicated
|
|
application linker that reserves the top 64 KiB of SRAM (<code>0x20030000..0x20040000</code>) plus a 72-byte
|
|
retained authorization record after the existing persistent clock bytes. Application deltas larger than
|
|
one flash page use a deterministic page-aligned flash prefix ending at <code>0xED000</code> and place up to 64 KiB
|
|
of the logical container tail in that SRAM. Packages of one page or less, and bootloader-update packages,
|
|
remain wholly in flash. Hybrid staging is enabled only when the installed bootloader contains exactly one
|
|
valid <code>MOTARAMA</code> capability marker; otherwise a larger hybrid-profile fetch fails before erase. Immediately
|
|
before a software reset the application publishes a valid-last <code>MOTAHYB1</code> record binding the split geometry
|
|
and normalized container hash. The bootloader consumes it once and rejects power-loss, stale-reset, corrupt,
|
|
or mismatched-RAM cases before its first application write. A hybrid transfer cannot resume after an
|
|
application restart because its suffix is deliberately volatile. The bootloader treats every unknown/legacy
|
|
GPREGRET2 handoff value as <code>0xD4000</code>, and accepts a flash-only container only at the bottom-aligned position
|
|
for the selected ceiling.</li>
|
|
<li><strong>nRF52 dynamic apply window:</strong> the post-build hook records the resolved app base, linked app end,
|
|
internal-ExtraFS/SD/QSPI/hybrid-RAM storage flags, and desired staging ceiling immediately before <code>EndF</code>.
|
|
<code>motatool</code> reads that authenticated firmware record and chooses <code>memory_size</code> from the actual patch size
|
|
and bottom-aligned stage address. For a hybrid base it charges only the deterministic flash prefix against
|
|
that workspace; the retained-RAM suffix is still part of the same hashed logical container.
|
|
Firmware without the record retains the conservative <code>0x98000</code> default. Before writing <code>APRV</code>, an
|
|
internal-store app validates the staged-address bound; an external SD/QSPI app validates the full detools
|
|
geometry against the application workspace. The bootloader independently parses and validates the same
|
|
geometry before its first application write. Expanded auto-sized packages require a bootloader
|
|
with the ceiling-handoff capability; use <code>--inplace-memory 0x98000</code> when intentionally targeting an older
|
|
bootloader and the images still fit that window.</li>
|
|
<li><strong>nRF52 EndF rescue:</strong> <code>ota rescue install <base_hash16></code> is a pre-provisioned recovery path for an
|
|
internal-flash nRF52 application that still runs but cannot validate its own EndF identity. It refuses
|
|
when normal EndF validation succeeds, requires the operator hash to exactly equal the staged delta's
|
|
<code>base_hash</code>, requires the package <code>target_id</code> to match and its <code>hw_id</code> to pass the normal hardware gate,
|
|
and retains the normal payload and signature/allowlist gates. Approval only delegates the base decision:
|
|
OTAFIX independently
|
|
locates the physical EndF, hashes the running app, and compares that value with the package before its
|
|
first app write. A physically absent/corrupt EndF or wrong base therefore returns to the unchanged app;
|
|
it still requires USB recovery if that app does not already contain this command.
|
|
A chain intended to cross historical firmware must introduce this command in its first bridge and retain
|
|
it in every later bridge. Manual pulls still use the build-provided target ID when app-side EndF parsing
|
|
fails, so a rescue-capable bridge can fetch its exact successor before invoking the guarded command. Such
|
|
a node must acknowledge the condition up front with <code>ota pull <mid8> flash rescue</code>; an ordinary flash pull
|
|
refuses before altering staged data. Firmware that predates both rescue commands still requires USB recovery.
|
|
Internal-bootloader-self-update builds are a stricter exception: because their ordinary linker may extend
|
|
through <code>0xED000</code>, an absent/corrupt live <code>EndF</code> disables every internal staging pull before erase instead
|
|
of trusting the legacy 608 KiB estimate.</li>
|
|
<li><strong>MeshTower V2 SD nRF52:</strong> the application stores a contiguous <code>/meshcore-ota.mota</code> on microSD. After
|
|
authenticating one exact signed manifest and verifying the leaves/payload/image, it hashes the exact full
|
|
container with only <code>APRV</code> normalized to zero and publishes a 72-byte reset-retained <code>MOTASDA2</code> record.
|
|
That record binds app-vs-boot purpose, format, raw sector range, total/card geometry, and normalized digest;
|
|
OTAFIX consumes and clears it before reading the card. There is no normal sector-1 handoff or additional
|
|
OTA-specific partition-layout requirement beyond what the bundled SdFat can mount. The matching
|
|
bootloader reads the authorized sectors without mounting FAT, supports either a full image or an in-place delta,
|
|
verifies the staged/full result hash, and never writes through <code>0xED000</code> where InternalFS begins. The
|
|
exact SD repeater also accepts a manually selected, signed v3 bootloader package when installed and
|
|
candidate markers are exactly <code>0x09</code> (<code>SD|BOOT_UPDATE</code>). MeshCore streams the same strict identity,
|
|
CRC, vector, signature, MID/hash-confirmation, and complete-image checks from the SD file. GPREGRET
|
|
<code>0x6B</code> plus GPREGRET2 <code>0x53</code> selects this privileged path. Both MeshCore and OTAFIX require a hash-valid
|
|
live <code>EndF</code> ending by <code>0xE0000</code>; when a nonzero boot-settings bank CRC is active, its recorded size must
|
|
also cover that complete live image and stop by <code>0xE0000</code>. For fmt3 MeshCore additionally writes a readback-checked
|
|
<code>MOTASDBL</code> token at <code>0xE0000</code> containing the exact total and signed <code>image_hash</code>. OTAFIX binds the parsed manifest,
|
|
streamed payload, and final scratch image to that
|
|
token, so a removable-media change can only fail closed. OTAFIX then uses <code>0xE0000..0xEA000</code> as temporary scratch; the normal
|
|
application linker remains at <code>0xED000</code> and ordinary application updates do not inherit this scratch
|
|
headroom restriction. Both fmt2 application apply and fmt3 bootloader apply require installed BLM2
|
|
continuity matching the live S140 FWID/application layout. Preview.12 must be upgraded locally over
|
|
USB/BLE DFU or SWD. MeshCore never writes a raw sector-1 handoff.</li>
|
|
<li><strong>Matched external-QSPI nRF52 repeaters:</strong> the application reserves the board's dedicated QSPI NOR as a
|
|
raw store beginning at offset zero. It obtains a 1-16 MiB capacity from JEDEC, checkpoints payload before
|
|
leaf metadata, and verifies each erased/programmed page. GPREGRET2 <code>0x51</code> selects QSPI only when the
|
|
matching bootloader advertises the QSPI storage bit; legacy markers retain the internal scan path. The
|
|
bootloader pre-hashes a full payload before invalidating the app, or applies an in-place delta with the
|
|
complete internal application region as workspace. Companion builds never enable this raw store: some use
|
|
QSPI as a filesystem, while others simply leave that chip outside OTA ownership. See
|
|
<a href="../ota_nrf52_qspi/">the nRF52 QSPI guide</a>.</li>
|
|
<li><strong>XIAO bootloader self-update (explicit only):</strong> selected XIAO-module QSPI repeater builds link the
|
|
ordinary application below <code>0xE0000</code>, reserving <code>0xE0000..0xEA000</code> as a 40 KiB internal scratch bank.
|
|
They accept a v3 bootloader package only through an exact manual MID pull. Ordinary <code>ota install</code>,
|
|
autofetch, autoinstall, and every application apply backend reject it. The operator then copies the
|
|
staged package's exact values from <code>ota bootloader</code> into
|
|
<code>ota bootloader install <MID8> <HASH16></code>. The app repeats the strict v3 geometry, installed/candidate
|
|
identity, vectors, embedded CRC/capabilities, complete payload/image hashes, signature, and trusted-key
|
|
gates before writing <code>APRV</code>. GPREGRET <code>0x6B</code> plus GPREGRET2 <code>0x51</code> hands the QSPI package to OTAFIX.
|
|
<code>APRV</code> carries the app's authenticated and explicitly confirmed authorization decision; OTAFIX does not
|
|
repeat Ed25519/allowlist, Merkle, or typed operator confirmation. It independently rechecks the strict v3
|
|
structure, canonical identity/capabilities, vectors, payload SHA, embedded CRC, and scratch/copy hashes,
|
|
uses the scratch bank to preserve the running application while replacing <code>0xF4000..0xFE000</code>, and reports
|
|
boot-update results in GPREGRET2 <code>0xC0..0xCF</code>
|
|
(<code>0xC8</code> success). This mechanism cannot bootstrap a stock/older bootloader; install an ABI-3,
|
|
boot-update-capable exact-board OTAFIX over USB/BLE DFU or SWD once first.</li>
|
|
<li><strong>Internal-flash bootloader self-update (explicit only):</strong> curated nRF52840 lean repeater/bridge targets
|
|
without an OTA-owned SD/QSPI store share the normal bottom-aligned internal store below <code>0xED000</code>.
|
|
It holds either an ordinary app delta or the exact 41,330-byte v3 container, never both. The boot package
|
|
bottom-aligns at <code>0xE2000</code>; a hash-valid live <code>EndF</code> must prove the complete running image ends at or below
|
|
that address before the first erase. OTAFIX reads each source window before erasing and compacts the
|
|
payload forward in the same eleven pages to raw <code>0xE2000..0xEC000</code>; no separate flash scratch bank exists.
|
|
The qualified application linker reserves a 64 KiB SRAM arena only for hybrid application deltas; the
|
|
bootloader package remains wholly in flash. GPREGRET <code>0x6B</code> plus GPREGRET2 <code>0xED</code> selects boot update,
|
|
while ordinary app apply uses GPREGRET <code>0x6A</code> plus the same storage source. Exact installed/candidate
|
|
capability flags are <code>0x0A</code> (<code>STAGE_CEILING|BOOT_UPDATE</code>). Ordinary deltas remain dynamically sized, may start below
|
|
<code>0xE2000</code>, and reconstruct only below the normal <code>0xED000</code> app ceiling. The same signature, explicit
|
|
confirmation, exact identity, vector, CRC, and single-marker rules as the XIAO path apply. Bootloader FULL
|
|
admission is isolated from ordinary application FULL policy, and privileged partials are never resumed
|
|
automatically after an application reboot. See
|
|
<a href="../ota_nrf52_bootloader_update/">the nRF52 bootloader-update guide</a> for the exact target inventory.</li>
|
|
</ul>
|
|
<p>A signature, when present, proves author authenticity and must pass the device allowlist. Unsigned v2
|
|
application packages remain installable when local policy permits them. A v3 bootloader package is always
|
|
signed and trusted. The one-shot <code>approval</code> marker records local consent before the bootloader may apply it.</p>
|
|
<blockquote>
|
|
<p><strong>Bootloader testing note:</strong> always test apply with a <em>real different</em> image (base != target). A same-image
|
|
(X->X) "delta" trivially reproduces the target and gives a false positive.</p>
|
|
</blockquote>
|
|
<hr />
|
|
<h2 id="13-versioning-of-this-spec">13. Versioning of this spec</h2>
|
|
<p>The fixed byte layout has two intentionally disjoint profiles: <code>format_ver = 2</code> for ordinary application
|
|
packages and <code>format_ver = 3</code> only for the exact privileged bootloader profile in Section 4.3. A parser must
|
|
reject v2+BOOTLOADER, v3 without exact <code>FULL|SIGNED|BOOTLOADER</code>, and every other version. The multihash
|
|
<code>hash_algo</code> separately allows swapping the digest family without a format bump. Unknown <code>codec_id</code> /
|
|
<code>ota_msg_type</code> values are ignored (a node simply will not fetch what it cannot apply).</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</article>
|
|
</div>
|
|
|
|
|
|
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer class="md-footer">
|
|
|
|
<div class="md-footer-meta md-typeset">
|
|
<div class="md-footer-meta__inner md-grid">
|
|
<div class="md-copyright">
|
|
|
|
|
|
Made with
|
|
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
|
Material for MkDocs
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
<div class="md-dialog" data-md-component="dialog">
|
|
<div class="md-dialog__inner md-typeset"></div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": ["content.action.edit", "content.code.copy", "search.highlight", "search.suggest"], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
|
|
|
|
|
|
<script src="../assets/javascripts/bundle.d7400e89.min.js"></script>
|
|
|
|
<script src="../_javascript/firmware_picker.js"></script>
|
|
|
|
<script src="../_javascript/telemetry_decoder.js"></script>
|
|
|
|
<script src="../_javascript/filter_tool.js"></script>
|
|
|
|
|
|
</body>
|
|
</html> |