meshtastic 0.0.180 → 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/admin-channel.md +51 -20
- data/documentation/admin-config.md +45 -14
- data/documentation/admin-firmware-nordic.md +86 -0
- data/documentation/admin-firmware-serial.md +180 -0
- data/documentation/admin-firmware.md +103 -46
- data/documentation/admin.md +88 -36
- data/documentation/mesh-interface.md +7 -0
- data/lib/meshtastic/admin/channel.rb +99 -25
- data/lib/meshtastic/admin/config.rb +70 -11
- 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 +208 -93
- data/lib/meshtastic/admin.rb +251 -27
- data/lib/meshtastic/config_pb.rb +2 -1
- data/lib/meshtastic/mesh_interface.rb +8 -0
- data/lib/meshtastic/storeforward_pb.rb +1 -1
- data/lib/meshtastic/version.rb +1 -1
- data/spec/lib/meshtastic/admin/channel_spec.rb +159 -2
- data/spec/lib/meshtastic/admin/config_spec.rb +66 -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 +292 -95
- data/spec/lib/meshtastic/admin_spec.rb +372 -1
- data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
- metadata +14 -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'
|
|
@@ -1,33 +1,64 @@
|
|
|
1
1
|
# Meshtastic::Admin::Channel
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
## Channel operations
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
8
12
|
|
|
9
|
-
-
|
|
10
|
-
- `build` — `Channel` (index, role, settings)
|
|
11
|
-
- `get(index:)` — `get_channel_request`
|
|
12
|
-
- `set` — `set_channel` with a Channel protobuf
|
|
13
|
-
- `help` / `authors`
|
|
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
14
|
|
|
15
|
-
|
|
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
|
|
16
33
|
|
|
17
|
-
|
|
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.
|
|
18
39
|
|
|
19
40
|
```ruby
|
|
20
|
-
|
|
21
|
-
Meshtastic::Admin::Channel.
|
|
22
|
-
|
|
23
|
-
index: 0,
|
|
24
|
-
role: :PRIMARY,
|
|
25
|
-
settings: settings
|
|
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
|
|
26
44
|
)
|
|
27
|
-
Meshtastic::Admin::Channel.
|
|
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)
|
|
28
49
|
```
|
|
29
50
|
|
|
30
|
-
|
|
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.
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
- [Meshtastic::Admin::Config](admin-config.md)
|
|
64
|
+
Related: [Admin](admin.md), [Config](admin-config.md).
|
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
# Meshtastic::Admin::Config
|
|
2
2
|
|
|
3
|
-
Admin
|
|
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
4
|
|
|
5
|
-
##
|
|
5
|
+
## Operations
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 |
|
|
12
19
|
|
|
13
|
-
`config_type`
|
|
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.
|
|
14
33
|
|
|
15
34
|
## Example
|
|
16
35
|
|
|
17
36
|
```ruby
|
|
18
37
|
Meshtastic::Admin::Config.get_lora(serial_obj: serial_obj)
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
)
|
|
23
49
|
```
|
|
24
50
|
|
|
25
|
-
|
|
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.
|
|
26
58
|
|
|
27
|
-
|
|
28
|
-
- [Meshtastic::Admin::Channel](admin-channel.md)
|
|
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.
|