scan_beacon 0.3.6 → 0.5.0
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 +4 -4
- data/.gitignore +3 -1
- data/README.md +51 -4
- data/Rakefile +9 -0
- data/ext/bluez/advertising.c +125 -0
- data/ext/bluez/bluez.c +43 -0
- data/ext/bluez/devices.c +101 -0
- data/ext/bluez/extconf.rb +14 -0
- data/ext/bluez/scanning.c +144 -0
- data/ext/bluez/utils.c +21 -0
- data/ext/bluez/utils.h +2 -0
- data/ext/core_bluetooth/core_bluetooth.m +150 -0
- data/ext/core_bluetooth/extconf.rb +23 -0
- data/lib/scan_beacon/beacon.rb +5 -2
- data/lib/scan_beacon/beacon_parser.rb +89 -9
- data/lib/scan_beacon/ble112_scanner.rb +8 -44
- data/lib/scan_beacon/bluez_advertiser.rb +45 -0
- data/lib/scan_beacon/bluez_scanner.rb +26 -0
- data/lib/scan_beacon/core_bluetooth_scanner.rb +25 -0
- data/lib/scan_beacon/generic_scanner.rb +59 -0
- data/lib/scan_beacon/version.rb +1 -1
- data/lib/scan_beacon.rb +10 -0
- data/scan_beacon.gemspec +2 -0
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 986f14b044864f3f4e4403ae4cf4babab759035c
|
4
|
+
data.tar.gz: 39fe84b3c5617b4b4ba965af4fc22c7084b5e978
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d9c4da68dac5591b0719c44587258aef4ecc632138d0878185341ac20174bbb3106e113b9709f3c1cd5f24812ca15c336b719736cbf404d7ec36811d6da35f7
|
7
|
+
data.tar.gz: 50ff7678d58cb831273184acbde2c64b3c7301c1a869348b5003e3900ae8f596916a22cd1bec805f2bbf85c63952484f252c8389e5c3d664b3182164329e5563
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ScanBeacon gem
|
2
2
|
|
3
|
-
A ruby gem that allows you to scan for beacon advertisements using a BlueGiga BLE112 device
|
3
|
+
A ruby gem that allows you to scan for beacon advertisements using CoreBluetooth (on Mac OS X) or a BlueGiga BLE112 device (on mac or linux)
|
4
4
|
|
5
5
|
# Example Usage
|
6
6
|
|
@@ -9,10 +9,19 @@ A ruby gem that allows you to scan for beacon advertisements using a BlueGiga BL
|
|
9
9
|
gem install scan_beacon
|
10
10
|
```
|
11
11
|
|
12
|
-
##
|
12
|
+
## Create your scanner
|
13
13
|
``` ruby
|
14
14
|
require 'scan_beacon'
|
15
|
+
# to scan using CoreBluetooth on a mac
|
16
|
+
scanner = ScanBeacon::CoreBluetoothScanner.new
|
17
|
+
# to scan using a BLE112 device
|
15
18
|
scanner = ScanBeacon::BLE112Scanner.new
|
19
|
+
# to scan using BlueZ on Linux (make sure you have privileges)
|
20
|
+
scanner = ScanBeacon::BlueZScanner.new
|
21
|
+
```
|
22
|
+
|
23
|
+
## Start a scan, yield beacons in a loop
|
24
|
+
``` ruby
|
16
25
|
scanner.scan do |beacons|
|
17
26
|
beacons.each do |beacon|
|
18
27
|
puts beacon.inspect
|
@@ -23,7 +32,7 @@ end
|
|
23
32
|
## Set a specific scan cycle period
|
24
33
|
``` ruby
|
25
34
|
require 'scan_beacon'
|
26
|
-
scanner = ScanBeacon::BLE112Scanner.new cycle_seconds:
|
35
|
+
scanner = ScanBeacon::BLE112Scanner.new cycle_seconds: 5
|
27
36
|
scanner.scan do |beacons|
|
28
37
|
beacons.each do |beacon|
|
29
38
|
puts beacon.inspect
|
@@ -31,6 +40,12 @@ scanner.scan do |beacons|
|
|
31
40
|
end
|
32
41
|
```
|
33
42
|
|
43
|
+
## Scan once for a set period and then return an array of beacons
|
44
|
+
``` ruby
|
45
|
+
scanner = ScanBeacon::CoreBluetoothScanner.new cycle_seconds: 2
|
46
|
+
beacons = scanner.scan
|
47
|
+
```
|
48
|
+
|
34
49
|
## Add a custom beacon layout
|
35
50
|
By default, this gem supports AltBeacon advertisements. But you can add a beacon parser to support other major beacon formats as well.
|
36
51
|
|
@@ -41,5 +56,37 @@ scanner.add_parser( ScanBeacon::BeaconParser.new(:mybeacon, "m:2-3=0000,i:4-19,i
|
|
41
56
|
...
|
42
57
|
```
|
43
58
|
|
59
|
+
## Advertise as a beacon on Linux using BlueZ
|
60
|
+
Example:
|
61
|
+
``` ruby
|
62
|
+
# altbeacon
|
63
|
+
beacon = ScanBeacon::Beacon.new(
|
64
|
+
ids: ["2F234454CF6D4A0FADF2F4911BA9FFA6", 11,11],
|
65
|
+
power: -59,
|
66
|
+
mfg_id: 0x0118,
|
67
|
+
beacon_type: :altbeacon
|
68
|
+
)
|
69
|
+
advertiser = ScanBeacon::BlueZAdvertiser.new(beacon: beacon)
|
70
|
+
advertiser.start
|
71
|
+
...
|
72
|
+
advertiser.stop
|
73
|
+
|
74
|
+
# Eddystone UID
|
75
|
+
beacon = ScanBeacon::Beacon.new(
|
76
|
+
ids: ["2F234454F4911BA9FFA6", 3],
|
77
|
+
power: -20,
|
78
|
+
service_uuid: 0xFEAA,
|
79
|
+
beacon_type: :eddystone_uid
|
80
|
+
)
|
81
|
+
advertiser = ScanBeacon::BlueZAdvertiser.new(beacon: beacon)
|
82
|
+
advertiser.start
|
83
|
+
...
|
84
|
+
advertiser.stop
|
85
|
+
```
|
86
|
+
|
87
|
+
|
44
88
|
# Dependencies
|
45
|
-
|
89
|
+
To scan for beacons, you must have a Linux machine with BlueZ installed, or a Mac, or a BLE112 device plugged in to a USB port (on Mac or Linux).
|
90
|
+
|
91
|
+
To advertise as a beacon, you must have a Linux machine with BlueZ installed.
|
92
|
+
|
data/Rakefile
CHANGED
@@ -0,0 +1,125 @@
|
|
1
|
+
#ifdef linux
|
2
|
+
#include "ruby.h"
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <errno.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <sys/ioctl.h>
|
7
|
+
#include <sys/socket.h>
|
8
|
+
#include <bluetooth/bluetooth.h>
|
9
|
+
#include <bluetooth/hci.h>
|
10
|
+
#include <bluetooth/hci_lib.h>
|
11
|
+
#pragma pack(1)
|
12
|
+
|
13
|
+
|
14
|
+
#define FLAGS_NOT_CONNECTABLE 0x1A
|
15
|
+
#define FLAGS_CONNECTABLE 0x18
|
16
|
+
|
17
|
+
typedef struct {
|
18
|
+
uint8_t len;
|
19
|
+
struct {
|
20
|
+
uint8_t len;
|
21
|
+
uint8_t type;
|
22
|
+
uint8_t data;
|
23
|
+
} flags;
|
24
|
+
uint8_t ad_data[28];
|
25
|
+
} Advertisement;
|
26
|
+
|
27
|
+
Advertisement advertisement;
|
28
|
+
int advertising;
|
29
|
+
|
30
|
+
VALUE method_start_advertising();
|
31
|
+
|
32
|
+
void init_advertisement()
|
33
|
+
{
|
34
|
+
memset(&advertisement, 0, sizeof(advertisement));
|
35
|
+
advertisement.flags.len = 2;
|
36
|
+
advertisement.flags.type = 1;
|
37
|
+
advertisement.flags.data = FLAGS_NOT_CONNECTABLE;
|
38
|
+
advertising = 0;
|
39
|
+
}
|
40
|
+
|
41
|
+
VALUE method_set_connectable(VALUE self, VALUE connectable)
|
42
|
+
{
|
43
|
+
if ( RTEST(connectable) )
|
44
|
+
{
|
45
|
+
advertisement.flags.data = FLAGS_CONNECTABLE;
|
46
|
+
} else {
|
47
|
+
advertisement.flags.data = FLAGS_NOT_CONNECTABLE;
|
48
|
+
};
|
49
|
+
if (advertising) {
|
50
|
+
method_start_advertising();
|
51
|
+
}
|
52
|
+
return connectable;
|
53
|
+
}
|
54
|
+
|
55
|
+
VALUE method_set_advertisement_bytes(VALUE self, VALUE bytes)
|
56
|
+
{
|
57
|
+
uint8_t len = RSTRING_LEN(bytes);
|
58
|
+
if (len > 28) {
|
59
|
+
len = 28;
|
60
|
+
}
|
61
|
+
advertisement.len = len + advertisement.flags.len + 1; // + 1 is to account for the flags length field
|
62
|
+
memcpy(advertisement.ad_data, RSTRING_PTR(bytes), len);
|
63
|
+
if (advertising) {
|
64
|
+
method_start_advertising();
|
65
|
+
}
|
66
|
+
return bytes;
|
67
|
+
}
|
68
|
+
|
69
|
+
VALUE method_start_advertising()
|
70
|
+
{
|
71
|
+
struct hci_request rq;
|
72
|
+
le_set_advertising_parameters_cp adv_params_cp;
|
73
|
+
uint8_t status;
|
74
|
+
|
75
|
+
// open connection to the device
|
76
|
+
int device_id = hci_get_route(NULL);
|
77
|
+
int device_handle = hci_open_dev(device_id);
|
78
|
+
|
79
|
+
// set advertising data
|
80
|
+
memset(&rq, 0, sizeof(rq));
|
81
|
+
rq.ogf = OGF_LE_CTL;
|
82
|
+
rq.ocf = OCF_LE_SET_ADVERTISING_DATA;
|
83
|
+
rq.cparam = &advertisement;
|
84
|
+
rq.clen = sizeof(advertisement);
|
85
|
+
rq.rparam = &status;
|
86
|
+
rq.rlen = 1;
|
87
|
+
hci_send_req(device_handle, &rq, 1000);
|
88
|
+
|
89
|
+
// set advertising params
|
90
|
+
memset(&adv_params_cp, 0, sizeof(adv_params_cp));
|
91
|
+
uint16_t interval_100ms = htobs(0x00A0); // 0xA0 * 0.625ms = 100ms
|
92
|
+
adv_params_cp.min_interval = interval_100ms;
|
93
|
+
adv_params_cp.max_interval = interval_100ms;
|
94
|
+
adv_params_cp.advtype = 0x03; // non-connectable undirected advertising
|
95
|
+
adv_params_cp.chan_map = 0x07;// all 3 channels
|
96
|
+
memset(&rq, 0, sizeof(rq));
|
97
|
+
rq.ogf = OGF_LE_CTL;
|
98
|
+
rq.ocf = OCF_LE_SET_ADVERTISING_PARAMETERS;
|
99
|
+
rq.cparam = &adv_params_cp;
|
100
|
+
rq.clen = LE_SET_ADVERTISING_PARAMETERS_CP_SIZE;
|
101
|
+
rq.rparam = &status;
|
102
|
+
rq.rlen = 1;
|
103
|
+
hci_send_req(device_handle, &rq, 1000);
|
104
|
+
|
105
|
+
// turn on advertising
|
106
|
+
hci_le_set_advertise_enable(device_handle, 0x01, 1000);
|
107
|
+
|
108
|
+
// and close the connection
|
109
|
+
hci_close_dev(device_handle);
|
110
|
+
advertising = 1;
|
111
|
+
return Qnil;
|
112
|
+
}
|
113
|
+
|
114
|
+
VALUE method_stop_advertising()
|
115
|
+
{
|
116
|
+
int device_id = hci_get_route(NULL);
|
117
|
+
int device_handle = hci_open_dev(device_id);
|
118
|
+
hci_le_set_advertise_enable(device_handle, 0x00, 1000);
|
119
|
+
hci_close_dev(device_handle);
|
120
|
+
advertising = 0;
|
121
|
+
return Qnil;
|
122
|
+
}
|
123
|
+
|
124
|
+
|
125
|
+
#endif // linux
|
data/ext/bluez/bluez.c
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#ifdef linux
|
2
|
+
#include "ruby.h"
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <errno.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <sys/ioctl.h>
|
7
|
+
#include <sys/socket.h>
|
8
|
+
#include <bluetooth/bluetooth.h>
|
9
|
+
#include <bluetooth/hci.h>
|
10
|
+
#include <bluetooth/hci_lib.h>
|
11
|
+
|
12
|
+
VALUE bluez_module = Qnil;
|
13
|
+
|
14
|
+
void Init_bluez();
|
15
|
+
VALUE method_device_up(VALUE self, VALUE device_id);
|
16
|
+
VALUE method_device_down(VALUE self, VALUE device_id);
|
17
|
+
VALUE method_set_connectable(VALUE self, VALUE connectable);
|
18
|
+
VALUE method_set_advertisement_bytes(VALUE self, VALUE bytes);
|
19
|
+
VALUE method_start_advertising();
|
20
|
+
VALUE method_stop_advertising();
|
21
|
+
VALUE method_scan(int argc, VALUE *argv, VALUE klass);
|
22
|
+
VALUE method_devices();
|
23
|
+
void init_advertisement();
|
24
|
+
|
25
|
+
void Init_bluez()
|
26
|
+
{
|
27
|
+
VALUE scan_beacon_module = rb_const_get(rb_cObject, rb_intern("ScanBeacon"));
|
28
|
+
bluez_module = rb_define_module_under(scan_beacon_module, "BlueZ");
|
29
|
+
rb_define_singleton_method(bluez_module, "device_up", method_device_up, 1);
|
30
|
+
rb_define_singleton_method(bluez_module, "device_down", method_device_down, 1);
|
31
|
+
rb_define_singleton_method(bluez_module, "start_advertising", method_start_advertising, 0);
|
32
|
+
rb_define_singleton_method(bluez_module, "stop_advertising", method_stop_advertising, 0);
|
33
|
+
rb_define_singleton_method(bluez_module, "advertisement_bytes=", method_set_advertisement_bytes, 1);
|
34
|
+
rb_define_singleton_method(bluez_module, "connectable=", method_set_connectable, 1);
|
35
|
+
rb_define_singleton_method(bluez_module, "scan", method_scan, -1);
|
36
|
+
rb_define_singleton_method(bluez_module, "devices", method_devices, 0);
|
37
|
+
|
38
|
+
// initialize the advertisement
|
39
|
+
init_advertisement();
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
#endif // linux
|
data/ext/bluez/devices.c
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
#ifdef linux
|
2
|
+
#include "ruby.h"
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <errno.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <sys/ioctl.h>
|
7
|
+
#include <sys/socket.h>
|
8
|
+
#include <bluetooth/bluetooth.h>
|
9
|
+
#include <bluetooth/hci.h>
|
10
|
+
#include <bluetooth/hci_lib.h>
|
11
|
+
|
12
|
+
#include "utils.h"
|
13
|
+
|
14
|
+
VALUE method_device_up(VALUE self, VALUE device_id)
|
15
|
+
{
|
16
|
+
VALUE success;
|
17
|
+
int ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
|
18
|
+
int hdev = NUM2INT(device_id);
|
19
|
+
/* Start HCI device */
|
20
|
+
if (ioctl(ctl, HCIDEVUP, hdev) < 0) {
|
21
|
+
if (errno == EALREADY) {
|
22
|
+
success = Qtrue;
|
23
|
+
} else {
|
24
|
+
rb_sys_fail("Can't init device");
|
25
|
+
success = Qfalse;
|
26
|
+
}
|
27
|
+
} else {
|
28
|
+
success = Qtrue;
|
29
|
+
}
|
30
|
+
close(ctl);
|
31
|
+
return success;
|
32
|
+
}
|
33
|
+
|
34
|
+
VALUE method_device_down(VALUE self, VALUE device_id)
|
35
|
+
{
|
36
|
+
VALUE success;
|
37
|
+
int ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
|
38
|
+
int hdev = NUM2INT(device_id);
|
39
|
+
/* Stop HCI device */
|
40
|
+
if (ioctl(ctl, HCIDEVDOWN, hdev) < 0) {
|
41
|
+
rb_sys_fail("Can't init device");
|
42
|
+
success = Qfalse;
|
43
|
+
} else {
|
44
|
+
success = Qtrue;
|
45
|
+
}
|
46
|
+
close(ctl);
|
47
|
+
return success;
|
48
|
+
}
|
49
|
+
|
50
|
+
VALUE di_to_hash(struct hci_dev_info *di)
|
51
|
+
{
|
52
|
+
VALUE hash = rb_hash_new();
|
53
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("device_id")), INT2FIX(di->dev_id));
|
54
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("name")), rb_str_new2(di->name));
|
55
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("addr")), ba2value(&di->bdaddr));
|
56
|
+
if (hci_test_bit(HCI_UP, &di->flags)) {
|
57
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("up")), Qtrue);
|
58
|
+
} else {
|
59
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("up")), Qfalse);
|
60
|
+
}
|
61
|
+
return hash;
|
62
|
+
}
|
63
|
+
|
64
|
+
VALUE method_devices()
|
65
|
+
{
|
66
|
+
struct hci_dev_list_req *dl;
|
67
|
+
struct hci_dev_req *dr;
|
68
|
+
struct hci_dev_info di;
|
69
|
+
int i;
|
70
|
+
|
71
|
+
if (!(dl = malloc(HCI_MAX_DEV * sizeof(struct hci_dev_req) + sizeof(uint16_t)))) {
|
72
|
+
rb_raise(rb_eException, "Can't allocate memory");
|
73
|
+
return Qnil;
|
74
|
+
}
|
75
|
+
dl->dev_num = HCI_MAX_DEV;
|
76
|
+
dr = dl->dev_req;
|
77
|
+
|
78
|
+
int ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
|
79
|
+
if (ioctl(ctl, HCIGETDEVLIST, (void *) dl) < 0) {
|
80
|
+
rb_raise(rb_eException, "Can't get device list");
|
81
|
+
return Qnil;
|
82
|
+
}
|
83
|
+
|
84
|
+
VALUE devices = rb_ary_new();
|
85
|
+
|
86
|
+
for (i = 0; i< dl->dev_num; i++) {
|
87
|
+
di.dev_id = (dr+i)->dev_id;
|
88
|
+
if (ioctl(ctl, HCIGETDEVINFO, (void *) &di) < 0)
|
89
|
+
continue;
|
90
|
+
if (hci_test_bit(HCI_RAW, &di.flags) &&
|
91
|
+
!bacmp(&di.bdaddr, BDADDR_ANY)) {
|
92
|
+
int dd = hci_open_dev(di.dev_id);
|
93
|
+
hci_read_bd_addr(dd, &di.bdaddr, 1000);
|
94
|
+
hci_close_dev(dd);
|
95
|
+
}
|
96
|
+
rb_ary_push(devices, di_to_hash(&di));
|
97
|
+
}
|
98
|
+
return devices;
|
99
|
+
}
|
100
|
+
|
101
|
+
#endif // linux
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Loads mkmf which is used to make makefiles for Ruby extensions
|
2
|
+
require 'mkmf'
|
3
|
+
|
4
|
+
# Give it a name
|
5
|
+
extension_name = 'scan_beacon/bluez'
|
6
|
+
|
7
|
+
# The destination
|
8
|
+
dir_config(extension_name)
|
9
|
+
|
10
|
+
if RUBY_PLATFORM =~ /linux/
|
11
|
+
abort 'could not find bluetooth library (libbluetooth-dev)' unless have_library("bluetooth")
|
12
|
+
end
|
13
|
+
|
14
|
+
create_makefile(extension_name)
|
@@ -0,0 +1,144 @@
|
|
1
|
+
#ifdef linux
|
2
|
+
#include "ruby.h"
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <errno.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <sys/ioctl.h>
|
7
|
+
#include <sys/socket.h>
|
8
|
+
#include <time.h>
|
9
|
+
#include <bluetooth/bluetooth.h>
|
10
|
+
#include <bluetooth/hci.h>
|
11
|
+
#include <bluetooth/hci_lib.h>
|
12
|
+
|
13
|
+
#include "utils.h"
|
14
|
+
|
15
|
+
VALUE method_scan();
|
16
|
+
|
17
|
+
|
18
|
+
VALUE stop_scan(VALUE device_id);
|
19
|
+
VALUE perform_scan(VALUE device_id);
|
20
|
+
|
21
|
+
struct hci_filter stored_filters[10];
|
22
|
+
int device_handles[10];
|
23
|
+
|
24
|
+
VALUE method_scan(int argc, VALUE *argv, VALUE klass)
|
25
|
+
{
|
26
|
+
VALUE rb_device_id;
|
27
|
+
int device_id;
|
28
|
+
int device_handle;
|
29
|
+
uint8_t scan_type = 0x01; //passive
|
30
|
+
uint8_t own_type = 0x00; // I think this specifies not to use a random MAC
|
31
|
+
uint8_t filter_dups = 0x00;
|
32
|
+
uint8_t filter_policy = 0x00; // ?
|
33
|
+
uint16_t interval = htobs(0x0005);
|
34
|
+
uint16_t window = htobs(0x0005);
|
35
|
+
|
36
|
+
struct hci_filter new_filter;
|
37
|
+
|
38
|
+
// which device was specified?
|
39
|
+
rb_scan_args(argc, argv, "01", &rb_device_id);
|
40
|
+
if (rb_device_id == Qnil) {
|
41
|
+
device_id = hci_get_route(NULL);
|
42
|
+
} else {
|
43
|
+
device_id = NUM2INT(rb_device_id);
|
44
|
+
}
|
45
|
+
// open the device
|
46
|
+
if ( (device_handle = hci_open_dev(device_id)) < 0) {
|
47
|
+
rb_raise(rb_eException, "Could not open device");
|
48
|
+
}
|
49
|
+
device_handles[device_id] = device_handle;
|
50
|
+
|
51
|
+
// save the old filter so we can restore it later
|
52
|
+
socklen_t filter_size = sizeof(stored_filters[0]);
|
53
|
+
if (getsockopt(device_handle, SOL_HCI, HCI_FILTER, &stored_filters[device_id], &filter_size) < 0) {
|
54
|
+
rb_raise(rb_eException, "Could not get socket options");
|
55
|
+
}
|
56
|
+
|
57
|
+
// new filter to only look for event packets
|
58
|
+
hci_filter_clear(&new_filter);
|
59
|
+
hci_filter_set_ptype(HCI_EVENT_PKT, &new_filter);
|
60
|
+
hci_filter_set_event(EVT_LE_META_EVENT, &new_filter);
|
61
|
+
if (setsockopt(device_handle, SOL_HCI, HCI_FILTER, &new_filter, sizeof(new_filter)) < 0) {
|
62
|
+
rb_raise(rb_eException, "Could not set socket options");
|
63
|
+
}
|
64
|
+
|
65
|
+
// set the params
|
66
|
+
hci_le_set_scan_parameters(device_handle, scan_type, interval, window, own_type, filter_policy, 1000);
|
67
|
+
hci_le_set_scan_enable(device_handle, 0x01, filter_dups, 1000);
|
68
|
+
|
69
|
+
// perform the scan and make sure device gets put back into a proper state
|
70
|
+
// even in the case of being interrupted by a ruby exception
|
71
|
+
rb_ensure(perform_scan, INT2FIX(device_id), stop_scan, INT2FIX(device_id));
|
72
|
+
return Qnil;
|
73
|
+
}
|
74
|
+
|
75
|
+
VALUE perform_scan(VALUE device_id_in)
|
76
|
+
{
|
77
|
+
int device_id = FIX2INT(device_id_in);
|
78
|
+
int device_handle = device_handles[device_id];
|
79
|
+
unsigned char buf[HCI_MAX_EVENT_SIZE], *ptr;
|
80
|
+
int len;
|
81
|
+
int keep_scanning = 1;
|
82
|
+
while (keep_scanning) {
|
83
|
+
evt_le_meta_event *meta;
|
84
|
+
le_advertising_info *info;
|
85
|
+
|
86
|
+
// wait for data with a timeout
|
87
|
+
fd_set set;
|
88
|
+
FD_ZERO(&set);
|
89
|
+
FD_SET(device_handle, &set);
|
90
|
+
struct timeval timeout;
|
91
|
+
timeout.tv_sec = 0;
|
92
|
+
timeout.tv_usec = 200000; // 200ms
|
93
|
+
int ret = select(device_handle + 1, &set, NULL, NULL, &timeout);
|
94
|
+
if (ret < 0) {
|
95
|
+
rb_raise(rb_eException, "Error waiting for data");
|
96
|
+
} else if (ret == 0) {
|
97
|
+
// timeout. yield nil to give ruby a chance to stop the scan.
|
98
|
+
keep_scanning = rb_yield(Qnil) != Qfalse;
|
99
|
+
continue;
|
100
|
+
}
|
101
|
+
|
102
|
+
// keep trying to read until we get something
|
103
|
+
while ((len = read(device_handle, buf, sizeof(buf))) < 0) {
|
104
|
+
if (errno == EAGAIN || errno == EINTR) {
|
105
|
+
continue;
|
106
|
+
}
|
107
|
+
keep_scanning = 0;
|
108
|
+
break;
|
109
|
+
}
|
110
|
+
|
111
|
+
if (len > 0) {
|
112
|
+
ptr = buf + (1 + HCI_EVENT_HDR_SIZE);
|
113
|
+
len -= (1 + HCI_EVENT_HDR_SIZE);
|
114
|
+
meta = (void *) ptr;
|
115
|
+
// check if this event is an advertisement
|
116
|
+
if (meta->subevent != EVT_LE_ADVERTISING_REPORT) {
|
117
|
+
continue;
|
118
|
+
}
|
119
|
+
// parse out the ad data, the mac, and the rssi
|
120
|
+
info = (le_advertising_info *) (meta->data + 1);
|
121
|
+
VALUE rssi = INT2FIX( (int8_t)info->data[info->length] );
|
122
|
+
VALUE ad_data = rb_str_new((void *)info->data, info->length);
|
123
|
+
VALUE addr = ba2value(&info->bdaddr);
|
124
|
+
keep_scanning = rb_yield_values(3, addr, ad_data, rssi) != Qfalse;
|
125
|
+
}
|
126
|
+
}
|
127
|
+
return Qnil;
|
128
|
+
}
|
129
|
+
|
130
|
+
VALUE stop_scan(VALUE device_id_in)
|
131
|
+
{
|
132
|
+
int device_id = FIX2INT(device_id_in);
|
133
|
+
int device_handle = device_handles[device_id];
|
134
|
+
|
135
|
+
// put back the old filter
|
136
|
+
setsockopt(device_handle, SOL_HCI, HCI_FILTER, &stored_filters[device_id], sizeof(stored_filters[0]));
|
137
|
+
|
138
|
+
// stop scanning
|
139
|
+
hci_le_set_scan_enable(device_handle, 0x00, 0x00, 1000);
|
140
|
+
hci_close_dev(device_handle);
|
141
|
+
return Qnil;
|
142
|
+
}
|
143
|
+
|
144
|
+
#endif // linux
|
data/ext/bluez/utils.c
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#ifdef linux
|
2
|
+
#include "ruby.h"
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <errno.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <sys/ioctl.h>
|
7
|
+
#include <sys/socket.h>
|
8
|
+
#include <bluetooth/bluetooth.h>
|
9
|
+
#include <bluetooth/hci.h>
|
10
|
+
#include <bluetooth/hci_lib.h>
|
11
|
+
|
12
|
+
#include "utils.h"
|
13
|
+
|
14
|
+
VALUE ba2value(bdaddr_t *bdaddr)
|
15
|
+
{
|
16
|
+
char addr[18];
|
17
|
+
ba2str(bdaddr, addr);
|
18
|
+
return rb_str_new2(addr);
|
19
|
+
}
|
20
|
+
|
21
|
+
#endif // linux
|
data/ext/bluez/utils.h
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
#ifdef __APPLE__
|
2
|
+
|
3
|
+
// Include the Ruby headers and goodies
|
4
|
+
#include "ruby.h"
|
5
|
+
#import <Foundation/Foundation.h>
|
6
|
+
#import <CoreBluetooth/CoreBluetooth.h>
|
7
|
+
|
8
|
+
// Defining a space for information and references about the module to be stored internally
|
9
|
+
VALUE cb_module = Qnil;
|
10
|
+
|
11
|
+
// Prototype for the initialization method - Ruby calls this, not you
|
12
|
+
void Init_core_bluetooth();
|
13
|
+
VALUE method_scan();
|
14
|
+
VALUE method_new_adverts();
|
15
|
+
VALUE new_scan_hash(NSString* device, NSData *data, NSNumber *rssi, NSData *service_uuid);
|
16
|
+
|
17
|
+
@interface BLEDelegate : NSObject <CBCentralManagerDelegate>
|
18
|
+
- (NSArray *)scans;
|
19
|
+
@end
|
20
|
+
|
21
|
+
@implementation BLEDelegate {
|
22
|
+
NSMutableArray *_scans;
|
23
|
+
}
|
24
|
+
|
25
|
+
- (id)init
|
26
|
+
{
|
27
|
+
self = [super init];
|
28
|
+
_scans = [[NSMutableArray alloc] init];
|
29
|
+
return self;
|
30
|
+
}
|
31
|
+
|
32
|
+
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
|
33
|
+
advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
|
34
|
+
{
|
35
|
+
NSData *mfgData = advertisementData[@"kCBAdvDataManufacturerData"];
|
36
|
+
NSDictionary *serviceData = advertisementData[@"kCBAdvDataServiceData"];
|
37
|
+
if (mfgData) {
|
38
|
+
NSDictionary *scan = @{@"device": peripheral.identifier.UUIDString,
|
39
|
+
@"data": mfgData,
|
40
|
+
@"rssi": RSSI
|
41
|
+
};
|
42
|
+
@synchronized(_scans) {
|
43
|
+
[_scans addObject: scan];
|
44
|
+
}
|
45
|
+
} else if (serviceData) {
|
46
|
+
NSData *svcData = [serviceData allValues][0];
|
47
|
+
CBUUID *uuid = serviceData.allKeys[0];
|
48
|
+
NSDictionary *scan = @{@"device": peripheral.identifier.UUIDString,
|
49
|
+
@"data": svcData,
|
50
|
+
@"rssi": RSSI,
|
51
|
+
@"service_uuid": uuid.data
|
52
|
+
};
|
53
|
+
@synchronized(_scans) {
|
54
|
+
[_scans addObject: scan];
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
|
60
|
+
{
|
61
|
+
[central scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @(YES)}];
|
62
|
+
}
|
63
|
+
|
64
|
+
- (NSArray *) scans
|
65
|
+
{
|
66
|
+
NSArray *scanCopy;
|
67
|
+
@synchronized(_scans) {
|
68
|
+
scanCopy = _scans;
|
69
|
+
_scans = [[NSMutableArray alloc] init];
|
70
|
+
}
|
71
|
+
return scanCopy;
|
72
|
+
}
|
73
|
+
|
74
|
+
@end
|
75
|
+
|
76
|
+
VALUE sym_device = Qnil;
|
77
|
+
VALUE sym_data = Qnil;
|
78
|
+
VALUE sym_rssi = Qnil;
|
79
|
+
VALUE sym_service_uuid = Qnil;
|
80
|
+
BLEDelegate *bleDelegate;
|
81
|
+
CBCentralManager *centralManager;
|
82
|
+
|
83
|
+
// initialize our module here
|
84
|
+
void Init_core_bluetooth()
|
85
|
+
{
|
86
|
+
VALUE scan_beacon_module = rb_const_get(rb_cObject, rb_intern("ScanBeacon"));
|
87
|
+
cb_module = rb_define_module_under(scan_beacon_module, "CoreBluetooth");
|
88
|
+
rb_define_singleton_method(cb_module, "scan", method_scan, 0);
|
89
|
+
rb_define_singleton_method(cb_module, "new_adverts", method_new_adverts, 0);
|
90
|
+
|
91
|
+
sym_device = ID2SYM(rb_intern("device"));
|
92
|
+
sym_data = ID2SYM(rb_intern("data"));
|
93
|
+
sym_rssi = ID2SYM(rb_intern("rssi"));
|
94
|
+
sym_service_uuid = ID2SYM(rb_intern("service_uuid"));
|
95
|
+
}
|
96
|
+
|
97
|
+
// create a ruby hash to yield back to ruby,
|
98
|
+
// of the form {device: "xxxx", data: "yyyy", rssi: -99}
|
99
|
+
VALUE new_scan_hash(NSString* device, NSData *data, NSNumber *rssi, NSData *service_uuid)
|
100
|
+
{
|
101
|
+
VALUE hash = rb_hash_new();
|
102
|
+
rb_hash_aset(hash, sym_device, rb_str_new_cstr(device.UTF8String));
|
103
|
+
rb_hash_aset(hash, sym_data, rb_str_new(data.bytes, data.length));
|
104
|
+
rb_hash_aset(hash, sym_rssi, INT2FIX( rssi.integerValue ));
|
105
|
+
if (service_uuid) {
|
106
|
+
uint16_t uuid = *((uint16_t *)service_uuid.bytes);
|
107
|
+
uuid = NSSwapShort(uuid);
|
108
|
+
rb_hash_aset(hash, sym_service_uuid, rb_str_new( (void*)&uuid, 2));
|
109
|
+
}
|
110
|
+
return hash;
|
111
|
+
}
|
112
|
+
|
113
|
+
VALUE method_new_adverts()
|
114
|
+
{
|
115
|
+
@autoreleasepool {
|
116
|
+
VALUE ary = rb_ary_new();
|
117
|
+
NSArray *scans = bleDelegate.scans;
|
118
|
+
for (NSDictionary *scan in scans) {
|
119
|
+
VALUE hash = new_scan_hash( scan[@"device"], scan[@"data"], scan[@"rssi"], scan[@"service_uuid"] );
|
120
|
+
rb_ary_push(ary, hash);
|
121
|
+
}
|
122
|
+
[scans release];
|
123
|
+
return ary;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
VALUE method_scan()
|
128
|
+
{
|
129
|
+
VALUE scans = rb_ary_new();
|
130
|
+
|
131
|
+
@autoreleasepool {
|
132
|
+
@try {
|
133
|
+
bleDelegate = [[BLEDelegate alloc] init];
|
134
|
+
dispatch_queue_t scanQueue;
|
135
|
+
scanQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
|
136
|
+
centralManager = [[CBCentralManager alloc] initWithDelegate:bleDelegate queue:scanQueue];
|
137
|
+
BOOL exit = NO;
|
138
|
+
while (!exit) {
|
139
|
+
VALUE ret = rb_yield( Qnil );
|
140
|
+
exit = (ret == Qfalse);
|
141
|
+
}
|
142
|
+
}
|
143
|
+
@finally {
|
144
|
+
[centralManager stopScan];
|
145
|
+
}
|
146
|
+
}
|
147
|
+
return Qnil;
|
148
|
+
}
|
149
|
+
|
150
|
+
#endif // TARGET_OS_MAC
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Loads mkmf which is used to make makefiles for Ruby extensions
|
2
|
+
require 'mkmf'
|
3
|
+
|
4
|
+
# Give it a name
|
5
|
+
extension_name = 'scan_beacon/core_bluetooth'
|
6
|
+
|
7
|
+
# The destination
|
8
|
+
dir_config(extension_name)
|
9
|
+
|
10
|
+
$DLDFLAGS << " -framework Foundation"
|
11
|
+
$DLDFLAGS << " -framework CoreBluetooth"
|
12
|
+
|
13
|
+
unless RUBY_PLATFORM =~ /darwin/
|
14
|
+
# don't compile the code on non-mac platforms because
|
15
|
+
# CoreBluetooth wont be there, and we may not even have
|
16
|
+
# the ability to compile ObjC code.
|
17
|
+
COMPILE_C = "echo"
|
18
|
+
# create a dummy .so file so RubyGems thinks everything
|
19
|
+
# was successful. We wont try to load it anyway.
|
20
|
+
LINK_SO = "touch $@"
|
21
|
+
end
|
22
|
+
|
23
|
+
create_makefile(extension_name)
|
data/lib/scan_beacon/beacon.rb
CHANGED
@@ -3,11 +3,14 @@ require 'set'
|
|
3
3
|
module ScanBeacon
|
4
4
|
class Beacon
|
5
5
|
|
6
|
-
attr_accessor :mac, :ids, :power, :beacon_types
|
6
|
+
attr_accessor :mac, :ids, :power, :beacon_types, :data, :mfg_id, :service_uuid
|
7
7
|
|
8
8
|
def initialize(opts={})
|
9
|
-
@ids = opts[:ids]
|
9
|
+
@ids = opts[:ids] || []
|
10
|
+
@data = opts[:data] || []
|
10
11
|
@power = opts[:power]
|
12
|
+
@mfg_id = opts[:mfg_id]
|
13
|
+
@service_uuid = opts[:service_uuid]
|
11
14
|
@beacon_types = Set.new [opts[:beacon_type]]
|
12
15
|
@rssis = []
|
13
16
|
end
|
@@ -1,12 +1,27 @@
|
|
1
1
|
module ScanBeacon
|
2
2
|
class BeaconParser
|
3
|
-
|
3
|
+
DEFAULT_LAYOUTS = {
|
4
|
+
altbeacon: "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25",
|
5
|
+
eddystone_uid: "s:0-1=aafe,m:2-2=00,p:3-3:-41,i:4-13,i:14-19;d:20-21"
|
6
|
+
}
|
7
|
+
AD_TYPE_MFG = 0xff
|
8
|
+
AD_TYPE_SERVICE = 0x03
|
9
|
+
BT_EIR_SERVICE_DATA = "\x16"
|
4
10
|
attr_accessor :beacon_type
|
5
11
|
|
12
|
+
def self.default_parsers
|
13
|
+
DEFAULT_LAYOUTS.map {|name, layout| BeaconParser.new name, layout }
|
14
|
+
end
|
15
|
+
|
6
16
|
def initialize(beacon_type, layout)
|
7
17
|
@beacon_type = beacon_type
|
8
18
|
@layout = layout.split(",")
|
9
|
-
|
19
|
+
if layout.include?("s")
|
20
|
+
@ad_type = AD_TYPE_SERVICE
|
21
|
+
else
|
22
|
+
@ad_type = AD_TYPE_MFG
|
23
|
+
end
|
24
|
+
@matchers = @layout.find_all {|item| ["m", "s"].include? item[0]}.map {|matcher|
|
10
25
|
_, range_start, range_end, expected = matcher.split(/:|=|-/)
|
11
26
|
{start: range_start.to_i, end: range_end.to_i, expected: expected}
|
12
27
|
}
|
@@ -14,6 +29,10 @@ module ScanBeacon
|
|
14
29
|
_, range_start, range_end = id.split(/:|-/)
|
15
30
|
{start: range_start.to_i, end: range_end.to_i}
|
16
31
|
}
|
32
|
+
@data_fields = @layout.find_all {|item| item[0] == "d"}.map {|field|
|
33
|
+
_, range_start, range_end = field.split(/:|-/)
|
34
|
+
{start: range_start.to_i, end: range_end.to_i}
|
35
|
+
}
|
17
36
|
_, power_start, power_end = @layout.find {|item| item[0] == "p"}.split(/:|-/)
|
18
37
|
@power = {start: power_start.to_i, end: power_end.to_i}
|
19
38
|
end
|
@@ -25,27 +44,88 @@ module ScanBeacon
|
|
25
44
|
return true
|
26
45
|
end
|
27
46
|
|
28
|
-
def parse(data)
|
29
|
-
return nil if !matches?(data)
|
30
|
-
|
47
|
+
def parse(data, ad_type = AD_TYPE_MFG)
|
48
|
+
return nil if ad_type != @ad_type || !matches?(data)
|
49
|
+
if @ad_type == AD_TYPE_MFG
|
50
|
+
Beacon.new(ids: parse_ids(data), power: parse_power(data), beacon_type: @beacon_type,
|
51
|
+
data: parse_data_fields(data), mfg_id: parse_mfg_or_service_id(data))
|
52
|
+
else
|
53
|
+
Beacon.new(ids: parse_ids(data), power: parse_power(data), beacon_type: @beacon_type,
|
54
|
+
data: parse_data_fields(data), service_uuid: parse_mfg_or_service_id(data))
|
55
|
+
end
|
31
56
|
end
|
32
57
|
|
33
58
|
def parse_ids(data)
|
34
|
-
@ids
|
35
|
-
|
59
|
+
parse_elems(@ids, data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_data_fields(data)
|
63
|
+
parse_elems(@data_fields, data)
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_elems(elems, data)
|
67
|
+
elems.map {|elem|
|
68
|
+
elem_str = data[elem[:start]..elem[:end]]
|
69
|
+
elem_length = elem_str.size
|
70
|
+
case elem_length
|
71
|
+
when 1
|
72
|
+
elem_str.unpack('C')[0]
|
73
|
+
when 2
|
36
74
|
# two bytes, so treat it as a short (big endian)
|
37
|
-
|
75
|
+
elem_str.unpack('S>')[0]
|
76
|
+
when 6
|
77
|
+
# 6 bytes, treat it is an eddystone instance id
|
78
|
+
("\x00\x00"+elem_str).unpack('Q>')[0]
|
38
79
|
else
|
39
80
|
# not two bytes, so treat it as a hex string
|
40
|
-
|
81
|
+
elem_str.unpack('H*').join
|
41
82
|
end
|
42
83
|
}
|
43
84
|
end
|
44
85
|
|
86
|
+
def parse_mfg_or_service_id(data)
|
87
|
+
data[0..1].unpack('S>')[0]
|
88
|
+
end
|
89
|
+
|
45
90
|
def parse_power(data)
|
46
91
|
data[@power[:start]..@power[:end]].unpack('c')[0]
|
47
92
|
end
|
48
93
|
|
94
|
+
def generate_ad(beacon)
|
95
|
+
length = [@matchers, @ids, @power, @data_fields].flatten.map {|elem| elem[:end] }.max + 1
|
96
|
+
ad = "\x00" * length
|
97
|
+
@matchers.each do |matcher|
|
98
|
+
ad[matcher[:start]..matcher[:end]] = [matcher[:expected]].pack("H*")
|
99
|
+
end
|
100
|
+
@ids.each_with_index do |id, index|
|
101
|
+
ad[id[:start]..id[:end]] = generate_field(id, beacon.ids[index])
|
102
|
+
end
|
103
|
+
@data_fields.each_with_index do |field, index|
|
104
|
+
ad[field[:start]..field[:end]] = generate_field(field, beacon.data[index]) unless beacon.data[index].nil?
|
105
|
+
end
|
106
|
+
ad[@power[:start]..@power[:end]] = [beacon.power].pack('c')
|
107
|
+
if @ad_type == AD_TYPE_SERVICE
|
108
|
+
"\x03\x03" + [beacon.service_uuid].pack("S<") + [length+1].pack('C') + BT_EIR_SERVICE_DATA + ad
|
109
|
+
elsif @ad_type == AD_TYPE_MFG
|
110
|
+
ad[0..1] = [beacon.mfg_id].pack("S<")
|
111
|
+
[length+1].pack('C') + [AD_TYPE_MFG].pack('C') + ad
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def generate_field(field, value)
|
116
|
+
field_length = field[:end] - field[:start] + 1
|
117
|
+
case field_length
|
118
|
+
when 1
|
119
|
+
[value].pack("c")
|
120
|
+
when 2
|
121
|
+
[value].pack("S>")
|
122
|
+
when 6
|
123
|
+
[value].pack("Q>")[2..-1]
|
124
|
+
else
|
125
|
+
[value].pack("H*")[0..field_length-1]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
49
129
|
def inspect
|
50
130
|
"<BeaconParser type=\"#{@beacon_type}\", layout=\"#{@layout.join(",")}\">"
|
51
131
|
end
|
@@ -1,62 +1,26 @@
|
|
1
|
-
require 'timeout'
|
2
|
-
|
3
1
|
module ScanBeacon
|
4
|
-
class BLE112Scanner
|
5
|
-
|
6
|
-
DEFAULT_LAYOUTS = {altbeacon: "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"}
|
7
|
-
|
8
|
-
attr_reader :beacons
|
2
|
+
class BLE112Scanner < GenericScanner
|
9
3
|
|
10
4
|
def initialize(opts = {})
|
5
|
+
super
|
11
6
|
@device = BLE112Device.new opts[:port]
|
12
|
-
@cycle_seconds = opts[:cycle_seconds] || 1
|
13
|
-
@parsers = DEFAULT_LAYOUTS.map {|name, layout| BeaconParser.new name, layout }
|
14
|
-
@beacons = []
|
15
7
|
end
|
16
8
|
|
17
|
-
def
|
18
|
-
@parsers << parser
|
19
|
-
end
|
20
|
-
|
21
|
-
def scan
|
9
|
+
def each_advertisement
|
22
10
|
@device.open do |device|
|
23
11
|
device.start_scan
|
24
|
-
cycle_end = Time.now + @cycle_seconds
|
25
|
-
|
26
12
|
begin
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
cycle_end = Time.now + @cycle_seconds
|
13
|
+
keep_scanning = true
|
14
|
+
while keep_scanning do
|
15
|
+
response = device.read
|
16
|
+
if response.advertisement?
|
17
|
+
keep_scanning = false if yield(response.advertisement_data, response.mac, response.rssi) == false
|
33
18
|
end
|
34
19
|
end
|
35
20
|
ensure
|
36
21
|
device.stop_scan
|
37
22
|
end
|
38
|
-
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def check_for_beacon(response)
|
43
|
-
if response.advertisement?
|
44
|
-
beacon = nil
|
45
|
-
if @parsers.detect {|parser| beacon = parser.parse(response.advertisement_data) }
|
46
|
-
beacon.mac = response.mac
|
47
|
-
add_beacon(beacon, response.rssi)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def add_beacon(beacon, rssi)
|
53
|
-
if idx = @beacons.find_index(beacon)
|
54
|
-
@beacons[idx].add_type beacon.beacon_types.first
|
55
|
-
beacon = @beacons[idx]
|
56
|
-
else
|
57
|
-
@beacons << beacon
|
58
23
|
end
|
59
|
-
beacon.add_rssi(rssi)
|
60
24
|
end
|
61
25
|
|
62
26
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ScanBeacon
|
2
|
+
class BlueZAdvertiser
|
3
|
+
|
4
|
+
attr_accessor :beacon, :parser, :ad
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@device_id = opts[:device_id]
|
8
|
+
self.beacon = opts[:beacon]
|
9
|
+
self.parser = opts[:parser]
|
10
|
+
self.parser ||= BeaconParser.default_parsers.find {|parser| parser.beacon_type == beacon.beacon_types.first}
|
11
|
+
end
|
12
|
+
|
13
|
+
def beacon=(value)
|
14
|
+
@beacon = value
|
15
|
+
update_ad
|
16
|
+
end
|
17
|
+
|
18
|
+
def parser=(value)
|
19
|
+
@parser = value
|
20
|
+
update_ad
|
21
|
+
end
|
22
|
+
|
23
|
+
def ad=(value)
|
24
|
+
@ad = value
|
25
|
+
BlueZ.advertisement_bytes = @ad
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
BlueZ.start_advertising
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
BlueZ.stop_advertising
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"<BlueZAdvertiser ad=#{@ad.inspect}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_ad
|
41
|
+
self.ad = @parser.generate_ad(@beacon) if @parser && @beacon
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ScanBeacon
|
2
|
+
class BlueZScanner < GenericScanner
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
super
|
6
|
+
@device_id = opts[:device_id] || BlueZ.devices[0][:device_id]
|
7
|
+
BlueZ.device_up @device_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def each_advertisement
|
11
|
+
ScanBeacon::BlueZ.scan(@device_id) do |mac, ad_data, rssi|
|
12
|
+
if ad_data.nil?
|
13
|
+
yield(nil)
|
14
|
+
elsif ad_data.size > 4
|
15
|
+
ad_type = ad_data[4].unpack("C")[0]
|
16
|
+
if ad_type == 0xff
|
17
|
+
yield(ad_data[5..-1], mac, rssi, ad_type)
|
18
|
+
else
|
19
|
+
yield(ad_data[9..-1], mac, rssi, ad_type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ScanBeacon
|
2
|
+
class CoreBluetoothScanner < GenericScanner
|
3
|
+
|
4
|
+
def each_advertisement
|
5
|
+
keep_scanning = true
|
6
|
+
CoreBluetooth::scan do
|
7
|
+
sleep 0.2
|
8
|
+
advertisements = CoreBluetooth::new_adverts
|
9
|
+
advertisements.each do |scan|
|
10
|
+
if scan[:service_uuid]
|
11
|
+
advert = scan[:service_uuid] + scan[:data]
|
12
|
+
keep_scanning = false if yield(advert, scan[:device], scan[:rssi], 0x03) == false
|
13
|
+
else
|
14
|
+
keep_scanning = false if yield(scan[:data], scan[:device], scan[:rssi], 0xff) == false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
if advertisements.empty?
|
18
|
+
keep_scanning = false if yield(nil, nil, nil) == false
|
19
|
+
end
|
20
|
+
keep_scanning
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ScanBeacon
|
2
|
+
class GenericScanner
|
3
|
+
|
4
|
+
attr_reader :beacons
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@cycle_seconds = opts[:cycle_seconds] || 1
|
8
|
+
@parsers = BeaconParser.default_parsers
|
9
|
+
@beacons = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_parser(parser)
|
13
|
+
@parsers << parser
|
14
|
+
end
|
15
|
+
|
16
|
+
def scan
|
17
|
+
@beacons = []
|
18
|
+
next_cycle = Time.now + @cycle_seconds
|
19
|
+
keep_scanning = true
|
20
|
+
each_advertisement do |data, device, rssi, ad_type = BeaconParser::AD_TYPE_MFG|
|
21
|
+
detect_beacon(data, device, rssi, ad_type) unless data.nil?
|
22
|
+
|
23
|
+
if Time.now > next_cycle
|
24
|
+
if block_given?
|
25
|
+
next_cycle = Time.now + @cycle_seconds
|
26
|
+
yield @beacons
|
27
|
+
@beacons = []
|
28
|
+
else
|
29
|
+
keep_scanning = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return @beacons unless block_given?
|
34
|
+
end
|
35
|
+
|
36
|
+
def each_advertisement
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def detect_beacon(data, device, rssi, ad_type)
|
41
|
+
beacon = nil
|
42
|
+
if @parsers.detect {|parser| beacon = parser.parse(data, ad_type) }
|
43
|
+
beacon.mac = device
|
44
|
+
add_beacon(beacon, rssi)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_beacon(beacon, rssi)
|
49
|
+
if idx = @beacons.find_index(beacon)
|
50
|
+
@beacons[idx].add_type beacon.beacon_types.first
|
51
|
+
beacon = @beacons[idx]
|
52
|
+
else
|
53
|
+
@beacons << beacon
|
54
|
+
end
|
55
|
+
beacon.add_rssi(rssi)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
data/lib/scan_beacon/version.rb
CHANGED
data/lib/scan_beacon.rb
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
require "scan_beacon/version"
|
2
2
|
require "scan_beacon/beacon"
|
3
3
|
require "scan_beacon/beacon_parser"
|
4
|
+
require "scan_beacon/generic_scanner"
|
4
5
|
require "scan_beacon/ble112_device"
|
5
6
|
require "scan_beacon/ble112_scanner"
|
7
|
+
case RUBY_PLATFORM
|
8
|
+
when /darwin/
|
9
|
+
require "scan_beacon/core_bluetooth"
|
10
|
+
require "scan_beacon/core_bluetooth_scanner"
|
11
|
+
when /linux/
|
12
|
+
require "scan_beacon/bluez"
|
13
|
+
require "scan_beacon/bluez_scanner"
|
14
|
+
require "scan_beacon/bluez_advertiser"
|
15
|
+
end
|
6
16
|
|
7
17
|
module ScanBeacon
|
8
18
|
end
|
data/scan_beacon.gemspec
CHANGED
@@ -16,9 +16,11 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
17
|
spec.bindir = "exe"
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.extensions = %w[ext/core_bluetooth/extconf.rb ext/bluez/extconf.rb]
|
19
20
|
spec.require_paths = ["lib"]
|
20
21
|
|
21
22
|
spec.add_development_dependency "bundler", "~> 1.9"
|
22
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
24
|
spec.add_development_dependency "rspec", "~> 3.2"
|
25
|
+
spec.add_development_dependency "rake-compile"
|
24
26
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scan_beacon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Radius Networks
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,11 +52,27 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake-compile
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description:
|
56
70
|
email:
|
57
71
|
- support@radiusnetworks.com
|
58
72
|
executables: []
|
59
|
-
extensions:
|
73
|
+
extensions:
|
74
|
+
- ext/core_bluetooth/extconf.rb
|
75
|
+
- ext/bluez/extconf.rb
|
60
76
|
extra_rdoc_files: []
|
61
77
|
files:
|
62
78
|
- ".gitignore"
|
@@ -64,11 +80,24 @@ files:
|
|
64
80
|
- LICENSE.txt
|
65
81
|
- README.md
|
66
82
|
- Rakefile
|
83
|
+
- ext/bluez/advertising.c
|
84
|
+
- ext/bluez/bluez.c
|
85
|
+
- ext/bluez/devices.c
|
86
|
+
- ext/bluez/extconf.rb
|
87
|
+
- ext/bluez/scanning.c
|
88
|
+
- ext/bluez/utils.c
|
89
|
+
- ext/bluez/utils.h
|
90
|
+
- ext/core_bluetooth/core_bluetooth.m
|
91
|
+
- ext/core_bluetooth/extconf.rb
|
67
92
|
- lib/scan_beacon.rb
|
68
93
|
- lib/scan_beacon/beacon.rb
|
69
94
|
- lib/scan_beacon/beacon_parser.rb
|
70
95
|
- lib/scan_beacon/ble112_device.rb
|
71
96
|
- lib/scan_beacon/ble112_scanner.rb
|
97
|
+
- lib/scan_beacon/bluez_advertiser.rb
|
98
|
+
- lib/scan_beacon/bluez_scanner.rb
|
99
|
+
- lib/scan_beacon/core_bluetooth_scanner.rb
|
100
|
+
- lib/scan_beacon/generic_scanner.rb
|
72
101
|
- lib/scan_beacon/version.rb
|
73
102
|
- scan_beacon.gemspec
|
74
103
|
homepage: https://github.com/RadiusNetworks/scanbeacon-gem
|