sb-ble 0.5.0

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.
@@ -0,0 +1,59 @@
1
+ module BLE
2
+ module Service
3
+ add 0x1800,
4
+ name: 'Generic Access',
5
+ type: 'org.bluetooth.service.generic_access'
6
+
7
+ add 0x1801,
8
+ name: 'Generic Attribute',
9
+ type: 'org.bluetooth.service.generic_attribute'
10
+
11
+ add 0x1802,
12
+ name: 'Immediate Alert',
13
+ type: 'org.bluetooth.service.immediate_alert'
14
+
15
+ add 0x1803,
16
+ name: 'Link Loss',
17
+ type: 'org.bluetooth.service.link_loss'
18
+
19
+ add 0x1804,
20
+ name: 'Tx Power',
21
+ type: 'org.bluetooth.service.tx_power'
22
+
23
+ add 0x1805,
24
+ name: 'Current Time Service',
25
+ type: 'org.bluetooth.service.current_time'
26
+
27
+ add 0x180A,
28
+ name: 'Device Information',
29
+ type: 'org.bluetooth.service.device_information'
30
+
31
+ add 0x180F,
32
+ name: 'Battery Service',
33
+ type: 'org.bluetooth.service.battery_service'
34
+
35
+ add 0x1811,
36
+ name: 'Alert Notification Service',
37
+ type: 'org.bluetooth.service.alert_notification'
38
+
39
+ add 0x1812,
40
+ name: 'Human Interface Device',
41
+ type: 'org.bluetooth.service.human_interface_device'
42
+
43
+ add 0x1819,
44
+ name: 'Location and Navigation',
45
+ type: 'org.bluetooth.service.location_and_navigation'
46
+
47
+ add 0x181A,
48
+ name: 'Environmental Sensing',
49
+ type: 'org.bluetooth.service.environmental_sensing'
50
+
51
+ add 0x181C,
52
+ name: 'User Data',
53
+ type: 'org.bluetooth.service.user_data'
54
+
55
+ add 0x181D,
56
+ name: 'Weight Scale',
57
+ type: 'org.bluetooth.service.weight_scale'
58
+ end
59
+ end
@@ -0,0 +1,494 @@
1
+ module BLE
2
+ # Create de Device object
3
+ # d = Device::new('hci0', 'aa:bb:dd:dd:ee:ff')
4
+ # d = Adapter.new('hci0')['aa:bb:dd:dd:ee:ff']
5
+ #
6
+ # d.services
7
+ # d.characteristics(:environmental_sensing)
8
+ # d[:environmental_sensing, :temperature]
9
+ #
10
+ class Device
11
+ include Notifications
12
+ # Notify that you need to have the device in a connected state
13
+ class NotConnected < Error ; end
14
+
15
+ # @param adapter [String] adapter unix device name
16
+ # @param dev [String] device MAC address
17
+ # @param auto_refresh [Boolean] gather information about device
18
+ # on connection
19
+ def initialize(adapter, dev, auto_refresh: true)
20
+ @adapter, @dev = adapter, dev
21
+ @auto_refresh = auto_refresh
22
+ @services = {}
23
+
24
+ @n_adapter = adapter
25
+ @p_adapter = "/org/bluez/#{@n_adapter}"
26
+ @o_adapter = BLUEZ.object(@p_adapter)
27
+ @o_adapter.introspect
28
+
29
+ @n_dev = 'dev_' + dev.tr(':', '_')
30
+ @p_dev = "/org/bluez/#{@n_adapter}/#{@n_dev}"
31
+ @o_dev = BLUEZ.object(@p_dev)
32
+ @o_dev.introspect
33
+
34
+ self.refresh if @auto_refresh
35
+
36
+ @o_dev[I_PROPERTIES]
37
+ .on_signal('PropertiesChanged') do |intf, props|
38
+ case intf
39
+ when I_DEVICE
40
+ case props['Connected']
41
+ when true
42
+ self.refresh if @auto_refresh
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # This removes the remote device object.
49
+ # It will remove also the pairing information.
50
+ # @return [Boolean]
51
+ def remove
52
+ @o_adapter[I_ADAPTER].RemoveDevice(@p_dev)
53
+ true
54
+ rescue DBus::Error => e
55
+ case e.name
56
+ when E_FAILED then false
57
+ when E_DOES_NOT_EXIST then raise StalledObject
58
+ when E_UNKNOWN_OBJECT then raise StalledObject
59
+ else raise ScriptError
60
+ end
61
+ end
62
+
63
+
64
+ # This method will connect to the remote device,
65
+ # initiate pairing and then retrieve all SDP records
66
+ # (or GATT primary services).
67
+ # If the application has registered its own agent,
68
+ # then that specific agent will be used. Otherwise
69
+ # it will use the default agent.
70
+ # Only for applications like a pairing wizard it
71
+ # would make sense to have its own agent. In almost
72
+ # all other cases the default agent will handle this just fine.
73
+ # In case there is no application agent and also
74
+ # no default agent present, this method will fail.
75
+ #
76
+ # @return [Boolean]
77
+ def pair
78
+ @o_dev[I_DEVICE].Pair
79
+ true
80
+ rescue DBus::Error => e
81
+ case e.name
82
+ when E_INVALID_ARGUMENTS then false
83
+ when E_FAILED then false
84
+ when E_ALREADY_EXISTS then true
85
+ when E_AUTH_CANCELED then raise NotAuthorized
86
+ when E_AUTH_FAILED then raise NotAuthorized
87
+ when E_AUTH_REJECTED then raise NotAuthorized
88
+ when E_AUTH_TIMEOUT then raise NotAuthorized
89
+ when E_AUTH_ATTEMPT_FAILED then raise NotAuthorized
90
+ else raise ScriptError
91
+ end
92
+ end
93
+
94
+ # This method can be used to cancel a pairing
95
+ # operation initiated by the Pair method.
96
+ # @return [Boolean]
97
+ def cancel_pairing
98
+ @o_dev[I_DEVICE].CancelPairing
99
+ true
100
+ rescue DBus::Error => e
101
+ case e.name
102
+ when E_DOES_NOT_EXIST then true
103
+ when E_FAILED then false
104
+ else raise ScriptError
105
+ end
106
+ end
107
+
108
+ # This connect to the specified profile UUID or to any (:all)
109
+ # profiles the remote device supports that can be connected to
110
+ # and have been flagged as auto-connectable on our side. If
111
+ # only subset of profiles is already connected it will try to
112
+ # connect currently disconnected ones. If at least one
113
+ # profile was connected successfully this method will indicate
114
+ # success.
115
+ # @return [Boolean]
116
+ def connect(profile=:all)
117
+ case profile
118
+ when UUID::REGEX
119
+ @o_dev[I_DEVICE].ConnectProfile(profile)
120
+ when :all
121
+ @o_dev[I_DEVICE].Connect()
122
+ else raise ArgumentError, "profile uuid or :all expected"
123
+ end
124
+ true
125
+ rescue DBus::Error => e
126
+ case e.name
127
+ when E_NOT_READY
128
+ when E_FAILED
129
+ when E_IN_PROGRESS
130
+ false
131
+ when E_ALREADY_CONNECTED
132
+ true
133
+ when E_UNKNOWN_OBJECT
134
+ raise StalledObject
135
+ else raise ScriptError
136
+ end
137
+ end
138
+
139
+ # This method gracefully disconnects :all connected profiles
140
+ # and then terminates low-level ACL connection.
141
+ # ACL connection will be terminated even if some profiles
142
+ # were not disconnected properly e.g. due to misbehaving device.
143
+ # This method can be also used to cancel a preceding #connect
144
+ # call before a reply to it has been received.
145
+ # If a profile UUID is specified, only this profile is disconnected,
146
+ # and as their is no connection tracking for a profile, so
147
+ # as long as the profile is registered this will always succeed
148
+ # @return [Boolean]
149
+ def disconnect(profile=:all)
150
+ case profile
151
+ when UUID::REGEX
152
+ @o_dev[I_DEVICE].DisconnectProfile(profile)
153
+ when :all
154
+ @o_dev[I_DEVICE].Disconnect()
155
+ else raise ArgumentError, "profile uuid or :all expected"
156
+ end
157
+ true
158
+ rescue DBus::Error => e
159
+ case e.name
160
+ when E_FAILED
161
+ when E_IN_PROGRESS
162
+ false
163
+ when E_INVALID_ARGUMENTS
164
+ raise ArgumentError, "unsupported profile (#{profile})"
165
+ when E_NOT_SUPPORTED
166
+ raise NotSupported
167
+ when E_NOT_CONNECTED
168
+ true
169
+ when E_UNKNOWN_OBJECT
170
+ raise StalledObject
171
+ else raise ScriptError
172
+ end
173
+ end
174
+
175
+ # Indicates if the remote device is paired
176
+ def is_paired?
177
+ @o_dev[I_DEVICE]['Paired']
178
+ rescue DBus::Error => e
179
+ case e.name
180
+ when E_UNKNOWN_OBJECT
181
+ raise StalledObject
182
+ else raise ScriptError
183
+ end
184
+ end
185
+
186
+ # Indicates if the remote device is currently connected.
187
+ def is_connected?
188
+ @o_dev[I_DEVICE]['Connected']
189
+ rescue DBus::Error => e
190
+ case e.name
191
+ when E_UNKNOWN_OBJECT
192
+ raise StalledObject
193
+ else raise ScriptError
194
+ end
195
+ end
196
+
197
+ # List of available services as UUID.
198
+ #
199
+ # @raise [NotConnected] if device is not in a connected state
200
+ # @note The list is retrieve once when object is
201
+ # connected if auto_refresh is enable, otherwise
202
+ # you need to call {#refresh}.
203
+ # @note This is the list of UUIDs for which we have an entry
204
+ # in the underlying api (bluez-dbus), which can be less
205
+ # that the list of advertised UUIDs.
206
+ # @example list available services
207
+ # $d.services.each {|uuid|
208
+ # info = BLE::Service[uuid]
209
+ # name = info.nil? ? uuid : info[:name]
210
+ # puts name
211
+ # }
212
+ #
213
+ # @return [Array<String>] List of service UUID
214
+ def services
215
+ _require_connection!
216
+ @services.keys
217
+ end
218
+
219
+ # Check if service is available on the device
220
+ # @return [Boolean]
221
+ def has_service?(service)
222
+ @service.key?(_uuid_service(service))
223
+ end
224
+
225
+ # List of available characteristics UUID for a service.
226
+ #
227
+ # @param service service can be a UUID, a service type or
228
+ # a service nickname
229
+ # @return [Array<String>, nil] list of characteristics or +nil+ if the
230
+ # service doesn't exist
231
+ # @raise [NotConnected] if device is not in a connected state
232
+ # @note The list is retrieve once when object is
233
+ # connected if auto_refresh is enable, otherwise
234
+ # you need to call {#refresh}.
235
+ def characteristics(service)
236
+ _require_connection!
237
+ if chars = _characteristics(service)
238
+ chars.keys
239
+ end
240
+ end
241
+
242
+ # The Bluetooth device address of the remote device.
243
+ # @return [String] MAC address
244
+ def address
245
+ @o_dev[I_DEVICE]['Address']
246
+ end
247
+
248
+ # The Bluetooth remote name.
249
+ # It is better to always use the {#alias} when displaying the
250
+ # devices name.
251
+ # @return [String] name
252
+ def name # optional
253
+ @o_dev[I_DEVICE]['Name']
254
+ end
255
+
256
+ # The name alias for the remote device.
257
+ # The alias can be used to have a different friendly name for the
258
+ # remote device.
259
+ # In case no alias is set, it will return the remote device name.
260
+ # @return [String]
261
+ def alias
262
+ @o_dev[I_DEVICE]['Alias']
263
+ end
264
+ # Setting an empty string or nil as alias will convert it
265
+ # back to the remote device name.
266
+ # @param val [String, nil]
267
+ # @return [void]
268
+ def alias=(val)
269
+ @o_dev[I_DEVICE]['Alias'] = val.nil? ? "" : val.to_str
270
+ end
271
+
272
+ # Is the device trusted?
273
+ # @return [Boolean]
274
+ def is_trusted?
275
+ @o_dev[I_DEVICE]['Trusted']
276
+ end
277
+
278
+ # Indicates if the remote is seen as trusted. This
279
+ # setting can be changed by the application.
280
+ # @param val [Boolean]
281
+ # @return [void]
282
+ def trusted=(val)
283
+ if ! [ true, false ].include?(val)
284
+ raise ArgumentError, "value must be a boolean"
285
+ end
286
+ @o_dev[I_DEVICE]['Trusted'] = val
287
+ end
288
+
289
+ # Is the device blocked?
290
+ # @return [Boolean]
291
+ def is_blocked?
292
+ @o_dev[I_DEVICE]['Blocked']
293
+ end
294
+
295
+ # If set to true any incoming connections from the
296
+ # device will be immediately rejected. Any device
297
+ # drivers will also be removed and no new ones will
298
+ # be probed as long as the device is blocked
299
+ # @param val [Boolean]
300
+ # @return [void]
301
+ def blocked=(val)
302
+ if ! [ true, false ].include?(val)
303
+ raise ArgumentError, "value must be a boolean"
304
+ end
305
+ @o_dev[I_DEVICE]['Blocked'] = val
306
+ end
307
+
308
+ # Received Signal Strength Indicator of the remote
309
+ # device (inquiry or advertising).
310
+ # @return [Integer]
311
+ def rssi # optional
312
+ @o_dev[I_DEVICE]['RSSI']
313
+ rescue DBus::Error => e
314
+ case e.name
315
+ when E_INVALID_ARGS then raise NotSupported
316
+ else raise ScriptError
317
+ end
318
+ end
319
+
320
+ # Advertised transmitted power level (inquiry or advertising).
321
+ # @return [Integer]
322
+ def tx_power # optional
323
+ @o_dev[I_DEVICE]['TxPower']
324
+ rescue DBus::Error => e
325
+ case e.name
326
+ when E_INVALID_ARGS then raise NotSupported
327
+ else raise ScriptError
328
+ end
329
+ end
330
+
331
+
332
+ # Refresh list of services and characteristics
333
+ # @return [Boolean]
334
+ def refresh
335
+ refresh!
336
+ true
337
+ rescue NotConnected, StalledObject
338
+ false
339
+ end
340
+
341
+ # Refresh list of services and characteristics
342
+ # @raise [NotConnected] if device is not in a connected state
343
+ # @return [self]
344
+ def refresh!
345
+ _require_connection!
346
+ max_wait ||= 1.5 # Use ||= due to the retry
347
+ @services = Hash[@o_dev[I_DEVICE]['GattServices'].map {|p_srv|
348
+ o_srv = BLUEZ.object(p_srv)
349
+ o_srv.introspect
350
+ srv = o_srv[I_PROPERTIES].GetAll(I_GATT_SERVICE).first
351
+ char = Hash[srv['Characteristics'].map {|p_char|
352
+ o_char = BLUEZ.object(p_char)
353
+ o_char.introspect
354
+ uuid = o_char[I_GATT_CHARACTERISTIC]['UUID' ].downcase
355
+ flags = o_char[I_GATT_CHARACTERISTIC]['Flags']
356
+ [ uuid, Characteristic.new({ :uuid => uuid, :flags => flags, :obj => o_char }) ]
357
+ }]
358
+ uuid = srv['UUID'].downcase
359
+ [ uuid, { :uuid => uuid,
360
+ :primary => srv['Primary'],
361
+ :characteristics => char } ]
362
+ }]
363
+ self
364
+ rescue DBus::Error => e
365
+ case e.name
366
+ when E_UNKNOWN_OBJECT
367
+ raise StalledObject
368
+ when E_INVALID_ARGS
369
+ # That's probably because all the bluez information
370
+ # haven't been collected yet on dbus for GattServices
371
+ if max_wait > 0
372
+ sleep(0.25) ; max_wait -= 0.25 ; retry
373
+ end
374
+ raise NotReady
375
+
376
+ else raise ScriptError
377
+ end
378
+ end
379
+
380
+ # Get value for a service/characteristic.
381
+ #
382
+ # @param service [String, Symbol]
383
+ # @param characteristic [String, Symbol]
384
+ # @param raw [Boolean] When raw is true the value get is a binary string, instead of an object corresponding to the decoded characteristic (float, integer, array, ...)
385
+ # @raise [NotConnected] if device is not in a connected state
386
+ # @raise [NotYetImplemented] encryption is not implemented yet
387
+ # @raise [Service::NotFound, Characteristic::NotFound] if service/characteristic doesn't exist on this device
388
+ # @raise [AccessUnavailable] if not available for reading
389
+ # @return [Object]
390
+ def [](service, characteristic, raw: false)
391
+ _require_connection!
392
+ uuid = _uuid_characteristic(characteristic)
393
+ chars = _characteristics(service)
394
+ raise Service::NotFound, service if chars.nil?
395
+ char = chars[uuid]
396
+ raise Characteristic::NotFound, characteristic if char.nil?
397
+
398
+ if char.flag?('read')
399
+ char.read(raw: raw)
400
+ elsif char.flag?('encrypt-read') ||
401
+ char.flag?('encrypt-authenticated-read')
402
+ raise NotYetImplemented
403
+ else
404
+ raise AccessUnavailable
405
+ end
406
+ end
407
+
408
+ # Set value for a service/characteristic
409
+ #
410
+ # @param service [String, Symbol]
411
+ # @param characteristic [String, Symbol]
412
+ # @param val [Boolean]
413
+ # @param raw [Boolean] When raw is true the value set is a binary string, instead of an object corresponding to the decoded characteristic (float, integer, array, ...).
414
+ # @raise [NotConnected] if device is not in a connected state
415
+ # @raise [NotYetImplemented] encryption is not implemented yet
416
+ # @raise [Service::NotFound, Characteristic::NotFound] if service/characteristic doesn't exist on this device
417
+ # @raise [AccessUnavailable] if not available for writing
418
+ # @return [void]
419
+ def []=(service, characteristic, val, raw: false)
420
+ _require_connection!
421
+ uuid = _uuid_characteristic(characteristic)
422
+ chars = _characteristics(service)
423
+ raise ServiceNotFound, service if chars.nil?
424
+ char = chars[uuid]
425
+ raise CharacteristicNotFound, characteristic if char.nil?
426
+
427
+ if char.flag?('write') ||
428
+ char.flag?('write-without-response')
429
+ char.write(val, raw: raw)
430
+ elsif char.flag?('encrypt-write') ||
431
+ char.flag?('encrypt-authenticated-write')
432
+ raise NotYetImplemented
433
+ else
434
+ raise AccessUnavailable
435
+ end
436
+ nil
437
+ end
438
+
439
+ #---------------------------------
440
+ private
441
+ #---------------------------------
442
+ def _require_connection!
443
+ raise NotConnected unless is_connected?
444
+ end
445
+
446
+ def _find_characteristic(service_id, char_id)
447
+ uuid= _uuid_characteristic(char_id)
448
+ chars= _characteristics(service_id)
449
+ raise Service::NotFound, service_id if chars.nil?
450
+ char= chars[uuid]
451
+ raise Characteristic::NotFound, char_id if char.nil?
452
+ char
453
+ end
454
+
455
+ # @param service [String, Symbol] The id of the service.
456
+ # @return [Hash] The descriptions of the characteristics for the given service.
457
+ def _characteristics(service)
458
+ if srv = @services[_uuid_service(service)]
459
+ srv[:characteristics]
460
+ end
461
+ end
462
+ def _uuid_service(service)
463
+ uuid = case service
464
+ when UUID::REGEX
465
+ service.downcase
466
+ else
467
+ if i = Service[service]
468
+ i[:uuid]
469
+ end
470
+ end
471
+ if uuid.nil?
472
+ raise ArgumentError, "unable to get UUID for service"
473
+ end
474
+
475
+ uuid
476
+ end
477
+ def _uuid_characteristic(characteristic)
478
+ uuid = case characteristic
479
+ when UUID::REGEX
480
+ characteristic.downcase
481
+ else
482
+ if char = Characteristic[characteristic]
483
+ char.uuid
484
+ end
485
+ end
486
+ if uuid.nil?
487
+ raise ArgumentError, "unable to get UUID for characteristic"
488
+ end
489
+
490
+ uuid
491
+ end
492
+
493
+ end
494
+ end