scan_beacon 0.6.8 → 0.7.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 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