radbeacon 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +1 -0
- data/lib/radbeacon.rb +9 -0
- data/lib/radbeacon/bluetooth_le_device.rb +135 -0
- data/lib/radbeacon/le_scanner.rb +65 -0
- data/lib/radbeacon/scanner.rb +28 -0
- data/lib/radbeacon/usb.rb +196 -0
- data/lib/radbeacon/utils.rb +41 -0
- data/lib/radbeacon/version.rb +3 -0
- data/radbeacon.gemspec +25 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 25442163473a2a7ce504430cce4a4ecb803abbd2
|
4
|
+
data.tar.gz: 423b8b43f661d3927f48cab858bea9c6cf86272c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9b4504f24ec9ae91fff3e6de568d9603ce377e3b25c26e5bd92f4aa45789c182d6149be3a69b84688e2515db76285b91183b27e15612a77d970ed66c66a5fbfb
|
7
|
+
data.tar.gz: e3be66fd90ef611358c15e6fd879d4b8caaa43e50887173e7bd18b5afb4585166ad161b2575b2123136ff5b9b72c900720b5998ce682b558146059d56f4e7282
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
radbeacon (0.1.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.2.5)
|
10
|
+
rake (10.1.0)
|
11
|
+
rspec (3.2.0)
|
12
|
+
rspec-core (~> 3.2.0)
|
13
|
+
rspec-expectations (~> 3.2.0)
|
14
|
+
rspec-mocks (~> 3.2.0)
|
15
|
+
rspec-core (3.2.2)
|
16
|
+
rspec-support (~> 3.2.0)
|
17
|
+
rspec-expectations (3.2.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.2.0)
|
20
|
+
rspec-mocks (3.2.1)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.2.0)
|
23
|
+
rspec-support (3.2.2)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.9)
|
30
|
+
radbeacon!
|
31
|
+
rake (~> 10.0)
|
32
|
+
rspec (~> 3.2)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 TODO: Write your name
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
<img src="http://i.imgur.com/gI6paLj.jpg" style="float:left" height="200">
|
2
|
+
|
3
|
+
# What's all this then?
|
4
|
+
|
5
|
+
A ruby gem that provides RadBeacon scanning and configuring capabilities on a linux machine. Currently the only supported beacon type is RadBeacon USB.
|
6
|
+
|
7
|
+
# Scanning
|
8
|
+
|
9
|
+
The `Radbeacon::Scanner` class has a `scan` method that returns an array of configurable RadBeacons (in the form of `Radbeacon::Usb` objects). The duration of a scan (default = 5 seconds) is an attribute that can be set during initialization.
|
10
|
+
|
11
|
+
```
|
12
|
+
scanner = Radbeacon::Scanner.new(10)
|
13
|
+
radbeacons = scanner.scan
|
14
|
+
```
|
15
|
+
|
16
|
+
There is also a `fetch` method that returns a `Radbeacon::Usb` object for a given MAC address.
|
17
|
+
|
18
|
+
```
|
19
|
+
radbeacon = scanner.fetch("11:22:33:44:55:66")
|
20
|
+
```
|
21
|
+
|
22
|
+
#### Scan Options
|
23
|
+
|
24
|
+
An (optional) `options` hash is available as an attribute on the `Scanner` class. Below are the following options that can be used:
|
25
|
+
|
26
|
+
- `:filter_mac` - Specify an array of MAC addresses that the scanner will filter for when doing a scan
|
27
|
+
|
28
|
+
```
|
29
|
+
options[:filter_mac] = ["11:22:33:44:55:66", "55:66:77:88:99:00"]
|
30
|
+
```
|
31
|
+
|
32
|
+
- `:enable_hcitool_duration` - Use the custom hcitool-duration binary that adds a duration option to the `hcitool lescan` command. To use this you must copy the new hcitool binary over the standard one (located at `/usr/bin/hcitool` on the ZBOX for example).
|
33
|
+
|
34
|
+
```
|
35
|
+
options[:enable_hcitool_duration] = true
|
36
|
+
```
|
37
|
+
|
38
|
+
#Configuring
|
39
|
+
|
40
|
+
All identifiers and other parameters are attributes on the `Radbeacon::Usb` class:
|
41
|
+
|
42
|
+
- `dev_name`
|
43
|
+
- `uuid`
|
44
|
+
- `major`
|
45
|
+
- `minor`
|
46
|
+
- `power`
|
47
|
+
- `tx_power`
|
48
|
+
- `adv_rate`
|
49
|
+
- `beacon_type`
|
50
|
+
|
51
|
+
To make a config change, simply assign one of these attributes to the desired value and call the `save()` method with the beacon's PIN (as a string).
|
52
|
+
|
53
|
+
```
|
54
|
+
radbeacon.save(pin)
|
55
|
+
```
|
56
|
+
|
57
|
+
For example:
|
58
|
+
|
59
|
+
```
|
60
|
+
radbeacon.dev_name = "Test Beacon"
|
61
|
+
radbeacon.uuid = "2F234454-CF6D-4A0F-ADF2-F4911BA9ABCD"
|
62
|
+
radbeacon.major = 1
|
63
|
+
radbeacon.minor = 1
|
64
|
+
radbeacon.power = -66
|
65
|
+
radbeacon.tx_power = 3
|
66
|
+
radbeacon.adv_rate = 10
|
67
|
+
radbeacon.beacon_type = "dual"
|
68
|
+
radbeacon.save('0000')
|
69
|
+
```
|
70
|
+
|
71
|
+
# Other Actions
|
72
|
+
|
73
|
+
All other RadBeacon actions are available as well
|
74
|
+
|
75
|
+
##### Change PIN
|
76
|
+
```
|
77
|
+
radbeacon.change_pin(new_pin, old_pin)
|
78
|
+
```
|
79
|
+
|
80
|
+
##### Factory Reset
|
81
|
+
```
|
82
|
+
radbeacon.factory_reset(pin)
|
83
|
+
```
|
84
|
+
|
85
|
+
##### Boot to DFU
|
86
|
+
```
|
87
|
+
radbeacon.boot_to_dfu(pin)
|
88
|
+
```
|
89
|
+
|
90
|
+
##### Lock
|
91
|
+
```
|
92
|
+
radbeacon.lock(pin)
|
93
|
+
```
|
94
|
+
|
95
|
+
# Dependencies
|
96
|
+
|
97
|
+
BlueZ (Linux Bluetooth stack) is required to scan for and communicate with RadBeacons via Bluetooth. Specifically, the `hcitool` and `gatttool` commands.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/radbeacon.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require 'expect'
|
3
|
+
|
4
|
+
module Radbeacon
|
5
|
+
class BluetoothLeDevice
|
6
|
+
attr_reader :mac_address, :name, :is_connectable, :characteristics, :values, :errors
|
7
|
+
|
8
|
+
TIMEOUT = 0.5
|
9
|
+
|
10
|
+
def initialize(mac_address, name)
|
11
|
+
@errors = []
|
12
|
+
@mac_address = mac_address
|
13
|
+
@name = name
|
14
|
+
@is_connectable = false
|
15
|
+
@characteristics = Array.new
|
16
|
+
@values = Hash.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def display
|
20
|
+
puts "MAC Address: " + @mac_address + " Name: " + @name + " Can connect: " + @is_connectable.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_characteristics
|
24
|
+
result = false
|
25
|
+
if self.can_connect?
|
26
|
+
@is_connectable = true
|
27
|
+
if self.discover_characteristics
|
28
|
+
if self.char_values
|
29
|
+
result = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def can_connect?
|
37
|
+
@errors = []
|
38
|
+
result = false
|
39
|
+
cmd = "gatttool -b #{@mac_address} --interactive"
|
40
|
+
PTY.spawn(cmd) do |output, input, pid|
|
41
|
+
output.expect(/\[LE\]>/)
|
42
|
+
input.puts "connect"
|
43
|
+
if output.expect(/Connection successful/, TIMEOUT)
|
44
|
+
@is_connectable = true
|
45
|
+
result = true
|
46
|
+
else
|
47
|
+
@errors << "Connection failed"
|
48
|
+
end
|
49
|
+
input.puts "quit"
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def characteristics_command
|
55
|
+
success = true
|
56
|
+
output = nil
|
57
|
+
rout, wout = IO.pipe
|
58
|
+
rerr, werr = IO.pipe
|
59
|
+
characteristics_command_str = "gatttool -b #{@mac_address} --characteristics"
|
60
|
+
pid = Process.spawn(characteristics_command_str, :out => wout, :err => werr)
|
61
|
+
begin
|
62
|
+
Timeout.timeout(5) do
|
63
|
+
Process.wait(pid)
|
64
|
+
end
|
65
|
+
rescue Timeout::Error
|
66
|
+
Process.kill('TERM', pid)
|
67
|
+
success = false
|
68
|
+
end
|
69
|
+
wout.close
|
70
|
+
werr.close
|
71
|
+
stdout = rout.readlines.join("")
|
72
|
+
stderr = rerr.readlines.join("")
|
73
|
+
rout.close
|
74
|
+
rerr.close
|
75
|
+
if success
|
76
|
+
output = [stdout, stderr].join("")
|
77
|
+
end
|
78
|
+
output
|
79
|
+
end
|
80
|
+
|
81
|
+
def discover_characteristics
|
82
|
+
@errors = []
|
83
|
+
result = false
|
84
|
+
output = characteristics_command
|
85
|
+
if output && output.strip != "Discover all characteristics failed: Internal application error: I/O"
|
86
|
+
@characteristics = []
|
87
|
+
output.each_line do |line|
|
88
|
+
result = line.scan(/^handle = (0x[a-f0-9]{4}), char properties = (0x[a-f0-9]{2}), char value handle = (0x[a-f0-9]{4}), uuid = ([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/)
|
89
|
+
if !result.empty?
|
90
|
+
characteristic = {"handle" => result[0][0], "properties" => result[0][1], "value_handle" => result[0][2], "uuid" => result[0][3]}
|
91
|
+
@characteristics << characteristic
|
92
|
+
end
|
93
|
+
end
|
94
|
+
result = true
|
95
|
+
else
|
96
|
+
@errors << "Discover characteristics failed"
|
97
|
+
end
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def char_values
|
102
|
+
@errors = []
|
103
|
+
result = false
|
104
|
+
cmd = "gatttool -b #{@mac_address} --interactive"
|
105
|
+
if @characteristics != []
|
106
|
+
PTY.spawn(cmd) do |output, input, pid|
|
107
|
+
output.expect(/\[LE\]>/)
|
108
|
+
input.puts "connect"
|
109
|
+
if output.expect(/Connection successful/, TIMEOUT)
|
110
|
+
@characteristics.each do |char|
|
111
|
+
input.puts "char-read-hnd #{char['value_handle']}"
|
112
|
+
if output.expect(/Characteristic value\/descriptor: /, TIMEOUT)
|
113
|
+
if value = output.expect(/^[0-9a-f\s]+\n/, TIMEOUT)
|
114
|
+
@values[char['value_handle']] = value.first.strip
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
result = true
|
119
|
+
else
|
120
|
+
@errors << "Fetch characteristic values failed"
|
121
|
+
end
|
122
|
+
input.puts "quit"
|
123
|
+
end
|
124
|
+
else
|
125
|
+
@errors << "No characteristics present"
|
126
|
+
end
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
attr_writer :mac_address, :name, :is_connectable, :characteristics, :values, :errors
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Radbeacon
|
2
|
+
|
3
|
+
class LeScanner
|
4
|
+
attr_accessor :duration, :options
|
5
|
+
|
6
|
+
def initialize(duration = 5)
|
7
|
+
@duration = duration
|
8
|
+
@options = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def scan_command
|
12
|
+
rout, wout = IO.pipe
|
13
|
+
scan_command_str = "sudo hcitool lescan"
|
14
|
+
pid = Process.spawn(scan_command_str, :out => wout)
|
15
|
+
begin
|
16
|
+
Timeout.timeout(@duration) do
|
17
|
+
Process.wait(pid)
|
18
|
+
end
|
19
|
+
rescue Timeout::Error
|
20
|
+
Process.kill('TERM', pid)
|
21
|
+
end
|
22
|
+
wout.close
|
23
|
+
scan_output = rout.readlines.join("")
|
24
|
+
rout.close
|
25
|
+
scan_output
|
26
|
+
end
|
27
|
+
|
28
|
+
def scan_command_duration
|
29
|
+
`sudo hcitool lescan --duration #{@duration}`
|
30
|
+
end
|
31
|
+
|
32
|
+
def passive_scan
|
33
|
+
devices = Array.new
|
34
|
+
if @options[:enable_hcitool_duration] == true
|
35
|
+
scan_output = self.scan_command_duration
|
36
|
+
else
|
37
|
+
scan_output = self.scan_command
|
38
|
+
end
|
39
|
+
scan_output.each_line do |line|
|
40
|
+
result = line.scan(/^([A-F0-9:]{15}[A-F0-9]{2}) (.*)$/)
|
41
|
+
if !result.empty?
|
42
|
+
mac_address = result[0][0]
|
43
|
+
name = result[0][1]
|
44
|
+
if !devices.find {|s| s.mac_address == mac_address}
|
45
|
+
filter_mac = @options[:filter_mac]
|
46
|
+
if !filter_mac or (filter_mac.include?(mac_address) if filter_mac.is_a?(Array))
|
47
|
+
device = BluetoothLeDevice.new(mac_address, name)
|
48
|
+
devices << device
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
devices
|
54
|
+
end
|
55
|
+
|
56
|
+
def scan
|
57
|
+
devices = self.passive_scan
|
58
|
+
devices.each do |dev|
|
59
|
+
dev.fetch_characteristics
|
60
|
+
end
|
61
|
+
devices
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Radbeacon
|
2
|
+
class Scanner < LeScanner
|
3
|
+
|
4
|
+
C_DEVICE_NAME = "0x0003"
|
5
|
+
RADBEACON_USB = "52 61 64 42 65 61 63 6f 6e 20 55 53 42"
|
6
|
+
|
7
|
+
def scan
|
8
|
+
devices = super
|
9
|
+
radbeacons = devices.map { |dev| radbeacon_check(dev) }
|
10
|
+
radbeacons.compact
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(mac_address)
|
14
|
+
dev = BluetoothLeDevice.new(mac_address, nil)
|
15
|
+
radbeacon_check(dev) if dev.fetch_characteristics
|
16
|
+
end
|
17
|
+
|
18
|
+
def radbeacon_check(device)
|
19
|
+
radbeacon = nil
|
20
|
+
case device.values[C_DEVICE_NAME]
|
21
|
+
when RADBEACON_USB
|
22
|
+
radbeacon = Usb.create_if_valid(device)
|
23
|
+
end
|
24
|
+
radbeacon
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Radbeacon
|
2
|
+
class Usb < BluetoothLeDevice
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
## Define GATT characteristic constants
|
6
|
+
# Generic Access Profile
|
7
|
+
C_DEVICE_NAME = "0x0003"
|
8
|
+
C_APPEARANCE = "0x0006"
|
9
|
+
# Device Information
|
10
|
+
C_MANUFACTURER_NAME = "0x000a"
|
11
|
+
C_MODEL_NUMBER = "0x000d"
|
12
|
+
C_SERIAL_STRING = "0x0010"
|
13
|
+
C_FIRMWARE_STRING = "0x0013"
|
14
|
+
# Configuration
|
15
|
+
GATT_DEV_MODEL = "0x0017"
|
16
|
+
GATT_DEV_ID = "0x001a"
|
17
|
+
GATT_DEV_NAME = "0x001d"
|
18
|
+
GATT_UUID = "0x0020"
|
19
|
+
GATT_MAJOR = "0x0023"
|
20
|
+
GATT_MINOR = "0x0026"
|
21
|
+
GATT_POWER = "0x0029"
|
22
|
+
GATT_TXPOWER = "0x002c"
|
23
|
+
GATT_INTERVAL = "0x002f"
|
24
|
+
GATT_RESULT = "0x0032"
|
25
|
+
GATT_NEW_PIN = "0x0035"
|
26
|
+
GATT_ACTION = "0x0038"
|
27
|
+
GATT_PIN = "0x003b"
|
28
|
+
GATT_BCTYPE = "0x003e"
|
29
|
+
GATT_FWVERSION = "0x0041"
|
30
|
+
GATT_CONN_TIMEOUT = "0x0044"
|
31
|
+
GATT_BEACON_SWITCH = "0x0047"
|
32
|
+
|
33
|
+
## Define GATT action/result constants
|
34
|
+
# Actions
|
35
|
+
GATT_ACTION_DONOTHING = "00000000"
|
36
|
+
GATT_ACTION_UPDATE_ADV = "00000001"
|
37
|
+
GATT_ACTION_UPDATE_PIN = "00000002"
|
38
|
+
GATT_ACTION_FACTORY_RESET = "00000003"
|
39
|
+
GATT_ACTION_DFU = "00000004"
|
40
|
+
GATT_ACTION_LOCK = "00000005"
|
41
|
+
GATT_ACTION_CONNECTABLE_TIME = "00000006"
|
42
|
+
# Results
|
43
|
+
GATT_SUCCES = "00000000"
|
44
|
+
GATT_INVALID_PIN = "00000001"
|
45
|
+
GATT_ERROR = "00000002" #not used
|
46
|
+
|
47
|
+
## Transmit power and advertisement frequency values
|
48
|
+
TRANSMIT_POWER_VALUES = {-23 => "00", -21 => "01", -18 => "03", -14 => "05",
|
49
|
+
-11 => "07", -7 => "09", -4 => "0b", 0 => "0d", 3 => "0f"}
|
50
|
+
DEFAULT_MEASURED_POWER_VALUES = [-94, -92, -90, -86, -84, -79, -74, -72, -66]
|
51
|
+
ADVERTISING_RATE_VALUES = {0 => "0000", 1 => "2006", 2 => "0003", 3 => "f501", 4 => "7001",
|
52
|
+
5 => "2001", 6 => "ea00", 7 => "c400", 8 => "a800", 9 => "9100", 10 => "8000"}
|
53
|
+
BEACON_TYPES = {"ibeacon" => "01", "altbeacon" => "02", "dual" => "03"}
|
54
|
+
|
55
|
+
## Timeout length for GATT commands
|
56
|
+
TIMEOUT = 0.5
|
57
|
+
|
58
|
+
## Valid UUID pattern
|
59
|
+
VALID_UUID = /^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/
|
60
|
+
|
61
|
+
attr_reader :mac_address, :errors, :dev_model, :dev_id, :dev_version
|
62
|
+
attr_accessor :dev_name, :uuid, :major, :minor, :power, :tx_power, :adv_rate, :beacon_type
|
63
|
+
|
64
|
+
def self.create_if_valid(device)
|
65
|
+
# Check everything
|
66
|
+
required_attrs = [GATT_DEV_MODEL, GATT_DEV_ID, GATT_FWVERSION, GATT_DEV_NAME,
|
67
|
+
GATT_UUID, GATT_MAJOR, GATT_MINOR, GATT_POWER, GATT_TXPOWER, GATT_INTERVAL, GATT_BCTYPE]
|
68
|
+
if required_attrs.all? { |key| device.values[key] }
|
69
|
+
self.new(device)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(device)
|
74
|
+
@errors = []
|
75
|
+
@mac_address = device.mac_address
|
76
|
+
@dev_model = bytes_to_text(device.values[GATT_DEV_MODEL])
|
77
|
+
@dev_id = bytes_to_text(device.values[GATT_DEV_ID])
|
78
|
+
@dev_version = bytes_to_text(device.values[GATT_FWVERSION])
|
79
|
+
@dev_name = bytes_to_text(device.values[GATT_DEV_NAME])
|
80
|
+
@uuid = bytes_to_uuid(device.values[GATT_UUID])
|
81
|
+
@major = bytes_to_major_minor(device.values[GATT_MAJOR])
|
82
|
+
@minor = bytes_to_major_minor(device.values[GATT_MINOR])
|
83
|
+
@power = bytes_to_power(device.values[GATT_POWER])
|
84
|
+
@tx_power = TRANSMIT_POWER_VALUES.key(device.values[GATT_TXPOWER])
|
85
|
+
@adv_rate = ADVERTISING_RATE_VALUES.key(device.values[GATT_INTERVAL].delete(' '))
|
86
|
+
@beacon_type = BEACON_TYPES.key(device.values[GATT_BCTYPE])
|
87
|
+
end
|
88
|
+
|
89
|
+
def valid?
|
90
|
+
@errors = []
|
91
|
+
@errors << "Invalid device name" unless @dev_name.length <= 20
|
92
|
+
@errors << "Invalid UUID" unless @uuid.match(VALID_UUID)
|
93
|
+
@errors << "Invalid major value" unless @major.to_i.between?(0, 65535)
|
94
|
+
@errors << "Invalid minor value" unless @minor.to_i.between?(0, 65535)
|
95
|
+
@errors << "Invalid measured power value" unless @power.to_i.between?(-127, -1)
|
96
|
+
@errors << "Invalid transmit power" unless TRANSMIT_POWER_VALUES.has_key?(@tx_power)
|
97
|
+
@errors << "Invalid advertising rate" unless ADVERTISING_RATE_VALUES.has_key?(@adv_rate)
|
98
|
+
@errors << "Invalid beacon type" unless BEACON_TYPES.has_key?(@beacon_type)
|
99
|
+
if @errors.empty?
|
100
|
+
true
|
101
|
+
else
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def save(pin)
|
107
|
+
if self.valid?
|
108
|
+
update_params_commands = ["#{GATT_DEV_NAME} #{text_to_bytes(@dev_name)}",
|
109
|
+
"#{GATT_UUID} #{uuid_to_bytes(@uuid)}", "#{GATT_MAJOR} #{major_minor_to_bytes(@major)}",
|
110
|
+
"#{GATT_MINOR} #{major_minor_to_bytes(@minor)}", "#{GATT_POWER} #{power_to_bytes(@power)}",
|
111
|
+
"#{GATT_TXPOWER} #{TRANSMIT_POWER_VALUES[@tx_power]}", "#{GATT_INTERVAL} #{ADVERTISING_RATE_VALUES[@adv_rate]}",
|
112
|
+
"#{GATT_BCTYPE} #{BEACON_TYPES[@beacon_type]}", "#{GATT_ACTION} #{GATT_ACTION_UPDATE_ADV}", "#{GATT_PIN} #{pin_to_bytes(pin)}"]
|
113
|
+
con(update_params_commands)
|
114
|
+
else
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def change_pin(new_pin, old_pin)
|
120
|
+
update_pin_commands = ["#{GATT_NEW_PIN} #{pin_to_bytes(new_pin)}", "#{GATT_ACTION} #{GATT_ACTION_UPDATE_PIN}",
|
121
|
+
"#{GATT_PIN} #{pin_to_bytes(old_pin)}"]
|
122
|
+
con(update_pin_commands)
|
123
|
+
end
|
124
|
+
|
125
|
+
def factory_reset(pin)
|
126
|
+
reset_commands = ["#{GATT_ACTION} #{GATT_ACTION_FACTORY_RESET}", "#{GATT_PIN} #{pin_to_bytes(pin)}"]
|
127
|
+
con(reset_commands) && defaults
|
128
|
+
end
|
129
|
+
|
130
|
+
def boot_to_dfu(pin)
|
131
|
+
dfu_commands = ["#{GATT_ACTION} #{GATT_ACTION_DFU}", "#{GATT_PIN} #{pin_to_bytes(pin)}"]
|
132
|
+
con(dfu_commands)
|
133
|
+
end
|
134
|
+
|
135
|
+
def lock(pin)
|
136
|
+
lock_commands = ["#{GATT_ACTION} #{GATT_ACTION_LOCK}", "#{GATT_PIN} #{pin_to_bytes(pin)}"]
|
137
|
+
con(lock_commands)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
attr_writer :mac_address, :errors, :dev_model, :dev_id, :dev_version
|
142
|
+
|
143
|
+
def defaults
|
144
|
+
@dev_name = "RadBeacon USB"
|
145
|
+
@uuid = "2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6"
|
146
|
+
@major = 1
|
147
|
+
@minor = 1
|
148
|
+
@power = -66
|
149
|
+
@tx_power = 3
|
150
|
+
@adv_rate = 10
|
151
|
+
@beacon_type = "dual"
|
152
|
+
end
|
153
|
+
|
154
|
+
def con(commands)
|
155
|
+
@errors = []
|
156
|
+
result = false
|
157
|
+
cmd = "gatttool -b #{@mac_address} --interactive"
|
158
|
+
PTY.spawn(cmd) do |output, input, pid|
|
159
|
+
output.expect(/\[LE\]>/)
|
160
|
+
input.puts "connect"
|
161
|
+
if output.expect(/Connection successful/, TIMEOUT)
|
162
|
+
commands.each do |cmd|
|
163
|
+
cmd = "char-write-req #{cmd}"
|
164
|
+
input.puts cmd
|
165
|
+
if output.expect(/Characteristic value was written successfully/, TIMEOUT)
|
166
|
+
result = true
|
167
|
+
else
|
168
|
+
@errors << "Action failed: #{cmd}"
|
169
|
+
result = false
|
170
|
+
break
|
171
|
+
end
|
172
|
+
end
|
173
|
+
if result
|
174
|
+
input.puts "char-read-hnd #{GATT_RESULT}"
|
175
|
+
if output.expect(/Characteristic value\/descriptor: 00 00 00 00/, TIMEOUT)
|
176
|
+
result = true
|
177
|
+
else
|
178
|
+
@errors << "Invalid PIN"
|
179
|
+
result = false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
else
|
183
|
+
@errors << "Connection failed"
|
184
|
+
end
|
185
|
+
input.puts "quit"
|
186
|
+
_, status = Process.waitpid2(pid)
|
187
|
+
if !status.success?
|
188
|
+
result = false
|
189
|
+
@errors << "Process failed to exit properly"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
result
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Radbeacon
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def text_to_bytes(text)
|
5
|
+
text.unpack('H*')[0]
|
6
|
+
end
|
7
|
+
|
8
|
+
def bytes_to_text(bytes)
|
9
|
+
[bytes.delete(' ')].pack('H*').gsub(/\x00/,'')
|
10
|
+
end
|
11
|
+
|
12
|
+
def uuid_to_bytes(uuid)
|
13
|
+
uuid.gsub(/-/, '')
|
14
|
+
end
|
15
|
+
|
16
|
+
def bytes_to_uuid(bytes)
|
17
|
+
bytes.delete(' ').sub(/([a-fA-F0-9]{8})([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{12})/, '\1-\2-\3-\4-\5').upcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def major_minor_to_bytes(value)
|
21
|
+
sprintf("%04x", value.to_i)
|
22
|
+
end
|
23
|
+
|
24
|
+
def bytes_to_major_minor(bytes)
|
25
|
+
bytes.delete(' ').to_i(16)
|
26
|
+
end
|
27
|
+
|
28
|
+
def power_to_bytes(power)
|
29
|
+
sprintf("%x", power.to_i + 256)
|
30
|
+
end
|
31
|
+
|
32
|
+
def bytes_to_power(bytes)
|
33
|
+
bytes.to_i(16) - 256
|
34
|
+
end
|
35
|
+
|
36
|
+
def pin_to_bytes(pin)
|
37
|
+
pin.unpack('H*')[0]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/radbeacon.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'radbeacon/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "radbeacon"
|
8
|
+
spec.version = Radbeacon::VERSION
|
9
|
+
spec.authors = ["Radius Networks"]
|
10
|
+
spec.email = ["support@radiusnetworks.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Provides RadBeacon (BLE Proximity Beacon) scanning and configuring capabilities on a linux machine.}
|
13
|
+
spec.homepage = "http://www.radiusnetworks.com"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
24
|
+
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: radbeacon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Radius Networks
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- support@radiusnetworks.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- Gemfile
|
65
|
+
- Gemfile.lock
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- lib/radbeacon.rb
|
70
|
+
- lib/radbeacon/bluetooth_le_device.rb
|
71
|
+
- lib/radbeacon/le_scanner.rb
|
72
|
+
- lib/radbeacon/scanner.rb
|
73
|
+
- lib/radbeacon/usb.rb
|
74
|
+
- lib/radbeacon/utils.rb
|
75
|
+
- lib/radbeacon/version.rb
|
76
|
+
- radbeacon.gemspec
|
77
|
+
homepage: http://www.radiusnetworks.com
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.4.3
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Provides RadBeacon (BLE Proximity Beacon) scanning and configuring capabilities
|
101
|
+
on a linux machine.
|
102
|
+
test_files: []
|