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
|
@@ -1,69 +1,126 @@
|
|
|
1
1
|
# Meshtastic::Admin::Firmware
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
##
|
|
5
|
+
## Supported operations
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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. |
|
|
10
19
|
|
|
11
|
-
|
|
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.
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
## ESP32 unified WiFi example
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Methods
|
|
18
|
-
|
|
19
|
-
- `install(firmware:, mode:, serial_obj: | tcp_obj: | bluetooth_obj: | mqtt_obj:)`
|
|
20
|
-
- `request_ota` — admin hash + mode only
|
|
21
|
-
- `enter_dfu`
|
|
22
|
-
- `reboot_ota(seconds: 10)`
|
|
23
|
-
- `xmodem_blocks(bytes)` / `send_xmodem(xmodem:)`
|
|
24
|
-
- `sha256`
|
|
25
|
-
- `help` / `authors`
|
|
26
|
-
|
|
27
|
-
`mode:` is `:OTA_BLE` or `:OTA_WIFI`. `firmware:` is a path; `bytes:` is raw image bytes.
|
|
28
|
-
|
|
29
|
-
## Serial / TCP / Bluetooth
|
|
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.
|
|
30
25
|
|
|
31
26
|
```ruby
|
|
32
|
-
|
|
33
|
-
serial_obj: serial_obj,
|
|
34
|
-
firmware: 'firmware-heltec-v3-update.bin',
|
|
35
|
-
mode: :OTA_BLE
|
|
36
|
-
)
|
|
27
|
+
image = File.binread('firmware-matching-board-update.bin')
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
41
33
|
mode: :OTA_WIFI
|
|
42
34
|
)
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
48
46
|
)
|
|
47
|
+
# { status: :verified, bytes: ..., sha256: '64 hexadecimal characters',
|
|
48
|
+
# loader_version: 'hardware firmware reboot_count loader_version' }
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
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.
|
|
52
66
|
|
|
53
|
-
|
|
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.
|
|
54
70
|
|
|
55
71
|
```ruby
|
|
56
|
-
Meshtastic::Admin::Firmware.install(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
85
|
)
|
|
86
|
+
# status: :boot_verified only after a fresh matching application reply
|
|
63
87
|
```
|
|
64
88
|
|
|
65
|
-
|
|
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.
|
|
66
125
|
|
|
67
|
-
|
|
68
|
-
- [Meshtastic::Xmodem](xmodem.md)
|
|
69
|
-
- [Meshtastic::MQTT](mqtt.md)
|
|
126
|
+
[Admin documentation](admin.md)
|
data/documentation/admin.md
CHANGED
|
@@ -1,55 +1,107 @@
|
|
|
1
1
|
# Meshtastic::Admin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build and send `AdminMessage` on `ADMIN_APP` through a connected `serial_obj`, `bluetooth_obj`, `tcp_obj`, or `mqtt_obj`. These operations can change configuration, reboot, erase files, or reset a device. Sending is not confirmation of successful execution.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Addressing and defaults
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Radio transports default `to` to their connected `my_node_num` and `from` to zero (the local PhoneAPI client). Complete the transport configuration handshake first, or supply `to` explicitly.
|
|
8
|
+
- Remote administration requires an explicit unicast destination: an integer or `!` followed by exactly eight hexadecimal digits. Broadcast, zero, malformed strings, and missing destinations are rejected. MQTT always requires `to`.
|
|
9
|
+
- Getter requests default `want_response: true`; state-changing operations default false. Explicit `want_response` overrides this. `want_ack` requests a separate routing acknowledgment. Some firmware setters answer with `ROUTING_APP` rather than an admin response when a response is requested.
|
|
10
|
+
- Common transport options include `channel`, `hop_limit`, `want_ack`, `last_packet_id`, and MQTT `psks`. Device-owned remote PKI uses `pki_encrypted` and `public_key` on radio transports; MQTT channel encryption is not PKI admin authorization.
|
|
11
|
+
- `get_channel(index: 0)` takes a **zero-based** index 0–7 and adds one exactly once for the wire. Raw `get_channel_request` passed to `encode`/`send` is already a wire value. `set_channel` retains the Channel protobuf's zero-based index.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- `send` — wrap and deliver (`want_response` default true)
|
|
11
|
-
- `reboot(seconds: 5)`
|
|
12
|
-
- `shutdown(seconds: 5)`
|
|
13
|
-
- `get_owner` / `set_owner(long_name:, short_name:, owner:)`
|
|
14
|
-
- `get_channel(index:)` / `set_channel(channel_settings:)`
|
|
15
|
-
- `get_config(config_type:)` / `set_config(config:)`
|
|
16
|
-
- `nodedb_reset`
|
|
17
|
-
- `help` / `authors`
|
|
13
|
+
## Encoding and validation
|
|
18
14
|
|
|
19
|
-
`
|
|
15
|
+
`encode` returns an `AdminMessage`, copying rather than modifying `message:` when provided. Supply exactly one payload variant. Conflicting variants, unknown option names, missing payloads, and nil payload values are rejected rather than silently overwriting the protobuf oneof. Protobuf field types and numeric bounds are enforced by the generated Ruby classes. False and zero are preserved, including `nodedb_reset: false`, `get_config_request: :DEVICE_CONFIG`, and backup location `:FLASH`.
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
Every generated payload field is available through `encode` and `send`, including response fields for tooling. Wrappers below accept the common transport options as well. Missing required scalar values are not silently converted to zero or empty text. An explicit empty ringtone or canned-message string may clear it.
|
|
22
18
|
|
|
23
|
-
##
|
|
19
|
+
## Operation catalog
|
|
20
|
+
|
|
21
|
+
| Group | Methods and payload arguments |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Identity | `get_owner`; `set_owner(owner:)` or `set_owner(long_name:, short_name:)`; `set_ham_mode(ham:)` or callsign/frequency/power/name fields |
|
|
24
|
+
| Channels | `get_channel(index: 0)`; `set_channel(channel_settings:)` (or `channel_pb:`) |
|
|
25
|
+
| Configuration | `get_config(config_type: :DEVICE_CONFIG)`; `set_config(config:)`; `get_module_config(module_config_type: :MQTT_CONFIG)`; `set_module_config(module_config:)` |
|
|
26
|
+
| UI | `get_ui_config`; `store_ui_config(ui_config:)`; `send_input_event(event:)` or `event_code`, `kb_char`, `touch_x`, `touch_y` |
|
|
27
|
+
| Canned messages | `get_canned_messages`; `set_canned_messages(messages:)` with pipe-separated text |
|
|
28
|
+
| Ringtone | `get_ringtone`; `set_ringtone(ringtone:)` with RTTTL text |
|
|
29
|
+
| Device information | `get_device_metadata`; `get_device_connection_status`; `get_node_remote_hardware_pins` |
|
|
30
|
+
| Position/time | `set_fixed_position(position:)` or `lat`, `lon`, `altitude`; `remove_fixed_position`; `set_time(time:)` with Unix seconds |
|
|
31
|
+
| Node database | `remove_by_nodenum(node_num:)`; `set_favorite_node(node_num:)`; `remove_favorite_node(node_num:)`; `set_ignored_node(node_num:)`; `remove_ignored_node(node_num:)`; `toggle_muted_node(node_num:)`; `add_contact(contact:)` |
|
|
32
|
+
| Edit transactions | `begin_edit`; `commit_edit` — defer implicit persistence/reboot for owner/channel/config/module changes until commit |
|
|
33
|
+
| Preferences | `backup_preferences(location: :FLASH)`; `restore_preferences(location: :FLASH)`; `remove_backup_preferences(location: :FLASH)`; location may also be `:SD` |
|
|
34
|
+
| Files and sensors | `delete_file(path:)`; `set_scale(scale:)`; `sensor_config(sensor_config:)` |
|
|
35
|
+
| Authentication | `key_verification(key_verification:)`; `lockdown_auth(lockdown_auth:)` — typed protobuf payloads, not an automatic authentication workflow |
|
|
36
|
+
| Power | `reboot(seconds: 5)`; `shutdown(seconds: 5)`; negative delays cancel pending actions |
|
|
37
|
+
| Firmware | `enter_dfu`; `ota_request(event:)` with `AdminMessage::OTAEvent`; `reboot_ota(seconds: 5)` for deprecated legacy firmware only |
|
|
38
|
+
| Resets | `factory_reset_device(value: 1)`; `factory_reset_config(value: 1)`; `nodedb_reset(preserve_favorites: true)`; `exit_simulator` |
|
|
39
|
+
| Utilities | `encode`; `send`; `request`; `decode`; `response`; `help`; `authors` |
|
|
40
|
+
|
|
41
|
+
Device reset clears BLE bonds; config reset preserves them. `nodedb_reset(preserve_favorites: false)` requests removal of favorites too, but firmware CLIENT_BASE/ROUTER/ROUTER_LATE roles can preserve favorites regardless. DFU is hardware-specific (upstream documents NRF52); OTA depends on the installed loader and supported mode. Modern firmware uses `ota_request`; `reboot_ota_seconds` is deprecated and absent from the inspected current AdminModule switch. Use [Admin::Firmware](admin-firmware.md) for the firmware transfer workflow.
|
|
42
|
+
|
|
43
|
+
## Responses, correlation and session passkeys
|
|
44
|
+
|
|
45
|
+
`decode(payload: bytes)` decodes raw AdminMessage bytes. `decode(packet:)` accepts a protobuf `FromRadio`, `MeshPacket`, or `Data` and requires decoded `ADMIN_APP` data. Malformed protobuf bytes raise the protobuf decoder error.
|
|
46
|
+
|
|
47
|
+
`response(packet:, request_id:, from:)` accepts a `FromRadio` or `MeshPacket`, optionally matches the outgoing packet ID and numeric sender, and returns:
|
|
24
48
|
|
|
25
49
|
```ruby
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
serial_obj = Meshtastic::Serial.connect(block_dev: '/dev/ttyACM0')
|
|
29
|
-
Meshtastic::Serial.wait_for_config(serial_obj: serial_obj)
|
|
30
|
-
|
|
31
|
-
Meshtastic::Admin.set_owner(serial_obj: serial_obj, long_name: 'Node', short_name: 'N1')
|
|
32
|
-
Meshtastic::Admin.get_owner(serial_obj: serial_obj)
|
|
33
|
-
Meshtastic::Admin.get_config(serial_obj: serial_obj, config_type: :LORA_CONFIG)
|
|
34
|
-
Meshtastic::Admin.get_channel(serial_obj: serial_obj, index: 0)
|
|
35
|
-
Meshtastic::Admin.reboot(serial_obj: serial_obj, seconds: 5)
|
|
36
|
-
# Meshtastic::Admin.shutdown(serial_obj: serial_obj, seconds: 5)
|
|
37
|
-
# Meshtastic::Admin.nodedb_reset(serial_obj: serial_obj)
|
|
50
|
+
{ message: admin_message, variant: :get_owner_response, value: owner,
|
|
51
|
+
session_passkey: passkey_bytes, request_id: packet_id, from: node_number }
|
|
38
52
|
```
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
Unrelated ports, non-response variants, encrypted/absent data, and mismatched IDs/senders return nil. These helpers do not authenticate packets or decrypt encrypted packets. `response` remains a pure decoder; use `request` for synchronous routing-error handling.
|
|
55
|
+
|
|
56
|
+
### Synchronous request/readback
|
|
57
|
+
|
|
58
|
+
`request` accepts raw `send` options or `message: AdminMessage`, plus `timeout:` (positive finite seconds, default 10), `request_id:` and `wait:`. By default it sends and waits on the connected radio's `from_radio_queue`. A getter returns the response hash above plus `result:` (the transport submission result). It requires both the outgoing request ID and target node, and the expected response variant. A local routing ACK is **not** remote readback. State-changing requests wait for a target-correlated `ROUTING_APP/NONE` and return `{ variant: :routing, value: :NONE, request_id:, from:, result: }`; even this is acknowledgment, not verification of persistent configuration.
|
|
41
59
|
|
|
42
60
|
```ruby
|
|
43
|
-
Meshtastic::Admin.
|
|
44
|
-
serial_obj: serial_obj,
|
|
45
|
-
|
|
61
|
+
reply = Meshtastic::Admin.request(
|
|
62
|
+
serial_obj: serial_obj, # alternatively bluetooth_obj: or tcp_obj:
|
|
63
|
+
to: '!aabbccdd',
|
|
64
|
+
message: Meshtastic::AdminMessage.new(get_device_metadata_request: true),
|
|
65
|
+
timeout: 10
|
|
46
66
|
)
|
|
67
|
+
version = reply[:value].firmware_version
|
|
47
68
|
```
|
|
48
69
|
|
|
70
|
+
After reboot, reconnect, complete the transport handshake, and make this request on the **new handle**. Match the returned version against the expected firmware; neither a successful upload nor an ACK establishes firmware health.
|
|
71
|
+
|
|
72
|
+
- `Timeout::Error` means no matching response arrived within the monotonic receive budget, or the receive queue closed. The budget includes automatic session acquisition. Blocking transport writes retain their underlying transport's I/O behavior; this is not an interrupting write timeout.
|
|
73
|
+
- `Admin::RoutingError` exposes `reason`, `request_id`, and `from`. Correlated nonzero routing errors from the target **or connected local radio** terminate immediately (including local PKI/no-route failures).
|
|
74
|
+
- Unrelated packets are retained and returned to the queue on success or failure. Deferred packets can move behind newer queued traffic. If the queue closes, the handle receives a replacement closed queue retaining those packets. Do not retain a separate queue reference across disconnects.
|
|
75
|
+
- Pause any external `subscribe`/`recv_from_radio` consumer while a synchronous request owns the receive queue. Concurrent synchronous requests on one handle fail fast with `IOError`; Admin does not install a global transport dispatcher.
|
|
76
|
+
- MQTT has no compatible radio queue; synchronous requests and automatic acquisition are rejected rather than pretending submission is readback.
|
|
77
|
+
- `request(wait: false, ...)` retains the old `{ request_id:, result: }` submission-only API, including MQTT. Use your existing receive loop with `response` in that mode. Automatic remote session acquisition can still wait unless an explicit key or `auto_session: false` is supplied.
|
|
78
|
+
- IDs are generated or supplied with `request_id:` (2 through `0xffffffff`), overriding `last_packet_id`. Use fresh IDs; deliberately reusing one cannot distinguish a stale reply. ID 1 is excluded because transport predecessor zero requests a random ID.
|
|
79
|
+
|
|
80
|
+
### Automatic sessions
|
|
81
|
+
|
|
82
|
+
Remote state-changing `send` calls (including convenience setters, reboot, and firmware commands) automatically request `get_config_request: :SESSIONKEY_CONFIG` before transmitting when no passkey was supplied. Acquisition must return a correlated `get_config_response` containing exactly eight passkey bytes; otherwise the write is not sent. Reads and local PhoneAPI operations do not need acquisition. An explicit eight-byte `session_passkey:` or a passkey already in `message:` bypasses acquisition; `auto_session: false` explicitly disables it. For MQTT provide the key obtained through your own authorized receive workflow.
|
|
83
|
+
|
|
84
|
+
The key is cached **only in the connection handle, scoped by target node**. A matching synchronous getter refreshes that target's cache. Cache entries expire conservatively after 150 seconds measured from the start of acquisition; `refresh_session: true` forces a new acquisition. `ADMIN_BAD_SESSION_KEY` invalidates the target cache, so the **next caller-initiated** write acquires again. No state-changing request is automatically replayed, including on timeout or rejection. Reconnect with a new handle after reboot to discard old state.
|
|
85
|
+
|
|
86
|
+
The node's passkey is not its public/private key, channel PSK, or a substitute for remote admin authorization. Upstream exempts local PhoneAPI commands (`from == 0`) and enumerated getters/responses from passkey checks. Other remote commands require it. Firmware expires keys after 300 seconds and can rotate them when issuing a response after 150 seconds; another controller can therefore invalidate even a locally unexpired key. The library does not log keys or payloads; response hashes and connection handles contain secrets, so do not log or serialize them. `:SESSIONKEY_CONFIG` carries the key in the **AdminMessage envelope**, not the Config payload.
|
|
87
|
+
|
|
88
|
+
Convenience getters and `send` still return the transport submission result. They do not wait for readback; use `request` when verification matters.
|
|
89
|
+
|
|
90
|
+
## Sources and compatibility
|
|
91
|
+
|
|
92
|
+
Official upstream sources inspected for the wire contract and firmware behavior:
|
|
93
|
+
|
|
94
|
+
- [admin.proto](https://github.com/meshtastic/protobufs/blob/master/meshtastic/admin.proto)
|
|
95
|
+
- [AdminModule.cpp](https://github.com/meshtastic/firmware/blob/master/src/modules/AdminModule.cpp)
|
|
96
|
+
|
|
97
|
+
The generated protobuf schema can expose fields not implemented by an installed firmware build. Sensor, key-verification, lockdown, module-specific handlers, simulator, SD, DFU, and OTA functionality remain firmware/hardware dependent. Encoding a field does not establish that a device supports it. This implementation was verified with real Ruby protobuf encoding and fake transports, not live hardware.
|
|
98
|
+
|
|
49
99
|
## Related
|
|
50
100
|
|
|
51
|
-
- [
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
101
|
+
- [Admin::Channel](admin-channel.md)
|
|
102
|
+
- [Admin::Config](admin-config.md)
|
|
103
|
+
- [Admin::Firmware](admin-firmware.md)
|
|
104
|
+
- [Channel](channel.md)
|
|
105
|
+
- [Config](config.md)
|
|
106
|
+
- [ModuleConfig](module-config.md)
|
|
107
|
+
- [RTTTL](rtttl.md)
|
|
@@ -18,6 +18,13 @@ Packet builder used by Serial, Bluetooth, TCP, and MQTT. Instantiated internally
|
|
|
18
18
|
|
|
19
19
|
On Serial/Bluetooth/TCP, transports pass `psks: nil` so the radio owns channel crypto. MQTT must pass `psks`.
|
|
20
20
|
|
|
21
|
+
`send_data` and `send_packet` preserve `pki_encrypted: true` and `public_key:`
|
|
22
|
+
(32 raw bytes) for remote administrative requests. The connected radio performs
|
|
23
|
+
the public-key encryption; the Ruby client does not encrypt these packets itself.
|
|
24
|
+
Explicit PKI requests reject MQTT and host-side PSK encryption instead of silently
|
|
25
|
+
falling back to channel encryption. Supplying a recipient key does not grant admin
|
|
26
|
+
rights: the target must authorize the sending radio's key.
|
|
27
|
+
|
|
21
28
|
`send_text` refuses payloads larger than `Meshtastic::Constants::DATA_PAYLOAD_LEN`.
|
|
22
29
|
|
|
23
30
|
## Example
|
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'meshtastic/channel_pb'
|
|
4
|
+
require 'meshtastic/apponly_pb'
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'uri'
|
|
4
7
|
|
|
5
8
|
module Meshtastic
|
|
6
9
|
module Admin
|
|
7
10
|
module Channel
|
|
8
11
|
public_class_method def self.build_settings(opts = {})
|
|
9
|
-
|
|
10
|
-
settings
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
source = opts[:settings]
|
|
13
|
+
settings = if source.is_a?(Hash)
|
|
14
|
+
Meshtastic::ChannelSettings.new(source)
|
|
15
|
+
elsif source
|
|
16
|
+
Meshtastic::ChannelSettings.decode(source.to_proto)
|
|
17
|
+
else
|
|
18
|
+
Meshtastic::ChannelSettings.new
|
|
19
|
+
end
|
|
20
|
+
Meshtastic::ChannelSettings.descriptor.each do |field|
|
|
21
|
+
key = field.name.to_sym
|
|
22
|
+
next unless opts.key?(key)
|
|
23
|
+
|
|
24
|
+
value = opts[key]
|
|
25
|
+
value = field.subtype.msgclass.new(value) if value.is_a?(Hash) && field.subtype
|
|
26
|
+
settings[field.name] = value
|
|
27
|
+
end
|
|
28
|
+
raise ArgumentError, 'PSK must contain 0, 1, 16, or 32 raw bytes' unless [0, 1, 16, 32].include?(settings.psk.bytesize)
|
|
29
|
+
raise ArgumentError, 'channel name must be fewer than 12 bytes' unless settings.name.bytesize < 12
|
|
30
|
+
|
|
18
31
|
settings
|
|
19
32
|
end
|
|
20
33
|
|
|
21
34
|
public_class_method def self.build(opts = {})
|
|
22
|
-
channel = opts[:channel]
|
|
23
|
-
|
|
35
|
+
channel = opts[:channel] ? Meshtastic::Channel.decode(opts[:channel].to_proto) : Meshtastic::Channel.new
|
|
36
|
+
index = opts.fetch(:index, channel.index)
|
|
37
|
+
raise ArgumentError, 'index must be an integer from 0 through 7' unless index.is_a?(Integer) && (0..7).cover?(index)
|
|
38
|
+
|
|
39
|
+
channel.index = index
|
|
24
40
|
channel.role = opts[:role] if opts[:role]
|
|
25
|
-
|
|
41
|
+
raise ArgumentError, 'role must be PRIMARY, SECONDARY, or DISABLED' unless %i[PRIMARY SECONDARY DISABLED].include?(channel.role)
|
|
42
|
+
|
|
43
|
+
channel.settings = build_settings(opts.merge(settings: opts.fetch(:settings, channel.settings)))
|
|
26
44
|
channel
|
|
27
45
|
end
|
|
28
46
|
|
|
@@ -33,12 +51,52 @@ module Meshtastic
|
|
|
33
51
|
end
|
|
34
52
|
|
|
35
53
|
public_class_method def self.set(opts = {})
|
|
36
|
-
channel =
|
|
37
|
-
|
|
38
|
-
merged.
|
|
54
|
+
channel = build(opts.merge({}))
|
|
55
|
+
builder_keys = Meshtastic::ChannelSettings.descriptor.map { |field| field.name.to_sym } + %i[channel index role settings]
|
|
56
|
+
merged = opts.except(*builder_keys).merge(channel_settings: channel)
|
|
39
57
|
Admin.set_channel(merged)
|
|
40
58
|
end
|
|
41
59
|
|
|
60
|
+
public_class_method def self.export_url(opts = {})
|
|
61
|
+
channels = opts.fetch(:channels)
|
|
62
|
+
raise ArgumentError, 'exactly one primary channel is required' unless channels.one? { |channel| channel.role == :PRIMARY }
|
|
63
|
+
|
|
64
|
+
enabled = channels.select { |channel| channel.role == :PRIMARY || (opts[:include_all] != false && channel.role == :SECONDARY) }
|
|
65
|
+
raise ArgumentError, 'at most eight enabled channels can be shared' if enabled.length > 8
|
|
66
|
+
|
|
67
|
+
enabled = enabled.sort_by { |channel| [channel.role == :PRIMARY ? 0 : 1, channel.index] }
|
|
68
|
+
settings = enabled.map { |channel| build_settings(settings: channel.settings) }
|
|
69
|
+
channel_set = Meshtastic::ChannelSet.new(settings: settings, lora_config: opts[:lora_config])
|
|
70
|
+
"https://meshtastic.org/e/##{Base64.urlsafe_encode64(channel_set.to_proto, padding: false)}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
public_class_method def self.import_url(opts = {})
|
|
74
|
+
uri = URI.parse(opts.fetch(:url))
|
|
75
|
+
valid = uri.scheme == 'https' && uri.host == 'meshtastic.org' && %w[/e/ /d/].include?(uri.path)
|
|
76
|
+
valid &&= uri.userinfo.nil? && uri.port == 443 && uri.fragment&.match?(/\A[A-Za-z0-9_-]+={0,2}\z/)
|
|
77
|
+
raise ArgumentError, 'invalid channel URL' unless valid
|
|
78
|
+
|
|
79
|
+
channel_set = Meshtastic::ChannelSet.decode(Base64.urlsafe_decode64(uri.fragment))
|
|
80
|
+
raise ArgumentError, 'invalid channel URL' unless (1..8).cover?(channel_set.settings.length)
|
|
81
|
+
|
|
82
|
+
channel_set
|
|
83
|
+
rescue URI::InvalidURIError, Google::Protobuf::ParseError, ArgumentError, TypeError
|
|
84
|
+
raise ArgumentError, 'invalid channel URL'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
public_class_method def self.apply_url(opts = {})
|
|
88
|
+
channel_set = import_url(url: opts[:url])
|
|
89
|
+
raise ArgumentError, 'add-only or query URL application is not supported; import offline and select slots explicitly' if URI.parse(opts[:url]).query
|
|
90
|
+
|
|
91
|
+
transport = opts.except(:url)
|
|
92
|
+
channels = channel_set.settings.each_with_index.map do |settings, index|
|
|
93
|
+
build(index: index, role: index.zero? ? :PRIMARY : :SECONDARY, settings: settings)
|
|
94
|
+
end
|
|
95
|
+
results = channels.map { |channel| set(transport.merge(channel: channel)) }
|
|
96
|
+
results << Config.set_lora(transport.merge(lora: channel_set.lora_config)) if channel_set.lora_config
|
|
97
|
+
results
|
|
98
|
+
end
|
|
99
|
+
|
|
42
100
|
public_class_method def self.authors
|
|
43
101
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
44
102
|
end
|
|
@@ -47,29 +105,29 @@ module Meshtastic
|
|
|
47
105
|
puts "USAGE:
|
|
48
106
|
# Build ChannelSettings for a mesh channel.
|
|
49
107
|
#{self}.build_settings(
|
|
50
|
-
settings: 'optional -
|
|
51
|
-
name: 'optional - channel name
|
|
52
|
-
psk: 'optional - raw PSK
|
|
53
|
-
channel_num: 'optional -
|
|
108
|
+
settings: 'optional - ChannelSettings protobuf or Hash copied before overlay',
|
|
109
|
+
name: 'optional - channel name shorter than twelve UTF-8 bytes',
|
|
110
|
+
psk: 'optional - raw PSK of zero, one, sixteen, or thirty-two bytes',
|
|
111
|
+
channel_num: 'optional - deprecated channel number; prefer Config LoRa channel_num',
|
|
54
112
|
id: 'optional - channel hash id',
|
|
55
113
|
uplink_enabled: 'optional - whether MQTT uplink is enabled',
|
|
56
114
|
downlink_enabled: 'optional - whether MQTT downlink is enabled',
|
|
57
|
-
module_settings: 'optional - ModuleSettings protobuf',
|
|
115
|
+
module_settings: 'optional - ModuleSettings protobuf or field Hash for precision and mute',
|
|
58
116
|
use_aead: 'optional - whether AEAD crypto is enabled'
|
|
59
117
|
)
|
|
60
118
|
|
|
61
119
|
# Build a Channel protobuf with index, role, and settings.
|
|
62
120
|
#{self}.build(
|
|
63
|
-
channel: 'optional - existing Channel protobuf
|
|
64
|
-
index: 'optional - channel slot
|
|
121
|
+
channel: 'optional - existing Channel protobuf copied before overlay',
|
|
122
|
+
index: 'optional - integer channel slot zero through seven',
|
|
65
123
|
role: 'optional - :PRIMARY, :SECONDARY, or :DISABLED',
|
|
66
|
-
settings: 'optional - ChannelSettings protobuf to attach'
|
|
124
|
+
settings: 'optional - ChannelSettings protobuf or field Hash to attach'
|
|
67
125
|
)
|
|
68
126
|
|
|
69
127
|
# Request a channel slot from the node.
|
|
70
128
|
#{self}.get(
|
|
71
129
|
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
|
|
72
|
-
index: 'optional - channel slot
|
|
130
|
+
index: 'optional - zero-based channel slot; Admin adds one on wire (default: 0)'
|
|
73
131
|
)
|
|
74
132
|
|
|
75
133
|
# Write a channel slot on the node.
|
|
@@ -78,7 +136,23 @@ module Meshtastic
|
|
|
78
136
|
channel: 'optional - Channel protobuf to write',
|
|
79
137
|
index: 'optional - channel slot index when building a channel',
|
|
80
138
|
role: 'optional - :PRIMARY, :SECONDARY, or :DISABLED',
|
|
81
|
-
settings: 'optional - ChannelSettings protobuf to attach'
|
|
139
|
+
settings: 'optional - ChannelSettings protobuf or field Hash to attach'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Export enabled channels to a sharing URL.
|
|
143
|
+
#{self}.export_url(
|
|
144
|
+
channels: 'required - array of Channel protobufs with one primary',
|
|
145
|
+
include_all: 'optional - include secondary channels unless false',
|
|
146
|
+
lora_config: 'optional - LoRaConfig protobuf included in the URL'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Decode a sharing URL without writing hardware.
|
|
150
|
+
#{self}.import_url(url: 'required - Meshtastic e or d channel URL')
|
|
151
|
+
|
|
152
|
+
# Write URL channels and optional LoRa configuration.
|
|
153
|
+
#{self}.apply_url(
|
|
154
|
+
url: 'required - Meshtastic channel URL replacing slots from zero',
|
|
155
|
+
serial_obj: 'optional - connected serial transport; BLE, TCP, MQTT also supported'
|
|
82
156
|
)
|
|
83
157
|
|
|
84
158
|
# Print the AUTHOR(S) string for this module.
|