Module: Meshtastic::Bluetooth

Defined in:
lib/meshtastic/bluetooth.rb,
lib/meshtastic/bluetooth/bluez.rb

Defined Under Namespace

Classes: BlueZ

Class Method Summary collapse

Class Method Details

.authorsObject



242
243
244
# File 'lib/meshtastic/bluetooth.rb', line 242

public_class_method def self.authors
  "AUTHOR(S):\n        0day Inc. <[email protected]>\n      "
end

.connect(opts = {}) ⇒ Object

Connect to an already paired BLE address (not a mesh node ID).



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/meshtastic/bluetooth.rb', line 16

public_class_method def self.connect(opts = {})
  connection = BlueZ.new(address: opts[:address], adapter: opts.fetch(:adapter, 'hci0'), timeout: opts.fetch(:timeout, 15))
  connection.connect
  bluetooth_obj = {
    bluetooth_conn: connection, address: opts[:address], tx_mutex: Mutex.new,
    rx_mutex: Mutex.new, from_radio_queue: Queue.new, config_queue: Queue.new,
    proto_data: [], console_data: []
  }
  bluetooth_obj[:rx_thread] = start_reader(handle: bluetooth_obj)
  if opts.fetch(:want_config, true)
    mesh = Meshtastic::MeshInterface.new
    bytes = mesh.start_config
    bluetooth_obj[:config_id] = mesh.config_id
    send_to_radio(bluetooth_obj: bluetooth_obj, to_radio: bytes)
  end
  @last_bluetooth_obj = bluetooth_obj
  bluetooth_obj
rescue StandardError
  bluetooth_obj ? disconnect(bluetooth_obj: bluetooth_obj) : connection&.close
  raise
end

.disconnect(opts = {}) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/meshtastic/bluetooth.rb', line 219

public_class_method def self.disconnect(opts = {})
  handle = opts[:bluetooth_obj]
  return unless handle
  return if handle[:closing]

  handle[:closing] = true
  handle[:from_radio_queue].close
  handle[:config_queue].close
  begin
    handle[:tx_mutex].synchronize do
      handle[:bluetooth_conn].write(Meshtastic::ToRadio.new(disconnect: true).to_proto)
    end
  rescue StandardError
    # A lost BLE link cannot receive a disconnect request.
    nil
  ensure
    handle[:bluetooth_conn].close
    reader = handle[:rx_thread]
    reader.join(1) if reader && reader != Thread.current
  end
  nil
end

.drain_from_radio(opts = {}) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/meshtastic/bluetooth.rb', line 108

public_class_method def self.drain_from_radio(opts = {})
  handle = handle_for(opts)
  messages = []
  opts.fetch(:max, 256).times do
    message = recv_from_radio(bluetooth_obj: handle, timeout: 0)
    break unless message

    messages << message
  end
  messages
end

.dump_stdout_data(opts = {}) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/meshtastic/bluetooth.rb', line 120

public_class_method def self.dump_stdout_data(opts = {})
  merged = opts.merge(serial_obj: handle_for(opts))
  if block_given?
    Meshtastic::Serial.dump_stdout_data(merged) { |row| yield row } # rubocop:disable Style/ExplicitBlockArgument
  else
    Meshtastic::Serial.dump_stdout_data(merged)
  end
end

.flush_data(opts = {}) ⇒ Object



129
130
131
# File 'lib/meshtastic/bluetooth.rb', line 129

public_class_method def self.flush_data(opts = {})
  Meshtastic::Serial.flush_data(opts.merge(serial_obj: handle_for(opts)))
end

.helpObject



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/meshtastic/bluetooth.rb', line 246

public_class_method def self.help
  puts "        USAGE:
    # Run the scan class method for this module.
    #{self}.scan

    # Run the connect class method for this module.
    #{self}.connect(
      address: 'optional - value for address passed into connect'
    )

    # Run the wait_for_config class method for this module.
    #{self}.wait_for_config

    # Run the recv_from_radio class method for this module.
    #{self}.recv_from_radio

    # Run the drain_from_radio class method for this module.
    #{self}.drain_from_radio

    # Run the dump_stdout_data class method for this module.
    #{self}.dump_stdout_data

    # Run the flush_data class method for this module.
    #{self}.flush_data

    # Run the subscribe class method for this module.
    #{self}.subscribe(
      include: 'optional - value for include passed into subscribe',
      exclude: 'optional - value for exclude passed into subscribe',
      timeout: 'optional - value for timeout passed into subscribe',
      gps_metadata: 'optional - value for gps_metadata passed into subscribe',
      include_raw: 'optional - value for include_raw passed into subscribe'
    )

    # Run the send_to_radio class method for this module.
    #{self}.send_to_radio(
      bluetooth_obj: 'optional - value for bluetooth_obj passed into send_to_radio',
      to_radio: 'optional - value for to_radio passed into send_to_radio'
    )

    # Run the send_text class method for this module.
    #{self}.send_text(
      bluetooth_obj: 'optional - value for bluetooth_obj passed into send_text',
      from: 'optional - value for from passed into send_text'
    )

    # Run the send_data class method for this module.
    #{self}.send_data(
      bluetooth_obj: 'optional - value for bluetooth_obj passed into send_data',
      data: 'optional - value for data passed into send_data',
      from: 'optional - value for from passed into send_data'
    )

    # Run the disconnect class method for this module.
    #{self}.disconnect(
      bluetooth_obj: 'optional - value for bluetooth_obj passed into disconnect'
    )

    # Run the authors class method for this module.
    #{self}.authors

  "
end

.recv_from_radio(opts = {}) ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'lib/meshtastic/bluetooth.rb', line 98

public_class_method def self.recv_from_radio(opts = {})
  handle = handle_for(opts)
  timeout = opts.fetch(:timeout, 5)
  timeout = nil if timeout&.negative?
  message = handle[:from_radio_queue].pop(timeout: timeout)
  raise handle[:rx_error] if message.nil? && handle[:rx_error]

  message
end

.scan(opts = {}) ⇒ Object



11
12
13
# File 'lib/meshtastic/bluetooth.rb', line 11

public_class_method def self.scan(opts = {})
  BlueZ.scan(opts.merge({}))
end

.send_data(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


210
211
212
213
214
215
216
217
# File 'lib/meshtastic/bluetooth.rb', line 210

public_class_method def self.send_data(opts = {})
  handle = opts[:bluetooth_obj]
  raise ArgumentError, 'bluetooth_obj is required' unless handle
  raise ArgumentError, 'data must be Meshtastic::Data' unless opts[:data].is_a?(Meshtastic::Data)

  args = opts.merge(via: :radio, psks: nil, channel: opts.fetch(:channel, 0), from: opts[:from] || handle[:my_node_num] || 0)
  send_to_radio(bluetooth_obj: handle, to_radio: Meshtastic::MeshInterface.new.send_data(args))
end

.send_text(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


198
199
200
201
202
203
204
205
206
207
208
# File 'lib/meshtastic/bluetooth.rb', line 198

public_class_method def self.send_text(opts = {})
  handle = opts[:bluetooth_obj]
  raise ArgumentError, 'bluetooth_obj is required' unless handle

  text = opts.fetch(:text, 'SYN').to_s
  max_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
  raise ArgumentError, "Text Length > #{max_len} Bytes" if text.bytesize > max_len

  args = opts.merge(text: text, via: :radio, psks: nil, channel: opts.fetch(:channel, 0), from: opts[:from] || handle[:my_node_num] || 0)
  send_to_radio(bluetooth_obj: handle, to_radio: Meshtastic::MeshInterface.new.send_text(args))
end

.send_to_radio(opts = {}) ⇒ Object

Write one complete serialized ToRadio to GATT. BLE does not use UART headers.

Raises:

  • (ArgumentError)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/meshtastic/bluetooth.rb', line 177

public_class_method def self.send_to_radio(opts = {})
  handle = opts[:bluetooth_obj]
  raise ArgumentError, 'bluetooth_obj is required' unless handle
  raise IOError, 'Bluetooth connection closed' if handle[:closing]

  message = opts[:to_radio]
  body = case message
         when Meshtastic::ToRadio then message.to_proto
         when String then message.b
         else raise ArgumentError, 'to_radio must be Meshtastic::ToRadio or a serialized String'
         end
  raise ArgumentError, 'ToRadio payload exceeds 512 bytes' if body.bytesize > Meshtastic::MAX_TO_FROM_RADIO_SIZE

  begin
    handle[:tx_mutex].synchronize { handle[:bluetooth_conn].write(body) }
  rescue StandardError
    disconnect(bluetooth_obj: handle)
    raise
  end
end

.subscribe(opts = {}) ⇒ Object

Yield the same enriched FromRadio hashes as Serial, without opening a UART.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/meshtastic/bluetooth.rb', line 134

public_class_method def self.subscribe(opts = {})
  handle = handle_for(opts)
  psks = opts.fetch(:psks, { LongFast: 'AQ==' }).dup
  raise ArgumentError, 'psks must be a hash' unless psks.is_a?(Hash)

  psks[:LongFast] = '1PG7OiApB1nwvP+rz05pAQ==' if psks[:LongFast] == 'AQ=='
  psks = Meshtastic::MeshInterface.new.get_cipher_keys(psks: psks)
  includes = opts[:include].to_s.split(',').map(&:strip)
  excludes = opts[:exclude].to_s.split(',').map(&:strip)
  loop do
    message = recv_from_radio(bluetooth_obj: handle, timeout: opts[:timeout])
    break if message.nil? && handle[:from_radio_queue].closed?
    next unless message

    decoded = message.to_h
    if decoded[:packet]
      # Share the existing pure packet decoder; BLE never uses Serial's IO methods.
      decoded[:packet] = Meshtastic::Serial.send(:enrich_packet,
                                                 message: decoded[:packet], psks: psks,
                                                 gps_metadata: opts[:gps_metadata], include_raw: opts[:include_raw],
                                                 raw_packet: opts[:include_raw] ? message.to_proto : nil)
    end
    source = decoded.inspect
    next unless includes.all? { |term| source.include?(term) } && excludes.none? { |term| source.include?(term) }

    if block_given?
      yield decoded
    else
      begin
        puts JSON.pretty_generate(decoded)
      rescue JSON::GeneratorError
        puts decoded.inspect
      end
    end
  end
rescue Interrupt
  disconnect(bluetooth_obj: handle)
rescue StandardError
  disconnect(bluetooth_obj: handle)
  raise
end

.wait_for_config(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


86
87
88
89
90
91
92
93
94
95
96
# File 'lib/meshtastic/bluetooth.rb', line 86

public_class_method def self.wait_for_config(opts = {})
  handle = handle_for(opts)
  raise ArgumentError, 'connect with want_config: true first' unless handle[:config_id]

  handle[:config_queue].pop(timeout: opts.fetch(:timeout, 10))
  raise handle[:rx_error] if handle[:rx_error]
  raise IOError, 'Bluetooth connection closed' if handle[:closing]
  raise Timeout::Error, "No configuration response from #{handle[:address]}" unless handle[:config_complete]

  handle
end