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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd8602914859f96e3568d364d41c7c144691353c
4
- data.tar.gz: db9efd8183e595469309bfb67e9682d9ac5318ff
3
+ metadata.gz: 05bba72a755368615142178e182af8ade6c5f97d
4
+ data.tar.gz: 0c0e6f32a4abd668bce46c26f845492e440a72a5
5
5
  SHA512:
6
- metadata.gz: be6b5f9ca0d679d208f875a1b6a0f8142e68e90873e0825953cf912559b06db1bb19e9e77813ebc271358c6e8b47cb65ede0913fb620318c99a6efd78f885c86
7
- data.tar.gz: d6aed04d4bc9fc9fdf0735788236675776b3ecfb7f74ab7df70979abccf6b81e9503bee76f3be065986942c1d5139a604a3ff902fc5a82f32eb300fa3771e1a1
6
+ metadata.gz: 223434f164e5c8adad7c0bfd043b4cf4955329d939c88210fc22d6abf422110ce8713dede74bec3641ce8b1b7f2dd1e81a03458496159793ae3832642c377438
7
+ data.tar.gz: 29dbc315196f93c8c5e2f2ddd8ba2dcc0788561a19061637938cafc6541f5978d3d9d2162437c74e4746d7d12c0ac4d3349c4d2ed2f499930ee10bf5d0442874
@@ -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 BLE device from ruby"
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
- DBUS = DBus.system_bus
45
- BLUEZ = DBUS.service('org.bluez')
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
- class NotConnected < Error ; end
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
- class Service
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