scan_beacon 0.6.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b2de8ba974098283895a45b85e443df9ee44d24d
4
- data.tar.gz: 9dad130a30cd2734545fc1c264c5abaf932e9a47
3
+ metadata.gz: 3b38f927f67bef75a626e0509245b4588b7cd62a
4
+ data.tar.gz: 6bffd5d1c56c0a5c560d1c403d66898ac9ec1053
5
5
  SHA512:
6
- metadata.gz: 54bdcd6601ca6dc33431c39049bc1792ee48774ed2d2e99b033569e5949f16bf36bd02e7a92ba86645ff7abdb2a50b80aa60941fb37b6de60d89dc0587252a3c
7
- data.tar.gz: ce40ebea3c1120b8ccbc8434853b5fb4697b347f5459cdc8fb922dae8bdee1c1b5b2d761d068a38db3666d436a0e53f41ae93b25783139838212d3913f9facbd
6
+ metadata.gz: da91c39c519a4d065c0ad22f29bb4d876350ecb03cfda92b13907f0bf5953df5e45e788742c81e590123c612ec6d9c4b9b40fa282559d0f61ffc75475a8b8ba5
7
+ data.tar.gz: 3840835c733003843718246959c5d76c913a44f70f0b0f7dbe35827b2a317bfcb0b16db9c6774d6abdf511c6caa5eff1bbe77c54c48d3b04c88bff6622eb81c1
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 CoreBluetooth (on Mac OS X) or a BlueGiga BLE112 device (on mac or linux)
3
+ A ruby gem that allows you to scan for beacon advertisements using IOBluetooth (on Mac OS X) or a BlueGiga BLE112 device (on mac or linux)
4
4
 
5
5
  # Example Usage
6
6
 
@@ -13,12 +13,14 @@ gem install scan_beacon
13
13
  ## Create your scanner
14
14
  ``` ruby
15
15
  require 'scan_beacon'
16
+ # to scan using the default device on mac or linux
17
+ scanner = ScanBeacon::DefaultScanner.new
16
18
  # to scan using CoreBluetooth on a mac
17
19
  scanner = ScanBeacon::CoreBluetoothScanner.new
18
- # to scan using a BLE112 device
19
- scanner = ScanBeacon::BLE112Scanner.new
20
20
  # to scan using BlueZ on Linux (make sure you have privileges)
21
21
  scanner = ScanBeacon::BlueZScanner.new
22
+ # to scan using a BLE112 device
23
+ scanner = ScanBeacon::BLE112Scanner.new
22
24
  ```
23
25
 
24
26
  ## Start a scan, yield beacons in a loop
@@ -57,7 +59,7 @@ scanner.add_parser( ScanBeacon::BeaconParser.new(:mybeacon, "m:2-3=0000,i:4-19,i
57
59
  ...
58
60
  ```
59
61
 
60
- ## Advertise as a beacon on Linux using BlueZ
62
+ ## Advertise as a beacon on Linux using BlueZ or a Mac using IOBluetooth
61
63
  Example:
62
64
  ``` ruby
63
65
  # altbeacon
@@ -67,7 +69,7 @@ beacon = ScanBeacon::Beacon.new(
67
69
  mfg_id: 0x0118,
68
70
  beacon_type: :altbeacon
69
71
  )
70
- advertiser = ScanBeacon::BlueZAdvertiser.new(beacon: beacon)
72
+ advertiser = ScanBeacon::DefaultAdvertiser.new(beacon: beacon)
71
73
  advertiser.start
72
74
  ...
73
75
  advertiser.stop
@@ -79,7 +81,17 @@ beacon = ScanBeacon::Beacon.new(
79
81
  service_uuid: 0xFEAA,
80
82
  beacon_type: :eddystone_uid
81
83
  )
82
- advertiser = ScanBeacon::BlueZAdvertiser.new(beacon: beacon)
84
+ advertiser = ScanBeacon::DefaultAdvertiser.new(beacon: beacon)
85
+ advertiser.start
86
+ ...
87
+ advertiser.stop
88
+
89
+ # Eddystone URL (PhysicalWeb)
90
+ beacon = ScanBeacon::EddystoneUrlBeacon.new(
91
+ url: "http://radiusnetworks.com",
92
+ power: -20,
93
+ )
94
+ advertiser = ScanBeacon::DefaultAdvertiser.new(beacon: beacon)
83
95
  advertiser.start
84
96
  ...
85
97
  advertiser.stop
@@ -87,6 +99,4 @@ advertiser.stop
87
99
 
88
100
 
89
101
  # Dependencies
90
- 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).
91
-
92
- To advertise as a beacon, you must have a Linux machine with BlueZ installed.
102
+ To scan for beacons or advertise, 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).
@@ -3,6 +3,7 @@
3
3
  // Include the Ruby headers and goodies
4
4
  #include "ruby.h"
5
5
  #import <Foundation/Foundation.h>
6
+ #import <IOBluetooth/IOBluetooth.h>
6
7
  #import <CoreBluetooth/CoreBluetooth.h>
7
8
 
8
9
  // Defining a space for information and references about the module to be stored internally
@@ -14,6 +15,19 @@ VALUE method_scan();
14
15
  VALUE method_new_adverts();
15
16
  VALUE new_scan_hash(NSString* device, NSData *data, NSNumber *rssi, NSData *service_uuid);
16
17
 
18
+ VALUE method_set_advertisement_data(VALUE klass, VALUE data);
19
+ VALUE method_start_advertising();
20
+ VALUE method_stop_advertising();
21
+
22
+ // define some hidden methods so we can call them more easily
23
+ @interface IOBluetoothHostController ()
24
+ - (int)BluetoothHCILESetAdvertiseEnable:(unsigned char)arg1;
25
+ - (int)BluetoothHCILESetAdvertisingData:(unsigned char)arg1 advertsingData:(char *)arg2;
26
+ - (int)BluetoothHCILESetAdvertisingParameters:(unsigned short)arg1 advertisingIntervalMax:(unsigned short)arg2 advertisingType:(unsigned char)arg3 ownAddressType:(unsigned char)arg4 directAddressType:(unsigned char)arg5 directAddress:(struct BluetoothDeviceAddress { unsigned char x1[6]; }*)arg6 advertisingChannelMap:(unsigned char)arg7 advertisingFilterPolicy:(unsigned char)arg8;
27
+ - (int)BluetoothHCILESetScanParameters:(unsigned char)arg1 LEScanInterval:(unsigned short)arg2 LEScanWindow:(unsigned short)arg3 ownAddressType:(unsigned char)arg4 scanningFilterPolicy:(unsigned char)arg5;
28
+ - (int)BluetoothHCILESetScanEnable:(unsigned char)arg1 filterDuplicates:(unsigned char)arg2;
29
+ @end
30
+
17
31
  @interface BLEDelegate : NSObject <CBCentralManagerDelegate> {
18
32
  @private
19
33
  NSMutableArray *_scans;
@@ -61,6 +75,15 @@ VALUE new_scan_hash(NSString* device, NSData *data, NSNumber *rssi, NSData *serv
61
75
  - (void)centralManagerDidUpdateState:(CBCentralManager *)central
62
76
  {
63
77
  [central scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @(YES)}];
78
+
79
+ // set custom scan params to achieve better scanning performance
80
+ IOBluetoothHostController * device = IOBluetoothHostController.defaultController;
81
+ [device BluetoothHCILESetScanParameters:0x01
82
+ LEScanInterval:200
83
+ LEScanWindow:200
84
+ ownAddressType:0x00
85
+ scanningFilterPolicy:0x00];
86
+ [device BluetoothHCILESetScanEnable:0x01 filterDuplicates:0x00];
64
87
  }
65
88
 
66
89
  - (NSArray *) scans
@@ -90,6 +113,10 @@ void Init_core_bluetooth()
90
113
  rb_define_singleton_method(cb_module, "scan", method_scan, 0);
91
114
  rb_define_singleton_method(cb_module, "new_adverts", method_new_adverts, 0);
92
115
 
116
+ rb_define_singleton_method(cb_module, "set_advertisement_data", method_set_advertisement_data, 1);
117
+ rb_define_singleton_method(cb_module, "start_advertising", method_start_advertising, 0);
118
+ rb_define_singleton_method(cb_module, "stop_advertising", method_stop_advertising, 0);
119
+
93
120
  sym_device = ID2SYM(rb_intern("device"));
94
121
  sym_data = ID2SYM(rb_intern("data"));
95
122
  sym_rssi = ID2SYM(rb_intern("rssi"));
@@ -149,4 +176,37 @@ VALUE method_scan()
149
176
  return Qnil;
150
177
  }
151
178
 
179
+ VALUE method_set_advertisement_data(VALUE klass, VALUE data)
180
+ {
181
+ IOBluetoothHostController *device = [IOBluetoothHostController defaultController];
182
+ char flags_and_data[40];
183
+ memcpy(flags_and_data, "\x02\x01\x1A", 3);
184
+ memcpy(flags_and_data+3, RSTRING_PTR(data), RSTRING_LEN(data));
185
+ // NOTE: Mac OS X has a typo in the method definition. This may get fixed in the future.
186
+ [device BluetoothHCILESetAdvertisingData: RSTRING_LEN(data)+3 advertsingData: flags_and_data];
187
+ return Qnil;
188
+ }
189
+
190
+ VALUE method_start_advertising()
191
+ {
192
+ IOBluetoothHostController *device = [IOBluetoothHostController defaultController];
193
+ [device BluetoothHCILESetAdvertisingParameters: 0x00A0
194
+ advertisingIntervalMax: 0x00A0 // 100ms
195
+ advertisingType: 0x03
196
+ ownAddressType: 0x00
197
+ directAddressType: 0x00
198
+ directAddress: (void*)"\x00\x00\x00\x00\x00\x00"
199
+ advertisingChannelMap: 0x07 // all 3 channels
200
+ advertisingFilterPolicy: 0x00];
201
+ [device BluetoothHCILESetAdvertiseEnable: 1];
202
+ return Qnil;
203
+ }
204
+
205
+ VALUE method_stop_advertising()
206
+ {
207
+ IOBluetoothHostController *device = [IOBluetoothHostController defaultController];
208
+ [device BluetoothHCILESetAdvertiseEnable: 0];
209
+ return Qnil;
210
+ }
211
+
152
212
  #endif // TARGET_OS_MAC
@@ -10,6 +10,7 @@ dir_config(extension_name)
10
10
  if RUBY_PLATFORM =~ /darwin/
11
11
  $DLDFLAGS << " -framework Foundation"
12
12
  $DLDFLAGS << " -framework CoreBluetooth"
13
+ $DLDFLAGS << " -framework IOBluetooth"
13
14
  else
14
15
  # don't compile the code on non-mac platforms because
15
16
  # CoreBluetooth wont be there, and we may not even have
@@ -3,7 +3,7 @@ require 'set'
3
3
  module ScanBeacon
4
4
  class Beacon
5
5
 
6
- attr_accessor :mac, :ids, :power, :beacon_types, :data, :mfg_id, :service_uuid
6
+ attr_accessor :mac, :ids, :power, :beacon_types, :data, :mfg_id, :service_uuid, :rssis
7
7
 
8
8
  def initialize(opts={})
9
9
  @ids = opts[:ids] || []
@@ -12,7 +12,7 @@ module ScanBeacon
12
12
  @mfg_id = opts[:mfg_id]
13
13
  @service_uuid = opts[:service_uuid]
14
14
  @beacon_types = Set.new [opts[:beacon_type]]
15
- @rssis = []
15
+ @rssis = opts[:rssis] || []
16
16
  end
17
17
 
18
18
  def ==(obj)
@@ -2,7 +2,8 @@ module ScanBeacon
2
2
  class BeaconParser
3
3
  DEFAULT_LAYOUTS = {
4
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=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19;d:20-21"
5
+ eddystone_uid: "s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19;d:20-21",
6
+ eddystone_url: "s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-21v",
6
7
  }
7
8
  AD_TYPE_MFG = 0xff
8
9
  AD_TYPE_SERVICE = 0x03
@@ -36,6 +37,9 @@ module ScanBeacon
36
37
  end: range_end.to_i,
37
38
  length: range_end.to_i - range_start.to_i + 1,
38
39
  }
40
+ if range_end.end_with? 'v'
41
+ field_params[:var_length] = true
42
+ end
39
43
  field_params[:expected] = [expected].pack("H*") unless expected.nil?
40
44
  case field_type
41
45
  when 'm'
@@ -100,14 +104,19 @@ module ScanBeacon
100
104
  end
101
105
 
102
106
  def generate_ad(beacon)
103
- length = [@matchers, @ids, @power, @data_fields].flatten.map {|elem| elem[:end] }.max + 1
107
+ length = [@matchers, @ids, @power, @data_fields].flatten.map {|elem| elem[:start] }.max + 1
104
108
  ad = ("\x00" * length).force_encoding("ASCII-8BIT")
105
109
  @matchers.each do |matcher|
106
110
  ad[matcher[:start]..matcher[:end]] = matcher[:expected]
107
111
  end
108
112
  @ids.each_with_index do |id, index|
109
- id_bytes = Beacon::Field.field_with_length(beacon.ids[index], id[:length]).bytes
110
- ad[id[:start]..id[:end]] = id_bytes
113
+ if id[:var_length]
114
+ id_bytes = Beacon::Field.new(hex: beacon.ids[index]).bytes
115
+ ad[id[:start]..id[:start]+id_bytes.size] = id_bytes
116
+ else
117
+ id_bytes = Beacon::Field.field_with_length(beacon.ids[index], id[:length]).bytes
118
+ ad[id[:start]..id[:end]] = id_bytes
119
+ end
111
120
  end
112
121
  @data_fields.each_with_index do |field, index|
113
122
  unless beacon.data[index].nil?
@@ -116,6 +125,7 @@ module ScanBeacon
116
125
  end
117
126
  end
118
127
  ad[@power[:start]..@power[:end]] = [beacon.power].pack('c')
128
+ length = ad.size
119
129
  if @ad_type == AD_TYPE_SERVICE
120
130
  "\x03\x03".force_encoding("ASCII-8BIT") + [beacon.service_uuid].pack("S<") + [length+1].pack('C') + BT_EIR_SERVICE_DATA + ad
121
131
  elsif @ad_type == AD_TYPE_MFG
@@ -3,7 +3,7 @@ module ScanBeacon
3
3
 
4
4
  def initialize(opts = {})
5
5
  @device = BLE112Device.new opts[:port]
6
- super()
6
+ super(opts)
7
7
  end
8
8
 
9
9
  def start(with_rotation = false)
@@ -137,26 +137,10 @@ module ScanBeacon
137
137
  device_count = possible_devices.count
138
138
  possible_devices.each do |device_path|
139
139
  File.open(device_path, 'r+b') do |file|
140
- file.write([BG_COMMAND, 1, BG_MSG_CLASS_SYSTEM, BG_RESET, 0].pack('C*'))
141
- end
142
- end
143
-
144
- # wait for them to show up again, but only wait for up to 5 secs
145
- sleep 1
146
- wait_count = 0
147
- while possible_devices.count < device_count && wait_count < 50
148
- sleep 0.1
149
- wait_count += 1
150
- end
151
-
152
- # try to open them - if we get a busy, wait and try again
153
- possible_devices.each do |device_path|
154
- begin
155
- File.open(device_path, 'r+b') {|f| f.close }
156
- rescue Errno::EBUSY
157
- sleep 0.1
158
- retry
140
+ file.write([BG_COMMAND, 0, BG_MSG_CLASS_GAP, BG_DISCOVER_STOP].pack('C*'))
159
141
  end
142
+ # open and close the file to clear the buffer
143
+ File.open(device_path, 'r+b') {|file| }
160
144
  end
161
145
  end
162
146
 
@@ -50,4 +50,5 @@ module ScanBeacon
50
50
  end
51
51
 
52
52
  end
53
+ DefaultAdvertiser = BlueZAdvertiser
53
54
  end
@@ -23,4 +23,5 @@ module ScanBeacon
23
23
  end
24
24
 
25
25
  end
26
+ DefaultScanner = BlueZScanner
26
27
  end
@@ -0,0 +1,27 @@
1
+ module ScanBeacon
2
+ class CoreBluetoothAdvertiser < GenericIndividualAdvertiser
3
+
4
+ def initialize(opts = {})
5
+ super
6
+ end
7
+
8
+ def ad=(value)
9
+ @ad = value
10
+ CoreBluetooth.set_advertisement_data @ad
11
+ end
12
+
13
+ def start
14
+ CoreBluetooth.start_advertising
15
+ end
16
+
17
+ def stop
18
+ CoreBluetooth.stop_advertising
19
+ end
20
+
21
+ def inspect
22
+ "<CoreBluetoothAdvertiser ad=#{@ad.inspect}>"
23
+ end
24
+
25
+ end
26
+ DefaultAdvertiser = CoreBluetoothAdvertiser
27
+ end
@@ -22,4 +22,5 @@ module ScanBeacon
22
22
  end
23
23
 
24
24
  end
25
+ DefaultScanner = CoreBluetoothScanner
25
26
  end
@@ -0,0 +1,72 @@
1
+ module ScanBeacon
2
+ # Convenience class for constructing & advertising Eddystone-URL frames
3
+ class EddystoneUrlBeacon < Beacon
4
+
5
+ SCHEMES = {"http://www." => "\x00",
6
+ "https://www." => "\x01",
7
+ "http://" => "\x02",
8
+ "https://" => "\x03"}
9
+
10
+ EXPANSIONS = {".com/" => "\x00",
11
+ ".org/" => "\x01",
12
+ ".edu/" => "\x02",
13
+ ".net/" => "\x03",
14
+ ".info/" => "\x04",
15
+ ".biz/" => "\x05",
16
+ ".gov/" => "\x06",
17
+ ".com" => "\x07",
18
+ ".org" => "\x08",
19
+ ".edu" => "\x09",
20
+ ".net" => "\x0a",
21
+ ".info" => "\x0b",
22
+ ".biz" => "\x0c",
23
+ ".gov" => "\x0d"}
24
+
25
+ def initialize(opts = {})
26
+ opts[:service_uuid] ||= 0xFEAA
27
+ opts[:beacon_type] ||= :eddystone_url
28
+ super opts
29
+ self.url = opts[:url] if opts[:url]
30
+ end
31
+
32
+ def self.from_beacon(beacon)
33
+ new(ids: beacon.ids, power: beacon.power, rssis: beacon.rssis)
34
+ end
35
+
36
+ def url
37
+ @url ||= self.class.decompress_url( self.ids[0].to_s )
38
+ end
39
+
40
+ def url=(new_url)
41
+ @url = new_url
42
+ self.ids = [self.class.compress_url(@url)]
43
+ end
44
+
45
+ def self.compress_url(url)
46
+ scheme, scheme_code = SCHEMES.find {|k, v| url.start_with? k}
47
+ raise ArgumentError, "Invalid URL" if scheme.nil?
48
+ compressed_url = scheme_code + url[scheme.size..-1]
49
+ EXPANSIONS.each do |k,v|
50
+ compressed_url.gsub! k,v
51
+ end
52
+ raise ArgumentError, "URL too long" if compressed_url.size > 18
53
+ compressed_url.force_encoding("ASCII-8BIT").unpack("H*")[0]
54
+ end
55
+
56
+ def self.decompress_url(hex)
57
+ compressed_url_string = [hex].pack("H*")
58
+ scheme_code = compressed_url_string[0]
59
+ scheme, scheme_code = SCHEMES.find {|k,v| v == scheme_code}
60
+ raise ArgumentError, "Invalid URL" if scheme.nil?
61
+ decompressed_url = scheme + compressed_url_string[1..-1]
62
+ EXPANSIONS.each do |k,v|
63
+ decompressed_url.gsub! v,k
64
+ end
65
+ decompressed_url
66
+ end
67
+
68
+ def inspect
69
+ "<EddystoneUrlBeacon url=\"#{url}\" rssi=#{rssi}, scans=#{ad_count}, power=#{@power}>"
70
+ end
71
+ end
72
+ end
@@ -6,12 +6,13 @@ module ScanBeacon
6
6
  def initialize(opts = {})
7
7
  self.beacon = opts[:beacon]
8
8
  self.parser = opts[:parser]
9
+ self.ad = opts[:ad] if opts[:ad]
9
10
  if beacon
10
11
  self.parser ||= BeaconParser.default_parsers.find {|parser| parser.beacon_type == beacon.beacon_types.first}
11
12
  end
12
13
  @advertising = false
13
14
  end
14
-
15
+
15
16
  def start(with_rotation = false)
16
17
  raise NotImplementedError
17
18
  end
@@ -24,4 +25,4 @@ module ScanBeacon
24
25
  raise NotImplementedError
25
26
  end
26
27
  end
27
- end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module ScanBeacon
2
- VERSION = "0.6.8"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/scan_beacon.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "scan_beacon/version"
2
2
  require "scan_beacon/beacon"
3
+ require "scan_beacon/eddystone_url_beacon"
3
4
  require "scan_beacon/beacon/field"
4
5
  require "scan_beacon/beacon_parser"
5
6
  require "scan_beacon/generic_scanner"
@@ -12,6 +13,7 @@ case RUBY_PLATFORM
12
13
  when /darwin/
13
14
  require "scan_beacon/core_bluetooth"
14
15
  require "scan_beacon/core_bluetooth_scanner"
16
+ require "scan_beacon/core_bluetooth_advertiser"
15
17
  when /linux/
16
18
  require "scan_beacon/bluez"
17
19
  require "scan_beacon/bluez_scanner"
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.6.8
4
+ version: 0.7.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-12-30 00:00:00.000000000 Z
11
+ date: 2016-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -99,7 +99,9 @@ files:
99
99
  - lib/scan_beacon/ble112_scanner.rb
100
100
  - lib/scan_beacon/bluez_advertiser.rb
101
101
  - lib/scan_beacon/bluez_scanner.rb
102
+ - lib/scan_beacon/core_bluetooth_advertiser.rb
102
103
  - lib/scan_beacon/core_bluetooth_scanner.rb
104
+ - lib/scan_beacon/eddystone_url_beacon.rb
103
105
  - lib/scan_beacon/generic_advertiser.rb
104
106
  - lib/scan_beacon/generic_individual_advertiser.rb
105
107
  - lib/scan_beacon/generic_scanner.rb