ble 0.0.1 → 0.0.2

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