Class: ScanBeacon::BeaconParser

Inherits:
Object
  • Object
show all
Defined in:
lib/scan_beacon/beacon_parser.rb

Constant Summary collapse

DEFAULT_LAYOUTS =
{
  altbeacon: "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25",
  eddystone_uid: "s:0-1=aafe,m:2-2=00,p:3-3:-41,i:4-13,i:14-19;d:20-21"
}
AD_TYPE_MFG =
0xff
AD_TYPE_SERVICE =
0x03
BT_EIR_SERVICE_DATA =
"\x16"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(beacon_type, layout) ⇒ BeaconParser

Returns a new instance of BeaconParser.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/scan_beacon/beacon_parser.rb', line 16

def initialize(beacon_type, layout)
  @beacon_type = beacon_type
  @layout = layout.split(",")
  if layout.include?("s")
    @ad_type = AD_TYPE_SERVICE
  else
    @ad_type = AD_TYPE_MFG
  end
  @matchers = @layout.find_all {|item| ["m", "s"].include? item[0]}.map {|matcher|
    _, range_start, range_end, expected = matcher.split(/:|=|-/)
    {start: range_start.to_i, end: range_end.to_i, expected: expected}
  }
  @ids = @layout.find_all {|item| item[0] == "i"}.map {|id|
    _, range_start, range_end = id.split(/:|-/)
    {start: range_start.to_i, end: range_end.to_i}
  }
  @data_fields = @layout.find_all {|item| item[0] == "d"}.map {|field|
    _, range_start, range_end = field.split(/:|-/)
    {start: range_start.to_i, end: range_end.to_i}
  }
  _, power_start, power_end = @layout.find {|item| item[0] == "p"}.split(/:|-/)
  @power = {start: power_start.to_i, end: power_end.to_i}
end

Instance Attribute Details

#beacon_typeObject

Returns the value of attribute beacon_type.



10
11
12
# File 'lib/scan_beacon/beacon_parser.rb', line 10

def beacon_type
  @beacon_type
end

Class Method Details

.default_parsersObject



12
13
14
# File 'lib/scan_beacon/beacon_parser.rb', line 12

def self.default_parsers
  DEFAULT_LAYOUTS.map {|name, layout| BeaconParser.new name, layout }
end

Instance Method Details

#generate_ad(beacon) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/scan_beacon/beacon_parser.rb', line 94

def generate_ad(beacon)
  length = [@matchers, @ids, @power, @data_fields].flatten.map {|elem| elem[:end] }.max + 1
  ad = "\x00" * length
  @matchers.each do |matcher|
    ad[matcher[:start]..matcher[:end]] = [matcher[:expected]].pack("H*")
  end
  @ids.each_with_index do |id, index|
    ad[id[:start]..id[:end]] = generate_field(id, beacon.ids[index])
  end
  @data_fields.each_with_index do |field, index|
    ad[field[:start]..field[:end]] = generate_field(field, beacon.data[index]) unless beacon.data[index].nil?
  end
  ad[@power[:start]..@power[:end]] = [beacon.power].pack('c')
  if @ad_type == AD_TYPE_SERVICE
    "\x03\x03" + [beacon.service_uuid].pack("S<") + [length+1].pack('C') + BT_EIR_SERVICE_DATA + ad
  elsif @ad_type == AD_TYPE_MFG
    ad[0..1] = [beacon.mfg_id].pack("S<")
    [length+1].pack('C') + [AD_TYPE_MFG].pack('C') +  ad
  end
end

#generate_field(field, value) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/scan_beacon/beacon_parser.rb', line 115

def generate_field(field, value)
  field_length = field[:end] - field[:start] + 1
  case field_length
  when 1
    [value].pack("c")
  when 2
    [value].pack("S>")
  when 6
    [value].pack("Q>")[2..-1]
  else
    [value].pack("H*")[0..field_length-1]
  end
end

#inspectObject



129
130
131
# File 'lib/scan_beacon/beacon_parser.rb', line 129

def inspect
  "<BeaconParser type=\"#{@beacon_type}\", layout=\"#{@layout.join(",")}\">"
end

#matches?(data) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
43
44
45
# File 'lib/scan_beacon/beacon_parser.rb', line 40

def matches?(data)
  @matchers.each do |matcher|
    return false unless data[matcher[:start]..matcher[:end]].unpack("H*").join == matcher[:expected]
  end
  return true
end

#parse(data, ad_type = AD_TYPE_MFG) ⇒ Object



47
48
49
50
51
52
53
54
55
56
# File 'lib/scan_beacon/beacon_parser.rb', line 47

def parse(data, ad_type = AD_TYPE_MFG)
  return nil if ad_type != @ad_type || !matches?(data)
  if @ad_type == AD_TYPE_MFG
    Beacon.new(ids: parse_ids(data), power: parse_power(data), beacon_type: @beacon_type,
      data: parse_data_fields(data), mfg_id: parse_mfg_or_service_id(data))
  else
    Beacon.new(ids: parse_ids(data), power: parse_power(data), beacon_type: @beacon_type,
      data: parse_data_fields(data), service_uuid: parse_mfg_or_service_id(data))
  end
end

#parse_data_fields(data) ⇒ Object



62
63
64
# File 'lib/scan_beacon/beacon_parser.rb', line 62

def parse_data_fields(data)
  parse_elems(@data_fields, data)
end

#parse_elems(elems, data) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/scan_beacon/beacon_parser.rb', line 66

def parse_elems(elems, data)
  elems.map {|elem|
    elem_str = data[elem[:start]..elem[:end]]
    elem_length = elem_str.size
    case elem_length
    when 1
      elem_str.unpack('C')[0]
    when 2 
      # two bytes, so treat it as a short (big endian)
      elem_str.unpack('S>')[0]
    when 6
      # 6 bytes, treat it is an eddystone instance id
      ("\x00\x00"+elem_str).unpack('Q>')[0]
    else
      # not two bytes, so treat it as a hex string
      elem_str.unpack('H*').join
    end
  }
end

#parse_ids(data) ⇒ Object



58
59
60
# File 'lib/scan_beacon/beacon_parser.rb', line 58

def parse_ids(data)
  parse_elems(@ids, data)
end

#parse_mfg_or_service_id(data) ⇒ Object



86
87
88
# File 'lib/scan_beacon/beacon_parser.rb', line 86

def parse_mfg_or_service_id(data)
  data[0..1].unpack('S>')[0]
end

#parse_power(data) ⇒ Object



90
91
92
# File 'lib/scan_beacon/beacon_parser.rb', line 90

def parse_power(data)
  data[@power[:start]..@power[:end]].unpack('c')[0]
end