ble 0.0.1 → 0.0.2
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/ble.gemspec +4 -2
- data/lib/ble.rb +23 -865
- data/lib/ble/adapter.rb +162 -0
- data/lib/ble/agent.rb +46 -0
- data/lib/ble/characteristic.rb +160 -0
- data/lib/ble/db_characteristic.rb +2 -3
- data/lib/ble/db_service.rb +1 -1
- data/lib/ble/device.rb +484 -0
- data/lib/ble/service.rb +128 -0
- data/lib/ble/version.rb +2 -1
- metadata +36 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05bba72a755368615142178e182af8ade6c5f97d
|
4
|
+
data.tar.gz: 0c0e6f32a4abd668bce46c26f845492e440a72a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 223434f164e5c8adad7c0bfd043b4cf4955329d939c88210fc22d6abf422110ce8713dede74bec3641ce8b1b7f2dd1e81a03458496159793ae3832642c377438
|
7
|
+
data.tar.gz: 29dbc315196f93c8c5e2f2ddd8ba2dcc0788561a19061637938cafc6541f5978d3d9d2162437c74e4746d7d12c0ac4d3349c4d2ed2f499930ee10bf5d0442874
|
data/ble.gemspec
CHANGED
@@ -8,13 +8,15 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.authors = [ "Stephane D'Alu" ]
|
9
9
|
s.email = [ "stephane.dalu@gmail.com" ]
|
10
10
|
s.homepage = "http://github.com/sdalu/ruby-ble"
|
11
|
-
s.summary = "Bluetooth Low Energy API"
|
12
|
-
s.description = "Allow access to
|
11
|
+
s.summary = "Bluetooth Low Energy (BLE) API"
|
12
|
+
s.description = "Allow access to Bluetooth Low Energy device from ruby"
|
13
13
|
|
14
14
|
s.add_dependency "ruby-dbus"
|
15
15
|
|
16
16
|
s.add_development_dependency "yard"
|
17
17
|
s.add_development_dependency "rake"
|
18
|
+
s.add_development_dependency "redcarpet"
|
19
|
+
s.add_development_dependency "github-markup"
|
18
20
|
|
19
21
|
s.has_rdoc = 'yard'
|
20
22
|
|
data/lib/ble.rb
CHANGED
@@ -8,9 +8,8 @@ require 'logger'
|
|
8
8
|
|
9
9
|
#
|
10
10
|
module BLE
|
11
|
-
|
12
|
-
|
13
11
|
private
|
12
|
+
# Interfaces
|
14
13
|
I_ADAPTER = 'org.bluez.Adapter1'
|
15
14
|
I_DEVICE = 'org.bluez.Device1'
|
16
15
|
I_AGENT_MANAGER = 'org.bluez.AgentManager1'
|
@@ -21,6 +20,7 @@ module BLE
|
|
21
20
|
I_PROPERTIES = 'org.freedesktop.DBus.Properties'
|
22
21
|
I_INTROSPECTABLE = 'org.freedesktop.DBus.Introspectable'
|
23
22
|
|
23
|
+
# Errors
|
24
24
|
E_IN_PROGRESS = 'org.bluez.Error.InProgress'
|
25
25
|
E_FAILED = 'org.bluez.Error.Failed'
|
26
26
|
E_NOT_READY = 'org.bluez.Error.NotReady'
|
@@ -36,24 +36,30 @@ module BLE
|
|
36
36
|
E_AUTH_REJECTED = 'org.bluez.Error.AuthenticationRejected'
|
37
37
|
E_AUTH_TIMEOUT = 'org.bluez.Error.AuthenticationTimeout'
|
38
38
|
E_AUTH_ATTEMPT_FAILED = 'org.bluez.Error.ConnectionAttemptFailed'
|
39
|
-
|
40
39
|
E_UNKNOWN_OBJECT = 'org.freedesktop.DBus.Error.UnknownObject'
|
41
40
|
E_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
|
42
41
|
E_INVALID_SIGNATURE = 'org.freedesktop.DBus.Error.InvalidSignature'
|
43
42
|
|
44
|
-
|
45
|
-
|
43
|
+
# Bus
|
44
|
+
DBUS = DBus.system_bus
|
45
|
+
BLUEZ = DBUS.service('org.bluez')
|
46
46
|
|
47
47
|
public
|
48
|
+
# Generic Error class
|
48
49
|
class Error < StandardError ; end
|
50
|
+
# Notify of unimplemented part
|
49
51
|
class NotYetImplemented < Error ; end
|
52
|
+
# Notify that the underlying API object is dead
|
50
53
|
class StalledObject < Error ; end
|
54
|
+
# Notify that execution wass not able to fulill as some requirement
|
55
|
+
# was not ready. Usually you can wait a little and restart the action.
|
51
56
|
class NotReady < Error ; end
|
57
|
+
# Notify that you don't have the necessary authorization to perfrom
|
58
|
+
# the operation
|
52
59
|
class NotAuthorized < Error ; end
|
53
|
-
|
60
|
+
# Notify that some service/characteristic/... is not found
|
61
|
+
# on this device
|
54
62
|
class NotFound < Error ; end
|
55
|
-
class ServiceNotFound < NotFound ; end
|
56
|
-
class CharacteristicNotFound < NotFound ; end
|
57
63
|
class AccessUnavailable < Error ; end
|
58
64
|
|
59
65
|
|
@@ -75,699 +81,7 @@ module BLE
|
|
75
81
|
o_bluez[I_AGENT_MANAGER].RegisterAgent(agent_path, "NoInputNoOutput")
|
76
82
|
end
|
77
83
|
|
78
|
-
|
79
|
-
class Agent < DBus::Object
|
80
|
-
@log = Logger.new($stdout)
|
81
|
-
# https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/refs/heads/master/doc/agent-api.txt
|
82
|
-
dbus_interface I_AGENT do
|
83
|
-
dbus_method :Release do
|
84
|
-
@log.debug "Release()"
|
85
|
-
exit false
|
86
|
-
end
|
87
|
-
|
88
|
-
dbus_method :RequestPinCode, "in device:o, out ret:s" do |device|
|
89
|
-
@log.debug{ "RequestPinCode(#{device})" }
|
90
|
-
["0000"]
|
91
|
-
end
|
92
|
-
|
93
|
-
dbus_method :RequestPasskey, "in device:o, out ret:u" do |device|
|
94
|
-
@log.debug{ "RequestPasskey(#{device})" }
|
95
|
-
raise DBus.error("org.bluez.Error.Rejected")
|
96
|
-
end
|
97
|
-
|
98
|
-
dbus_method :DisplayPasskey, "in device:o, in passkey:u, in entered:y" do |device, passkey, entered|
|
99
|
-
@log.debug{ "DisplayPasskey(#{device}, #{passkey}, #{entered})" }
|
100
|
-
raise DBus.error("org.bluez.Error.Rejected")
|
101
|
-
end
|
102
|
-
|
103
|
-
dbus_method :RequestConfirmation, "in device:o, in passkey:u" do |device, passkey|
|
104
|
-
@log.debug{ "RequestConfirmation(#{device}, #{passkey})" }
|
105
|
-
raise DBus.error("org.bluez.Error.Rejected")
|
106
|
-
end
|
107
|
-
|
108
|
-
dbus_method :Authorize, "in device:o, in uuid:s" do |device, uuid|
|
109
|
-
@log.debug{ "Authorize(#{device}, #{uuid})" }
|
110
|
-
end
|
111
|
-
|
112
|
-
dbus_method :ConfirmModeChange, "in mode:s" do |mode|
|
113
|
-
@log.debug{ "ConfirmModeChange(#{mode})" }
|
114
|
-
raise DBus.error("org.bluez.Error.Rejected")
|
115
|
-
end
|
116
|
-
|
117
|
-
dbus_method :Cancel do
|
118
|
-
@log.debug "Cancel()"
|
119
|
-
raise DBus.error("org.bluez.Error.Rejected")
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
|
125
|
-
# Adapter class
|
126
|
-
# Adapter.list
|
127
|
-
# a = Adapter.new('hci0')
|
128
|
-
# a.start_discover ; sleep(10) ; a.stop_discovery
|
129
|
-
# a.devices
|
130
|
-
#
|
131
|
-
class Adapter
|
132
|
-
# Return a list of available unix device name for the
|
133
|
-
# adapter installed on the system.
|
134
|
-
# @return [Array<String>] list of unix device name
|
135
|
-
def self.list
|
136
|
-
o_bluez = BLUEZ.object('/org/bluez')
|
137
|
-
o_bluez.introspect
|
138
|
-
o_bluez.subnodes.reject {|adapter| ['test'].include?(adapter) }
|
139
|
-
end
|
140
|
-
|
141
|
-
# Create a new Adapter
|
142
|
-
#
|
143
|
-
# @param iface [String] name of the Unix device
|
144
|
-
def initialize(iface)
|
145
|
-
@iface = iface.dup.freeze
|
146
|
-
@o_adapter = BLUEZ.object("/org/bluez/#{@iface}")
|
147
|
-
@o_adapter.introspect
|
148
|
-
|
149
|
-
@o_adapter[I_PROPERTIES]
|
150
|
-
.on_signal('PropertiesChanged') do |intf, props|
|
151
|
-
puts "#{intf}: #{props.inspect}"
|
152
|
-
case intf
|
153
|
-
when I_ADAPTER
|
154
|
-
case props['Discovering']
|
155
|
-
when true
|
156
|
-
when false
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# The Bluetooth interface name
|
163
|
-
# @return [String] name of the Unix device
|
164
|
-
def iface
|
165
|
-
@iface
|
166
|
-
end
|
167
|
-
|
168
|
-
# The Bluetooth device address.
|
169
|
-
# @return [String] MAC address of the adapter
|
170
|
-
def address
|
171
|
-
@o_adapter[I_ADAPTER]['Address']
|
172
|
-
end
|
173
|
-
|
174
|
-
# The Bluetooth system name (pretty hostname).
|
175
|
-
# @return [String]
|
176
|
-
def name
|
177
|
-
@o_adapter[I_ADAPTER]['Name']
|
178
|
-
end
|
179
|
-
|
180
|
-
# The Bluetooth friendly name.
|
181
|
-
# In case no alias is set, it will return the system provided name.
|
182
|
-
# @return [String]
|
183
|
-
def alias
|
184
|
-
@o_adapter[I_ADAPTER]['Alias']
|
185
|
-
end
|
186
|
-
|
187
|
-
# Set the alias name.
|
188
|
-
#
|
189
|
-
# When resetting the alias with an empty string, the
|
190
|
-
# property will default back to system name
|
191
|
-
#
|
192
|
-
# @param val [String] new alias name.
|
193
|
-
# @return [void]
|
194
|
-
def alias=(val)
|
195
|
-
@o_adapter[I_ADAPTER]['Alias'] = val.nil? ? '' : val.to_str
|
196
|
-
nil
|
197
|
-
end
|
198
|
-
|
199
|
-
# Return the device corresponding to the given address.
|
200
|
-
# @note The device object returned has a dependency on the adapter.
|
201
|
-
#
|
202
|
-
# @param address MAC address of the device
|
203
|
-
# @return [Device] a device
|
204
|
-
def [](address)
|
205
|
-
Device.new(@iface, address)
|
206
|
-
end
|
207
|
-
|
208
|
-
# This method sets the device discovery filter for the caller.
|
209
|
-
# When this method is called with nil or an empty list of UUIDs,
|
210
|
-
# filter is removed.
|
211
|
-
#
|
212
|
-
# @param uuids a list of uuid to filter on
|
213
|
-
# @param rssi RSSI threshold
|
214
|
-
# @param pathloss pathloss threshold
|
215
|
-
# @param transport [:auto, :bredr, :le]
|
216
|
-
# type of scan to run (default: :le)
|
217
|
-
# @note need to sync with the adapter-api.txt
|
218
|
-
def filter(uuids, rssi: nil, pathloss: nil, transport: :le)
|
219
|
-
unless [:auto, :bredr, :le].include?(transport)
|
220
|
-
raise ArgumentError,
|
221
|
-
"transport must be one of :auto, :bredr, :le"
|
222
|
-
end
|
223
|
-
filter = { }
|
224
|
-
|
225
|
-
unless uuids.nil? || uuids.empty?
|
226
|
-
filter['UUIDs' ] = DBus.variant('as', uuids)
|
227
|
-
end
|
228
|
-
unless rssi.nil?
|
229
|
-
filter['RSSI' ] = DBus.variant('n', rssi)
|
230
|
-
end
|
231
|
-
unless pathloss.nil?
|
232
|
-
filter['Pathloss' ] = DBus.variant('q', pathloss)
|
233
|
-
end
|
234
|
-
unless transport.nil?
|
235
|
-
filter['Transport'] = DBus.variant('s', transport.to_s)
|
236
|
-
end
|
237
|
-
|
238
|
-
@o_adapter[I_ADAPTER].SetDiscoveryFilter(filter)
|
239
|
-
|
240
|
-
self
|
241
|
-
end
|
242
|
-
|
243
|
-
# Starts the device discovery session.
|
244
|
-
# This includes an inquiry procedure and remote device name resolving.
|
245
|
-
# Use stop_discovery to release the sessions acquired.
|
246
|
-
# This process will start creating device objects as new devices
|
247
|
-
# are discovered.
|
248
|
-
#
|
249
|
-
# @return [Boolean]
|
250
|
-
def start_discovery
|
251
|
-
@o_adapter[I_ADAPTER].StartDiscovery
|
252
|
-
true
|
253
|
-
rescue DBus::Error => e
|
254
|
-
case e.name
|
255
|
-
when E_IN_PROGRESS then true
|
256
|
-
when E_FAILED then false
|
257
|
-
else raise ScriptError
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
# This method will cancel any previous #start_discovery
|
262
|
-
# transaction.
|
263
|
-
# @note The discovery procedure is shared
|
264
|
-
# between all discovery sessions thus calling stop_discovery
|
265
|
-
# will only release a single session.
|
266
|
-
#
|
267
|
-
# @return [Boolean]
|
268
|
-
def stop_discovery
|
269
|
-
@o_adapter[I_ADAPTER].StopDiscovery
|
270
|
-
true
|
271
|
-
rescue DBus::Error => e
|
272
|
-
case e.name
|
273
|
-
when E_FAILED then false
|
274
|
-
when E_NOT_READY then false
|
275
|
-
when E_NOT_AUTHORIZED then raise NotAuthorized
|
276
|
-
else raise ScriptError
|
277
|
-
end
|
278
|
-
|
279
|
-
end
|
280
|
-
|
281
|
-
# List of devices MAC address that have been discovered.
|
282
|
-
#
|
283
|
-
# @return [Array<String>] List of devices MAC address.
|
284
|
-
def devices
|
285
|
-
@o_adapter.introspect # Force refresh
|
286
|
-
@o_adapter.subnodes.map {|dev| # Format: dev_aa_bb_cc_dd_ee_ff
|
287
|
-
dev[4..-1].tr('_', ':') }
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
# Create de Device object
|
292
|
-
# d = Device::new('hci0', 'aa:bb:dd:dd:ee:ff')
|
293
|
-
# d = Adapter.new('hci0')['aa:bb:dd:dd:ee:ff']
|
294
|
-
#
|
295
|
-
# d.services
|
296
|
-
# d.characteristics(:environmental_sensing)
|
297
|
-
# d[:environmental_sensing, :temperature]
|
298
|
-
#
|
299
|
-
class Device
|
300
|
-
# @param adapter
|
301
|
-
# @param dev
|
302
|
-
# @param auto_refresh
|
303
|
-
def initialize(adapter, dev, auto_refresh: true)
|
304
|
-
@adapter, @dev = adapter, dev
|
305
|
-
@auto_refresh = auto_refresh
|
306
|
-
@services = {}
|
307
|
-
|
308
|
-
@n_adapter = adapter
|
309
|
-
@p_adapter = "/org/bluez/#{@n_adapter}"
|
310
|
-
@o_adapter = BLUEZ.object(@p_adapter)
|
311
|
-
@o_adapter.introspect
|
312
|
-
|
313
|
-
@n_dev = 'dev_' + dev.tr(':', '_')
|
314
|
-
@p_dev = "/org/bluez/#{@n_adapter}/#{@n_dev}"
|
315
|
-
@o_dev = BLUEZ.object(@p_dev)
|
316
|
-
@o_dev.introspect
|
317
|
-
|
318
|
-
self.refresh if @auto_refresh
|
319
|
-
|
320
|
-
@o_dev[I_PROPERTIES]
|
321
|
-
.on_signal('PropertiesChanged') do |intf, props|
|
322
|
-
puts "#{intf}: #{props.inspect}"
|
323
|
-
case intf
|
324
|
-
when I_DEVICE
|
325
|
-
case props['Connected']
|
326
|
-
when true
|
327
|
-
self.refresh if @auto_refresh
|
328
|
-
end
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
end
|
335
|
-
|
336
|
-
# This removes the remote device object.
|
337
|
-
# It will remove also the pairing information.
|
338
|
-
# @return [Boolean]
|
339
|
-
def remove
|
340
|
-
@o_adapter[I_ADAPTER].RemoveDevice(@p_dev)
|
341
|
-
true
|
342
|
-
rescue DBus::Error => e
|
343
|
-
case e.name
|
344
|
-
when E_FAILED then false
|
345
|
-
when E_DOES_NOT_EXIST then raise StalledObject
|
346
|
-
when E_UNKNOWN_OBJECT then raise StalledObject
|
347
|
-
else raise ScriptError
|
348
|
-
end
|
349
|
-
end
|
350
|
-
|
351
|
-
|
352
|
-
# This method will connect to the remote device,
|
353
|
-
# initiate pairing and then retrieve all SDP records
|
354
|
-
# (or GATT primary services).
|
355
|
-
# If the application has registered its own agent,
|
356
|
-
# then that specific agent will be used. Otherwise
|
357
|
-
# it will use the default agent.
|
358
|
-
# Only for applications like a pairing wizard it
|
359
|
-
# would make sense to have its own agent. In almost
|
360
|
-
# all other cases the default agent will handle this just fine.
|
361
|
-
# In case there is no application agent and also
|
362
|
-
# no default agent present, this method will fail.
|
363
|
-
# @return [Boolean]
|
364
|
-
def pair
|
365
|
-
@o_dev[I_DEVICE].Pair
|
366
|
-
true
|
367
|
-
rescue DBus::Error => e
|
368
|
-
case e.name
|
369
|
-
when E_INVALID_ARGUMENTS then false
|
370
|
-
when E_FAILED then false
|
371
|
-
when E_ALREADY_EXISTS then true
|
372
|
-
when E_AUTH_CANCELED then raise NotAutorized
|
373
|
-
when E_AUTH_FAILED then raise NotAutorized
|
374
|
-
when E_AUTH_REJECTED then raise NotAutorized
|
375
|
-
when E_AUTH_TIMEOUT then raise NotAutorized
|
376
|
-
when E_AUTH_ATTEMPT_FAILED then raise NotAutorized
|
377
|
-
else raise ScriptError
|
378
|
-
end
|
379
|
-
end
|
380
|
-
|
381
|
-
# This method can be used to cancel a pairing
|
382
|
-
# operation initiated by the Pair method.
|
383
|
-
# @return [Boolean]
|
384
|
-
def cancel_pairing
|
385
|
-
@o_dev[I_DEVICE].CancelPairing
|
386
|
-
true
|
387
|
-
rescue DBus::Error => e
|
388
|
-
case e.name
|
389
|
-
when E_DOES_NOT_EXIST then true
|
390
|
-
when E_FAILED then false
|
391
|
-
else raise ScriptError
|
392
|
-
end
|
393
|
-
end
|
394
|
-
|
395
|
-
# This connect to the specified profile UUID or to any (:all)
|
396
|
-
# profiles the remote device supports that can be connected to
|
397
|
-
# and have been flagged as auto-connectable on our side. If
|
398
|
-
# only subset of profiles is already connected it will try to
|
399
|
-
# connect currently disconnected ones. If at least one
|
400
|
-
# profile was connected successfully this method will indicate
|
401
|
-
# success.
|
402
|
-
# @return [Boolean]
|
403
|
-
def connect(profile=:all)
|
404
|
-
case profile
|
405
|
-
when UUID::REGEX
|
406
|
-
@o_dev[I_DEVICE].ConnectProfile(profile)
|
407
|
-
when :all
|
408
|
-
@o_dev[I_DEVICE].Connect()
|
409
|
-
else raise ArgumentError, "profile uuid or :all expected"
|
410
|
-
end
|
411
|
-
true
|
412
|
-
rescue DBus::Error => e
|
413
|
-
case e.name
|
414
|
-
when E_NOT_READY
|
415
|
-
when E_FAILED
|
416
|
-
when E_IN_PROGRESS
|
417
|
-
false
|
418
|
-
when E_ALREADY_CONNECTED
|
419
|
-
true
|
420
|
-
when E_UNKNOWN_OBJECT
|
421
|
-
raise StalledObject
|
422
|
-
else raise ScriptError
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# This method gracefully disconnects :all connected profiles
|
427
|
-
# and then terminates low-level ACL connection.
|
428
|
-
# ACL connection will be terminated even if some profiles
|
429
|
-
# were not disconnected properly e.g. due to misbehaving device.
|
430
|
-
# This method can be also used to cancel a preceding #connect
|
431
|
-
# call before a reply to it has been received.
|
432
|
-
# If a profile UUID is specified, only this profile is disconnected,
|
433
|
-
# and as their is no connection tracking for a profile, so
|
434
|
-
# as long as the profile is registered this will always succeed
|
435
|
-
# @return [Boolean]
|
436
|
-
def disconnect(profile=:all)
|
437
|
-
case profile
|
438
|
-
when UUID::REGEX
|
439
|
-
@o_dev[I_DEVICE].DisconnectProfile(profile)
|
440
|
-
when :all
|
441
|
-
@o_dev[I_DEVICE].Disconnect()
|
442
|
-
else raise ArgumentError, "profile uuid or :all expected"
|
443
|
-
end
|
444
|
-
true
|
445
|
-
rescue DBus::Error => e
|
446
|
-
case e.name
|
447
|
-
when E_FAILED
|
448
|
-
when E_IN_PROGRESS
|
449
|
-
false
|
450
|
-
when E_INVALID_ARGUMENTS
|
451
|
-
raise ArgumentError, "unsupported profile (#{profile})"
|
452
|
-
when E_NOT_SUPPORTED
|
453
|
-
raise NotSuppported
|
454
|
-
when E_NOT_CONNECTED
|
455
|
-
true
|
456
|
-
when E_UNKNOWN_OBJECT
|
457
|
-
raise StalledObject
|
458
|
-
else raise ScriptError
|
459
|
-
end
|
460
|
-
end
|
461
|
-
|
462
|
-
# Indicates if the remote device is paired
|
463
|
-
def is_paired?
|
464
|
-
@o_dev[I_DEVICE]['Paired']
|
465
|
-
rescue DBus::Error => e
|
466
|
-
case e.name
|
467
|
-
when E_UNKNOWN_OBJECT
|
468
|
-
raise StalledObject
|
469
|
-
else raise ScriptError
|
470
|
-
end
|
471
|
-
end
|
472
|
-
|
473
|
-
# Indicates if the remote device is currently connected.
|
474
|
-
def is_connected?
|
475
|
-
@o_dev[I_DEVICE]['Connected']
|
476
|
-
rescue DBus::Error => e
|
477
|
-
case e.name
|
478
|
-
when E_UNKNOWN_OBJECT
|
479
|
-
raise StalledObject
|
480
|
-
else raise
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
|
-
# List of available services as UUID
|
485
|
-
#
|
486
|
-
# @raise [NotConnected] if device is not in a connected state
|
487
|
-
# @note The list is retrieve once when object is
|
488
|
-
# connected if auto_refresh is enable, otherwise
|
489
|
-
# you need to call #refresh
|
490
|
-
# @note This is the list of UUID for which we have an entry
|
491
|
-
# in the bluez-dbus
|
492
|
-
# @return [Array<String>] List of service UUID
|
493
|
-
def services
|
494
|
-
raise NotConnected unless is_connected?
|
495
|
-
@services.keys
|
496
|
-
end
|
497
|
-
|
498
|
-
# Check if service is available on the device
|
499
|
-
# @return [Boolean]
|
500
|
-
def has_service?(service)
|
501
|
-
@service.key?(_uuid_service(service))
|
502
|
-
end
|
503
|
-
|
504
|
-
# List of available characteristics UUID for a service
|
505
|
-
#
|
506
|
-
# @param service service can be a UUID, a service type or
|
507
|
-
# a service nickname
|
508
|
-
# @return [Array<String>, nil] list of characteristics or nil if the
|
509
|
-
# service doesn't exist
|
510
|
-
# @raise [NotConnected] if device is not in a connected state
|
511
|
-
# @note The list is retrieve once when object is
|
512
|
-
# connected if auto_refresh is enable, otherwise
|
513
|
-
# you need to call #refresh
|
514
|
-
def characteristics(service)
|
515
|
-
raise NotConnected unless is_connected?
|
516
|
-
if chars = _characteristics(service)
|
517
|
-
chars.keys
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
# The Bluetooth device address of the remote device
|
522
|
-
# @return [String]
|
523
|
-
def address
|
524
|
-
@o_dev[I_DEVICE]['Address']
|
525
|
-
end
|
526
|
-
|
527
|
-
# The Bluetooth remote name.
|
528
|
-
# It is better to always use the #alias when displaying the
|
529
|
-
# devices name.
|
530
|
-
# @return [String]
|
531
|
-
def name # optional
|
532
|
-
@o_dev[I_DEVICE]['Name']
|
533
|
-
end
|
534
|
-
|
535
|
-
# The name alias for the remote device.
|
536
|
-
# The alias can be used to have a different friendly name for the
|
537
|
-
# remote device.
|
538
|
-
# In case no alias is set, it will return the remote device name.
|
539
|
-
# @return [String]
|
540
|
-
def alias
|
541
|
-
@o_dev[I_DEVICE]['Alias']
|
542
|
-
end
|
543
|
-
# Setting an empty string or nil as alias will convert it
|
544
|
-
# back to the remote device name.
|
545
|
-
# @param val [String, nil]
|
546
|
-
# @return [void]
|
547
|
-
def alias=(val)
|
548
|
-
@o_dev[I_DEVICE]['Alias'] = val.nil? ? "" : val.to_str
|
549
|
-
end
|
550
|
-
|
551
|
-
# Is the device trusted?
|
552
|
-
# @return [Boolean]
|
553
|
-
def is_trusted?
|
554
|
-
@o_dev[I_DEVICE]['Trusted']
|
555
|
-
end
|
556
|
-
|
557
|
-
# Indicates if the remote is seen as trusted. This
|
558
|
-
# setting can be changed by the application.
|
559
|
-
# @param val [Boolean]
|
560
|
-
# @return [void]
|
561
|
-
def trusted=(val)
|
562
|
-
if ! [ true, false ].include?(val)
|
563
|
-
raise ArgumentError, "value must be a boolean"
|
564
|
-
end
|
565
|
-
@o_dev[I_DEVICE]['Trusted'] = val
|
566
|
-
end
|
567
84
|
|
568
|
-
# Is the device blocked?
|
569
|
-
# @return [Boolean]
|
570
|
-
def is_blocked?
|
571
|
-
@o_dev[I_DEVICE]['Blocked']
|
572
|
-
end
|
573
|
-
|
574
|
-
# if set to true any incoming connections from the
|
575
|
-
# device will be immediately rejected. Any device
|
576
|
-
# drivers will also be removed and no new ones will
|
577
|
-
# be probed as long as the device is blocked
|
578
|
-
# @param val [Boolean]
|
579
|
-
# @return [void]
|
580
|
-
def blocked=(val)
|
581
|
-
if ! [ true, false ].include?(val)
|
582
|
-
raise ArgumentError, "value must be a boolean"
|
583
|
-
end
|
584
|
-
@o_dev[I_DEVICE]['Blocked'] = val
|
585
|
-
end
|
586
|
-
|
587
|
-
# Received Signal Strength Indicator of the remote
|
588
|
-
# device (inquiry or advertising).
|
589
|
-
# @return [Integer]
|
590
|
-
def rssi # optional
|
591
|
-
@o_dev[I_DEVICE]['RSSI']
|
592
|
-
rescue DBus::Error => e
|
593
|
-
case e.name
|
594
|
-
when E_INVALID_ARGS then raise NotSupported
|
595
|
-
else raise ScriptError
|
596
|
-
end
|
597
|
-
end
|
598
|
-
|
599
|
-
# Advertised transmitted power level (inquiry or advertising).
|
600
|
-
# @return [Integer]
|
601
|
-
def tx_power # optional
|
602
|
-
@o_dev[I_DEVICE]['TxPower']
|
603
|
-
rescue DBus::Error => e
|
604
|
-
case e.name
|
605
|
-
when E_INVALID_ARGS then raise NotSupported
|
606
|
-
else raise ScriptError
|
607
|
-
end
|
608
|
-
end
|
609
|
-
|
610
|
-
|
611
|
-
# Refresh list of services and characteristics
|
612
|
-
# @return [Boolean]
|
613
|
-
def refresh
|
614
|
-
refresh!
|
615
|
-
true
|
616
|
-
rescue NotConnected, StalledObject
|
617
|
-
false
|
618
|
-
end
|
619
|
-
|
620
|
-
# Refresh list of services and characteristics
|
621
|
-
# @raise [NotConnected] if device is not in a connected state
|
622
|
-
# @return [self]
|
623
|
-
def refresh!
|
624
|
-
raise NotConnected unless is_connected?
|
625
|
-
max_wait ||= 1.5 # Use ||= due to the retry
|
626
|
-
@services = Hash[@o_dev[I_DEVICE]['GattServices'].map {|p_srv|
|
627
|
-
o_srv = BLUEZ.object(p_srv)
|
628
|
-
o_srv.introspect
|
629
|
-
srv = o_srv[I_PROPERTIES].GetAll(I_GATT_SERVICE).first
|
630
|
-
char = Hash[srv['Characteristics'].map {|p_char|
|
631
|
-
o_char = BLUEZ.object(p_char)
|
632
|
-
o_char.introspect
|
633
|
-
uuid = o_char[I_GATT_CHARACTERISTIC]['UUID' ].downcase
|
634
|
-
flags = o_char[I_GATT_CHARACTERISTIC]['Flags']
|
635
|
-
[ uuid, { :uuid => uuid, :flags => flags, :obj => o_char } ]
|
636
|
-
}]
|
637
|
-
uuid = srv['UUID'].downcase
|
638
|
-
[ uuid, { :uuid => uuid,
|
639
|
-
:primary => srv['Primary'],
|
640
|
-
:characteristics => char } ]
|
641
|
-
}]
|
642
|
-
self
|
643
|
-
rescue DBus::Error => e
|
644
|
-
case e.name
|
645
|
-
when E_UNKNOWN_OBJECT
|
646
|
-
raise StalledObject
|
647
|
-
when E_INVALID_ARGS
|
648
|
-
# That's probably because all the bluez information
|
649
|
-
# haven't been collected yet on dbus for GattServices
|
650
|
-
if max_wait > 0
|
651
|
-
sleep(0.25) ; max_wait -= 0.25 ; retry
|
652
|
-
end
|
653
|
-
raise NotReady
|
654
|
-
|
655
|
-
else raise ScriptError
|
656
|
-
end
|
657
|
-
end
|
658
|
-
|
659
|
-
# @param service [String, Symbol]
|
660
|
-
# @param characteristic [String, Symbol]
|
661
|
-
# @param raw [Boolean]
|
662
|
-
# @raise [NotYetImplemented, NotConnected, ServiceNotFound,
|
663
|
-
# CharacteristicNotFound, AccessUnavailable ]
|
664
|
-
def [](service, characteristic, raw: false)
|
665
|
-
raise NotConnected unless is_connected?
|
666
|
-
uuid = _uuid_characteristic(characteristic)
|
667
|
-
chars = _characteristics(service)
|
668
|
-
raise ServiceNotFound, service if chars.nil?
|
669
|
-
char = chars[uuid]
|
670
|
-
raise CharacteristicNotFound, characteristic if char.nil?
|
671
|
-
flags = char[:flags]
|
672
|
-
obj = char[:obj]
|
673
|
-
info = Characteristic[uuid]
|
674
|
-
|
675
|
-
if flags.include?('read')
|
676
|
-
val = obj[I_GATT_CHARACTERISTIC].ReadValue().first
|
677
|
-
val = val.pack('C*')
|
678
|
-
val = info[:in].call(val) if !raw && info && info[:in]
|
679
|
-
val
|
680
|
-
elsif flags.include?('encrypt-read') ||
|
681
|
-
flags.include?('encrypt-authenticated-read')
|
682
|
-
raise NotYetImplemented
|
683
|
-
else
|
684
|
-
raise AccessUnavailable
|
685
|
-
end
|
686
|
-
end
|
687
|
-
|
688
|
-
# @param service [String, Symbol]
|
689
|
-
# @param characteristic [String, Symbol]
|
690
|
-
# @param val [Boolean]
|
691
|
-
# @raise [NotYetImplemented, NotConnected, ServiceNotFound,
|
692
|
-
# CharacteristicNotFound, AccessUnavailable ]
|
693
|
-
def []=(service, characteristic, val, raw: false)
|
694
|
-
raise NotConnected unless is_connected?
|
695
|
-
uuid = _uuid_characteristic(characteristic)
|
696
|
-
chars = _characteristics(service)
|
697
|
-
raise ServiceNotFound, service if chars.nil?
|
698
|
-
char = chars[uuid]
|
699
|
-
raise CharacteristicNotFound, characteristic if char.nil?
|
700
|
-
flags = char[:flags]
|
701
|
-
obj = char[:obj]
|
702
|
-
info = Characteristic[uuid]
|
703
|
-
|
704
|
-
if flags.include?('write') ||
|
705
|
-
flags.include?('write-without-response')
|
706
|
-
if !raw && info
|
707
|
-
if info[:vrfy] && !info[:vrfy].call(vall)
|
708
|
-
raise ArgumentError,
|
709
|
-
"bad value for characteristic '#{characteristic}'"
|
710
|
-
end
|
711
|
-
val = info[:out].call(val) if info[:out]
|
712
|
-
end
|
713
|
-
val = val.unpack('C*')
|
714
|
-
obj[I_GATT_CHARACTERISTIC].WriteValue(val)
|
715
|
-
elsif flags.include?('encrypt-write') ||
|
716
|
-
flags.include?('encrypt-authenticated-write')
|
717
|
-
raise NotYetImplemented
|
718
|
-
else
|
719
|
-
raise AccessUnavailable
|
720
|
-
end
|
721
|
-
end
|
722
|
-
|
723
|
-
private
|
724
|
-
|
725
|
-
def _characteristics(service)
|
726
|
-
if srv = @services[_uuid_service(service)]
|
727
|
-
srv[:characteristics]
|
728
|
-
end
|
729
|
-
end
|
730
|
-
def _uuid_service(service)
|
731
|
-
uuid = case service
|
732
|
-
when Symbol
|
733
|
-
if i = Service::NICKNAME[service]
|
734
|
-
i[:uuid]
|
735
|
-
end
|
736
|
-
when UUID::REGEX
|
737
|
-
service.downcase
|
738
|
-
when String
|
739
|
-
if i = Service::TYPE[service]
|
740
|
-
i[:uuid]
|
741
|
-
end
|
742
|
-
end
|
743
|
-
if uuid.nil?
|
744
|
-
raise ArgumentError, "unable to get UUID for service"
|
745
|
-
end
|
746
|
-
|
747
|
-
uuid
|
748
|
-
end
|
749
|
-
def _uuid_characteristic(characteristic)
|
750
|
-
uuid = case characteristic
|
751
|
-
when Symbol
|
752
|
-
if i = Characteristic::NICKNAME[characteristic]
|
753
|
-
i[:uuid]
|
754
|
-
end
|
755
|
-
when UUID::REGEX
|
756
|
-
characteristic.downcase
|
757
|
-
when String
|
758
|
-
if i = Characteristic::TYPE[characteristic]
|
759
|
-
i[:uuid]
|
760
|
-
end
|
761
|
-
end
|
762
|
-
if uuid.nil?
|
763
|
-
raise ArgumentError, "unable to get UUID for service"
|
764
|
-
end
|
765
|
-
|
766
|
-
uuid
|
767
|
-
end
|
768
|
-
|
769
|
-
|
770
|
-
end
|
771
85
|
|
772
86
|
def self.UUID(val)
|
773
87
|
val.downcase
|
@@ -776,178 +90,22 @@ module BLE
|
|
776
90
|
class UUID
|
777
91
|
REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
778
92
|
end
|
93
|
+
|
779
94
|
|
780
|
-
|
781
|
-
UUID = {}
|
782
|
-
TYPE = {}
|
783
|
-
NICKNAME = {}
|
784
|
-
|
785
|
-
# Get a service description from it's id
|
786
|
-
# @param id [Symbol,String]
|
787
|
-
# @return [Hash]
|
788
|
-
def self.[](id)
|
789
|
-
case id
|
790
|
-
when Symbol then NICKNAME[id]
|
791
|
-
when UUID::REGEX then UUID[id]
|
792
|
-
when String then TYPE[id]
|
793
|
-
else raise ArgumentError, "invalid type for service id"
|
794
|
-
end
|
795
|
-
end
|
796
|
-
|
797
|
-
# Add a service description
|
798
|
-
# @param uuid [String]
|
799
|
-
# @param name [String]
|
800
|
-
# @param type [String]
|
801
|
-
def self.add(uuid, name:, type:, **opts)
|
802
|
-
if opts.first
|
803
|
-
raise ArgumentError, "unknown keyword: #{opts.first[0]}"
|
804
|
-
end
|
805
|
-
|
806
|
-
uuid = case uuid
|
807
|
-
when Integer
|
808
|
-
if !(0..4294967296).include?(uuid)
|
809
|
-
raise ArgumentError, "not a 16bit or 32bit uuid"
|
810
|
-
end
|
811
|
-
([uuid].pack("L>").unpack('H*').first +
|
812
|
-
"-0000-1000-8000-00805F9B34FB")
|
813
|
-
|
814
|
-
when String
|
815
|
-
if uuid !~ UUID::REGEX
|
816
|
-
raise ArgumentError, "not a 128bit uuid string"
|
817
|
-
end
|
818
|
-
uuid
|
819
|
-
else raise ArgumentError, "invalid uuid type"
|
820
|
-
end
|
821
|
-
uuid = uuid.downcase
|
822
|
-
type = type.downcase
|
823
|
-
|
824
|
-
TYPE[type] = UUID[uuid] = {
|
825
|
-
name: name,
|
826
|
-
type: type,
|
827
|
-
uuid: uuid,
|
828
|
-
}
|
829
|
-
|
830
|
-
stype = type.split('.')
|
831
|
-
key = stype.pop.to_sym
|
832
|
-
prefix = stype.join('.')
|
833
|
-
case prefix
|
834
|
-
when 'org.bluetooth.service'
|
835
|
-
if NICKNAME.include?(key)
|
836
|
-
raise ArgumentError,
|
837
|
-
"nickname '#{key}' already registered (type: #{type})"
|
838
|
-
end
|
839
|
-
NICKNAME[key] = UUID[uuid]
|
840
|
-
end
|
841
|
-
end
|
842
|
-
|
843
|
-
def initialize(service)
|
844
|
-
@o_srv = BLUEZ.object(service)
|
845
|
-
@o_srv.introspect
|
846
|
-
end
|
847
|
-
|
848
|
-
|
849
|
-
end
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
class Characteristic
|
855
|
-
FLAGS = [ 'broadcast',
|
856
|
-
'read',
|
857
|
-
'write-without-response',
|
858
|
-
'write',
|
859
|
-
'notify',
|
860
|
-
'indicate',
|
861
|
-
'authenticated-signed-writes',
|
862
|
-
'reliable-write',
|
863
|
-
'writable-auxiliaries',
|
864
|
-
'encrypt-read',
|
865
|
-
'encrypt-write',
|
866
|
-
'encrypt-authenticated-read',
|
867
|
-
'encrypt-authenticated-write' ]
|
868
|
-
|
869
|
-
|
870
|
-
UUID = {}
|
871
|
-
TYPE = {}
|
872
|
-
NICKNAME = {}
|
873
|
-
|
874
|
-
# Get a characteristic description from it's id
|
875
|
-
# @param id [Symbol,String]
|
876
|
-
# @return [Hash]
|
877
|
-
def self.[](id)
|
878
|
-
case id
|
879
|
-
when Symbol then NICKNAME[id]
|
880
|
-
when UUID::REGEX then UUID[id]
|
881
|
-
when String then TYPE[id]
|
882
|
-
else raise ArgumentError, "invalid type for characteristic id"
|
883
|
-
end
|
884
|
-
end
|
885
|
-
|
886
|
-
|
887
|
-
# Add a characteristic description
|
888
|
-
# @param uuid [String]
|
889
|
-
# @param name [String]
|
890
|
-
# @param type [String]
|
891
|
-
# @option opts :in [Proc] convert to ruby
|
892
|
-
# @option opts :out [Proc] convert to bluetooth data
|
893
|
-
# @option opts :vry [Proc] verify
|
894
|
-
def self.add(uuid, name:, type:, **opts)
|
895
|
-
_in = opts.delete :in
|
896
|
-
_out = opts.delete :out
|
897
|
-
vrfy = opts.delete :vrfy
|
898
|
-
if opts.first
|
899
|
-
raise ArgumentError, "unknown keyword: #{opts.first[0]}"
|
900
|
-
end
|
901
|
-
|
902
|
-
uuid = case uuid
|
903
|
-
when Integer
|
904
|
-
if !(0..4294967296).include?(uuid)
|
905
|
-
raise ArgumentError, "not a 16bit or 32bit uuid"
|
906
|
-
end
|
907
|
-
([uuid].pack("L>").unpack('H*').first +
|
908
|
-
"-0000-1000-8000-00805F9B34FB")
|
909
|
-
|
910
|
-
when String
|
911
|
-
if uuid !~ UUID::REGEX
|
912
|
-
raise ArgumentError, "not a 128bit uuid string"
|
913
|
-
end
|
914
|
-
uuid
|
915
|
-
else raise ArgumentError, "invalid uuid type"
|
916
|
-
end
|
917
|
-
uuid = uuid.downcase
|
918
|
-
type = type.downcase
|
919
|
-
|
920
|
-
TYPE[type] = UUID[uuid] = {
|
921
|
-
name: name,
|
922
|
-
type: type,
|
923
|
-
uuid: uuid,
|
924
|
-
in: _in,
|
925
|
-
out: _out,
|
926
|
-
vrfy: vrfy
|
927
|
-
}
|
928
|
-
|
929
|
-
stype = type.split('.')
|
930
|
-
key = stype.pop.to_sym
|
931
|
-
prefix = stype.join('.')
|
932
|
-
case prefix
|
933
|
-
when 'org.bluetooth.characteristic'
|
934
|
-
if NICKNAME.include?(key)
|
935
|
-
raise ArgumentError,
|
936
|
-
"nickname '#{key}' already registered (type: #{type})"
|
937
|
-
end
|
938
|
-
NICKNAME[key] = UUID[uuid]
|
939
|
-
end
|
940
|
-
end
|
941
|
-
end
|
942
|
-
|
943
|
-
|
944
|
-
# Check if Bluetooth API is accessible
|
95
|
+
# Check if Bluetooth underlying API is accessible
|
945
96
|
def self.ok?
|
946
97
|
BLUEZ.exists?
|
947
98
|
end
|
948
99
|
|
949
100
|
end
|
950
101
|
|
102
|
+
require_relative 'ble/version'
|
103
|
+
require_relative 'ble/adapter'
|
104
|
+
require_relative 'ble/device'
|
105
|
+
require_relative 'ble/service'
|
106
|
+
require_relative 'ble/characteristic'
|
107
|
+
require_relative 'ble/agent'
|
108
|
+
|
951
109
|
require_relative 'ble/db_service'
|
952
110
|
require_relative 'ble/db_characteristic'
|
953
111
|
|