ble 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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