ble 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
1
+ module BLE
2
+ # Adapter class
3
+ # Adapter.list
4
+ # a = Adapter.new('hci0')
5
+ # a.start_discover ; sleep(10) ; a.stop_discovery
6
+ # a.devices
7
+ #
8
+ class Adapter
9
+ # Return a list of available unix device name for the
10
+ # adapter installed on the system.
11
+ # @return [Array<String>] list of unix device name
12
+ def self.list
13
+ o_bluez = BLUEZ.object('/org/bluez')
14
+ o_bluez.introspect
15
+ o_bluez.subnodes.reject {|adapter| ['test'].include?(adapter) }
16
+ end
17
+
18
+ # Create a new Adapter
19
+ #
20
+ # @param iface [String] name of the Unix device
21
+ def initialize(iface)
22
+ @iface = iface.dup.freeze
23
+ @o_adapter = BLUEZ.object("/org/bluez/#{@iface}")
24
+ @o_adapter.introspect
25
+
26
+ # @o_adapter[I_PROPERTIES]
27
+ # .on_signal('PropertiesChanged') do |intf, props|
28
+ # end
29
+ # end
30
+ end
31
+
32
+ # The Bluetooth interface name
33
+ # @return [String] name of the Unix device
34
+ def iface
35
+ @iface
36
+ end
37
+
38
+ # The Bluetooth device address.
39
+ # @return [String] MAC address of the adapter
40
+ def address
41
+ @o_adapter[I_ADAPTER]['Address']
42
+ end
43
+
44
+ # The Bluetooth system name (pretty hostname).
45
+ # @return [String]
46
+ def name
47
+ @o_adapter[I_ADAPTER]['Name']
48
+ end
49
+
50
+ # The Bluetooth friendly name.
51
+ # In case no alias is set, it will return the system provided name.
52
+ # @return [String]
53
+ def alias
54
+ @o_adapter[I_ADAPTER]['Alias']
55
+ end
56
+
57
+ # Set the alias name.
58
+ #
59
+ # When resetting the alias with an empty string, the
60
+ # property will default back to system name
61
+ #
62
+ # @param val [String] new alias name.
63
+ # @return [void]
64
+ def alias=(val)
65
+ @o_adapter[I_ADAPTER]['Alias'] = val.nil? ? '' : val.to_str
66
+ nil
67
+ end
68
+
69
+ # Return the device corresponding to the given address.
70
+ # @note The device object returned has a dependency on the adapter.
71
+ #
72
+ # @param address MAC address of the device
73
+ # @return [Device] a device
74
+ def [](address)
75
+ Device.new(@iface, address)
76
+ end
77
+
78
+ # This method sets the device discovery filter for the caller.
79
+ # When this method is called with +nil+ or an empty list of UUIDs,
80
+ # filter is removed.
81
+ #
82
+ # @todo Need to sync with the adapter-api.txt
83
+ #
84
+ # @param uuids a list of uuid to filter on
85
+ # @param rssi RSSI threshold
86
+ # @param pathloss pathloss threshold
87
+ # @param transport [:auto, :bredr, :le]
88
+ # type of scan to run (default: :le)
89
+ # @return [self]
90
+ def filter(uuids, rssi: nil, pathloss: nil, transport: :le)
91
+ unless [:auto, :bredr, :le].include?(transport)
92
+ raise ArgumentError,
93
+ "transport must be one of :auto, :bredr, :le"
94
+ end
95
+ filter = { }
96
+
97
+ unless uuids.nil? || uuids.empty?
98
+ filter['UUIDs' ] = DBus.variant('as', uuids)
99
+ end
100
+ unless rssi.nil?
101
+ filter['RSSI' ] = DBus.variant('n', rssi)
102
+ end
103
+ unless pathloss.nil?
104
+ filter['Pathloss' ] = DBus.variant('q', pathloss)
105
+ end
106
+ unless transport.nil?
107
+ filter['Transport'] = DBus.variant('s', transport.to_s)
108
+ end
109
+
110
+ @o_adapter[I_ADAPTER].SetDiscoveryFilter(filter)
111
+
112
+ self
113
+ end
114
+
115
+ # Starts the device discovery session.
116
+ # This includes an inquiry procedure and remote device name resolving.
117
+ # Use {#stop_discovery} to release the sessions acquired.
118
+ # This process will start creating device in the underlying api
119
+ # as new devices are discovered.
120
+ #
121
+ # @return [Boolean]
122
+ def start_discovery
123
+ @o_adapter[I_ADAPTER].StartDiscovery
124
+ true
125
+ rescue DBus::Error => e
126
+ case e.name
127
+ when E_IN_PROGRESS then true
128
+ when E_FAILED then false
129
+ else raise ScriptError
130
+ end
131
+ end
132
+
133
+ # This method will cancel any previous {#start_discovery}
134
+ # transaction.
135
+ # @note The discovery procedure is shared
136
+ # between all discovery sessions thus calling {#stop_discovery}
137
+ # will only release a single session.
138
+ #
139
+ # @return [Boolean]
140
+ def stop_discovery
141
+ @o_adapter[I_ADAPTER].StopDiscovery
142
+ true
143
+ rescue DBus::Error => e
144
+ case e.name
145
+ when E_FAILED then false
146
+ when E_NOT_READY then false
147
+ when E_NOT_AUTHORIZED then raise NotAuthorized
148
+ else raise ScriptError
149
+ end
150
+
151
+ end
152
+
153
+ # List of devices MAC address that have been discovered.
154
+ #
155
+ # @return [Array<String>] List of devices MAC address.
156
+ def devices
157
+ @o_adapter.introspect # Force refresh
158
+ @o_adapter.subnodes.map {|dev| # Format: dev_aa_bb_cc_dd_ee_ff
159
+ dev[4..-1].tr('_', ':') }
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,46 @@
1
+ module BLE
2
+ class Agent < DBus::Object
3
+ @log = Logger.new($stdout)
4
+ # https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/refs/heads/master/doc/agent-api.txt
5
+ dbus_interface I_AGENT do
6
+ dbus_method :Release do
7
+ @log.debug "Release()"
8
+ exit false
9
+ end
10
+
11
+ dbus_method :RequestPinCode, "in device:o, out ret:s" do |device|
12
+ @log.debug{ "RequestPinCode(#{device})" }
13
+ ["0000"]
14
+ end
15
+
16
+ dbus_method :RequestPasskey, "in device:o, out ret:u" do |device|
17
+ @log.debug{ "RequestPasskey(#{device})" }
18
+ raise DBus.error("org.bluez.Error.Rejected")
19
+ end
20
+
21
+ dbus_method :DisplayPasskey, "in device:o, in passkey:u, in entered:y" do |device, passkey, entered|
22
+ @log.debug{ "DisplayPasskey(#{device}, #{passkey}, #{entered})" }
23
+ raise DBus.error("org.bluez.Error.Rejected")
24
+ end
25
+
26
+ dbus_method :RequestConfirmation, "in device:o, in passkey:u" do |device, passkey|
27
+ @log.debug{ "RequestConfirmation(#{device}, #{passkey})" }
28
+ raise DBus.error("org.bluez.Error.Rejected")
29
+ end
30
+
31
+ dbus_method :Authorize, "in device:o, in uuid:s" do |device, uuid|
32
+ @log.debug{ "Authorize(#{device}, #{uuid})" }
33
+ end
34
+
35
+ dbus_method :ConfirmModeChange, "in mode:s" do |mode|
36
+ @log.debug{ "ConfirmModeChange(#{mode})" }
37
+ raise DBus.error("org.bluez.Error.Rejected")
38
+ end
39
+
40
+ dbus_method :Cancel do
41
+ @log.debug "Cancel()"
42
+ raise DBus.error("org.bluez.Error.Rejected")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,160 @@
1
+ module BLE
2
+ # Build information about {https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx Bluetooth Characteristics}
3
+ #
4
+ # To add a new characteristic description:
5
+ # BLE::Characteristic.add 'c4935338-4307-47cf-ae1f-feac9e2b3ae7',
6
+ # name: 'Controlled mind',
7
+ # type: 'net.cortex-minus.characteristic.controlled_mind'
8
+ # vrfy: ->(x) { x >= 0 },
9
+ # in: ->(s) { s.unpack('s<').first },
10
+ # out: ->(v) { [ v ].pack('s<') }
11
+ #
12
+ # Returned characteristic description will be a hash:
13
+ # {
14
+ # name: "Bluetooth characteristic name",
15
+ # type: "org.bluetooth.characteristic.name",
16
+ # uuid: "128bit-uuid-string",
17
+ # vrfy: ->(x) { verify_value(x) },
18
+ # in: ->(s) { s.unpack(....) ... },
19
+ # out: ->(v) { [ v ].pack(....) ... }
20
+ # }
21
+ #
22
+ module Characteristic
23
+ # Notify of characteristic not found
24
+ class NotFound < BLE::NotFound
25
+ end
26
+
27
+ private
28
+ FLAGS = [ 'broadcast',
29
+ 'read',
30
+ 'write-without-response',
31
+ 'write',
32
+ 'notify',
33
+ 'indicate',
34
+ 'authenticated-signed-writes',
35
+ 'reliable-write',
36
+ 'writable-auxiliaries',
37
+ 'encrypt-read',
38
+ 'encrypt-write',
39
+ 'encrypt-authenticated-read',
40
+ 'encrypt-authenticated-write' ]
41
+
42
+ DB_UUID = {}
43
+ DB_TYPE = {}
44
+ DB_NICKNAME = {}
45
+
46
+ public
47
+ # Get characteristic description from nickname.
48
+ #
49
+ # @param id [Symbol] nickname
50
+ # @return [Hash] characteristic description
51
+ def self.by_nickname(id)
52
+ DB_NICKNAME[id]
53
+ end
54
+
55
+ # Get characteristic description from uuid.
56
+ #
57
+ # @param id [String] uuid
58
+ # @return [Hash] characteristic description
59
+ def self.by_uuid(id)
60
+ DB_UUID[id]
61
+ end
62
+
63
+ # Get characteristic description from type
64
+ #
65
+ # @param id [Strig] type
66
+ # @return [Hash] characteristic description
67
+ def self.by_type(id)
68
+ DB_TYPE[id]
69
+ end
70
+
71
+ # Get a characteristic description from it's id
72
+ # @param id [Symbol,String]
73
+ # @return [Hash]
74
+ def self.[](id)
75
+ case id
76
+ when Symbol then DB_NICKNAME[id]
77
+ when UUID::REGEX then DB_UUID[id]
78
+ when String then DB_TYPE[id]
79
+ else raise ArgumentError, "invalid type for characteristic id"
80
+ end
81
+ end
82
+
83
+
84
+ # Add a characteristic description.
85
+ # @example Add a characteristic description with a 16-bit uuid
86
+ # module Characteristic
87
+ # add 0x2A6E,
88
+ # name: 'Temperature',
89
+ # type: 'org.bluetooth.characteristic.temperature',
90
+ # vrfy: ->(x) { (0..100).include?(x) },
91
+ # in: ->(s) { s.unpack('s<').first.to_f / 100 },
92
+ # out: ->(v) { [ v*100 ].pack('s<') }
93
+ # end
94
+ #
95
+ # @example Add a characteristic description with a 128-bit uuid
96
+ # module Characteristic
97
+ # add 'c4935338-4307-47cf-ae1f-feac9e2b3ae7',
98
+ # name: 'Controlled mind',
99
+ # type: 'net.cortex-minus.characteristic.controlled_mind',
100
+ # vrfy: ->(x) { x >= 0 },
101
+ # in: ->(s) { s.unpack('s<').first },
102
+ # out: ->(v) { [ v ].pack('s<') }
103
+ # end
104
+ #
105
+ # @param uuid [Integer, String] 16-bit, 32-bit or 128-bit uuid
106
+ # @param name [String]
107
+ # @param type [String]
108
+ # @option opts :in [Proc] convert to ruby
109
+ # @option opts :out [Proc] convert to bluetooth data
110
+ # @option opts :vry [Proc] verify
111
+ # @return [Hash] characteristic description
112
+ def self.add(uuid, name:, type:, **opts)
113
+ _in = opts.delete :in
114
+ _out = opts.delete :out
115
+ vrfy = opts.delete :vrfy
116
+ if opts.first
117
+ raise ArgumentError, "unknown keyword: #{opts.first[0]}"
118
+ end
119
+
120
+ uuid = case uuid
121
+ when Integer
122
+ if !(0..4294967296).include?(uuid)
123
+ raise ArgumentError, "not a 16bit or 32bit uuid"
124
+ end
125
+ ([uuid].pack("L>").unpack('H*').first +
126
+ "-0000-1000-8000-00805F9B34FB")
127
+
128
+ when String
129
+ if uuid !~ UUID::REGEX
130
+ raise ArgumentError, "not a 128bit uuid string"
131
+ end
132
+ uuid
133
+ else raise ArgumentError, "invalid uuid type"
134
+ end
135
+ uuid = uuid.downcase
136
+ type = type.downcase
137
+
138
+ DB_TYPE[type] = DB_UUID[uuid] = {
139
+ name: name,
140
+ type: type,
141
+ uuid: uuid,
142
+ in: _in,
143
+ out: _out,
144
+ vrfy: vrfy
145
+ }
146
+
147
+ stype = type.split('.')
148
+ key = stype.pop.to_sym
149
+ prefix = stype.join('.')
150
+ case prefix
151
+ when 'org.bluetooth.characteristic'
152
+ if DB_NICKNAME.include?(key)
153
+ raise ArgumentError,
154
+ "nickname '#{key}' already registered (type: #{type})"
155
+ end
156
+ DB_NICKNAME[key] = DB_UUID[uuid]
157
+ end
158
+ end
159
+ end
160
+ end
@@ -1,5 +1,5 @@
1
1
  module BLE
2
- class Characteristic
2
+ module Characteristic
3
3
  # C | Integer | 8-bit unsigned (unsigned char)
4
4
  # S | Integer | 16-bit unsigned, native endian (uint16_t)
5
5
  # L | Integer | 32-bit unsigned, native endian (uint32_t)
@@ -14,12 +14,11 @@ class Characteristic
14
14
  # > | Big endian
15
15
 
16
16
 
17
-
18
17
  add 0x2A6E,
19
18
  name: 'Temperature',
20
19
  type: 'org.bluetooth.characteristic.temperature',
21
20
  vrfy: ->(x) { (0..100).include?(x) },
22
- in: ->(s) { puts s.inspect ; s.unpack('s<').first.to_f / 100 },
21
+ in: ->(s) { s.unpack('s<').first.to_f / 100 },
23
22
  out: ->(v) { [ v*100 ].pack('s<') }
24
23
 
25
24
  add 0x2A76,
@@ -1,5 +1,5 @@
1
1
  module BLE
2
- class Service
2
+ module Service
3
3
  add 0x1800,
4
4
  name: 'Generic Access',
5
5
  type: 'org.bluetooth.service.generic_access'
@@ -0,0 +1,484 @@
1
+ module BLE
2
+ # Create de Device object
3
+ # d = Device::new('hci0', 'aa:bb:dd:dd:ee:ff')
4
+ # d = Adapter.new('hci0')['aa:bb:dd:dd:ee:ff']
5
+ #
6
+ # d.services
7
+ # d.characteristics(:environmental_sensing)
8
+ # d[:environmental_sensing, :temperature]
9
+ #
10
+ class Device
11
+ # Notify that you need to have the device in a connected state
12
+ class NotConnected < Error ; end
13
+
14
+ # @param adapter [String] adapter unix device name
15
+ # @param dev [String] device MAC address
16
+ # @param auto_refresh [Boolean] gather information about device
17
+ # on connection
18
+ def initialize(adapter, dev, auto_refresh: true)
19
+ @adapter, @dev = adapter, dev
20
+ @auto_refresh = auto_refresh
21
+ @services = {}
22
+
23
+ @n_adapter = adapter
24
+ @p_adapter = "/org/bluez/#{@n_adapter}"
25
+ @o_adapter = BLUEZ.object(@p_adapter)
26
+ @o_adapter.introspect
27
+
28
+ @n_dev = 'dev_' + dev.tr(':', '_')
29
+ @p_dev = "/org/bluez/#{@n_adapter}/#{@n_dev}"
30
+ @o_dev = BLUEZ.object(@p_dev)
31
+ @o_dev.introspect
32
+
33
+ self.refresh if @auto_refresh
34
+
35
+ @o_dev[I_PROPERTIES]
36
+ .on_signal('PropertiesChanged') do |intf, props|
37
+ case intf
38
+ when I_DEVICE
39
+ case props['Connected']
40
+ when true
41
+ self.refresh if @auto_refresh
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # This removes the remote device object.
48
+ # It will remove also the pairing information.
49
+ # @return [Boolean]
50
+ def remove
51
+ @o_adapter[I_ADAPTER].RemoveDevice(@p_dev)
52
+ true
53
+ rescue DBus::Error => e
54
+ case e.name
55
+ when E_FAILED then false
56
+ when E_DOES_NOT_EXIST then raise StalledObject
57
+ when E_UNKNOWN_OBJECT then raise StalledObject
58
+ else raise ScriptError
59
+ end
60
+ end
61
+
62
+
63
+ # This method will connect to the remote device,
64
+ # initiate pairing and then retrieve all SDP records
65
+ # (or GATT primary services).
66
+ # If the application has registered its own agent,
67
+ # then that specific agent will be used. Otherwise
68
+ # it will use the default agent.
69
+ # Only for applications like a pairing wizard it
70
+ # would make sense to have its own agent. In almost
71
+ # all other cases the default agent will handle this just fine.
72
+ # In case there is no application agent and also
73
+ # no default agent present, this method will fail.
74
+ # @return [Boolean]
75
+ def pair
76
+ @o_dev[I_DEVICE].Pair
77
+ true
78
+ rescue DBus::Error => e
79
+ case e.name
80
+ when E_INVALID_ARGUMENTS then false
81
+ when E_FAILED then false
82
+ when E_ALREADY_EXISTS then true
83
+ when E_AUTH_CANCELED then raise NotAutorized
84
+ when E_AUTH_FAILED then raise NotAutorized
85
+ when E_AUTH_REJECTED then raise NotAutorized
86
+ when E_AUTH_TIMEOUT then raise NotAutorized
87
+ when E_AUTH_ATTEMPT_FAILED then raise NotAutorized
88
+ else raise ScriptError
89
+ end
90
+ end
91
+
92
+ # This method can be used to cancel a pairing
93
+ # operation initiated by the Pair method.
94
+ # @return [Boolean]
95
+ def cancel_pairing
96
+ @o_dev[I_DEVICE].CancelPairing
97
+ true
98
+ rescue DBus::Error => e
99
+ case e.name
100
+ when E_DOES_NOT_EXIST then true
101
+ when E_FAILED then false
102
+ else raise ScriptError
103
+ end
104
+ end
105
+
106
+ # This connect to the specified profile UUID or to any (:all)
107
+ # profiles the remote device supports that can be connected to
108
+ # and have been flagged as auto-connectable on our side. If
109
+ # only subset of profiles is already connected it will try to
110
+ # connect currently disconnected ones. If at least one
111
+ # profile was connected successfully this method will indicate
112
+ # success.
113
+ # @return [Boolean]
114
+ def connect(profile=:all)
115
+ case profile
116
+ when UUID::REGEX
117
+ @o_dev[I_DEVICE].ConnectProfile(profile)
118
+ when :all
119
+ @o_dev[I_DEVICE].Connect()
120
+ else raise ArgumentError, "profile uuid or :all expected"
121
+ end
122
+ true
123
+ rescue DBus::Error => e
124
+ case e.name
125
+ when E_NOT_READY
126
+ when E_FAILED
127
+ when E_IN_PROGRESS
128
+ false
129
+ when E_ALREADY_CONNECTED
130
+ true
131
+ when E_UNKNOWN_OBJECT
132
+ raise StalledObject
133
+ else raise ScriptError
134
+ end
135
+ end
136
+
137
+ # This method gracefully disconnects :all connected profiles
138
+ # and then terminates low-level ACL connection.
139
+ # ACL connection will be terminated even if some profiles
140
+ # were not disconnected properly e.g. due to misbehaving device.
141
+ # This method can be also used to cancel a preceding #connect
142
+ # call before a reply to it has been received.
143
+ # If a profile UUID is specified, only this profile is disconnected,
144
+ # and as their is no connection tracking for a profile, so
145
+ # as long as the profile is registered this will always succeed
146
+ # @return [Boolean]
147
+ def disconnect(profile=:all)
148
+ case profile
149
+ when UUID::REGEX
150
+ @o_dev[I_DEVICE].DisconnectProfile(profile)
151
+ when :all
152
+ @o_dev[I_DEVICE].Disconnect()
153
+ else raise ArgumentError, "profile uuid or :all expected"
154
+ end
155
+ true
156
+ rescue DBus::Error => e
157
+ case e.name
158
+ when E_FAILED
159
+ when E_IN_PROGRESS
160
+ false
161
+ when E_INVALID_ARGUMENTS
162
+ raise ArgumentError, "unsupported profile (#{profile})"
163
+ when E_NOT_SUPPORTED
164
+ raise NotSuppported
165
+ when E_NOT_CONNECTED
166
+ true
167
+ when E_UNKNOWN_OBJECT
168
+ raise StalledObject
169
+ else raise ScriptError
170
+ end
171
+ end
172
+
173
+ # Indicates if the remote device is paired
174
+ def is_paired?
175
+ @o_dev[I_DEVICE]['Paired']
176
+ rescue DBus::Error => e
177
+ case e.name
178
+ when E_UNKNOWN_OBJECT
179
+ raise StalledObject
180
+ else raise ScriptError
181
+ end
182
+ end
183
+
184
+ # Indicates if the remote device is currently connected.
185
+ def is_connected?
186
+ @o_dev[I_DEVICE]['Connected']
187
+ rescue DBus::Error => e
188
+ case e.name
189
+ when E_UNKNOWN_OBJECT
190
+ raise StalledObject
191
+ else raise ScriptError
192
+ end
193
+ end
194
+
195
+ # List of available services as UUID.
196
+ #
197
+ # @raise [NotConnected] if device is not in a connected state
198
+ # @note The list is retrieve once when object is
199
+ # connected if auto_refresh is enable, otherwise
200
+ # you need to call {#refresh}.
201
+ # @note This is the list of UUIDs for which we have an entry
202
+ # in the underlying api (bluez-dbus), which can be less
203
+ # that the list of advertised UUIDs.
204
+ # @return [Array<String>] List of service UUID
205
+ def services
206
+ raise NotConnected unless is_connected?
207
+ @services.keys
208
+ end
209
+
210
+ # Check if service is available on the device
211
+ # @return [Boolean]
212
+ def has_service?(service)
213
+ @service.key?(_uuid_service(service))
214
+ end
215
+
216
+ # List of available characteristics UUID for a service.
217
+ #
218
+ # @param service service can be a UUID, a service type or
219
+ # a service nickname
220
+ # @return [Array<String>, nil] list of characteristics or +nil+ if the
221
+ # service doesn't exist
222
+ # @raise [NotConnected] if device is not in a connected state
223
+ # @note The list is retrieve once when object is
224
+ # connected if auto_refresh is enable, otherwise
225
+ # you need to call {#refresh}.
226
+ def characteristics(service)
227
+ raise NotConnected unless is_connected?
228
+ if chars = _characteristics(service)
229
+ chars.keys
230
+ end
231
+ end
232
+
233
+ # The Bluetooth device address of the remote device.
234
+ # @return [String] MAC address
235
+ def address
236
+ @o_dev[I_DEVICE]['Address']
237
+ end
238
+
239
+ # The Bluetooth remote name.
240
+ # It is better to always use the {#alias} when displaying the
241
+ # devices name.
242
+ # @return [String] name
243
+ def name # optional
244
+ @o_dev[I_DEVICE]['Name']
245
+ end
246
+
247
+ # The name alias for the remote device.
248
+ # The alias can be used to have a different friendly name for the
249
+ # remote device.
250
+ # In case no alias is set, it will return the remote device name.
251
+ # @return [String]
252
+ def alias
253
+ @o_dev[I_DEVICE]['Alias']
254
+ end
255
+ # Setting an empty string or nil as alias will convert it
256
+ # back to the remote device name.
257
+ # @param val [String, nil]
258
+ # @return [void]
259
+ def alias=(val)
260
+ @o_dev[I_DEVICE]['Alias'] = val.nil? ? "" : val.to_str
261
+ end
262
+
263
+ # Is the device trusted?
264
+ # @return [Boolean]
265
+ def is_trusted?
266
+ @o_dev[I_DEVICE]['Trusted']
267
+ end
268
+
269
+ # Indicates if the remote is seen as trusted. This
270
+ # setting can be changed by the application.
271
+ # @param val [Boolean]
272
+ # @return [void]
273
+ def trusted=(val)
274
+ if ! [ true, false ].include?(val)
275
+ raise ArgumentError, "value must be a boolean"
276
+ end
277
+ @o_dev[I_DEVICE]['Trusted'] = val
278
+ end
279
+
280
+ # Is the device blocked?
281
+ # @return [Boolean]
282
+ def is_blocked?
283
+ @o_dev[I_DEVICE]['Blocked']
284
+ end
285
+
286
+ # If set to true any incoming connections from the
287
+ # device will be immediately rejected. Any device
288
+ # drivers will also be removed and no new ones will
289
+ # be probed as long as the device is blocked
290
+ # @param val [Boolean]
291
+ # @return [void]
292
+ def blocked=(val)
293
+ if ! [ true, false ].include?(val)
294
+ raise ArgumentError, "value must be a boolean"
295
+ end
296
+ @o_dev[I_DEVICE]['Blocked'] = val
297
+ end
298
+
299
+ # Received Signal Strength Indicator of the remote
300
+ # device (inquiry or advertising).
301
+ # @return [Integer]
302
+ def rssi # optional
303
+ @o_dev[I_DEVICE]['RSSI']
304
+ rescue DBus::Error => e
305
+ case e.name
306
+ when E_INVALID_ARGS then raise NotSupported
307
+ else raise ScriptError
308
+ end
309
+ end
310
+
311
+ # Advertised transmitted power level (inquiry or advertising).
312
+ # @return [Integer]
313
+ def tx_power # optional
314
+ @o_dev[I_DEVICE]['TxPower']
315
+ rescue DBus::Error => e
316
+ case e.name
317
+ when E_INVALID_ARGS then raise NotSupported
318
+ else raise ScriptError
319
+ end
320
+ end
321
+
322
+
323
+ # Refresh list of services and characteristics
324
+ # @return [Boolean]
325
+ def refresh
326
+ refresh!
327
+ true
328
+ rescue NotConnected, StalledObject
329
+ false
330
+ end
331
+
332
+ # Refresh list of services and characteristics
333
+ # @raise [NotConnected] if device is not in a connected state
334
+ # @return [self]
335
+ def refresh!
336
+ raise NotConnected unless is_connected?
337
+ max_wait ||= 1.5 # Use ||= due to the retry
338
+ @services = Hash[@o_dev[I_DEVICE]['GattServices'].map {|p_srv|
339
+ o_srv = BLUEZ.object(p_srv)
340
+ o_srv.introspect
341
+ srv = o_srv[I_PROPERTIES].GetAll(I_GATT_SERVICE).first
342
+ char = Hash[srv['Characteristics'].map {|p_char|
343
+ o_char = BLUEZ.object(p_char)
344
+ o_char.introspect
345
+ uuid = o_char[I_GATT_CHARACTERISTIC]['UUID' ].downcase
346
+ flags = o_char[I_GATT_CHARACTERISTIC]['Flags']
347
+ [ uuid, { :uuid => uuid, :flags => flags, :obj => o_char } ]
348
+ }]
349
+ uuid = srv['UUID'].downcase
350
+ [ uuid, { :uuid => uuid,
351
+ :primary => srv['Primary'],
352
+ :characteristics => char } ]
353
+ }]
354
+ self
355
+ rescue DBus::Error => e
356
+ case e.name
357
+ when E_UNKNOWN_OBJECT
358
+ raise StalledObject
359
+ when E_INVALID_ARGS
360
+ # That's probably because all the bluez information
361
+ # haven't been collected yet on dbus for GattServices
362
+ if max_wait > 0
363
+ sleep(0.25) ; max_wait -= 0.25 ; retry
364
+ end
365
+ raise NotReady
366
+
367
+ else raise ScriptError
368
+ end
369
+ end
370
+
371
+ # Get value for a service/characteristic.
372
+ #
373
+ # @param service [String, Symbol]
374
+ # @param characteristic [String, Symbol]
375
+ # @param raw [Boolean]
376
+ # @raise [NotConnected] if device is not in a connected state
377
+ # @raise [NotYetImplemented] encryption is not implemented yet
378
+ # @raise [Service::NotFound, Characteristic::NotFound] if service/characteristic doesn't exist on this device
379
+ # @raise [AccessUnavailable] if not available for reading
380
+ # @return [Object]
381
+ def [](service, characteristic, raw: false)
382
+ raise NotConnected unless is_connected?
383
+ uuid = _uuid_characteristic(characteristic)
384
+ chars = _characteristics(service)
385
+ raise Service::NotFound, service if chars.nil?
386
+ char = chars[uuid]
387
+ raise Characteristic::NotFound, characteristic if char.nil?
388
+ flags = char[:flags]
389
+ obj = char[:obj]
390
+ info = Characteristic[uuid]
391
+
392
+ if flags.include?('read')
393
+ val = obj[I_GATT_CHARACTERISTIC].ReadValue().first
394
+ val = val.pack('C*')
395
+ val = info[:in].call(val) if !raw && info && info[:in]
396
+ val
397
+ elsif flags.include?('encrypt-read') ||
398
+ flags.include?('encrypt-authenticated-read')
399
+ raise NotYetImplemented
400
+ else
401
+ raise AccessUnavailable
402
+ end
403
+ end
404
+
405
+ # Set value for a service/characteristic
406
+ #
407
+ # @param service [String, Symbol]
408
+ # @param characteristic [String, Symbol]
409
+ # @param val [Boolean]
410
+ # @raise [NotConnected] if device is not in a connected state
411
+ # @raise [NotYetImplemented] encryption is not implemented yet
412
+ # @raise [Service::NotFound, Characteristic::NotFound] if service/characteristic doesn't exist on this device
413
+ # @raise [AccessUnavailable] if not available for writing
414
+ # @return [void]
415
+ def []=(service, characteristic, val, raw: false)
416
+ raise NotConnected unless is_connected?
417
+ uuid = _uuid_characteristic(characteristic)
418
+ chars = _characteristics(service)
419
+ raise ServiceNotFound, service if chars.nil?
420
+ char = chars[uuid]
421
+ raise CharacteristicNotFound, characteristic if char.nil?
422
+ flags = char[:flags]
423
+ obj = char[:obj]
424
+ info = Characteristic[uuid]
425
+
426
+ if flags.include?('write') ||
427
+ flags.include?('write-without-response')
428
+ if !raw && info
429
+ if info[:vrfy] && !info[:vrfy].call(vall)
430
+ raise ArgumentError,
431
+ "bad value for characteristic '#{characteristic}'"
432
+ end
433
+ val = info[:out].call(val) if info[:out]
434
+ end
435
+ val = val.unpack('C*')
436
+ obj[I_GATT_CHARACTERISTIC].WriteValue(val)
437
+ elsif flags.include?('encrypt-write') ||
438
+ flags.include?('encrypt-authenticated-write')
439
+ raise NotYetImplemented
440
+ else
441
+ raise AccessUnavailable
442
+ end
443
+ nil
444
+ end
445
+
446
+ private
447
+
448
+ def _characteristics(service)
449
+ if srv = @services[_uuid_service(service)]
450
+ srv[:characteristics]
451
+ end
452
+ end
453
+ def _uuid_service(service)
454
+ uuid = case service
455
+ when UUID::REGEX
456
+ service.downcase
457
+ else
458
+ if i = Service[service]
459
+ i[:uuid]
460
+ end
461
+ end
462
+ if uuid.nil?
463
+ raise ArgumentError, "unable to get UUID for service"
464
+ end
465
+
466
+ uuid
467
+ end
468
+ def _uuid_characteristic(characteristic)
469
+ uuid = case characteristic
470
+ when UUID::REGEX
471
+ characteristic.downcase
472
+ else
473
+ if i = Characteristic[characteristic]
474
+ i[:uuid]
475
+ end
476
+ end
477
+ if uuid.nil?
478
+ raise ArgumentError, "unable to get UUID for service"
479
+ end
480
+
481
+ uuid
482
+ end
483
+ end
484
+ end