scan_beacon 0.3.6 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|