ble 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dd8602914859f96e3568d364d41c7c144691353c
4
+ data.tar.gz: db9efd8183e595469309bfb67e9682d9ac5318ff
5
+ SHA512:
6
+ metadata.gz: be6b5f9ca0d679d208f875a1b6a0f8142e68e90873e0825953cf912559b06db1bb19e9e77813ebc271358c6e8b47cb65ede0913fb620318c99a6efd78f885c86
7
+ data.tar.gz: d6aed04d4bc9fc9fdf0735788236675776b3ecfb7f74ab7df70979abccf6b81e9503bee76f3be065986942c1d5139a604a3ff902fc5a82f32eb300fa3771e1a1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 sdalu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift File.expand_path("../lib", __FILE__)
3
+ require "ble/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ble"
7
+ s.version = BLE::VERSION
8
+ s.authors = [ "Stephane D'Alu" ]
9
+ s.email = [ "stephane.dalu@gmail.com" ]
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"
13
+
14
+ s.add_dependency "ruby-dbus"
15
+
16
+ s.add_development_dependency "yard"
17
+ s.add_development_dependency "rake"
18
+
19
+ s.has_rdoc = 'yard'
20
+
21
+ s.license = 'MIT'
22
+
23
+
24
+ s.files = %w[ LICENSE Gemfile ble.gemspec ] +
25
+ Dir['lib/**/*.rb']
26
+ #s.require_path = 'lib'
27
+ end
@@ -0,0 +1,954 @@
1
+ # coding: utf-8
2
+ require 'dbus'
3
+ require 'logger'
4
+
5
+ # https://github.com/mvidner/ruby-dbus/blob/master/doc/Tutorial.md
6
+ # https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/refs/heads/master/doc/
7
+
8
+
9
+ #
10
+ module BLE
11
+
12
+
13
+ private
14
+ I_ADAPTER = 'org.bluez.Adapter1'
15
+ I_DEVICE = 'org.bluez.Device1'
16
+ I_AGENT_MANAGER = 'org.bluez.AgentManager1'
17
+ I_AGENT = 'org.bluez.Agent1'
18
+ I_GATT_CHARACTERISTIC = 'org.bluez.GattCharacteristic1'
19
+ I_GATT_SERVICE = 'org.bluez.GattService1'
20
+ I_PROXIMITY_REPORTER = 'org.bluez.ProximityReporter1'
21
+ I_PROPERTIES = 'org.freedesktop.DBus.Properties'
22
+ I_INTROSPECTABLE = 'org.freedesktop.DBus.Introspectable'
23
+
24
+ E_IN_PROGRESS = 'org.bluez.Error.InProgress'
25
+ E_FAILED = 'org.bluez.Error.Failed'
26
+ E_NOT_READY = 'org.bluez.Error.NotReady'
27
+ E_ALREADY_CONNECTED = 'org.bluez.Error.AlreadyConnected'
28
+ E_NOT_CONNECTED = 'org.bluez.Error.NotConnected'
29
+ E_DOES_NOT_EXIST = 'org.bluez.Error.DoesNotExist'
30
+ E_NOT_SUPPORTED = 'org.bluez.Error.NotSupported'
31
+ E_NOT_AUTHORIZED = 'org.bluez.Error.NotAuthorized'
32
+ E_INVALID_ARGUMENTS = 'org.bluez.Error.InvalidArguments'
33
+ E_ALREADY_EXISTS = 'org.bluez.Error.AlreadyExists'
34
+ E_AUTH_CANCELED = 'org.bluez.Error.AuthenticationCanceled'
35
+ E_AUTH_FAILED = 'org.bluez.Error.AuthenticationFailed'
36
+ E_AUTH_REJECTED = 'org.bluez.Error.AuthenticationRejected'
37
+ E_AUTH_TIMEOUT = 'org.bluez.Error.AuthenticationTimeout'
38
+ E_AUTH_ATTEMPT_FAILED = 'org.bluez.Error.ConnectionAttemptFailed'
39
+
40
+ E_UNKNOWN_OBJECT = 'org.freedesktop.DBus.Error.UnknownObject'
41
+ E_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
42
+ E_INVALID_SIGNATURE = 'org.freedesktop.DBus.Error.InvalidSignature'
43
+
44
+ DBUS = DBus.system_bus
45
+ BLUEZ = DBUS.service('org.bluez')
46
+
47
+ public
48
+ class Error < StandardError ; end
49
+ class NotYetImplemented < Error ; end
50
+ class StalledObject < Error ; end
51
+ class NotReady < Error ; end
52
+ class NotAuthorized < Error ; end
53
+ class NotConnected < Error ; end
54
+ class NotFound < Error ; end
55
+ class ServiceNotFound < NotFound ; end
56
+ class CharacteristicNotFound < NotFound ; end
57
+ class AccessUnavailable < Error ; end
58
+
59
+
60
+ GATT_BASE_UUID="00000000-0000-1000-8000-00805F9B34FB"
61
+
62
+ #"DisplayOnly", "DisplayYesNo", "KeyboardOnly",
63
+ # "NoInputNoOutput" and "KeyboardDisplay" which
64
+
65
+
66
+ def self.registerAgent(agent, service, path)
67
+ raise NotYetImplemented
68
+ bus = DBus.session_bus
69
+ service = bus.request_service("org.ruby.service")
70
+
71
+ service.export(BLE::Agent.new(agent_path))
72
+
73
+ o_bluez = BLUEZ.object('/org/bluez')
74
+ o_bluez.introspect
75
+ o_bluez[I_AGENT_MANAGER].RegisterAgent(agent_path, "NoInputNoOutput")
76
+ end
77
+
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
+
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
+
772
+ def self.UUID(val)
773
+ val.downcase
774
+ end
775
+
776
+ class UUID
777
+ REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
778
+ end
779
+
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
945
+ def self.ok?
946
+ BLUEZ.exists?
947
+ end
948
+
949
+ end
950
+
951
+ require_relative 'ble/db_service'
952
+ require_relative 'ble/db_characteristic'
953
+
954
+
@@ -0,0 +1,71 @@
1
+ module BLE
2
+ class Characteristic
3
+ # C | Integer | 8-bit unsigned (unsigned char)
4
+ # S | Integer | 16-bit unsigned, native endian (uint16_t)
5
+ # L | Integer | 32-bit unsigned, native endian (uint32_t)
6
+ # Q | Integer | 64-bit unsigned, native endian (uint64_t)
7
+ # | |
8
+ # c | Integer | 8-bit signed (signed char)
9
+ # s | Integer | 16-bit signed, native endian (int16_t)
10
+ # l | Integer | 32-bit signed, native endian (int32_t)
11
+ # q | Integer | 64-bit signed, native endian (int64_t)
12
+ #
13
+ # < | Little endian
14
+ # > | Big endian
15
+
16
+
17
+
18
+ add 0x2A6E,
19
+ name: 'Temperature',
20
+ type: 'org.bluetooth.characteristic.temperature',
21
+ vrfy: ->(x) { (0..100).include?(x) },
22
+ in: ->(s) { puts s.inspect ; s.unpack('s<').first.to_f / 100 },
23
+ out: ->(v) { [ v*100 ].pack('s<') }
24
+
25
+ add 0x2A76,
26
+ name: 'UV Index',
27
+ type: 'org.bluetooth.characteristic.uv_index',
28
+ in: ->(s) { s.unpack('C').first },
29
+ out: ->(v) { [ v ].pack('C') }
30
+
31
+ add 0x2A77,
32
+ name: 'Irradiance',
33
+ type: 'org.bluetooth.characteristic.irradiance',
34
+ in: ->(s) { s.unpack('S<').first.to_f / 10 },
35
+ out: ->(v) { [ v*10 ].pack('S<') }
36
+
37
+ add 0x2A7A,
38
+ name: 'Heat Index',
39
+ type: 'org.bluetooth.characteristic.heat_index',
40
+ in: ->(s) { s.unpack('c').first },
41
+ out: ->(v) { [ v ].pack('c') }
42
+
43
+ add 0x2A19,
44
+ name: 'Battery Level',
45
+ type: 'org.bluetooth.characteristic.battery_level',
46
+ vrfy: ->(x) { (0..100).include?(x) },
47
+ in: ->(s) { s.unpack('C').first },
48
+ out: ->(v) { [ v ].pack('C') }
49
+
50
+ add 0x2A6F,
51
+ name: 'Humidity',
52
+ type: 'org.bluetooth.characteristic.humidity',
53
+ vrfy: ->(x) { (0..100).include?(x) },
54
+ in: ->(s) { s.unpack('S<').first.to_f / 100 },
55
+ out: ->(v) { [ v*100 ].pack('S<') }
56
+
57
+ add 0x2A6D,
58
+ name: 'Pressure',
59
+ type: 'org.bluetooth.characteristic.pressure',
60
+ vrfy: ->(x) { x >= 0 },
61
+ in: ->(s) { s.unpack('L<').first.to_f / 10 },
62
+ out: ->(v) { [ v*10 ].pack('L<') }
63
+
64
+ add 0x2AB3,
65
+ name: 'Altitude',
66
+ type: 'org.bluetooth.characteristic.altitude',
67
+ vrfy: ->(x) { x >= 0 },
68
+ in: ->(s) { s.unpack('S<').first },
69
+ out: ->(v) { [ v ].pack('S<') }
70
+ end
71
+ end
@@ -0,0 +1,60 @@
1
+ module BLE
2
+ class 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
+
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module BLE
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ble
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stephane D'Alu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-dbus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Allow access to BLE device from ruby
56
+ email:
57
+ - stephane.dalu@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - Gemfile
63
+ - LICENSE
64
+ - ble.gemspec
65
+ - lib/ble.rb
66
+ - lib/ble/db_characteristic.rb
67
+ - lib/ble/db_service.rb
68
+ - lib/ble/version.rb
69
+ homepage: http://github.com/sdalu/ruby-ble
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.2.2
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Bluetooth Low Energy API
93
+ test_files: []
94
+ has_rdoc: yard