meshtastic 0.0.176 → 0.0.177
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/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +0 -21
- data/Gemfile +1 -0
- data/README.md +79 -82
- data/lib/meshtastic/bluetooth/bluez.rb +187 -0
- data/lib/meshtastic/bluetooth.rb +299 -0
- data/lib/meshtastic/lorawan_bridge_pb.rb +20 -0
- data/lib/meshtastic/mesh_interface.rb +3 -1
- data/lib/meshtastic/mqtt.rb +7 -34
- data/lib/meshtastic/{serial_interface.rb → serial.rb} +144 -106
- data/lib/meshtastic/stream_interface.rb +1 -1
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic.rb +3 -1
- data/spec/lib/meshtastic/bluetooth/bluez_spec.rb +190 -0
- data/spec/lib/meshtastic/bluetooth_spec.rb +146 -0
- data/spec/lib/meshtastic/lorawan_bridge_pb_spec.rb +6 -0
- data/spec/lib/meshtastic/mqtt_spec.rb +160 -1
- data/spec/lib/meshtastic/serial_spec.rb +295 -0
- metadata +23 -3
- data/spec/lib/meshtastic/serial_interface_spec.rb +0 -78
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f02b5c39c6c878904d2d2d7fb765bf0a66d48de021323ebc20ca55221e29f64
|
|
4
|
+
data.tar.gz: 592e7bf735c228217e2484f139f8682c8923d5ba1846a2f6212563df1f841a67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef44a3860a9a12450cff1902e4167377d47268a20a399741de32f5da0e0e6d9be9084d9485f4699e3726e23f7ff4de027b36b9a04e6b2164f27f1e438e591ee2
|
|
7
|
+
data.tar.gz: e355b9bf3afb4fc6394528ba85764307b9d7b2acc01d22dd85ae9c597388af793214a90d7cd7af0829d521ae2d430ab72e1fe365a9e92fd98bfd9bc3cd5bd309
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
|
@@ -22,13 +22,6 @@ Layout/EmptyLineAfterMagicComment:
|
|
|
22
22
|
Layout/EmptyLines:
|
|
23
23
|
Enabled: false
|
|
24
24
|
|
|
25
|
-
# Offense count: 2
|
|
26
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
27
|
-
# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
|
|
28
|
-
# SupportedStylesAlignWith: start_of_line, relative_to_receiver
|
|
29
|
-
Layout/IndentationWidth:
|
|
30
|
-
Exclude:
|
|
31
|
-
- 'lib/meshtastic/serial_interface.rb'
|
|
32
25
|
|
|
33
26
|
# Offense count: 26
|
|
34
27
|
# This cop supports safe autocorrection (--autocorrect).
|
|
@@ -37,14 +30,6 @@ Layout/IndentationWidth:
|
|
|
37
30
|
Layout/LineLength:
|
|
38
31
|
Enabled: false
|
|
39
32
|
|
|
40
|
-
# Offense count: 1
|
|
41
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
42
|
-
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
|
43
|
-
# SupportedStyles: space, no_space
|
|
44
|
-
# SupportedStylesForEmptyBraces: space, no_space
|
|
45
|
-
Layout/SpaceBeforeBlockBraces:
|
|
46
|
-
Exclude:
|
|
47
|
-
- 'lib/meshtastic/serial_interface.rb'
|
|
48
33
|
|
|
49
34
|
# Offense count: 49
|
|
50
35
|
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
|
@@ -63,7 +48,6 @@ Lint/UnusedMethodArgument:
|
|
|
63
48
|
# This cop supports safe autocorrection (--autocorrect).
|
|
64
49
|
Lint/UselessAssignment:
|
|
65
50
|
Exclude:
|
|
66
|
-
- 'lib/meshtastic/serial_interface.rb'
|
|
67
51
|
- 'lib/meshtastic/stream_interface.rb'
|
|
68
52
|
- 'meshtastic.gemspec'
|
|
69
53
|
|
|
@@ -86,11 +70,6 @@ Style/CommentAnnotation:
|
|
|
86
70
|
Style/Documentation:
|
|
87
71
|
Enabled: false
|
|
88
72
|
|
|
89
|
-
# Offense count: 1
|
|
90
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
91
|
-
Style/ExplicitBlockArgument:
|
|
92
|
-
Exclude:
|
|
93
|
-
- 'lib/meshtastic/serial_interface.rb'
|
|
94
73
|
|
|
95
74
|
# Offense count: 1
|
|
96
75
|
# This cop supports safe autocorrection (--autocorrect).
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -24,7 +24,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
24
24
|
|
|
25
25
|
## Usage
|
|
26
26
|
|
|
27
|
-
The primary interaction modules today are `Meshtastic::MQTT` (broker)
|
|
27
|
+
The primary interaction modules today are `Meshtastic::MQTT` (broker), `Meshtastic::Serial` (USB/UART), and `Meshtastic::Bluetooth` (BLE via Linux BlueZ). Examples for each follow.
|
|
28
28
|
|
|
29
29
|
### MQTT
|
|
30
30
|
|
|
@@ -101,111 +101,108 @@ You should see something like this:
|
|
|
101
101
|
|
|
102
102
|
Note where is says `channel: 93`. This is the `channel` value required to send messages in this particular example.
|
|
103
103
|
|
|
104
|
-
### Serial
|
|
104
|
+
### Serial and Bluetooth (send / receive)
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
`Meshtastic::Serial` (USB/UART) and `Meshtastic::Bluetooth` (Linux BLE via BlueZ) talk to a local radio using the same client API. The radio encrypts with its configured channel keys. Payloads are sent decoded; `channel:` is the index on the device, not an MQTT channel hash.
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Do not open Serial and Bluetooth to the same radio at once. Disconnect when finished (`ensure` is the reliable pattern). `send_text` / `send_data` report bytes written, not mesh delivery. `want_ack: true` requests a `ROUTING_APP` acknowledgment (`error_reason: NONE` means the local radio accepted the route). Incoming text is a UTF-8 string under `message[:packet][:decoded][:payload]`.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
require 'meshtastic'
|
|
112
|
-
Meshtastic::SerialInterface.help
|
|
113
|
-
serial_obj = Meshtastic::SerialInterface.connect(
|
|
114
|
-
block_dev: '/dev/ttyUSB0', # or /dev/ttyACM0
|
|
115
|
-
baud: 115_200
|
|
116
|
-
)
|
|
117
|
-
puts serial_obj.inspect
|
|
118
|
-
```
|
|
110
|
+
Call `wait_for_config` before using `my_node_num` or sending. It raises `Timeout::Error` if the firmware never completes the handshake. Opening a port or pairing is not the same as a completed handshake.
|
|
119
111
|
|
|
120
|
-
|
|
112
|
+
#### Serial (`Meshtastic::Serial`)
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
require 'meshtastic'
|
|
124
|
-
serial_obj = Meshtastic::SerialInterface.connect(
|
|
125
|
-
block_dev: '/dev/ttyUSB0',
|
|
126
|
-
baud: 115_200
|
|
127
|
-
)
|
|
128
|
-
Meshtastic::SerialInterface.subscribe(
|
|
129
|
-
serial_obj: serial_obj
|
|
130
|
-
) do |message|
|
|
131
|
-
puts message.inspect
|
|
132
|
-
end
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Filter with `include` / `exclude` (comma-delimited substrings), same idea as MQTT:
|
|
114
|
+
Use the device’s USB CDC port (`/dev/ttyACM*` or `/dev/ttyUSB*`). Enable Radio Configuration → Security → Serial Console (`security.serial_enabled`). That is not Module Configuration → Serial (`TEXTMSG` / `PROTO` on GPIO). Keep “Override Console Serial Port” off.
|
|
136
115
|
|
|
137
116
|
```ruby
|
|
138
117
|
require 'meshtastic'
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
|
|
119
|
+
serial_obj = nil
|
|
120
|
+
begin
|
|
121
|
+
serial_obj = Meshtastic::Serial.connect(block_dev: '/dev/ttyACM2', baud: 115_200)
|
|
122
|
+
Meshtastic::Serial.wait_for_config(serial_obj: serial_obj, timeout: 10)
|
|
123
|
+
puts "local node: !#{serial_obj[:my_node_num].to_s(16)}"
|
|
124
|
+
|
|
125
|
+
# Direct message, or to: '!ffffffff' for the shared channel.
|
|
126
|
+
Meshtastic::Serial.send_text(
|
|
127
|
+
serial_obj: serial_obj,
|
|
128
|
+
to: '!83726fb1',
|
|
129
|
+
channel: 0,
|
|
130
|
+
text: 'Hello over serial!',
|
|
131
|
+
want_ack: true
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
Meshtastic::Serial.subscribe(
|
|
135
|
+
serial_obj: serial_obj,
|
|
136
|
+
include: 'TEXT_MESSAGE_APP'
|
|
137
|
+
) do |message|
|
|
138
|
+
packet = message[:packet]
|
|
139
|
+
puts "#{packet[:node_id_from]}: #{packet.dig(:decoded, :payload)}"
|
|
140
|
+
end
|
|
141
|
+
ensure
|
|
142
|
+
Meshtastic::Serial.disconnect(serial_obj: serial_obj)
|
|
146
143
|
end
|
|
147
144
|
```
|
|
148
145
|
|
|
149
|
-
|
|
146
|
+
Drive the loop yourself with `recv_from_radio(serial_obj:, timeout:)` (`0` polls, `nil` blocks) or `drain_from_radio`. A closed empty queue returns `nil`; an unplugged device raises `IOError`. If `wait_for_config` times out, the USB path is up but the Stream API is not (wrong port, Serial Console disabled, or firmware not responding).
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
require 'meshtastic'
|
|
153
|
-
serial_obj = Meshtastic::SerialInterface.connect(
|
|
154
|
-
block_dev: '/dev/ttyUSB0',
|
|
155
|
-
baud: 115_200
|
|
156
|
-
)
|
|
157
|
-
Meshtastic::SerialInterface.send_text(
|
|
158
|
-
serial_obj: serial_obj,
|
|
159
|
-
to: '!ffffffff', # broadcast; or a node id like '!f33ddad5'
|
|
160
|
-
channel: 0, # primary channel index on the radio
|
|
161
|
-
text: 'Hello over serial!'
|
|
162
|
-
)
|
|
163
|
-
Meshtastic::SerialInterface.disconnect(serial_obj: serial_obj)
|
|
164
|
-
```
|
|
148
|
+
#### Bluetooth (`Meshtastic::Bluetooth`)
|
|
165
149
|
|
|
166
|
-
|
|
150
|
+
Linux only (BlueZ + `ruby-dbus`). Connect with a BLE address (`AA:BB:CC:DD:EE:FF`), not a mesh id (`!03d52a07`). Pair first; this gem does not guess a PIN. BLE writes unframed ToRadio protobufs (no UART `0x94 0xC3` header).
|
|
151
|
+
|
|
152
|
+
Scan:
|
|
167
153
|
|
|
168
154
|
```ruby
|
|
169
155
|
require 'meshtastic'
|
|
156
|
+
Meshtastic::Bluetooth.scan(adapter: 'hci0', timeout: 5)
|
|
157
|
+
# => [{ address: 'E8:EE:03:D5:2A:07', name: '📺_2a07', paired: true }, ...]
|
|
158
|
+
```
|
|
170
159
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
to: '!ffffffff',
|
|
183
|
-
channel: 0,
|
|
184
|
-
text: 'Hello over serial!'
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
# Blocks; CTRL+C disconnects cleanly
|
|
188
|
-
Meshtastic::SerialInterface.subscribe(serial_obj: serial_obj) do |message|
|
|
189
|
-
puts message.inspect
|
|
190
|
-
end
|
|
160
|
+
Pair while discovery is running. Screen devices typically show a random 6-digit PIN:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
bluetoothctl
|
|
164
|
+
agent KeyboardDisplay
|
|
165
|
+
default-agent
|
|
166
|
+
scan on
|
|
167
|
+
pair E8:EE:03:D5:2A:07
|
|
168
|
+
trust E8:EE:03:D5:2A:07
|
|
169
|
+
scan off
|
|
170
|
+
quit
|
|
191
171
|
```
|
|
192
172
|
|
|
193
|
-
|
|
173
|
+
`Failed to pair: AuthenticationFailed` means the agent never got the PIN. `Device … not available` means scan first; the advertisement dropped.
|
|
194
174
|
|
|
195
|
-
|
|
196
|
-
# Pop one framed FromRadio (or nil on timeout)
|
|
197
|
-
fr = Meshtastic::SerialInterface.recv_from_radio(timeout: 2)
|
|
198
|
-
puts fr&.to_h
|
|
175
|
+
Send and receive (same options as Serial, `bluetooth_obj:` instead of `serial_obj:`):
|
|
199
176
|
|
|
200
|
-
|
|
201
|
-
|
|
177
|
+
```ruby
|
|
178
|
+
require 'meshtastic'
|
|
202
179
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
180
|
+
bluetooth_obj = nil
|
|
181
|
+
begin
|
|
182
|
+
bluetooth_obj = Meshtastic::Bluetooth.connect(address: 'E8:EE:03:D5:2A:07')
|
|
183
|
+
Meshtastic::Bluetooth.wait_for_config(bluetooth_obj: bluetooth_obj, timeout: 30)
|
|
184
|
+
|
|
185
|
+
Meshtastic::Bluetooth.send_text(
|
|
186
|
+
bluetooth_obj: bluetooth_obj,
|
|
187
|
+
to: '!83726fb1',
|
|
188
|
+
channel: 0,
|
|
189
|
+
text: 'Hello over BLE!',
|
|
190
|
+
want_ack: true
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
Meshtastic::Bluetooth.subscribe(
|
|
194
|
+
bluetooth_obj: bluetooth_obj,
|
|
195
|
+
include: 'TEXT_MESSAGE_APP'
|
|
196
|
+
) do |message|
|
|
197
|
+
packet = message[:packet]
|
|
198
|
+
puts "#{packet[:node_id_from]}: #{packet.dig(:decoded, :payload)}"
|
|
199
|
+
end
|
|
200
|
+
ensure
|
|
201
|
+
Meshtastic::Bluetooth.disconnect(bluetooth_obj: bluetooth_obj)
|
|
202
|
+
end
|
|
206
203
|
```
|
|
207
204
|
|
|
208
|
-
|
|
205
|
+
Disconnect the phone’s Meshtastic BLE session while Linux is connected. After an aborted reconnect (`le-connection-abort-by-local`), `bluetoothctl disconnect <addr>` and wait a couple of seconds before `connect` again. Config dumps over BLE can take longer than serial; 30 seconds is a reasonable `wait_for_config` timeout.
|
|
209
206
|
|
|
210
207
|
## Contributing
|
|
211
208
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dbus'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module Meshtastic
|
|
7
|
+
module Bluetooth
|
|
8
|
+
# Linux BlueZ transport for raw Meshtastic protobuf messages.
|
|
9
|
+
class BlueZ # rubocop:disable Metrics/ClassLength
|
|
10
|
+
# https://github.com/meshtastic/python/blob/master/meshtastic/ble_interface.py
|
|
11
|
+
SERVICE_UUID = '6ba1b218-15a8-461f-9fa8-5dcae273eafd'
|
|
12
|
+
TORADIO_UUID = 'f75c76d2-129e-4dad-a1dd-7866124401e7'
|
|
13
|
+
FROMRADIO_UUID = '2c55e69e-4993-11ed-b878-0242ac120002'
|
|
14
|
+
|
|
15
|
+
def self.scan(adapter: 'hci0', timeout: 5)
|
|
16
|
+
new(address: '00:00:00:00:00:00', adapter: adapter, timeout: timeout).scan_adapter
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def connect
|
|
20
|
+
@mutex.synchronize { connect_locked }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def connect_locked
|
|
24
|
+
@bus = DBus::ASystemBus.allocate
|
|
25
|
+
bounded { @bus.__send__(:initialize) }
|
|
26
|
+
objects = managed_objects
|
|
27
|
+
check_adapter(objects)
|
|
28
|
+
@device_path = objects.find do |_path, interfaces|
|
|
29
|
+
device = interfaces['org.bluez.Device1']
|
|
30
|
+
device && device['Address']&.casecmp?(@address) && device['Adapter'] == "/org/bluez/#{@adapter}"
|
|
31
|
+
end&.first
|
|
32
|
+
raise IOError, "Bluetooth device #{@address} not found on #{@adapter}; scan first" unless @device_path
|
|
33
|
+
raise IOError, "Bluetooth device is not Paired. Run: bluetoothctl pair #{@address} (enter the PIN shown by your radio), then retry." unless properties(@device_path, 'org.bluez.Device1')['Paired']
|
|
34
|
+
|
|
35
|
+
@disconnect_needed = true
|
|
36
|
+
call(@device_path, 'org.bluez.Device1', 'Connect')
|
|
37
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
38
|
+
until properties(@device_path, 'org.bluez.Device1')['ServicesResolved']
|
|
39
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
40
|
+
raise IOError, 'Bluetooth ServicesResolved timed out' unless remaining.positive?
|
|
41
|
+
|
|
42
|
+
sleep [0.1, remaining].min
|
|
43
|
+
end
|
|
44
|
+
@to_radio = characteristic(TORADIO_UUID)
|
|
45
|
+
@from_radio = characteristic(FROMRADIO_UUID)
|
|
46
|
+
self
|
|
47
|
+
rescue StandardError
|
|
48
|
+
close_locked
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def close
|
|
53
|
+
@mutex.synchronize { close_locked }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close_locked
|
|
57
|
+
call(@device_path, 'org.bluez.Device1', 'Disconnect') if @bus && @disconnect_needed
|
|
58
|
+
ensure
|
|
59
|
+
@disconnect_needed = false
|
|
60
|
+
release_bus
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scan_adapter
|
|
64
|
+
started = false
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
@bus = DBus::ASystemBus.allocate
|
|
67
|
+
bounded { @bus.__send__(:initialize) }
|
|
68
|
+
objects = managed_objects
|
|
69
|
+
check_adapter(objects)
|
|
70
|
+
adapter_path = "/org/bluez/#{@adapter}"
|
|
71
|
+
begin
|
|
72
|
+
call(adapter_path, 'org.bluez.Adapter1', 'StartDiscovery')
|
|
73
|
+
started = true
|
|
74
|
+
rescue DBus::Error
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
sleep @timeout
|
|
78
|
+
managed_objects.filter_map do |_path, interfaces|
|
|
79
|
+
device = interfaces['org.bluez.Device1']
|
|
80
|
+
next unless device && device['Adapter'] == adapter_path
|
|
81
|
+
next unless Array(device['UUIDs']).any? { |uuid| uuid.to_s.casecmp?(SERVICE_UUID) }
|
|
82
|
+
|
|
83
|
+
{ address: device['Address'], name: device['Name'], paired: device['Paired'] }
|
|
84
|
+
end
|
|
85
|
+
ensure
|
|
86
|
+
if @bus && started
|
|
87
|
+
begin
|
|
88
|
+
call("/org/bluez/#{@adapter}", 'org.bluez.Adapter1', 'StopDiscovery')
|
|
89
|
+
rescue DBus::Error
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
release_bus
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def connected!
|
|
98
|
+
raise IOError, 'Bluetooth device disconnected' unless @bus && @from_radio && properties(@device_path, 'org.bluez.Device1')['Connected']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def read
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
connected!
|
|
104
|
+
call(@from_radio, 'org.bluez.GattCharacteristic1', 'ReadValue', ['a{sv}', {}]).first.pack('C*')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write(bytes)
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
connected!
|
|
111
|
+
# BlueZ request uses Write Request <= MTU-3, Prepare/Execute Write above it.
|
|
112
|
+
# Never fragment protobufs ourselves: each WriteValue is one message.
|
|
113
|
+
call(@to_radio, 'org.bluez.GattCharacteristic1', 'WriteValue', ['ay', bytes.bytes], ['a{sv}', { 'type' => %w[s request] }])
|
|
114
|
+
bytes.bytesize
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def characteristic(uuid)
|
|
119
|
+
objects = managed_objects
|
|
120
|
+
service = objects.find do |_path, interfaces|
|
|
121
|
+
properties = interfaces['org.bluez.GattService1']
|
|
122
|
+
properties && properties['Device'] == @device_path && properties['UUID']&.casecmp?(SERVICE_UUID)
|
|
123
|
+
end&.first
|
|
124
|
+
raise IOError, 'Meshtastic GATT service not found on selected device' unless service
|
|
125
|
+
|
|
126
|
+
match = objects.find do |_path, interfaces|
|
|
127
|
+
properties = interfaces['org.bluez.GattCharacteristic1']
|
|
128
|
+
properties && properties['Service'] == service && properties['UUID']&.casecmp?(uuid)
|
|
129
|
+
end&.first
|
|
130
|
+
raise IOError, "Meshtastic GATT characteristic #{uuid} not found" unless match
|
|
131
|
+
|
|
132
|
+
match
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def check_adapter(objects)
|
|
136
|
+
adapter = objects.dig("/org/bluez/#{@adapter}", 'org.bluez.Adapter1')
|
|
137
|
+
raise IOError, "Bluetooth adapter #{@adapter} not found" unless adapter
|
|
138
|
+
raise IOError, "Bluetooth adapter #{@adapter} is not powered" unless adapter['Powered']
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def release_bus
|
|
142
|
+
socket = @bus&.message_queue&.socket
|
|
143
|
+
socket.close if socket && !socket.closed?
|
|
144
|
+
@bus = nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def properties(path, interface)
|
|
148
|
+
call(path, 'org.freedesktop.DBus.Properties', 'GetAll', ['s', interface]).first
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def managed_objects
|
|
152
|
+
call('/', 'org.freedesktop.DBus.ObjectManager', 'GetManagedObjects').first
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def call(path, interface, member, *parameters)
|
|
156
|
+
message = DBus::Message.new(DBus::Message::METHOD_CALL)
|
|
157
|
+
message.destination = 'org.bluez'
|
|
158
|
+
message.path = path
|
|
159
|
+
message.interface = interface
|
|
160
|
+
message.member = member
|
|
161
|
+
parameters.each { |type, value| message.add_param(type, value) }
|
|
162
|
+
bounded { @bus.send_sync_or_async(message) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ruby-dbus 0.25 has no method timeout API. An interrupted bus must never
|
|
166
|
+
# be reused because its pending reply bookkeeping may be inconsistent.
|
|
167
|
+
def bounded(&)
|
|
168
|
+
Timeout.timeout(@timeout, &)
|
|
169
|
+
rescue Timeout::Error
|
|
170
|
+
release_bus
|
|
171
|
+
raise IOError, 'Bluetooth D-Bus operation timed out'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def initialize(address:, adapter: 'hci0', timeout: 15)
|
|
175
|
+
raise ArgumentError, 'Invalid Bluetooth address' unless address.is_a?(String) && /\A(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\z/.match?(address)
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, 'Invalid Bluetooth adapter (expected hciN)' unless adapter.is_a?(String) && /\Ahci\d+\z/.match?(adapter)
|
|
178
|
+
raise ArgumentError, 'Invalid Bluetooth timeout (expected positive finite seconds)' unless timeout.is_a?(Numeric) && timeout.real? && timeout.finite? && timeout.positive?
|
|
179
|
+
|
|
180
|
+
@address = address
|
|
181
|
+
@adapter = adapter
|
|
182
|
+
@timeout = timeout
|
|
183
|
+
@mutex = Mutex.new
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|