meshtastic 0.0.179 → 0.0.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/documentation/README.md +3 -2
- data/documentation/admin-channel.md +64 -0
- data/documentation/admin-config.md +59 -0
- data/documentation/admin-firmware-nordic.md +86 -0
- data/documentation/admin-firmware-serial.md +180 -0
- data/documentation/admin-firmware.md +126 -0
- data/documentation/admin.md +88 -35
- data/documentation/atak.md +93 -10
- data/documentation/channel.md +1 -28
- data/documentation/config.md +1 -27
- data/documentation/mesh-interface.md +7 -0
- data/documentation/meshtastic.md +2 -2
- data/lib/meshtastic/admin/channel.rb +164 -0
- data/lib/meshtastic/admin/config.rb +213 -0
- data/lib/meshtastic/admin/firmware/ble.rb +207 -0
- data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
- data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
- data/lib/meshtastic/admin/firmware.rb +288 -0
- data/lib/meshtastic/admin.rb +688 -34
- data/lib/meshtastic/apponly.rb +19 -7
- data/lib/meshtastic/atak.rb +295 -17
- data/lib/meshtastic/bluetooth/bluez.rb +20 -1
- data/lib/meshtastic/bluetooth.rb +70 -59
- data/lib/meshtastic/cannedmessages.rb +13 -6
- data/lib/meshtastic/channel.rb +3 -25
- data/lib/meshtastic/clientonly.rb +19 -7
- data/lib/meshtastic/config.rb +3 -24
- data/lib/meshtastic/config_pb.rb +2 -1
- data/lib/meshtastic/connection_status.rb +12 -5
- data/lib/meshtastic/deviceonly.rb +19 -7
- data/lib/meshtastic/localonly.rb +19 -7
- data/lib/meshtastic/mesh_interface.rb +19 -0
- data/lib/meshtastic/module_config.rb +13 -7
- data/lib/meshtastic/mqtt.rb +72 -35
- data/lib/meshtastic/paxcount.rb +17 -6
- data/lib/meshtastic/portnums.rb +13 -6
- data/lib/meshtastic/position.rb +16 -6
- data/lib/meshtastic/remote_hardware.rb +27 -11
- data/lib/meshtastic/rtttl.rb +17 -8
- data/lib/meshtastic/serial.rb +81 -70
- data/lib/meshtastic/storeforward.rb +13 -6
- data/lib/meshtastic/storeforward_pb.rb +1 -1
- data/lib/meshtastic/stream_interface.rb +11 -0
- data/lib/meshtastic/tcp.rb +73 -22
- data/lib/meshtastic/telemetry.rb +15 -6
- data/lib/meshtastic/traceroute.rb +15 -6
- data/lib/meshtastic/util.rb +3 -1
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic/xmodem.rb +12 -5
- data/lib/meshtastic.rb +22 -3
- data/spec/conventions_spec.rb +321 -0
- data/spec/lib/meshtastic/admin/channel_spec.rb +202 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
- data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
- data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
- data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
- data/spec/lib/meshtastic/admin_spec.rb +412 -1
- data/spec/lib/meshtastic/apponly_spec.rb +1 -1
- data/spec/lib/meshtastic/atak_spec.rb +129 -4
- data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
- data/spec/lib/meshtastic/channel_spec.rb +2 -20
- data/spec/lib/meshtastic/config_spec.rb +2 -20
- data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
- data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
- data/spec/lib/meshtastic/localonly_spec.rb +1 -1
- data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
- data/spec/lib/meshtastic/portnums_spec.rb +2 -2
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27355386b939c892e3fb2bc5ab98181fa0a02995663fadd66021153f9fbbf219
|
|
4
|
+
data.tar.gz: 5de937dd2654b8bbb7b8a5859251a63de35cef66f3fd6304bb714f3e6344d0ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 718d1b5cf1fe122909636032011da2ebe28271305f01bf2ea44a50b003780299bdc851a2c68029e599982d270439c7062b2b8504494671bfc9775413457caca5
|
|
7
|
+
data.tar.gz: 5b8435e2d71adcb7a3634b758ca8678a89774b8ba62f3e3868cb2ffca265f7b4369421e417689a551f97443c80b73fe70d0c8044fb53b842ab45374964a821f4
|
data/Gemfile
CHANGED
|
@@ -6,13 +6,13 @@ source 'https://rubygems.org'
|
|
|
6
6
|
|
|
7
7
|
gemspec
|
|
8
8
|
|
|
9
|
-
gem 'bundler', '>=4.0.
|
|
9
|
+
gem 'bundler', '>=4.0.21'
|
|
10
10
|
gem 'bundler-audit', '>=0.9.3'
|
|
11
11
|
gem 'executable-hooks', '1.7.1'
|
|
12
12
|
gem 'gem-wrappers', '1.4.0'
|
|
13
13
|
gem 'geocoder', '1.8.6'
|
|
14
14
|
gem 'google-protobuf', '4.36.1'
|
|
15
|
-
gem 'grpc-tools', '1.
|
|
15
|
+
gem 'grpc-tools', '1.84.0'
|
|
16
16
|
gem 'mqtt', '0.7.0'
|
|
17
17
|
gem 'rake', '13.4.2'
|
|
18
18
|
gem 'rdoc', '7.0.4'
|
data/documentation/README.md
CHANGED
|
@@ -28,8 +28,9 @@ Do not open Serial and Bluetooth to the same radio at once. Always `disconnect`
|
|
|
28
28
|
## Feature modules
|
|
29
29
|
|
|
30
30
|
- [Meshtastic::Admin](admin.md)
|
|
31
|
-
- [Meshtastic::Channel](channel.md)
|
|
32
|
-
- [Meshtastic::Config](config.md)
|
|
31
|
+
- [Meshtastic::Admin::Channel](admin-channel.md)
|
|
32
|
+
- [Meshtastic::Admin::Config](admin-config.md)
|
|
33
|
+
- [Meshtastic::Admin::Firmware](admin-firmware.md)
|
|
33
34
|
- [Meshtastic::ModuleConfig](module-config.md)
|
|
34
35
|
- [Meshtastic::Position](position.md)
|
|
35
36
|
- [Meshtastic::Telemetry](telemetry.md)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Meshtastic::Admin::Channel
|
|
2
|
+
|
|
3
|
+
Build, read, write, and share mesh channels using `ADMIN_APP`. The generated protobuf remains `Meshtastic::Channel`. All public methods accept one options Hash.
|
|
4
|
+
|
|
5
|
+
## Channel operations
|
|
6
|
+
|
|
7
|
+
- `build_settings(settings:, ...)` copies an optional `ChannelSettings` protobuf or field Hash and overlays explicit settings without mutating the input. Supports **every bundled field**: `channel_num`, raw-byte `psk`, `name`, `id`, `uplink_enabled`, `downlink_enabled`, `module_settings`, `use_aead`. Nested `module_settings` accepts a `Meshtastic::ModuleSettings` or Hash, including `position_precision` and `is_muted`. Explicit `false` clears booleans.
|
|
8
|
+
- `build(channel:, index:, role:, settings:, ...)` copies an optional Channel protobuf, preserving its settings and overlaying supplied values. Defaults to slot zero and protobuf role `:DISABLED`; choose `:PRIMARY` or `:SECONDARY` explicitly when enabling a channel.
|
|
9
|
+
- `get(index:)` requests a **zero-based** slot (default 0). `Admin.get_channel` converts to the protocol's **one-based request**: slot 0 sends `get_channel_request = 1`. Response and set-channel indexes remain zero-based. Never add one yourself.
|
|
10
|
+
- `set(channel:, index:, role:, settings:, ...)` builds/validates a Channel and sends it through Admin. A supplied Channel can be overlaid with explicit settings. Slots must be Integers from 0 through 7; numeric strings and fractional indexes are rejected. Unsupported roles, invalid PSK lengths, and overlong names are rejected before sending.
|
|
11
|
+
- `help` / `authors` show usage and attribution.
|
|
12
|
+
|
|
13
|
+
Channel names must occupy fewer than 12 UTF-8 bytes. PSKs accept 0, 1, 16, or 32 raw bytes; they are **not** hex/base64 strings. Empty keys disable encryption; one-byte keys are public shorthand keys, not secure random keys. `channel_num` is deprecated in ChannelSettings: configure frequency selection with `Admin::Config.set_lora` instead. AEAD is experimental and peers must agree on its use.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
settings = Meshtastic::Admin::Channel.build_settings(
|
|
17
|
+
name: 'Example', psk: "\x01".b,
|
|
18
|
+
uplink_enabled: false,
|
|
19
|
+
module_settings: { position_precision: 13, is_muted: false }
|
|
20
|
+
)
|
|
21
|
+
Meshtastic::Admin::Channel.set(
|
|
22
|
+
serial_obj: serial_obj, index: 1, role: :SECONDARY, settings: settings
|
|
23
|
+
)
|
|
24
|
+
Meshtastic::Admin::Channel.get(serial_obj: serial_obj, index: 1)
|
|
25
|
+
|
|
26
|
+
# Explicitly disable a slot.
|
|
27
|
+
Meshtastic::Admin::Channel.set(serial_obj: serial_obj, index: 1, role: :DISABLED)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Admin transport/routing/authentication options pass through (`serial_obj`, `bluetooth_obj`, `tcp_obj`, `mqtt_obj`, `to`, `from`, `session_passkey`, etc.). Here `channel:` means a **Channel protobuf**, not the outgoing mesh transport channel selector; it is removed before delivery. Use the lower-level `Admin.set_channel(channel_settings: protobuf, channel: numeric_index, ...)` when a particular transport channel is necessary.
|
|
31
|
+
|
|
32
|
+
## Channel URLs
|
|
33
|
+
|
|
34
|
+
Official clients share a protobuf `Meshtastic::ChannelSet` using unpadded URL-safe Base64 in the fragment of `https://meshtastic.org/e/#...` (legacy `/d/#...` imports are also accepted).
|
|
35
|
+
|
|
36
|
+
- `export_url(channels:, lora_config:, include_all:)` is offline. Requires exactly one primary, puts it first, sorts secondary channels by slot, and omits disabled slots. `include_all: false` exports only the primary; default includes secondaries. An optional LoRaConfig protobuf/Hash is included without fetching hardware. Up to eight enabled channels are accepted.
|
|
37
|
+
- `import_url(url:)` is offline and returns the decoded ChannelSet, preserving settings and optional LoRaConfig. Accepts padded or unpadded Base64 from HTTPS `meshtastic.org` e/d URLs only, with no userinfo or alternate port. Malformed protobuf/Base64, missing fragments, and zero or more than eight settings are rejected with a generic error that does not echo the secret URL. It does not assign device slots or modify a radio.
|
|
38
|
+
- `apply_url(url:, transport_options...)` explicitly writes imported settings into consecutive slots starting at zero: first PRIMARY, remaining SECONDARY. All channel settings are validated before the first write. Optional LoRa config is written last; absent LoRa config is left unchanged. Returns the individual transport submission results. **Existing higher slots are left untouched**, matching the official Python client's replacement behavior. Add-only/query links are rejected before transmission; for adding channels, import offline, inspect existing slots, and call `set` with explicitly chosen secondary slots.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# channels and lora_config are protobufs already obtained from the node.
|
|
42
|
+
url = Meshtastic::Admin::Channel.export_url(
|
|
43
|
+
channels: channels, lora_config: lora_config
|
|
44
|
+
)
|
|
45
|
+
channel_set = Meshtastic::Admin::Channel.import_url(url: url)
|
|
46
|
+
|
|
47
|
+
# Only when replacement of slots from zero is intended:
|
|
48
|
+
Meshtastic::Admin::Channel.apply_url(serial_obj: serial_obj, url: url)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**URLs contain channel keys.** Treat them as credentials: do not log, publish, or send them to third-party QR services. Export does not redact PSKs. A URL carries settings, not original slot indexes/roles: its first entry becomes primary when applied.
|
|
52
|
+
|
|
53
|
+
## Operational limits
|
|
54
|
+
|
|
55
|
+
Writes are submissions, not delivery/persistence acknowledgements. No live hardware was exercised. No automatic reply collection, session-key acquisition, readback, edit transaction, rollback, or add-only merge is performed. URL application can partially succeed if transport or firmware fails mid-sequence, and changing LoRa/primary settings can disconnect remote administration. For radio use, manage edit transactions and ACK/readback through Admin as appropriate for the firmware; verify slots and LoRa configuration after writing.
|
|
56
|
+
|
|
57
|
+
## Protocol evidence
|
|
58
|
+
|
|
59
|
+
- [Official Admin schema](https://github.com/meshtastic/protobufs/blob/master/meshtastic/admin.proto): `get_channel_request` is index + 1.
|
|
60
|
+
- [Official Channel schema](https://github.com/meshtastic/protobufs/blob/master/meshtastic/channel.proto): roles, settings, PSK/name constraints, deprecated channel_num.
|
|
61
|
+
- [Official app-only schema](https://github.com/meshtastic/protobufs/blob/master/meshtastic/apponly.proto): ChannelSet contains primary-first settings, secondaries, and optional LoRa config.
|
|
62
|
+
- [Official Python node client](https://github.com/meshtastic/python/blob/master/meshtastic/node.py): `_requestChannel`, `getURL`, and `setURL` establish index and sharing/application semantics. Python is reference evidence only; this implementation is Ruby.
|
|
63
|
+
|
|
64
|
+
Related: [Admin](admin.md), [Config](admin-config.md).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Meshtastic::Admin::Config
|
|
2
|
+
|
|
3
|
+
Admin requests and writes for every section in the bundled `Meshtastic::Config` protobuf. Public methods take a single options Hash; `help` describes the options. The generated protobuf remains `Meshtastic::Config`.
|
|
4
|
+
|
|
5
|
+
## Operations
|
|
6
|
+
|
|
7
|
+
| Section | Read | Write | Value type |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| Device | `get_device` | `set_device(device:)` | `Config::DeviceConfig` or field Hash |
|
|
10
|
+
| Position | `get_position` | `set_position(position:)` | `Config::PositionConfig` or field Hash |
|
|
11
|
+
| Power | `get_power` | `set_power(power:)` | `Config::PowerConfig` or field Hash |
|
|
12
|
+
| Network | `get_network` | `set_network(network:)` | `Config::NetworkConfig` or field Hash |
|
|
13
|
+
| Display | `get_display` | `set_display(display:)` | `Config::DisplayConfig` or field Hash |
|
|
14
|
+
| LoRa | `get_lora` | `set_lora(lora:)` | `Config::LoRaConfig` or field Hash |
|
|
15
|
+
| Bluetooth | `get_bluetooth` | `set_bluetooth(bluetooth:)` | `Config::BluetoothConfig` or field Hash |
|
|
16
|
+
| Security | `get_security` | `set_security(security:)` | `Config::SecurityConfig` or field Hash |
|
|
17
|
+
| Session key | `get_sessionkey` | Request-only; writes raise `ArgumentError` | Empty `Config::SessionkeyConfig` placeholder |
|
|
18
|
+
| Device UI | `get_device_ui` | `set_device_ui(device_ui:)` | `Meshtastic::DeviceUIConfig` or field Hash |
|
|
19
|
+
|
|
20
|
+
- `get(config_type:)` is the raw ConfigType request (default `:DEVICE_CONFIG`). Supported types are `:DEVICE_CONFIG`, `:POSITION_CONFIG`, `:POWER_CONFIG`, `:NETWORK_CONFIG`, `:DISPLAY_CONFIG`, `:LORA_CONFIG`, `:BLUETOOTH_CONFIG`, `:SECURITY_CONFIG`, `:SESSIONKEY_CONFIG`, and `:DEVICEUI_CONFIG`.
|
|
21
|
+
- `set(config:)` accepts a `Meshtastic::Config` with exactly one selected section; an absent/empty config is rejected before transmission. All fields of the section, including nested/repeated fields, are supported by the generated protobuf, not a hand-picked subset.
|
|
22
|
+
- Device UI uses **`get_ui_config_request` / `store_ui_config`**, not the firmware's no-op Config device-UI handling. Generic `set` also routes a `device_ui` section correctly. The raw `get(config_type: :DEVICEUI_CONFIG)` remains a raw enum request; use `get_device_ui` for useful UI data.
|
|
23
|
+
- `SessionkeyConfig` is empty and request-only. The authorization bytes come from **`AdminMessage.session_passkey`**, not the Config payload. Both `set_sessionkey` and generic `set` reject this non-writable section.
|
|
24
|
+
- `help` / `authors` provide usage and attribution.
|
|
25
|
+
|
|
26
|
+
ModuleConfig is a different protobuf: use `Admin.get_module_config` / `Admin.set_module_config` for MQTT, telemetry, and other module configuration.
|
|
27
|
+
|
|
28
|
+
## Transport and authorization
|
|
29
|
+
|
|
30
|
+
Read/write helpers pass through Admin options, including `serial_obj`, `bluetooth_obj`, `tcp_obj`, `mqtt_obj`, `to`, `from`, numeric transport `channel`, `want_ack`, `want_response`, `hop_limit`, and `session_passkey`. Use a supported connected transport. Session authentication and remote-node routing follow [Admin](admin.md).
|
|
31
|
+
|
|
32
|
+
A return value means transport submission, **not confirmed persistence**. These methods do not wait for a reply, automatically acquire a session key, or read settings back. Obtain the prior configuration, edit it, write, and request it again to confirm; configuration writes replace a whole section rather than patching only non-default fields. Omitting a field in a Hash can reset that setting to its protobuf default. Firmware version and hardware determine which fields are applied, and writes may reboot/disconnect the node.
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Meshtastic::Admin::Config.get_lora(serial_obj: serial_obj)
|
|
38
|
+
|
|
39
|
+
# Supply the full desired section; prefer editing the returned protobuf.
|
|
40
|
+
Meshtastic::Admin::Config.set_position(
|
|
41
|
+
serial_obj: serial_obj,
|
|
42
|
+
position: Meshtastic::Config::PositionConfig.new(position_broadcast_secs: 900)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
Meshtastic::Admin::Config.set_device_ui(
|
|
46
|
+
serial_obj: serial_obj,
|
|
47
|
+
device_ui: Meshtastic::DeviceUIConfig.new
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Ruby protobuf's `display` reader collides with `Object#display`; inspect `config['display']`, not `config.display`. Do not log configuration objects: network/security settings and session passkeys can contain secrets.
|
|
52
|
+
|
|
53
|
+
## Protocol evidence
|
|
54
|
+
|
|
55
|
+
- [Official Config schema](https://github.com/meshtastic/protobufs/blob/master/meshtastic/config.proto): all ten variants and request-only SessionkeyConfig.
|
|
56
|
+
- [Official Admin schema](https://github.com/meshtastic/protobufs/blob/master/meshtastic/admin.proto): ConfigType and dedicated UI operations.
|
|
57
|
+
- [Official firmware AdminModule](https://github.com/meshtastic/firmware/blob/master/src/modules/AdminModule.cpp): `handleSetConfig` / config request handling mark device-UI Config operations as no-ops and point to the dedicated handlers.
|
|
58
|
+
|
|
59
|
+
Related: [Admin](admin.md), [Channel](admin-channel.md).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Nordic/Adafruit BLE DFU
|
|
2
|
+
|
|
3
|
+
## Supported path
|
|
4
|
+
|
|
5
|
+
`Meshtastic::Admin::Firmware::NordicDFU` installs **application-only, unsigned legacy Adafruit CRC16 DFU ZIP packages** over the SDK11 BLE bootloader protocol. It does not invoke Python, nrfutil, unzip, or any other external program. Use the correct board's `firmware-…-ota.zip`, not a UF2, HEX, raw binary, release bundle, or bootloader/SoftDevice upgrade ZIP.
|
|
6
|
+
|
|
7
|
+
This is a genuinely supported Nordic path, **not Nordic Secure DFU**. Meshtastic's application-side `BLEDfuSecure` advertises FE59 as a buttonless reboot service; that name does not establish that the bootloader implements Secure DFU's protobuf init packets and object/CRC32 transfer protocol. The Adafruit SDK11 bootloader uses legacy service 1530 and its own control-point commands.
|
|
8
|
+
|
|
9
|
+
Unsupported packages fail before any GATT writes: SoftDevice/bootloader or multi-image updates, Nordic Secure DFU init packets, Adafruit extended hash/signed init packets, encrypted/split/ZIP64 archives, data-descriptor ZIPs, missing or malformed manifest entries, inconsistent directory/local headers, duplicate or unsafe names, oversized entries, CRC32 failures, non-word-aligned binaries, and image CRC16 mismatches. Stored and raw-DEFLATE ZIP entries are supported, bounded in memory; nothing is extracted to disk.
|
|
10
|
+
|
|
11
|
+
Adafruit's optional `SIGNED_FW` build is a **different legacy init extension**, not Secure DFU: extension identifier 2, image length, SHA-256 and raw P-256 signature R/S. Its bootloader verifies a compiled-in public key. This installer rejects that extension rather than discarding signatures or silently downgrading security. Ordinary CRC16 packages provide corruption detection, **not authenticity**. Obtain the board-specific ZIP from a trusted release source. Device type 0x0052 does not uniquely identify a board; the bootloader checks the package's SoftDevice requirements but host-side board compatibility cannot be inferred from this format.
|
|
12
|
+
|
|
13
|
+
## Integration API
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require 'meshtastic'
|
|
17
|
+
require 'meshtastic/admin/firmware/nordic_dfu'
|
|
18
|
+
|
|
19
|
+
result = Meshtastic::Admin::Firmware::NordicDFU.install(
|
|
20
|
+
protocol: :nordic_dfu,
|
|
21
|
+
package: '/path/to/firmware-board-version-ota.zip',
|
|
22
|
+
address: 'AA:BB:CC:DD:EE:FF',
|
|
23
|
+
adapter: 'hci0',
|
|
24
|
+
timeout: 30
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The public signature is `install(opts = {})`. Exactly one source is required:
|
|
29
|
+
|
|
30
|
+
- `package:` — DFU ZIP filename.
|
|
31
|
+
- `package_bytes:` — raw ZIP String instead of a filename.
|
|
32
|
+
|
|
33
|
+
Other accepted keys: `protocol:` (optional, must be `:nordic_dfu`), `gatt:` (already-connected adapter), `address:` (required without injection), `adapter:` (default `hci0`), `timeout:` (positive finite per-notification timeout, default 30 seconds). Unknown options are rejected. In particular, no `retries:`, Secure DFU mode, or generic `bytes:`/`firmware:` aliases are silently accepted. The orchestrator should pass the ZIP source using the explicit package keys.
|
|
34
|
+
|
|
35
|
+
Production construction uses the shared BLE module:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Meshtastic::Admin::Firmware::BLE::BlueZ.new(
|
|
39
|
+
address: address, adapter: adapter, timeout: timeout,
|
|
40
|
+
service_uuid: '00001530-1212-efde-1523-785feabcd123'
|
|
41
|
+
).connect
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The BLE backend is shared with the unified installer; this module does not implement another BlueZ backend. BlueZ must already know the explicitly selected bootloader address. Discovery, pairing, application-to-bootloader reboot, and post-reboot reconnect belong to the caller/orchestrator.
|
|
45
|
+
|
|
46
|
+
### Exact injected GATT interface
|
|
47
|
+
|
|
48
|
+
All methods use Ruby keywords:
|
|
49
|
+
|
|
50
|
+
- `subscribe(uuid:)` enables and queues notifications before the first write.
|
|
51
|
+
- `write(uuid:, bytes:, response:)` writes a binary String. Control point uses `response: true`; packet characteristic uses `false`.
|
|
52
|
+
- `notification(timeout:)` returns `{ uuid: String, bytes: String }`, or nil on timeout; transport exceptions propagate. The implementation must enforce the supplied timeout.
|
|
53
|
+
- `close` releases the connection. The installer closes after an attempted session, including injected adapters. Package validation failures do not acquire or close an injected adapter.
|
|
54
|
+
|
|
55
|
+
Control UUID: `00001531-1212-efde-1523-785feabcd123`.
|
|
56
|
+
Packet UUID: `00001532-1212-efde-1523-785feabcd123`.
|
|
57
|
+
The backend resolves these only within the selected device/service and fails if absent; no fallback to unrelated characteristics or Secure DFU.
|
|
58
|
+
|
|
59
|
+
## Wire sequence and verification
|
|
60
|
+
|
|
61
|
+
1. Subscribe to control-point notifications.
|
|
62
|
+
2. Write Start DFU `[01 04]` (application), then three little-endian uint32 sizes `[0, 0, image_size]` to the packet characteristic. Require `[10 01 01]` success.
|
|
63
|
+
3. Write Init Start `[02 00]`, stream the **unchanged** `.dat` in at-most-20-byte packets, then Init Complete `[02 01]`. Require `[10 02 01]` success. Legacy init is little-endian device type uint16, revision uint16, application version uint32, SoftDevice count uint16, count uint16 IDs, then CRC16.
|
|
64
|
+
4. Enable packet receipt notifications with `[08 01 00]` (PRN=1); this command has no protocol response. Write Receive Firmware `[03]`.
|
|
65
|
+
5. Stream binary in at-most-20-byte, word-aligned packets. After each non-final packet require `[11 <received uint32 little-endian>]` with the **exact cumulative byte offset**. SDK11 sends no PRN on the final packet: instead require Receive Firmware success `[10 03 01]`, emitted after its final flash operation completes.
|
|
66
|
+
6. Write Validate `[04]`; require `[10 04 01]`, which confirms device-side image CRC16 validation. Only then write Activate and Reset `[05]`. This command has no DFU response; a failed ATT write is not suppressed as presumed reboot success.
|
|
67
|
+
|
|
68
|
+
The result contains `status: :verified`, `protocol: :nordic_dfu`, image `bytes:`, image `sha256:`, and `reboot_verified: false`. Verified means **bootloader validation acknowledged**, not a confirmed successful reboot or healthy Meshtastic application. The SHA-256 is computed locally for identification, not reported by the bootloader. The caller must reconnect and verify application identity/version separately.
|
|
69
|
+
|
|
70
|
+
### Retry policy
|
|
71
|
+
|
|
72
|
+
No automatic wire retries or resume. Legacy packets have no sequence number; retransmitting after an ambiguous write, missing PRN, timeout, wrong opcode, rejection, or disconnect could duplicate bytes or corrupt state. The installer closes and raises; it never sends Activate after a failed validation. The operator may explicitly restart from a newly entered bootloader session after resolving the failure. GATT/ATT link-layer retransmissions remain the Bluetooth stack's responsibility.
|
|
73
|
+
|
|
74
|
+
## Evidence and testing
|
|
75
|
+
|
|
76
|
+
The matching spec uses an independently implemented stateful DFU peer which parses every write, checks command order and sizes, accumulates `.dat`/image packets, generates PRNs from received byte counts, and calculates image CRC16 with a separate bitwise algorithm. It includes corruption/rejection/timeout/offset faults and validates ZIP parsing, supported compression, unsafe packages and production-backend construction. No hardware was opened; emulation does not prove device flash behavior or post-reboot health.
|
|
77
|
+
|
|
78
|
+
Upstream source references inspected:
|
|
79
|
+
|
|
80
|
+
- [Adafruit SDK11 BLE DFU service](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/lib/sdk11/components/ble/ble_services/ble_dfu/ble_dfu.c): legacy opcodes and characteristic behavior.
|
|
81
|
+
- [Adafruit SDK11 BLE transport](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/lib/sdk11/components/libraries/bootloader_dfu/dfu_transport_ble.c): `on_dfu_evt`, `app_data_process`, PRNs only on non-final packets, final receive response after flash completion, init/validate/reset handling.
|
|
82
|
+
- [Adafruit init validation](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/src/dfu_init.c): device type, SoftDevice matching, CRC16 versus optional `SIGNED_FW` P-256 extension.
|
|
83
|
+
- [Adafruit package generation](https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/package.py) and [init encoding](https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/init_packet.py): manifest, `.bin`/`.dat`, default legacy v0.5 CRC16 and distinct v0.6/v0.7/v0.8 extensions.
|
|
84
|
+
- [Meshtastic buttonless DFU](https://github.com/meshtastic/firmware/blob/develop/src/platform/nrf52/BLEDfuSecure.cpp): FE59 application service initiates bootloader transition, not the image-transfer implementation.
|
|
85
|
+
- [Meshtastic NRF52 release script](https://github.com/meshtastic/firmware/blob/develop/bin/build-nrf52.sh): release OTA ZIP naming.
|
|
86
|
+
- [Meshtastic OTAFIX bootloader fork](https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX): device-specific bootloader deployments can differ; legacy-only support must not be generalized to every NRF52 bootloader.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Native serial ROM firmware installation
|
|
2
|
+
|
|
3
|
+
`Meshtastic::Admin::Firmware::SerialBootloader` implements Espressif's UART ROM
|
|
4
|
+
protocol in Ruby using the existing `uart`/`termios` dependencies. It does not
|
|
5
|
+
execute esptool, Python, nrfutil, or an uploaded flasher stub.
|
|
6
|
+
|
|
7
|
+
## Supported targets and boundaries
|
|
8
|
+
|
|
9
|
+
| `chip:` | Hardware identification | Write format |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `:esp32` | ROM magic register `0x40001000 == 0x00f01d83`; security eFuse reads | 16-byte FLASH_BEGIN |
|
|
12
|
+
| `:esp32s3` | GET_SECURITY_INFO chip ID 9 and security flags | 20-byte FLASH_BEGIN, encryption disabled |
|
|
13
|
+
| `:esp32c3` | GET_SECURITY_INFO chip ID 5 and security flags | 20-byte FLASH_BEGIN, encryption disabled |
|
|
14
|
+
|
|
15
|
+
Support is for **UART ROM download mode**, normally through an external USB/UART
|
|
16
|
+
bridge. Automatic reset requires the conventional DTR-to-BOOT and RTS-to-EN
|
|
17
|
+
circuit. Native USB-Serial-JTAG/USB-OTG reset and re-enumeration are not implemented.
|
|
18
|
+
Other chips, including ESP8266, ESP32-S2, C2, C6, H2 and Nordic nRF52, are rejected.
|
|
19
|
+
No device was physically flashed during development: verification uses an
|
|
20
|
+
independent ROM emulator over a PTY and the real UART opener.
|
|
21
|
+
|
|
22
|
+
Only a **single, unmerged ESP application `.bin`** is accepted. This is not a
|
|
23
|
+
factory/recovery installer: it does not install a bootloader, partition table,
|
|
24
|
+
filesystem, OTA selection metadata, UF2 image, ZIP package, signed image or merged
|
|
25
|
+
release image. The existing bootloader and partition table must already be valid.
|
|
26
|
+
Trailing padding, signatures and merged images are deliberately rejected.
|
|
27
|
+
|
|
28
|
+
## Direct API
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require 'meshtastic'
|
|
32
|
+
require 'meshtastic/admin/firmware/serial_bootloader'
|
|
33
|
+
|
|
34
|
+
result = Meshtastic::Admin::Firmware::SerialBootloader.install(
|
|
35
|
+
protocol: :esp_rom,
|
|
36
|
+
port: '/dev/ttyUSB0',
|
|
37
|
+
chip: :esp32s3,
|
|
38
|
+
firmware: '/path/to/board-matched-application.bin',
|
|
39
|
+
offset: 0x10000, # example only: consult this board's partition table
|
|
40
|
+
flash_size: 4 * 1024 * 1024, # actual physical capacity, not image length
|
|
41
|
+
reset: :classic,
|
|
42
|
+
timeout: 120
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Supply exactly one of `firmware:` (path) or `bytes:` (binary String). `protocol:`
|
|
47
|
+
is optional on the direct backend and defaults to `:esp_rom`. Unknown options
|
|
48
|
+
raise `ArgumentError` before opening the serial device.
|
|
49
|
+
|
|
50
|
+
- `port:` is a required dedicated serial device path, **not** `serial_obj:` from
|
|
51
|
+
an active Meshtastic PhoneAPI connection. Close that connection and stop its
|
|
52
|
+
reader before installing. Never flash while another process uses this port.
|
|
53
|
+
- `chip:` is required and must match both the image header and the connected ROM.
|
|
54
|
+
- `offset:` is required, at least `0x10000`, and aligned to a 4096-byte flash sector.
|
|
55
|
+
This check prevents writes into low-address bootloader/partition metadata; it
|
|
56
|
+
does **not** discover which application partition the board actually boots.
|
|
57
|
+
- `flash_size:` is required, a power of two between 1 and 16 MiB. It is declared
|
|
58
|
+
by the caller; this backend does not probe JEDEC flash capacity. The complete
|
|
59
|
+
sector-rounded erase range must fit within it.
|
|
60
|
+
- `reset: :classic` (default) drives DTR/RTS to enter ROM, then pulses EN after
|
|
61
|
+
verified completion. If the bridge does not support modem-control ioctls, the
|
|
62
|
+
operation fails rather than assuming a reset occurred.
|
|
63
|
+
- `reset: :none` means the operator has already entered ROM download mode. It
|
|
64
|
+
suppresses modem-line changes, not the final FLASH_END reboot request. Opening
|
|
65
|
+
a UART can still affect modem lines on some operating systems/bridges.
|
|
66
|
+
- `timeout:` is a finite positive per-command deadline, default 120 seconds.
|
|
67
|
+
SYNC has at most three attempts, each capped at one second. The whole transfer
|
|
68
|
+
can take longer than `timeout:`. Data, erase and reboot commands are not replayed
|
|
69
|
+
after a timeout or rejection.
|
|
70
|
+
|
|
71
|
+
**Before flashing:** verify the board model, hardware revision requirements,
|
|
72
|
+
partition address and partition capacity independently. The ESP image chip ID is
|
|
73
|
+
not a board model identifier. This backend does not validate minimum silicon
|
|
74
|
+
revision against eFuses, discover partitions, select an inactive OTA slot, or
|
|
75
|
+
change OTA selection metadata. It cannot guarantee that the next boot selects
|
|
76
|
+
this image. Back up configuration before installing. Sector erase destroys the
|
|
77
|
+
rest of the final sector, even where the image itself ends earlier. Do not use an
|
|
78
|
+
offset/range belonging to NVS, a filesystem, another application or OTA metadata.
|
|
79
|
+
|
|
80
|
+
## Protocol and verification
|
|
81
|
+
|
|
82
|
+
Before opening the port, validation checks image magic, segment count and bounds,
|
|
83
|
+
segment word alignment, embedded chip ID, segment XOR checksum, the appended
|
|
84
|
+
SHA-256 digest when present, and exact end-of-image. These are corruption checks,
|
|
85
|
+
not firmware authenticity or signature verification.
|
|
86
|
+
|
|
87
|
+
The backend then:
|
|
88
|
+
|
|
89
|
+
1. Opens UART at 115200, 8N1, disables inherited RTS/CTS flow control and HUPCL,
|
|
90
|
+
and performs the requested reset.
|
|
91
|
+
2. Sends escaped SLIP SYNC and accepts a nonzero ROM response value. A zero value
|
|
92
|
+
is rejected as a running flasher stub. Extra ROM SYNC responses are drained
|
|
93
|
+
while waiting for the next matching command response.
|
|
94
|
+
3. Identifies the chip and rejects secure boot, secure download, or any programmed
|
|
95
|
+
flash-encryption count. Conservatively rejecting even previously used encryption
|
|
96
|
+
counters avoids silently treating a security-configured device as plain flash.
|
|
97
|
+
4. Sends SPI_ATTACH with default pins and SPI_SET_PARAMS for declared NOR capacity.
|
|
98
|
+
Custom SPI pin mappings and NAND are unsupported.
|
|
99
|
+
5. Erases the selected range via FLASH_BEGIN, then sends 1024-byte FLASH_DATA
|
|
100
|
+
blocks with sequence numbers, `0xff` final-block padding and XOR checksum
|
|
101
|
+
seeded with `0xef`. Every command requires a matching response and successful
|
|
102
|
+
ROM status. Invalid framing, opcode, length, timeout or error status aborts.
|
|
103
|
+
6. Requests SPI_FLASH_MD5 over the **original unpadded image length** and compares
|
|
104
|
+
the ROM's 32 ASCII hexadecimal characters with Ruby's MD5 of the exact source.
|
|
105
|
+
A mismatch prevents FLASH_END and explicit hard reset.
|
|
106
|
+
7. Sends FLASH_END with reboot requested, requires its acknowledgment, and performs
|
|
107
|
+
the final EN pulse for `:classic`. The UART closes on success or failure.
|
|
108
|
+
|
|
109
|
+
On success the return hash contains:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
{
|
|
113
|
+
status: :verified,
|
|
114
|
+
chip: :esp32s3,
|
|
115
|
+
bytes: image_byte_count,
|
|
116
|
+
offset: application_offset,
|
|
117
|
+
md5: rom_verified_hex_md5,
|
|
118
|
+
sha256: source_hex_sha256,
|
|
119
|
+
reboot_requested: true,
|
|
120
|
+
boot_verified: false
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`:verified` means **the ROM-reported flash digest matched**. It is not a
|
|
125
|
+
post-reboot health check or a guarantee the booted application is this image.
|
|
126
|
+
Automatic Admin session/readback and post-reboot verification belong in the
|
|
127
|
+
higher-level Firmware orchestrator, after this backend has closed its UART.
|
|
128
|
+
The backend does not return an invented firmware version or treat port reopening
|
|
129
|
+
as proof of application health. On failure it raises `ArgumentError`, `IOError`,
|
|
130
|
+
`EOFError`, `Timeout::Error`, or the underlying filesystem/serial exception. A
|
|
131
|
+
failed transfer may have already erased or partially written the application;
|
|
132
|
+
no rollback is implemented.
|
|
133
|
+
|
|
134
|
+
## Unsupported commands and Nordic DFU
|
|
135
|
+
|
|
136
|
+
Implemented ROM commands are SYNC (`0x08`), READ_REG (`0x0a`, ESP32 only),
|
|
137
|
+
GET_SECURITY_INFO (`0x14`, S3/C3), SPI_ATTACH (`0x0d`), SPI_SET_PARAMS (`0x0b`),
|
|
138
|
+
FLASH_BEGIN/DATA/END (`0x02`/`0x03`/`0x04`) and SPI_FLASH_MD5 (`0x13`).
|
|
139
|
+
|
|
140
|
+
There is no compressed flashing, RAM/stub upload, arbitrary register writing,
|
|
141
|
+
baud-rate change, encrypted write, secure-download controller, raw flash readback,
|
|
142
|
+
full-chip erase, or stub-only `0xd0`/`0xd1`/`0xd2` erase/read protocol. ROM MD5 is
|
|
143
|
+
not interchangeable with the stub's 16-byte binary digest response.
|
|
144
|
+
|
|
145
|
+
Adafruit's nRF52 serial/CDC DFU is a **different protocol**: its upstream uploader
|
|
146
|
+
uses reliable HCI/SLIP framing with sequence acknowledgments, CRC16 and Nordic
|
|
147
|
+
START/INIT/DATA/STOP records. It is not ESP SLIP command framing, PhoneAPI framing,
|
|
148
|
+
a UF2 copy, or Nordic Secure DFU's object protocol. This backend explicitly does
|
|
149
|
+
not implement it or invoke its Python uploader. A correct future backend needs
|
|
150
|
+
package/init-packet validation, exact bootloader capability matching, acknowledged
|
|
151
|
+
HCI sequencing and honest device validation/activation evidence; transport ACKs
|
|
152
|
+
alone must not be reported as a verified installation.
|
|
153
|
+
|
|
154
|
+
## Sources and tests
|
|
155
|
+
|
|
156
|
+
Protocol choices were checked against the official source, not inferred from a
|
|
157
|
+
successful port open:
|
|
158
|
+
|
|
159
|
+
- [Espressif serial protocol reference](https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/serial-protocol.html)
|
|
160
|
+
- [Espressif command implementation](https://github.com/espressif/esptool/blob/master/esptool/loader.py)
|
|
161
|
+
- [ESP32 target/eFuses](https://github.com/espressif/esptool/blob/master/esptool/targets/esp32.py),
|
|
162
|
+
[ESP32-S3](https://github.com/espressif/esptool/blob/master/esptool/targets/esp32s3.py),
|
|
163
|
+
[ESP32-C3](https://github.com/espressif/esptool/blob/master/esptool/targets/esp32c3.py)
|
|
164
|
+
- [Espressif image format parser](https://github.com/espressif/esptool/blob/master/esptool/bin_image.py)
|
|
165
|
+
- [Classic reset sequence](https://github.com/espressif/esptool/blob/v4.8.1/esptool/reset.py)
|
|
166
|
+
- [Adafruit nRF52 bootloader](https://github.com/adafruit/Adafruit_nRF52_Bootloader)
|
|
167
|
+
- [Adafruit serial DFU transport](https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py)
|
|
168
|
+
|
|
169
|
+
Run the hardware-free tests:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
bundle exec rspec spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The emulator independently checks wire framing, little-endian command fields,
|
|
176
|
+
sequence numbers, block padding/checksums, chip-specific FLASH_BEGIN layout and
|
|
177
|
+
verification range. Tests cover all three chips, file input, optional image
|
|
178
|
+
digests, classic reset controls, security/chip mismatches, malformed/error replies,
|
|
179
|
+
MD5 mismatch, timeouts, synchronization retry, resource teardown and rejection
|
|
180
|
+
before opening a device. Actual board behavior remains hardware-unverified.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Meshtastic::Admin::Firmware
|
|
2
|
+
|
|
3
|
+
**PhoneAPI XModem is not a firmware flashing protocol.** The old implementation incorrectly sent padded XModem data after an OTA admin request and could report completion without any device acknowledgement. That path is now rejected, including MQTT's former silent request-only `install`.
|
|
4
|
+
|
|
5
|
+
## Supported operations
|
|
6
|
+
|
|
7
|
+
| Operation | What the Ruby implementation actually does |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `sha256(firmware: ... \| bytes: ...)` | Returns a raw 32-byte SHA-256 digest. Exactly one nonempty image source is required. |
|
|
10
|
+
| `request_ota(...)` | Sends the real ESP32 `OTAEvent` admin request: raw SHA-256 plus `:OTA_BLE` (default) or `:OTA_WIFI`. This pins the image hash and requests a reboot into an **already installed compatible loader**; it does not upload firmware or prove that the loader started. |
|
|
11
|
+
| `install(protocol: :unified_wifi, host: ..., firmware: ... \| bytes: ...)` | Separate ESP32 unified-loader TCP protocol, normally port 3232. Requires the image hash to have been provisioned using `request_ota`. |
|
|
12
|
+
| `install(protocol: :unified_ble, address: ..., firmware: ... \| bytes: ...)` | ESP32 unified-loader custom GATT protocol, with a native Ruby BlueZ backend and application ACK flow control. Not the old BLE-only firmware-ota protocol. |
|
|
13
|
+
| `install(protocol: :nordic_dfu, address: ..., package: ...)` | Adafruit SDK11 legacy Nordic BLE DFU for application-only legacy ZIP packages. [Exact scope and options](admin-firmware-nordic.md). Not Nordic Secure DFU or UF2. |
|
|
14
|
+
| `install(protocol: :esp_rom, ...)` | Native ESP ROM serial flashing with explicit chip, flash geometry and offset, ROM acknowledgements and flash MD5 verification. [Exact scope and options](admin-firmware-serial.md). |
|
|
15
|
+
| `verify_reboot(...)` or `install(..., verify: {...})` | Fresh application connection, matching configuration handshake and a new request-ID/source-correlated Admin device-metadata reply. Checks exact firmware version and optional node identity. |
|
|
16
|
+
| `enter_dfu(...)` | Sends `enter_dfu_mode_request`; current upstream handles entry on nRF52/RP2040. It neither transfers a DFU package nor copies a UF2 image. |
|
|
17
|
+
| `reboot_ota`, `xmodem_blocks`, `send_xmodem` | Raise `NotImplementedError`. The legacy reboot field has no handler in the inspected firmware; XModem is filesystem transfer, not firmware installation. |
|
|
18
|
+
| `help`, `authors` | Usage and attribution. |
|
|
19
|
+
|
|
20
|
+
Admin commands accept the transport, addressing, and authentication options documented in [Admin](admin.md). Use exactly one transport. Successful submission is **not confirmation that hardware supports or performed the operation**. A routing acknowledgement alone cannot prove an ESP32 OTA loader/partition exists. No automatic board detection is performed.
|
|
21
|
+
|
|
22
|
+
## ESP32 unified WiFi example
|
|
23
|
+
|
|
24
|
+
Only use a matching application update `.bin`, not a merged full-flash image, UF2, ZIP, or bootloader. Confirm board, flash layout, power, WiFi configuration, and loader compatibility yourself. Wrong images or interrupted writes may leave the device unbootable; keep a recovery method available.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
image = File.binread('firmware-matching-board-update.bin')
|
|
28
|
+
|
|
29
|
+
# Phase 1: use an existing authenticated Admin connection to pin this image.
|
|
30
|
+
Meshtastic::Admin::Firmware.request_ota(
|
|
31
|
+
serial_obj: serial_obj,
|
|
32
|
+
bytes: image,
|
|
33
|
+
mode: :OTA_WIFI
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Phase 2: connect to the separate loader after it reboots and joins WiFi.
|
|
37
|
+
# Not the Meshtastic TCP PhoneAPI port 4403, nor an existing tcp_obj.
|
|
38
|
+
result = Meshtastic::Admin::Firmware.install(
|
|
39
|
+
protocol: :unified_wifi,
|
|
40
|
+
host: '192.0.2.10',
|
|
41
|
+
bytes: image,
|
|
42
|
+
port: 3232,
|
|
43
|
+
timeout: 120,
|
|
44
|
+
retries: 3,
|
|
45
|
+
retry_delay: 1
|
|
46
|
+
)
|
|
47
|
+
# { status: :verified, bytes: ..., sha256: '64 hexadecimal characters',
|
|
48
|
+
# loader_version: 'hardware firmware reboot_count loader_version' }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`request_ota(ota_hash: ...)` also accepts an explicitly supplied **raw** 32-byte digest (not hex). When an image is supplied alongside the digest they must match. It rejects unknown modes. The loader itself checks that the upload hash equals its provisioned NVS hash and verifies the downloaded bytes.
|
|
52
|
+
|
|
53
|
+
`install` intentionally rejects `serial_obj`, `tcp_obj`, `bluetooth_obj`, `mqtt_obj`, `mode`, mesh destinations, and other unknown options. These are not the unified WiFi transport. `host` must address the actual prepared loader. Protocol selection is explicit: no guessing or fallback to a different flasher.
|
|
54
|
+
|
|
55
|
+
### Wire behavior and failure handling
|
|
56
|
+
|
|
57
|
+
1. Connect with a bounded timeout; retry **only** connection refusal/timeouts, up to `retries` additional attempts (0..20). The default is three additional attempts with a one-second delay. Each connect has its own timeout.
|
|
58
|
+
2. Send `VERSION\n`; require `OK <hw> <fw> <count> <loader-version>\n` before sending an OTA command.
|
|
59
|
+
3. Send `OTA <exact-byte-count> <sha256-hex>\n`; accept `ERASING\n` followed by `OK\n`, or immediate `OK\n`. No image bytes are sent until this handshake succeeds.
|
|
60
|
+
4. Send the exact binary without XModem, padding, or EOT. Drain optional TCP `ACK\n` responses concurrently to avoid socket backpressure deadlocks. TCP handles retransmission; no firmware bytes are replayed at application level.
|
|
61
|
+
5. Require the final `OK\n`. `ACK`, successful socket writes, disconnects, and admin submission do not count as completion. `ERR ...`, malformed/oversized lines, premature EOF, and timeout raise errors; the connection and upload worker are cleaned up.
|
|
62
|
+
|
|
63
|
+
`timeout` also bounds the **whole VERSION/erase/upload/final-verification exchange**, not each received line. Increase it for slow devices/large images. A lost connection after starting OTA is not retried automatically: completion can be ambiguous and replaying raw bytes can corrupt the transfer. Reconnect manually and inspect the device before retrying.
|
|
64
|
+
|
|
65
|
+
`:verified` means **the loader reported successful integrity verification and boot-partition selection**. It does not mean the new application rebooted, is healthy, or matches a specific board. Independently reconnect and inspect its firmware version. This code does not authenticate the TCP server or add TLS; use a trusted local network. SHA-256 is integrity pinning, not a publisher signature.
|
|
66
|
+
|
|
67
|
+
## ESP32 unified BLE example
|
|
68
|
+
|
|
69
|
+
The compatible unified loader must already be installed and its NVS hash pinned with `request_ota(mode: :OTA_BLE, bytes: image, ...)`. Close the application transport before opening the loader; never open serial and BLE on the same radio concurrently. Identify the bootloader's **actual** address explicitly (it can differ from the application's). The backend never guesses an incremented MAC, discovers/selects another device, or pairs automatically. If BlueZ does not know the address, discover that loader explicitly before calling `install`. Application reconnection still requires normal Meshtastic BLE pairing.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = Meshtastic::Admin::Firmware.install(
|
|
73
|
+
protocol: :unified_ble,
|
|
74
|
+
address: 'AA:BB:CC:DD:EE:FF', # selected loader address
|
|
75
|
+
adapter: 'hci0',
|
|
76
|
+
firmware: 'firmware-matching-board-update.bin',
|
|
77
|
+
timeout: 120,
|
|
78
|
+
verify: {
|
|
79
|
+
transport: :bluetooth,
|
|
80
|
+
connection: { address: 'AA:BB:CC:DD:EE:FF', adapter: 'hci0' }, # application address
|
|
81
|
+
expected_version: '2.7.1.example', # exact version from your chosen release
|
|
82
|
+
expected_node: 0xaabbccdd,
|
|
83
|
+
timeout: 90
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
# status: :boot_verified only after a fresh matching application reply
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Service UUID is `4fafc201-1fb5-459e-8fcc-c5c9c331914b`; writes use `62ec0272-3ec5-11eb-b378-0242ac130005`, notifications use `62ec0272-3ec5-11eb-b378-0242ac130003`. Subscribe **before** VERSION. Commands are fragmented at 20 bytes (safe for ATT MTU 23), then binary is transferred in 20-byte writes with ATT responses. Each nonfinal binary write must receive `ACK\n`; the final write must receive `OK\n`, not ACK. The source sends final OK instead of final ACK. Newline buffering handles coalesced or fragmented notification data, with a 512-byte response-line bound. Unexpected UUID, ERR, malformed/version responses, silence or missing final OK fail closed. Neither binary chunks nor uncertain sessions are automatically replayed. Conservative 20-byte chunks trade speed for MTU portability; increase the total timeout for large images. BlueZ negotiates link MTU; an undersized/truncated VERSION notification fails safely rather than bypassing the handshake.
|
|
90
|
+
|
|
91
|
+
For tests or an alternative Ruby GATT implementation, unified BLE accepts `backend:` (Nordic uses `gatt:`). The shared production class is `Meshtastic::Admin::Firmware::BLE::BlueZ.new(address:, adapter:, service_uuid:, timeout:).connect`. Its instance API is `subscribe(uuid:)`, `write(uuid:, bytes:, response: true/false)`, `notification(timeout:)` returning `{uuid:, bytes:}`, and `close`. All GATT service/characteristic lookup and notification match paths are scoped to the selected device. A private Ruby D-Bus connection is used; notification dispatch and synchronous method calls remain on the caller thread. No Python or external flasher process is used.
|
|
92
|
+
|
|
93
|
+
## Post-reboot verification
|
|
94
|
+
|
|
95
|
+
`verify:` is an **optional Hash** of `verify_reboot` options, validated before transfer. Omitting it preserves loader-only `:verified` results; it never silently claims boot health. Providing it runs verification only after the installer returns and closes its loader connection. You can also call `verify_reboot` independently.
|
|
96
|
+
|
|
97
|
+
- Required: `transport: :tcp | :bluetooth | :serial`, `expected_version:` (exact nonempty String).
|
|
98
|
+
- Production default: `connection:` Hash for a **new** application connection, explicitly specifying `host`, `address`, or `block_dev`, respectively. TCP uses PhoneAPI port **4403**, not updater port 3232; set `connection[:port]` only for a custom PhoneAPI port. Do not supply an existing socket/handle.
|
|
99
|
+
- Optional: `expected_node:` numeric node ID, `timeout:` whole-operation deadline (60 seconds), `reboot_delay:` initial wait (3 seconds).
|
|
100
|
+
- Optional advanced `reconnect:` callable receives `{transport:, connection:, timeout:}` and must return a newly connected handle of the selected transport, with configuration requested. The verifier still waits for configuration and issues a fresh Admin metadata request; a callback cannot substitute a cached metadata Hash. The returned handle is closed afterward.
|
|
101
|
+
- Pre-metadata connection/configuration I/O failures retry every 0.25 seconds within the total deadline. Metadata errors/version mismatches do not cause a reflash or get converted to success.
|
|
102
|
+
- A fresh source/request-ID-matched `get_device_metadata_response` is required. Cached `handle[:metadata]`, configuration metadata, loader VERSION, a routing ACK or successful port open cannot satisfy verification.
|
|
103
|
+
- Success merges `status: :boot_verified`, `loader_status: :verified`, `boot_verified: true`, `reboot_verified: true`, current firmware version, node number and metadata into the transfer result. Failure raises; firmware might already have been written, so inspect the device rather than blindly rerunning the installer.
|
|
104
|
+
|
|
105
|
+
This proves that the selected application responds and reports the expected version/optional identity. It does **not** cryptographically attest the running image, establish board compatibility, or test radio/RF operation. Use the correct release artifact and retain a recovery path.
|
|
106
|
+
|
|
107
|
+
## Explicit gaps
|
|
108
|
+
|
|
109
|
+
- No legacy BLE-only updater, ArduinoOTA/espota WiFi updater, Nordic Secure DFU, serial Nordic DFU, RP2040 UF2 filesystem copying, or automatic bootloader installation. Native protocol support is deliberately scoped; a board name alone does not establish its installed bootloader or transport capabilities.
|
|
110
|
+
- No automatic discovery, board/image compatibility parser, OTA partition creation, or firmware downloads. ESP32 unified source targets ESP32/ESP32-S3; this is not a claim that every ESP32 variant or every Meshtastic board has that loader.
|
|
111
|
+
- MQTT can carry an authorized preparation request; it is not an image transport or synchronous post-reboot verifier.
|
|
112
|
+
- Tests exercise fake GATT loaders, real Ruby D-Bus signal marshalling over UNIX sockets, loopback TCP PhoneAPI/configuration/Admin exchanges and the retained TCP uploader. No hardware was contacted or flashed; physical device compatibility and reboot behavior remain hardware-unverified.
|
|
113
|
+
|
|
114
|
+
## Upstream evidence
|
|
115
|
+
|
|
116
|
+
Inspected main firmware commit `6d41e279f1f51bd59f687b9d441c1bf47b1594fc` and unified loader commit `e7c0b95e14b6a1ffeca81b71c1ac477593911213`:
|
|
117
|
+
|
|
118
|
+
- [AdminModule.cpp](https://github.com/meshtastic/firmware/blob/6d41e279f1f51bd59f687b9d441c1bf47b1594fc/src/modules/AdminModule.cpp): `ota_request` checks the 32-byte hash, loader partition and capability; stores settings and schedules reboot. DFU entry is guarded by nRF52/RP2040 architecture. No `reboot_ota_seconds` handler appears.
|
|
119
|
+
- [MeshtasticOTA.cpp](https://github.com/meshtastic/firmware/blob/6d41e279f1f51bd59f687b9d441c1bf47b1594fc/src/platform/esp32/MeshtasticOTA.cpp): stores the hash in NVS and identifies combined/BLE-only/WiFi-only loader project names.
|
|
120
|
+
- [xmodem.cpp](https://github.com/meshtastic/firmware/blob/6d41e279f1f51bd59f687b9d441c1bf47b1594fc/src/xmodem.cpp): sequence-zero packet contains a filename; receiver uses `FSCom.open`/`file.write`. This is filesystem transfer, not an application updater.
|
|
121
|
+
- [Unified protocol README](https://github.com/meshtastic/esp32-unified-ota/blob/e7c0b95e14b6a1ffeca81b71c1ac477593911213/README.md): VERSION/OTA commands, GATT UUIDs, hashes and completion response.
|
|
122
|
+
- [ota_processor.cpp](https://github.com/meshtastic/esp32-unified-ota/blob/e7c0b95e14b6a1ffeca81b71c1ac477593911213/src/ota_processor.cpp): actual command parser, NVS hash gate, erasure, binary streaming, final hash check and boot-partition selection.
|
|
123
|
+
- [ble_ota.cpp](https://github.com/meshtastic/esp32-unified-ota/blob/main/src/ble_ota.cpp): custom GATT UUIDs, 4096-byte input stream buffer, ACK-enabled processor, and two-second reboot delay. [ota_processor.cpp](https://github.com/meshtastic/esp32-unified-ota/blob/main/src/ota_processor.cpp) sends ACK only for nonfinal chunks and OK after final integrity/boot-partition checks.
|
|
124
|
+
- [net_ota.cpp](https://github.com/meshtastic/esp32-unified-ota/blob/e7c0b95e14b6a1ffeca81b71c1ac477593911213/src/net_ota.cpp): TCP 3232 and **`setAckEnabled(true)`**. This differs from the README claim that WiFi has no application ACK. The implementation accepts either behavior. UDP discovery is emitted as broadcasts by this implementation; no discovery behavior is assumed by the Ruby client.
|
|
125
|
+
|
|
126
|
+
[Admin documentation](admin.md)
|