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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/documentation/README.md +3 -2
  4. data/documentation/admin-channel.md +64 -0
  5. data/documentation/admin-config.md +59 -0
  6. data/documentation/admin-firmware-nordic.md +86 -0
  7. data/documentation/admin-firmware-serial.md +180 -0
  8. data/documentation/admin-firmware.md +126 -0
  9. data/documentation/admin.md +88 -35
  10. data/documentation/atak.md +93 -10
  11. data/documentation/channel.md +1 -28
  12. data/documentation/config.md +1 -27
  13. data/documentation/mesh-interface.md +7 -0
  14. data/documentation/meshtastic.md +2 -2
  15. data/lib/meshtastic/admin/channel.rb +164 -0
  16. data/lib/meshtastic/admin/config.rb +213 -0
  17. data/lib/meshtastic/admin/firmware/ble.rb +207 -0
  18. data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
  19. data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
  20. data/lib/meshtastic/admin/firmware.rb +288 -0
  21. data/lib/meshtastic/admin.rb +688 -34
  22. data/lib/meshtastic/apponly.rb +19 -7
  23. data/lib/meshtastic/atak.rb +295 -17
  24. data/lib/meshtastic/bluetooth/bluez.rb +20 -1
  25. data/lib/meshtastic/bluetooth.rb +70 -59
  26. data/lib/meshtastic/cannedmessages.rb +13 -6
  27. data/lib/meshtastic/channel.rb +3 -25
  28. data/lib/meshtastic/clientonly.rb +19 -7
  29. data/lib/meshtastic/config.rb +3 -24
  30. data/lib/meshtastic/config_pb.rb +2 -1
  31. data/lib/meshtastic/connection_status.rb +12 -5
  32. data/lib/meshtastic/deviceonly.rb +19 -7
  33. data/lib/meshtastic/localonly.rb +19 -7
  34. data/lib/meshtastic/mesh_interface.rb +19 -0
  35. data/lib/meshtastic/module_config.rb +13 -7
  36. data/lib/meshtastic/mqtt.rb +72 -35
  37. data/lib/meshtastic/paxcount.rb +17 -6
  38. data/lib/meshtastic/portnums.rb +13 -6
  39. data/lib/meshtastic/position.rb +16 -6
  40. data/lib/meshtastic/remote_hardware.rb +27 -11
  41. data/lib/meshtastic/rtttl.rb +17 -8
  42. data/lib/meshtastic/serial.rb +81 -70
  43. data/lib/meshtastic/storeforward.rb +13 -6
  44. data/lib/meshtastic/storeforward_pb.rb +1 -1
  45. data/lib/meshtastic/stream_interface.rb +11 -0
  46. data/lib/meshtastic/tcp.rb +73 -22
  47. data/lib/meshtastic/telemetry.rb +15 -6
  48. data/lib/meshtastic/traceroute.rb +15 -6
  49. data/lib/meshtastic/util.rb +3 -1
  50. data/lib/meshtastic/version.rb +1 -1
  51. data/lib/meshtastic/xmodem.rb +12 -5
  52. data/lib/meshtastic.rb +22 -3
  53. data/spec/conventions_spec.rb +321 -0
  54. data/spec/lib/meshtastic/admin/channel_spec.rb +202 -0
  55. data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
  56. data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
  57. data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
  58. data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
  59. data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
  60. data/spec/lib/meshtastic/admin_spec.rb +412 -1
  61. data/spec/lib/meshtastic/apponly_spec.rb +1 -1
  62. data/spec/lib/meshtastic/atak_spec.rb +129 -4
  63. data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
  64. data/spec/lib/meshtastic/channel_spec.rb +2 -20
  65. data/spec/lib/meshtastic/config_spec.rb +2 -20
  66. data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
  67. data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
  68. data/spec/lib/meshtastic/localonly_spec.rb +1 -1
  69. data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
  70. data/spec/lib/meshtastic/portnums_spec.rb +2 -2
  71. metadata +24 -6
@@ -1,54 +1,107 @@
1
1
  # Meshtastic::Admin
2
2
 
3
- Builds `Meshtastic::AdminMessage` and sends it on `ADMIN_APP` (port 6) through [Meshtastic.deliver_data](meshtastic.md). Pass a connected `serial_obj`, `bluetooth_obj`, or `tcp_obj`.
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
- Responses come back as `FromRadio` packets on the same transport (`subscribe` / `recv_from_radio`).
5
+ ## Addressing and defaults
6
6
 
7
- ## Methods
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
- - `encode` — copies matching keys onto an `AdminMessage`
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
- `config_type` values: `:DEVICE_CONFIG`, `:POSITION_CONFIG`, `:POWER_CONFIG`, `:NETWORK_CONFIG`, `:DISPLAY_CONFIG`, `:LORA_CONFIG`, `:BLUETOOTH_CONFIG`, `:SECURITY_CONFIG`, `:SESSIONKEY_CONFIG`, `:DEVICEUI_CONFIG`.
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
- Any other `AdminMessage` field can be passed to `send` / `encode` (for example `set_ringtone_message`, `get_module_config_request`).
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
- ## Examples
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
- require 'meshtastic'
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
- Raw field:
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.send(
44
- serial_obj: serial_obj,
45
- factory_reset_config: true
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
- - [Meshtastic::Channel](channel.md)
52
- - [Meshtastic::Config](config.md)
53
- - [Meshtastic::ModuleConfig](module-config.md)
54
- - [Meshtastic::RTTTL](rtttl.md)
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)
@@ -1,27 +1,110 @@
1
1
  # Meshtastic::ATAK
2
2
 
3
- Encodes `Meshtastic::TAKPacket` and sends it on `ATAK_PLUGIN` (port 72).
3
+ TAK / ATAK over Meshtastic. Three wire formats, same as the official clients:
4
+
5
+ | Format | Port | Payload |
6
+ | --- | --- | --- |
7
+ | V1 `ATAK_PLUGIN` | 72 | Bare `TAKPacket` (PLI, GeoChat, optional `detail` bytes) |
8
+ | V2 `ATAK_PLUGIN_V2` | 78 | `[flags][TAKPacketV2]`. This gem emits uncompressed frames (`flags=0xFF`) |
9
+ | V1 `ATAK_FORWARDER` | 257 | zlib-compressed CoT XML (single packet, max `DATA_PAYLOAD_LEN`) |
10
+
11
+ V2 typed payloads: GeoChat, aircraft, shapes, markers, range-and-bearing, routes, CASEVAC, emergency, task, TAKTALK, raw detail. Contact / group / status on V1; callsign / team / role / lat-lon on V2.
12
+
13
+ Firmware ≥ 2.8.0 speaks V2. Older radios use V1 PLI/chat, and generic CoT on the forwarder port.
14
+
15
+ Compressed V2 (zstd dictionary id 0 or 1) is not unpacked here; `decode_v2` raises unless `flags=0xFF`.
4
16
 
5
17
  ## Methods
6
18
 
7
- - `encode(is_compressed:, chat:, message:)` — `message:` builds a `GeoChat`
8
- - `send`
19
+ - `encode` / `encode_v1` — `TAKPacket`
20
+ - `send` / `send_v1` / `send_chat` / `send_pli` — port 72
21
+ - `build_v2` / `encode_v2` / `wrap_v2` / `decode_v2` / `send_v2` — port 78
22
+ - `compress_cot` / `decompress_cot` / `send_cot` — port 257 (`cot:` XML string)
23
+ - `decode(payload:, portnum:)` — dispatches on port
9
24
  - `help` / `authors`
10
25
 
11
- ## Example
26
+ `lat` / `lon` are decimal degrees (stored × 1e7). Pass a connected `serial_obj`, `bluetooth_obj`, or `tcp_obj`.
27
+
28
+ ## V1 GeoChat and PLI
12
29
 
13
30
  ```ruby
14
- Meshtastic::ATAK.send(
31
+ Meshtastic::ATAK.send_chat(
32
+ serial_obj: serial_obj,
33
+ message: 'ATAK chat',
34
+ to: 'ANDROID-aabbccdd',
35
+ callsign: 'ALPHA',
36
+ device_callsign: 'RADIO-1',
37
+ team: :Cyan,
38
+ role: :TeamMember,
39
+ battery: 87
40
+ )
41
+
42
+ Meshtastic::ATAK.send_pli(
43
+ serial_obj: serial_obj,
44
+ lat: 37.7749,
45
+ lon: -122.4194,
46
+ altitude: 10,
47
+ speed: 0,
48
+ course: 90,
49
+ callsign: 'ALPHA',
50
+ team: :Cyan
51
+ )
52
+ ```
53
+
54
+ ## V2 typed events
55
+
56
+ ```ruby
57
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, message: 'v2 chat', callsign: 'ALPHA')
58
+
59
+ Meshtastic::ATAK.send_v2(
15
60
  serial_obj: serial_obj,
16
- message: 'ATAK chat'
61
+ callsign: 'ALPHA',
62
+ lat: 37.7749,
63
+ lon: -122.4194,
64
+ aircraft: Meshtastic::AircraftTrack.new(icao: 'ABC123', flight: 'N1')
17
65
  )
18
66
 
19
- packet = Meshtastic::ATAK.encode(message: 'ATAK chat')
20
- packet.chat.message # => "ATAK chat"
67
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, shape: Meshtastic::DrawnShape.new(kind: :Kind_Circle, major_cm: 1000))
68
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, marker: Meshtastic::Marker.new(kind: :Kind_Spot))
69
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, route: Meshtastic::Route.new(prefix: 'R1'))
70
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, casevac: Meshtastic::CasevacReport.new(title: 'CASEVAC'))
71
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, emergency: Meshtastic::EmergencyAlert.new(type: :Type_Alert911))
72
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, task: Meshtastic::TaskRequest.new(task_type: 'recon'))
73
+ Meshtastic::ATAK.send_v2(serial_obj: serial_obj, taktalk: Meshtastic::TakTalkMessage.new(text: 'hi', chatroom_id: 'room1'))
21
74
  ```
22
75
 
23
- Related protobufs: `TAKPacket`, `GeoChat`, `Contact`, `Group`, `PLI`, `TAKPacketV2`. See [protobufs.md](protobufs.md).
76
+ Receive V2:
77
+
78
+ ```ruby
79
+ Meshtastic::Serial.subscribe(serial_obj: serial_obj, include: 'ATAK_PLUGIN') do |message|
80
+ port = message.dig(:packet, :decoded, :portnum)
81
+ payload = message.dig(:packet, :decoded, :payload)
82
+ decoded = Meshtastic::ATAK.decode(payload: payload, portnum: port)
83
+ p decoded
84
+ end
85
+ ```
86
+
87
+ ## Generic CoT (forwarder)
88
+
89
+ ```ruby
90
+ Meshtastic::ATAK.send_cot(
91
+ serial_obj: serial_obj,
92
+ cot: '<event type="b-m-p-s-m" uid="marker-1"><point lat="37.77" lon="-122.41"/></event>'
93
+ )
94
+ ```
95
+
96
+ Raises if zlib output exceeds `Meshtastic::Constants::DATA_PAYLOAD_LEN`. Multi-packet fountain (FTN) is not implemented; keep CoT small or use V2 typed payloads.
97
+
98
+ ## Radio TAK module
99
+
100
+ Device role `TAK` / `TAK_TRACKER` and Module Config → TAK (`team` / `role`) are [Admin](admin.md) / [ModuleConfig](module-config.md), not this client:
101
+
102
+ ```ruby
103
+ Meshtastic::ModuleConfig.get(serial_obj: serial_obj, module_config_type: :TAK_CONFIG)
104
+ ```
24
105
 
25
106
  ## Related
26
107
 
27
- - [Meshtastic::ModuleConfig](module-config.md) (`:TAK_CONFIG`)
108
+ - [Meshtastic::ModuleConfig](module-config.md)
109
+ - [Generated protobuf types](protobufs.md)
110
+ - [TAK wire formats](https://meshtastic.org/docs/software/apple/developer/tak-protocol/)
@@ -1,30 +1,3 @@
1
1
  # Meshtastic::Channel
2
2
 
3
- Reopens the generated `Meshtastic::Channel` protobuf class with get/set helpers that call [Admin](admin.md).
4
-
5
- This is a protobuf class, not a Ruby module. Do not add class methods named `send` or `encode`.
6
-
7
- ## Methods
8
-
9
- - `get(serial_obj:, index:)` — `get_channel_request`
10
- - `set(serial_obj:, channel:)` — `set_channel`
11
- - `help` / `authors`
12
-
13
- Also all protobuf instance fields: `index`, `settings` (`ChannelSettings`), `role` (`:DISABLED`, `:PRIMARY`, `:SECONDARY`).
14
-
15
- ## Example
16
-
17
- ```ruby
18
- Meshtastic::Channel.get(serial_obj: serial_obj, index: 0)
19
-
20
- settings = Meshtastic::ChannelSettings.new(name: 'LongFast', psk: "\x01")
21
- channel = Meshtastic::Channel.new(index: 0, settings: settings, role: :PRIMARY)
22
- Meshtastic::Channel.set(serial_obj: serial_obj, channel: channel)
23
- ```
24
-
25
- Listen for `ADMIN_APP` replies on the same transport.
26
-
27
- ## Related
28
-
29
- - [Meshtastic::Admin](admin.md)
30
- - [Meshtastic::Apponly](apponly.md) (`ChannelSet`)
3
+ Generated protobuf (`index`, `settings`, `role`). Get/set on a live node is [Meshtastic::Admin::Channel](admin-channel.md).
@@ -1,29 +1,3 @@
1
1
  # Meshtastic::Config
2
2
 
3
- Reopens the generated `Meshtastic::Config` protobuf class. Get/set go through [Admin](admin.md).
4
-
5
- ## Methods
6
-
7
- - `get(serial_obj:, config_type: :DEVICE_CONFIG)`
8
- - `set(serial_obj:, config:)` — `config` is a `Meshtastic::Config`
9
- - `help` / `authors`
10
-
11
- Protobuf oneofs include `device`, `position`, `power`, `network`, `display`, `lora`, `bluetooth`, `security`, `sessionkey`, `device_ui`.
12
-
13
- ## Example
14
-
15
- ```ruby
16
- Meshtastic::Config.get(serial_obj: serial_obj, config_type: :LORA_CONFIG)
17
-
18
- config = Meshtastic::Config.new
19
- config.device = Meshtastic::Config::DeviceConfig.new(role: :CLIENT)
20
- Meshtastic::Config.set(serial_obj: serial_obj, config: config)
21
- ```
22
-
23
- `config_type` list: [Admin](admin.md).
24
-
25
- ## Related
26
-
27
- - [Meshtastic::Admin](admin.md)
28
- - [Meshtastic::ModuleConfig](module-config.md)
29
- - [Meshtastic::Localonly](localonly.md)
3
+ Generated protobuf (device, position, power, network, display, lora, bluetooth, security, sessionkey, device_ui). Get/set on a live node is [Meshtastic::Admin::Config](admin-config.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
@@ -31,13 +31,13 @@ Routes a `Meshtastic::Data` payload to a connected radio.
31
31
 
32
32
  ```ruby
33
33
  Meshtastic.deliver_data(
34
- serial_obj: serial_obj, # or bluetooth_obj: or tcp_obj:
34
+ serial_obj: serial_obj, # or bluetooth_obj: / tcp_obj: / mqtt_obj:
35
35
  data: Meshtastic::Data.new(portnum: :TEXT_MESSAGE_APP, payload: 'hi'),
36
36
  port_num: Meshtastic::PortNum::TEXT_MESSAGE_APP
37
37
  )
38
38
  ```
39
39
 
40
- Raises `ArgumentError` unless `data` is a `Meshtastic::Data` and one of `serial_obj`, `bluetooth_obj`, or `tcp_obj` is present.
40
+ Raises `ArgumentError` unless `data` is a `Meshtastic::Data` and one of `serial_obj`, `bluetooth_obj`, `tcp_obj`, or `mqtt_obj` is present.
41
41
 
42
42
  ## Related
43
43
 
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'meshtastic/channel_pb'
4
+ require 'meshtastic/apponly_pb'
5
+ require 'base64'
6
+ require 'uri'
7
+
8
+ module Meshtastic
9
+ module Admin
10
+ module Channel
11
+ public_class_method def self.build_settings(opts = {})
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
+
31
+ settings
32
+ end
33
+
34
+ public_class_method def self.build(opts = {})
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
40
+ channel.role = opts[:role] if opts[:role]
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)))
44
+ channel
45
+ end
46
+
47
+ public_class_method def self.get(opts = {})
48
+ index = opts[:index]
49
+ index = 0 if index.nil?
50
+ Admin.get_channel(opts.merge(index: index))
51
+ end
52
+
53
+ public_class_method def self.set(opts = {})
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)
57
+ Admin.set_channel(merged)
58
+ end
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
+
100
+ public_class_method def self.authors
101
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
102
+ end
103
+
104
+ public_class_method def self.help
105
+ puts "USAGE:
106
+ # Build ChannelSettings for a mesh channel.
107
+ #{self}.build_settings(
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',
112
+ id: 'optional - channel hash id',
113
+ uplink_enabled: 'optional - whether MQTT uplink is enabled',
114
+ downlink_enabled: 'optional - whether MQTT downlink is enabled',
115
+ module_settings: 'optional - ModuleSettings protobuf or field Hash for precision and mute',
116
+ use_aead: 'optional - whether AEAD crypto is enabled'
117
+ )
118
+
119
+ # Build a Channel protobuf with index, role, and settings.
120
+ #{self}.build(
121
+ channel: 'optional - existing Channel protobuf copied before overlay',
122
+ index: 'optional - integer channel slot zero through seven',
123
+ role: 'optional - :PRIMARY, :SECONDARY, or :DISABLED',
124
+ settings: 'optional - ChannelSettings protobuf or field Hash to attach'
125
+ )
126
+
127
+ # Request a channel slot from the node.
128
+ #{self}.get(
129
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
130
+ index: 'optional - zero-based channel slot; Admin adds one on wire (default: 0)'
131
+ )
132
+
133
+ # Write a channel slot on the node.
134
+ #{self}.set(
135
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
136
+ channel: 'optional - Channel protobuf to write',
137
+ index: 'optional - channel slot index when building a channel',
138
+ role: 'optional - :PRIMARY, :SECONDARY, or :DISABLED',
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'
156
+ )
157
+
158
+ # Print the AUTHOR(S) string for this module.
159
+ #{self}.authors
160
+ "
161
+ end
162
+ end
163
+ end
164
+ end