sb-ble 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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