BLE Beacon scan - initial support fro iBeacons
[openhab-process.git] / mqtt-bt / scan-beacons
1 #!/usr/bin/env lua
2
3 function getConfig(configname)
4
5   local uci=require("uci")
6   local cur=uci.cursor()
7   local config
8   if configname then
9     config=configname
10   else
11     config="beacon"
12   end
13
14   logging = cur.get(config,"logging","enabled") 
15
16   mqtt_host = cur.get(config,"mqtt","host")
17   mqtt_port = cur.get(config,"mqtt","port")
18   mqtt_id = cur.get(config,"mqtt","id")
19   mqtt_topic = cur.get(config,"mqtt","topic")
20
21   mqtt_user = cur.get(config,"mqtt","user")
22   mqtt_passwd = cur.get(config,"mqtt","password")
23
24   if mqtt_host and not mqtt_id then
25     mqtt_id="beaconmon"
26   end
27
28   if mqtt_host and not mqtt_port then
29     mqtt_port = 1883
30   end
31
32   if mqtt_host and not mqtt_topic then
33     mqtt_topic = 'beaconmon/{type}/{details}'
34   end
35
36 end
37
38 function capture(cmd, raw)
39   local f = assert(io.popen(cmd, 'r'))
40   local s = assert(f:read('*a'))
41   f:close()
42   if raw then return s end
43   s = string.gsub(s, '^%s+', '')
44   s = string.gsub(s, '%s+$', '')
45   s = string.gsub(s, '[\n\r]+', ' ')
46   return s
47 end
48
49 function mqtt_encode(str)
50   if (str) then
51     str = string.gsub (str, "\n", "")
52     str = string.gsub (str, "/", "-")
53   end
54   return str    
55 end
56
57 function printLog(str)
58   if logging=="on" then
59     capture("logger -t beaconmon "..str)
60   else 
61     print(str)  
62   end 
63 end
64
65 function run_command(cmd)
66
67   local file = assert(io.popen(cmd, 'r'))
68   local output = file:read('*all')
69   file:close()
70
71 end
72
73 function open_dump()
74
75   f = assert(io.popen ("hcidump --raw"))
76   run_command("kill `pgrep hcitool`")
77   run_command("hciconfig hci0 down")
78   run_command("hciconfig hci0 up")
79   f_null = assert(io.popen ("hcitool lescan --duplicates"))
80
81   return f
82
83 end
84
85 function dump(o)
86    if type(o) == 'table' then
87       local s = '{ '
88       for k,v in pairs(o) do
89          if type(k) ~= 'number' then k = '"'..k..'"' end
90 --         s = s .. '['..k..'] = ' .. dump(v) .. ','
91          s = s .. dump(v) .. ','
92       end
93       return s .. '} '
94    else
95       return tostring(o)
96    end
97 end
98
99 function trim(s)
100   return (s:gsub("^%s*(.-)%s*$", "%1"))
101 end
102
103 function process_packet(packet)
104
105   local bytes={}
106   local idx=1
107   local len
108   local mac
109   local flags
110   local power
111   local tx
112   local type
113   local paysublen
114   local uuid
115   local major
116   local minor
117   local details
118
119   if packet:len()>1 then
120
121     while packet:len()>2 do
122       bytes[idx]=trim(packet:sub(1,3))
123       idx=idx+1
124       packet=packet:sub(4)
125     end
126     len = idx-1
127
128     if bytes[1]=='04' and bytes[2]=='3E' then
129       -- BLE Beacon?
130       type=""
131       mac=bytes[13]..':'..bytes[12]..':'..bytes[11]..':'..bytes[10]..':'..bytes[9]..':'..bytes[8]
132       flags=bytes[14]
133       power=tonumber("0x"..bytes[len-1])-256
134       tx=tonumber("0x"..bytes[len])-256
135       local j = 15
136       while j<len-2 do
137         paysublen=tonumber('0x'..bytes[j])
138         if bytes[j+1]=="FF" and bytes[j+2]=="4C" and bytes[j+3]=="00" and bytes[j+4]=="02" and bytes[j+5]=="15" then
139           -- Standard UUID iBeacon
140           type="ibeacon"
141           local uuid1=bytes[j+6]..bytes[j+7]..bytes[j+8]..bytes[j+9]
142           local uuid2=bytes[j+10]..bytes[j+11]
143           local uuid3=bytes[j+12]..bytes[j+13]
144           local uuid4=bytes[j+14]..bytes[j+15]
145           local uuid5=bytes[j+16]..bytes[j+17]..bytes[j+18]..bytes[j+19]..bytes[j+20]..bytes[j+21]
146           uuid=string.lower(uuid1..'-'..uuid2..'-'..uuid3..'-'..uuid4..'-'..uuid5)
147           major=bytes[j+23]..bytes[j+22]
148           minor=bytes[j+25]..bytes[j+24]
149         end
150         j=j+1+paysublen
151       end
152       if type=="ibeacon" then
153         printLog(string.format("{type:'ibeacon',mac:'%s',uuid:'%s',major:'%s',minor:'%s',power:%d,tx:%d}",mac,uuid,major,minor,power,tx))
154         details=uuid..'/'..major..'/'..minor..'/'
155       else
156         type='unknown'
157         details=dump(bytes)
158         printLog(details)
159       end
160       if mqtt_client then
161         mqtt_path=string.gsub(mqtt_topic,"{(.-)}", 
162           function (name) 
163             if name=="type" then
164               return mqtt_encode(type)
165             elseif name=="details" then
166               return mqtt_encode(details)
167             else
168               return '{'..name..'}'
169             end      
170           end)
171         mqtt_client:publish(mqtt_path,tx)
172       end        
173     end
174   end
175
176 end
177
178 function read_loop()
179
180   packet = ""
181
182   for line in inp:lines() do
183
184     lchr=line:sub(1,1)
185     line=trim(line)
186
187     if lchr=="<" or lchr==">" then
188       line = trim(line:sub(2))
189     end
190
191     if not (lchr == " ") then
192       process_packet(packet)
193       packet = ""
194     end
195
196     llen=line:len()
197
198     if llen<59 then
199       packet = packet .. " " .. line
200       process_packet(packet)
201       packet=""
202     else
203       packet = packet .. " " .. line
204     end    
205     
206   end
207
208 end
209
210 getConfig(arg[1])
211
212 if mqtt_host then
213   MQTT = require "paho.mqtt"
214   mqtt_client = MQTT.client.create(mqtt_host, mqtt_port)
215   if mqtt_user then
216     mqtt_client:auth(mqtt_user, mqtt_passwd)
217   end
218   mqtt_client:connect(mqtt_id)
219 end
220
221 inp = open_dump()
222 read_loop()