├── Vestigo Base ├── web │ ├── coords.cfg │ ├── logo.png │ ├── blueprint.png │ ├── coord.html │ ├── style.css │ └── view.html ├── addresses.cfg ├── vestigo_base.ini ├── locations.cfg ├── vestigo_base.py ├── logger.py ├── settings.py └── server.py ├── Vestigo ├── vestigo.ini ├── vestigo.py ├── logger.py ├── settings.py └── scan.py ├── LICENSE.txt └── readme.md /Vestigo Base/web/coords.cfg: -------------------------------------------------------------------------------- 1 | {"Office":[553,387],"Bedroom":[177,200]} -------------------------------------------------------------------------------- /Vestigo Base/web/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteveAbb/Vestigo/HEAD/Vestigo Base/web/logo.png -------------------------------------------------------------------------------- /Vestigo Base/web/blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteveAbb/Vestigo/HEAD/Vestigo Base/web/blueprint.png -------------------------------------------------------------------------------- /Vestigo Base/addresses.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "ble": { 3 | "FF:E7:CC:83:D0:8E": { 4 | "name": "Watch", 5 | "timeout": 5 6 | } 7 | }, 8 | "disc": { }, 9 | "nonDisc": { } 10 | } -------------------------------------------------------------------------------- /Vestigo Base/vestigo_base.ini: -------------------------------------------------------------------------------- 1 | [Base Server] 2 | #Port: 8080 3 | #ForwardData: http://abbagnaro.com 4 | #ForwardTimeout: 8 5 | #Recache: 60 6 | #ForwardLocation: True 7 | 8 | [Logging] 9 | #UseLog: False 10 | #File: vestigoBase.log 11 | #Size In Kilobytes 12 | #MaxSize: 1024 13 | #FileCount=5 14 | #STDOUT: True -------------------------------------------------------------------------------- /Vestigo/vestigo.ini: -------------------------------------------------------------------------------- 1 | [Scan Modes] 2 | #LE: True 3 | #Discoverable: True 4 | #NonDiscoverable: True 5 | 6 | [Read Timeouts] 7 | #Timeout In Seconds 8 | #Discoverable: 2 9 | #NonDiscoverable: 5 10 | 11 | [Base Server] 12 | #Reader: Reader1 13 | URL: http://localhost:8080 14 | #Timeout: 10 15 | #Recache: 60 16 | 17 | [Logging] 18 | #UseLog: False 19 | #File: vestigo.log 20 | #Size In Kilobytes 21 | #MaxSize: 1024 22 | #FileCount=5 23 | #STDOUT: True -------------------------------------------------------------------------------- /Vestigo Base/locations.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "Office": 3 | [ 4 | { 5 | "reader":"Reader1", 6 | "rssi": 7 | { 8 | "min":-70, 9 | "max":0 10 | }, 11 | "grpr": 12 | { 13 | "min":-10, 14 | "max":50 15 | } 16 | } 17 | ], 18 | "Bedroom": 19 | [ 20 | { 21 | "reader":"Reader2", 22 | "rssi": 23 | { 24 | "min":-70, 25 | "max":0 26 | }, 27 | "grpr": 28 | { 29 | "min":-50, 30 | "max":50 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /Vestigo Base/vestigo_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from settings import Settings 4 | from server import Server 5 | from logger import Logger 6 | 7 | def main(): 8 | try: 9 | #Read config file 10 | settings=Settings() 11 | 12 | #Set up logger 13 | logger=Logger(settings) 14 | 15 | #Create scanner 16 | server=Server(settings,logger) 17 | 18 | #Begin scanning 19 | server.start() 20 | 21 | except KeyboardInterrupt: 22 | server.stop() 23 | 24 | if __name__ == "__main__": 25 | main() 26 | 27 | -------------------------------------------------------------------------------- /Vestigo/vestigo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from settings import Settings 4 | from scan import Scanner 5 | from logger import Logger 6 | 7 | def main(): 8 | try: 9 | #Read config file 10 | settings=Settings() 11 | 12 | #Set up logger 13 | logger=Logger(settings) 14 | 15 | #Create scanner 16 | scanner=Scanner(settings,logger) 17 | 18 | #Begin scanning 19 | scanner.StartScanning() 20 | 21 | except KeyboardInterrupt: 22 | scanner.StopScanning() 23 | 24 | if __name__ == "__main__": 25 | main() 26 | 27 | -------------------------------------------------------------------------------- /Vestigo/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from settings import Settings 4 | import logging 5 | import logging.handlers 6 | import time 7 | 8 | class Logger(): 9 | def __init__(self,settings): 10 | self._logFile=settings.logging_File 11 | self._logger = logging.getLogger('vestigo') 12 | self._logger.setLevel(logging.DEBUG) 13 | self._settings=settings 14 | handler = logging.handlers.RotatingFileHandler(self._logFile, maxBytes=1024*int(settings.logging_MaxSize), backupCount=int(settings.logging_FileCount)) 15 | 16 | self._logger.addHandler(handler) 17 | 18 | def log(self,data): 19 | data="["+time.strftime('%X %x')+"] "+str(data) 20 | if(self._settings.logging_STDOUT): 21 | print data 22 | if(self._settings.logging_UseLog): 23 | self._logger.debug(data) -------------------------------------------------------------------------------- /Vestigo Base/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from settings import Settings 4 | import logging 5 | import logging.handlers 6 | import time 7 | 8 | class Logger(): 9 | def __init__(self,settings): 10 | self._logFile=settings.logging_File 11 | self._logger = logging.getLogger('VestigoBase') 12 | self._logger.setLevel(logging.DEBUG) 13 | self._settings=settings 14 | handler = logging.handlers.RotatingFileHandler(self._logFile, maxBytes=1024*int(settings.logging_MaxSize), backupCount=int(settings.logging_FileCount)) 15 | 16 | self._logger.addHandler(handler) 17 | 18 | def log(self,data): 19 | data="["+time.strftime('%X %x')+"] "+str(data) 20 | if(self._settings.logging_STDOUT): 21 | print data 22 | if(self._settings.logging_UseLog): 23 | self._logger.debug(data) -------------------------------------------------------------------------------- /Vestigo Base/web/coord.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 |
24 |
Click a point on the image below to get the coordinates.
25 |
26 | 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Steven Abbagnaro 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Vestigo Base/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os.path 4 | import ConfigParser 5 | 6 | class Settings(): 7 | def __init__(self): 8 | #Set defaults 9 | self.baseServer_Port=8080 10 | self.baseServer_ForwardData=None 11 | self.baseServer_Recache=60 12 | self.baseServer_ForwardTimeout=8 13 | self.baseServer_ForwardLocation=True 14 | 15 | self.logging_File="vestigo_base.log" 16 | self.logging_MaxSize=1024*1024 # 1MB 17 | self.logging_FileCount=5 18 | self.logging_UseLog=False 19 | self.logging_STDOUT=True 20 | 21 | if not os.path.isfile("vestigo_base.ini"): 22 | raise "Error reading configuration file" 23 | 24 | Config = ConfigParser.ConfigParser() 25 | Config.read("vestigo_base.ini") 26 | 27 | try: self.baseServer_Port=int(Config.get("Base Server","port")) 28 | except: pass 29 | try: self.baseServer_ForwardData=Config.get("Base Server","forwarddata") 30 | except: pass 31 | try: self.baseServer_RecacheRules=int(Config.get("Base Server","recacherules")) 32 | except: pass 33 | try: self.baseServer_ForwardTimeout=int(Config.get("Base Server","forwardtimeout")) 34 | except: pass 35 | try: self.baseServer_ForwardLocation=Config.getboolean("Base Server","forwardlocation") 36 | except: pass 37 | 38 | 39 | try: self.logging_File=Config.get("Logging","file") 40 | except: pass 41 | try: self.logging_MaxSize=Config.get("Logging","maxsize") 42 | except: pass 43 | try: self.logging_FileCount=Config.get("Logging","filecount") 44 | except: pass 45 | try: self.logging_UseLog=Config.getboolean("Logging","uselog") 46 | except: pass 47 | try: self.logging_STDOUT=Config.getboolean("Logging","stdout") 48 | except: pass -------------------------------------------------------------------------------- /Vestigo/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os.path 4 | import ConfigParser 5 | 6 | class Settings(): 7 | def __init__(self): 8 | #Set defaults 9 | self.scanMode_LE=False 10 | self.scanMode_Disc=False 11 | self.scanMode_NonDisc=False 12 | 13 | self.readTimeout_Disc=0 14 | self.readTimeout_NonDisc=0 15 | 16 | self.baseServer_URL=None 17 | self.baseServer_Timeout=8 18 | self.baseServer_Recache=60 19 | self.baseServer_Reader="Unknown" 20 | 21 | self.logging_File="vestigo.log" 22 | self.logging_MaxSize=1024*1024 # 1MB 23 | self.logging_FileCount=5 24 | self.logging_UseLog=False 25 | self.logging_STDOUT=True 26 | 27 | 28 | if not os.path.isfile("vestigo.ini"): 29 | raise "Error reading configuration file" 30 | 31 | Config = ConfigParser.ConfigParser() 32 | Config.read("vestigo.ini") 33 | 34 | try: self.scanMode_LE=Config.getboolean("Scan Modes","le") 35 | except: pass 36 | try: self.scanMode_Disc=Config.getboolean("Scan Modes","discoverable") 37 | except: pass 38 | try: self.scanMode_NonDisc=Config.getboolean("Scan Modes","nondiscoverable") 39 | except: pass 40 | 41 | try: self.readTimeout_Disc=Config.get("Read Timeout","discoverable") 42 | except: pass 43 | try: self.readTimeout_NonDisc=Config.get("Read Timeout","nondiscoverable") 44 | except: pass 45 | 46 | try: self.baseServer_URL=Config.get("Base Server","url") 47 | except: pass 48 | try: self.baseServer_Timeout=Config.get("Base Server","timeout") 49 | except: pass 50 | try: self.baseServer_Recache=Config.get("Base Server","recache") 51 | except: pass 52 | try: self.baseServer_Reader=Config.get("Base Server","reader") 53 | except: pass 54 | 55 | try: self.logging_File=Config.get("Logging","file") 56 | except: pass 57 | try: self.logging_MaxSize=Config.get("Logging","maxsize") 58 | except: pass 59 | try: self.logging_FileCount=Config.get("Logging","filecount") 60 | except: pass 61 | try: self.logging_UseLog=Config.getboolean("Logging","uselog") 62 | except: pass 63 | try: self.logging_STDOUT=Config.getboolean("Logging","stdout") 64 | except: pass -------------------------------------------------------------------------------- /Vestigo Base/web/style.css: -------------------------------------------------------------------------------- 1 | canvas{ 2 | padding:11px 15px 12px 15px; 3 | border-top:1px solid #fafafa; 4 | border-bottom:1px solid #e0e0e0; 5 | 6 | background: #ededed; 7 | background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#ebebeb)); 8 | background: -moz-linear-gradient(top, #ededed, #ebebeb); 9 | } 10 | 11 | table a:link { 12 | color: #666; 13 | font-weight: bold; 14 | text-decoration:none; 15 | } 16 | table a:visited { 17 | color: #999999; 18 | font-weight:bold; 19 | text-decoration:none; 20 | } 21 | table a:active, 22 | table a:hover { 23 | color: #bd5a35; 24 | text-decoration:underline; 25 | } 26 | table, canvas { 27 | font-family:Arial, Helvetica, sans-serif; 28 | color:#666; 29 | font-size:12px; 30 | text-shadow: 1px 1px 0px #fff; 31 | background:#eaebec; 32 | margin:20px; 33 | border:#ccc 1px solid; 34 | 35 | margin-left:auto; 36 | margin-right:auto; 37 | 38 | -moz-border-radius:3px; 39 | -webkit-border-radius:3px; 40 | border-radius:3px; 41 | 42 | -moz-box-shadow: 0 1px 2px #d1d1d1; 43 | -webkit-box-shadow: 0 1px 2px #d1d1d1; 44 | box-shadow: 0 1px 2px #d1d1d1; 45 | } 46 | table th { 47 | padding:21px 25px 22px 25px; 48 | border-top:1px solid #fafafa; 49 | border-bottom:1px solid #e0e0e0; 50 | 51 | background: #ededed; 52 | background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#ebebeb)); 53 | background: -moz-linear-gradient(top, #ededed, #ebebeb); 54 | } 55 | table th:first-child { 56 | text-align: left; 57 | padding-left:20px; 58 | } 59 | table tr:first-child th:first-child { 60 | -moz-border-radius-topleft:3px; 61 | -webkit-border-top-left-radius:3px; 62 | border-top-left-radius:3px; 63 | } 64 | table tr:first-child th:last-child { 65 | -moz-border-radius-topright:3px; 66 | -webkit-border-top-right-radius:3px; 67 | border-top-right-radius:3px; 68 | } 69 | table tr { 70 | text-align: center; 71 | padding-left:20px; 72 | } 73 | table td:first-child { 74 | text-align: left; 75 | padding-left:20px; 76 | border-left: 0; 77 | } 78 | table td { 79 | padding:18px; 80 | border-top: 1px solid #ffffff; 81 | border-bottom:1px solid #e0e0e0; 82 | border-left: 1px solid #e0e0e0; 83 | 84 | background: #fafafa; 85 | background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#fafafa)); 86 | background: -moz-linear-gradient(top, #fbfbfb, #fafafa); 87 | } 88 | table tr.even td { 89 | background: #f6f6f6; 90 | background: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#f6f6f6)); 91 | background: -moz-linear-gradient(top, #f8f8f8, #f6f6f6); 92 | } 93 | table tr:last-child td { 94 | border-bottom:0; 95 | } 96 | table tr:last-child td:first-child { 97 | -moz-border-radius-bottomleft:3px; 98 | -webkit-border-bottom-left-radius:3px; 99 | border-bottom-left-radius:3px; 100 | } 101 | table tr:last-child td:last-child { 102 | -moz-border-radius-bottomright:3px; 103 | -webkit-border-bottom-right-radius:3px; 104 | border-bottom-right-radius:3px; 105 | } 106 | table tr:hover td { 107 | background: #f2f2f2; 108 | background: -webkit-gradient(linear, left top, left bottom, from(#f2f2f2), to(#f0f0f0)); 109 | background: -moz-linear-gradient(top, #f2f2f2, #f0f0f0); 110 | } -------------------------------------------------------------------------------- /Vestigo Base/web/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 113 | 114 | 115 | 116 | 117 |
By: Steve Abbagnaro
118 |
119 | 120 | 121 |
Configure Coordinates
122 | 123 | 124 | -------------------------------------------------------------------------------- /Vestigo/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pexpect 4 | import subprocess 5 | import thread 6 | from multiprocessing.pool import ThreadPool 7 | import time 8 | from settings import Settings 9 | import requests 10 | import json 11 | from logger import Logger 12 | 13 | class Scanner(): 14 | def __init__(self,settings,logger): 15 | self._logger=logger 16 | self._bleScanProc = None 17 | self._scanProc=None 18 | self._addrToRssi={} 19 | self._keepScanning=True 20 | self._settings=settings 21 | self._lastFetch=None 22 | self._nonDiscKeepScanning=True 23 | self._addresses=None 24 | 25 | def log(self,data): 26 | self._logger.log(data) 27 | 28 | def getAddresses(self,type=None): 29 | fetchNewAssets=False 30 | if(self._lastFetch is not None): 31 | if((time.time()-self._lastFetch)>int(self._settings.baseServer_Recache)): 32 | fetchNewAssets=True 33 | else: 34 | fetchNewAssets=True 35 | 36 | if(fetchNewAssets): 37 | try: 38 | self._lastFetch=time.time() 39 | self.log("Recaching") 40 | resp=requests.get(self._settings.baseServer_URL+"/addresses/?reader="+self._settings.baseServer_Reader,timeout=int(self._settings.baseServer_Timeout)) 41 | self._addresses=resp.json() 42 | self.log("Finished recache.") 43 | self._nonDiscKeepScanning=False 44 | except Exception,error: 45 | self.log("Error rechaching addresses: "+str(error)+". Will retry on next recache.") 46 | 47 | if(type is None): 48 | return self._addresses 49 | elif(type is "all"): 50 | return dict(self._addresses["ble"].items() + self._addresses["disc"].items()+self._addresses["nonDisc"].items()) 51 | else: 52 | return self._addresses[type] 53 | 54 | def send_payload(self,addr,rssi): 55 | if(addr in self.getAddresses("all")): 56 | self._addrToRssi[addr]=rssi; 57 | if(addr in self.getAddresses("ble")): 58 | type="BLE" 59 | elif(addr in self.getAddresses("disc")): 60 | type="Discoverable" 61 | else: 62 | type="Non-Discoverable" 63 | 64 | if((type=="BLE" and self._settings.scanMode_LE) or (type=="Discoverable" and self._settings.scanMode_Disc) or (type=="Non-Discoverable" and self._settings.scanMode_NonDisc)): 65 | try: 66 | payload={"reader":self._settings.baseServer_Reader,"name":self.getAddresses("all")[addr]["name"],"address":addr,"rssi":rssi,"type":type} 67 | self.log("Sending payload to: "+self._settings.baseServer_URL) 68 | self.log("Payload:") 69 | self.log(json.dumps(payload,indent=4)) 70 | headers = {'content-type': 'application/json'} 71 | resp = requests.post(self._settings.baseServer_URL, data=json.dumps(payload), headers=headers,timeout=int(self._settings.baseServer_Timeout)) 72 | self.log("Resp: "+str(resp.status_code)) 73 | except Exception, error: 74 | self.log("Error with request: "+str(error)) 75 | 76 | def scanProcessSpawner(self): 77 | while self._keepScanning: 78 | self._scanProc = subprocess.Popen(['hcitool', 'scan'],cwd='/usr/bin',stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 79 | self._scanProc.wait() 80 | if(self._settings.readTimeout_Disc>0): 81 | time.sleep(self._settings.readTimeout_Disc) 82 | 83 | def nonDiscScanProcessSpawner(self,addr): 84 | while (self._nonDiscKeepScanning): 85 | result=pexpect.run("hcitool rssi "+addr,timeout=None) 86 | if "RSSI return value:" not in result: 87 | pexpect.run("hcitool cc "+addr,timeout=None) 88 | else: 89 | rssi=result.rstrip().split(": ")[1] 90 | 91 | if(addr in self._addrToRssi): 92 | if (rssi!=self._addrToRssi[addr]): 93 | self.send_payload(addr,rssi) 94 | else: 95 | self.send_payload(addr,rssi) 96 | if(self._settings.readTimeout_NonDisc>0): 97 | time.sleep(self._settings.readTimeout_NonDisc) 98 | 99 | def nonDiscScanPoolInitiate(self): 100 | pool = ThreadPool(processes=10) 101 | while self._keepScanning: 102 | self.log("Mapping non-discoverable addresses to thread pool executors") 103 | self._nonDiscKeepScanning=True 104 | pool.map(self.nonDiscScanProcessSpawner, self.getAddresses("nonDisc").keys()) 105 | 106 | def bleScanProccessSpawnerAsync(self): 107 | self._bleScanProc = subprocess.Popen(['hcitool', 'lescan', '--duplicates'],cwd='/usr/bin',stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 108 | 109 | def parseHcidump(self): 110 | self.log("Starting parse of hcidump stdout") 111 | child=pexpect.spawn("hcidump",timeout=None) 112 | while self._keepScanning: 113 | child.expect("(([0-9A-F]{2}[:-]){5}([0-9A-F]{2}))",timeout=None) 114 | addr=child.after 115 | if (addr in self.getAddresses("nonDisc")): 116 | continue 117 | child.expect("(-\d{2})",timeout=None) 118 | rssi=child.after 119 | 120 | if(addr in self._addrToRssi): 121 | if (rssi!=self._addrToRssi[addr]): 122 | self.send_payload(addr,rssi) 123 | else: 124 | self.send_payload(addr,rssi) 125 | 126 | def StartScanning(self): 127 | self.log(json.dumps(self.getAddresses(), indent=4)) 128 | if(self._settings.scanMode_LE): 129 | self.log("Spawning BLE scan child proccess") 130 | self.bleScanProccessSpawnerAsync() 131 | if(self._settings.scanMode_Disc): 132 | self.log("Spawning discoverable child process") 133 | thread.start_new_thread (self.scanProcessSpawner, ()) 134 | if(self._settings.scanMode_NonDisc): 135 | self.log("Initiating thread pool for non discoverable addresses") 136 | thread.start_new_thread (self.nonDiscScanPoolInitiate, ()) 137 | self.parseHcidump() 138 | 139 | def StopScanning(self): 140 | self._keepScanning=False 141 | if(self._settings.scanMode_LE): 142 | self.log("Killing low energy scan child process") 143 | self.log(self._bleScanProc.pid) 144 | try: self._bleScanProc.terminate() 145 | except(OSError): pass 146 | self.log("Killed.") 147 | if(self._settings.scanMode_Disc): 148 | self.log("Killing normal scan child process") 149 | self.log(self._scanProc.pid) 150 | try: self._scanProc.terminate() 151 | except(OSError): pass 152 | self.log("Killed.") -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Vestigo - Bluetooth tracking system 2 | ======================================================== 3 | 4 | What is it 5 | ------------ 6 | Vestigo is a proof of concept and educational project I took on to learn about, develop and show the ability and practicality of using the Bluetooth standard as a means to track asset locations in real-time. 7 | 8 | How it works 9 | ------------ 10 | Vestigo currently supports three modes of reading a Bluetooth device: 11 | 12 | - Discoverable 13 | - A Bluetooth device that is configured as discoverable. 14 | - Non-Discoverable 15 | - A Bluetooth device that accepts connections but is not Broadcasting itself 16 | - Low Energy (BLE) 17 | - A Bluetooth 4.0 Low Energy device that is discoverable 18 | 19 | Discoverable and BLE devices return an standard RSSI where-as Non-Discoverable devices currently only return their golden receive power range (GRPR). It is important to note that Vestigo does not attempt to pair with any devices (unless it's non-discoverable, in which case it continuously attempts to connect, to obtain the device's GRPR, but it will never successfully pair). 20 | 21 | There are two sets up scripts. One set: `Vestigo`, is meant to be run on the reading devices, and the other set: `Vestigo Base` is meant to be run on a central server that each reader can connect to. I've been using Raspberry Pi's as cheap readers with a WiFi and Bluetooth adapter and they work great. 22 | 23 | On the base server, you will be able to configure the location rules that associate a device's RSSI or GRPR to a corresponding location identifier through the definition of RSSI/GRPR thresholds. 24 | 25 | Configuration 26 | ------------- 27 | ### Vestigo (Reader) 28 | 29 | Vestigo has one configuration file: `Vestigo\vestigo.ini`. 30 | This configuration file allows the user to 31 | 32 | - Enable/disable scan modes (BLE, Discoverable, Non-Discoverable) 33 | *It is important to note that enabling all scan modes at once is with no timeouts is discouraged as I've noticed it tends to bog down most adapters I've tested with 34 | - Define read timeouts - seconds between each attempt to read 35 | - Configure base server host, request timeout and rechace period in seconds (period between each request for updated assets) 36 | - Configure logging 37 | 38 | ### Vestigo Base (Server) 39 | Vestigo Base has one configuration file: `Vestigo Base\vestigo.ini`. 40 | This configuration file allows the user to 41 | 42 | - Define listen port for HTTP server 43 | - Define URL to forward all data to (useful if you want to write a web application that uses WebSockets and not have to poll the server, or if you want to store the data to a database) 44 | - Set recache period in seconds, for reloading locations and assets 45 | - Configure logging 46 | 47 | To configure assest and location rules, edit the JSON located in: `Vestigo Base\addresses.cfg` and `Vestigo Base\locations.cfg` 48 | 49 | ### Vestigo Base Web Server 50 | 51 | The Vestigo Base web server allows you to poll it for asset state information using the following request: http://HOST:PORT/states. It's states will be returned in JSON. 52 | 53 | I made a simple web application that servers up all the content in `Vestigo Base\web\` to a browser when a request is made to it's host with no parameters: http://HOST:PORT 54 | 55 | The web application allows you to provide a blueprint image: `Vestigo Base\web\blueprint.png` and configure locations to coordinates on that image through the use of the `Vestigo Base\web\coords.cfg` file. 56 | 57 | Sample screenshot below: 58 | 59 | ![Vestigo Screenshot](http://i.imgur.com/tgLXrN3.png "Vestigo Screenshot") 60 | 61 | Hardware 62 | -------- 63 | For testing I used a series of Model B Raspberry Pis as readers. I am running Raspbian on each of them, but have also tested it with Arch. The adapter I find that works the best for the Raspberry Pi (requires no drivers on Raspbian) and supports Bluetooth 4.0 is the Plugable USB-BT4LE Bluetooth 4.0 USB Adapter. 64 | 65 | I am currently using a series of Raspberry Pis to track my iPad and iPhone (non-discoverable devices with Bluetooth enabled), and a set of Stick N' Find BLE tags, throughout my house using a series of nested location rules with appropriate RSSI/GRPR thresholds. 66 | 67 | How to install it 68 | ----------------- 69 | To install the Vestigo reader on a Raspberry Pi running Raspbian, follow these steps: 70 | 71 | Install the Bluetooth support package 72 | 73 | apt-get install bluetooth 74 | 75 | Verify the bluetooth daemon is running 76 | 77 | /etc/init.d/bluetooth status 78 | 79 | Verify your adapter is recognized 80 | 81 | hcitool dev 82 | 83 | Install hcidump 84 | 85 | apt-get install bluez-hcidump 86 | 87 | Install python library "requests" 88 | Using easy_install: 89 | 90 | easy_install install requests 91 | 92 | or using pip: 93 | 94 | pip install requests 95 | 96 | 97 | That's it! 98 | 99 | To start the reader run 100 | 101 | python vestigo.py 102 | 103 | To start the base server run 104 | 105 | python vestigo_base.py 106 | 107 | 108 | Remember to add devices to the addresses.cfg file. An easy way to find an address of a device is to use 109 | 110 | hcitool scan 111 | 112 | for discoverable devices, or 113 | 114 | hcitool lescan 115 | 116 | for BLE devices. For non-discoverable devices, put them into discoverable (such as opening up the bluetooth screen on iOS), and then capture their address before making them non-discoverable. 117 | 118 | Future features 119 | --------------- 120 | I have a load of features and enhancements I'd like to add to this project when I find more time. Here is a list of some that I'd like to add: 121 | - Proper daemonization of the reader and server processes 122 | - WebSocket based web application demo 123 | - Normalization modules for different types of device adapters and reader adapters. Allowing you to relationally skew the incoming SSI of each device based on the reader and devices adapter (since different types of adapters give off different SSI readings) 124 | - Research better means for getting a more accurate distance read of devices. Factor in Link Query? 125 | - Triangulation 126 | - Support for Non-Discoverable BLE devices (New Stick N' Finds) 127 | -------------------------------------------------------------------------------- /Vestigo Base/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import thread 4 | import time 5 | from settings import Settings 6 | import requests 7 | import json 8 | from logger import Logger 9 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 10 | from urlparse import urlparse, parse_qs 11 | from SocketServer import ThreadingMixIn 12 | import threading 13 | from threading import Timer 14 | import collections 15 | import copy 16 | 17 | class Server(): 18 | def __init__(self,settings,logger): 19 | self.assetToPayload={} 20 | self._logger=logger 21 | self._settings=settings 22 | self._lastFetchLocations=None 23 | self._lastFetchAddresses=None 24 | self._locations=None 25 | self._addresses=None 26 | self._addressToTimer = {} 27 | self._server = VestigoHTTPServer(("", self._settings.baseServer_Port), HTTPHandler, self.log,self.processPayload,self.getAddresses,self.assetToPayload) 28 | 29 | def processPayload(self, payload): 30 | 31 | lastLocation = None; 32 | 33 | if(self._settings.baseServer_ForwardLocation): 34 | if(payload["address"] in self.assetToPayload and "location" in self.assetToPayload[payload["address"]]): 35 | lastLocation = self.assetToPayload[payload["address"]]["location"] 36 | 37 | if("outofrange" in payload and payload["outofrange"]): 38 | self.log("Timeout timer elapsed. Moving " + payload["name"] + " into location: out of range."); 39 | payload["location"]="out of range"; 40 | payload.pop("outofrange",None); 41 | else: 42 | ruleMatches=False 43 | for location in self.getLocations(): 44 | for rule in self.getLocations(location): 45 | if(rule["reader"]==payload["reader"]): 46 | if(payload["address"] in self.getAddresses("nonDisc")): 47 | if(int(payload["rssi"])>=int(rule["grpr"]["min"]) and int(payload["rssi"])<=int(rule["grpr"]["max"])): 48 | ruleMatches=True 49 | else: 50 | if(int(payload["rssi"])>=int(rule["rssi"]["min"]) and int(payload["rssi"])<=int(rule["rssi"]["max"])): 51 | ruleMatches=True 52 | 53 | if(ruleMatches): 54 | self.log("Name: "+payload["name"]+" is in location: "+location) 55 | payload["location"]=location 56 | break 57 | if(ruleMatches): 58 | break 59 | 60 | if(ruleMatches and "timeout" in self.getAddresses("all")[payload["address"]]): 61 | if(payload["address"] in self._addressToTimer): 62 | 63 | self.log("Reseting timeout timer for " + payload["name"]); 64 | self._addressToTimer[payload["address"]].cancel(); 65 | else: 66 | self.log("Starting timer for " + payload["name"] + " for " + str( self.getAddresses("all")[payload["address"]]["timeout"]) + " seconds."); 67 | 68 | timerPayload = copy.deepcopy(payload); 69 | timerPayload["rssi"] = 0 70 | timerPayload["reader"] = "" 71 | timerPayload["outofrange"] = True; 72 | 73 | timer = Timer(self.getAddresses("all")[payload["address"]]["timeout"], self.processPayload, (timerPayload,)); 74 | 75 | self._addressToTimer[payload["address"]] = timer 76 | self._addressToTimer[payload["address"]].start() 77 | 78 | self.assetToPayload[payload["address"]]=payload; 79 | 80 | if(self._settings.baseServer_ForwardData is not None): 81 | if(self._settings.baseServer_ForwardLocation): 82 | if("location" not in payload or "location" in payload and lastLocation == payload["location"]): 83 | return 84 | self.log("Forwarding payload off to: "+self._settings.baseServer_ForwardData) 85 | try: 86 | self.log("Forward Payload: ") 87 | self.log(json.dumps(payload,indent=4)) 88 | headers = {'content-type': 'application/json'} 89 | resp = requests.post(self._settings.baseServer_ForwardData, data=json.dumps(payload), headers=headers,timeout=int(self._settings.baseServer_ForwardTimeout)) 90 | self.log("Resp: "+str(resp.status_code)) 91 | except Exception, error: 92 | self.log("Error with forward request: "+str(error)) 93 | 94 | def getAddresses(self,type=None): 95 | fetchNew=False 96 | if(self._lastFetchAddresses is not None): 97 | if((time.time()-self._lastFetchAddresses)>int(self._settings.baseServer_Recache)): 98 | fetchNew=True 99 | else: 100 | fetchNew=True 101 | 102 | if(fetchNew): 103 | try: 104 | self._lastFetchAddresses=time.time() 105 | self.log("Recaching addresses.") 106 | f = open("addresses.cfg") 107 | self._addresses=json.loads(f.read()) 108 | f.close() 109 | self.log("Finished recache.") 110 | except Exception,error: 111 | self.log("Error rechaching addresses: "+str(error)+". Will retry on next recache.") 112 | 113 | if(type is None): 114 | return self._addresses 115 | elif(type is "all"): 116 | return dict(self._addresses["ble"].items() + self._addresses["disc"].items()+self._addresses["nonDisc"].items()) 117 | else: 118 | return self._addresses[type] 119 | 120 | def getLocations(self,location=None): 121 | fetchNew=False 122 | if(self._lastFetchLocations is not None): 123 | if((time.time()-self._lastFetchLocations)>int(self._settings.baseServer_Recache)): 124 | fetchNew=True 125 | else: 126 | fetchNew=True 127 | 128 | if(fetchNew): 129 | try: 130 | self._lastFetchLocations=time.time() 131 | self.log("Recaching locations.") 132 | f = open("locations.cfg") 133 | self._locations=json.loads(f.read(),object_pairs_hook=collections.OrderedDict) 134 | f.close() 135 | self.log("Finished recache.") 136 | except Exception,error: 137 | self.log("Error rechaching locations: "+str(error)+". Will retry on next recache.") 138 | if location is None: 139 | return self._locations 140 | else: 141 | return self._locations[location] 142 | 143 | def log(self,data): 144 | self._logger.log(data) 145 | 146 | def start(self): 147 | self.log("Base server starting on port: "+str(self._settings.baseServer_Port)) 148 | self._server.serve_forever() 149 | 150 | def stop(self): 151 | self.log("Base server shutting down...") 152 | self._server.shutdown() 153 | self.log("Killing all timers."); 154 | for key in self._addressToTimer: 155 | try: 156 | self._addressToTimer[key].cancel(); 157 | except: 158 | pass 159 | 160 | class VestigoHTTPServer(ThreadingMixIn, HTTPServer): 161 | def __init__(self, server_address, RequestHandlerClass, log, processPayload,getAddresses,assetToPayload): 162 | HTTPServer.__init__(self, server_address, RequestHandlerClass) 163 | self.log = log 164 | self.processPayload=processPayload 165 | self.getAddresses=getAddresses 166 | self.assetToPayload=assetToPayload 167 | 168 | class HTTPHandler(BaseHTTPRequestHandler): 169 | def do_GET(self): 170 | dirs=self.path.split("/") 171 | if(len(dirs)>1): 172 | resource=dirs[1] 173 | if(resource == "addresses"): 174 | try: 175 | queryStr=parse_qs(urlparse(self.path).query) 176 | reader=str(queryStr["reader"][0]) 177 | self.send_response(200) 178 | self.send_header('Content-type',"application/json") 179 | self.end_headers() 180 | self.wfile.write(json.dumps(self.server.getAddresses())) 181 | except IOError as e: 182 | self.send_response(404) 183 | self.server.log("Error with processing readers request for addresses: "+str(e)) 184 | elif(resource == "states"): 185 | try: 186 | self.send_response(200) 187 | self.send_header('Content-type','application/json') 188 | self.end_headers() 189 | self.wfile.write(json.dumps(self.server.assetToPayload.values())) 190 | except IOError as e: 191 | self.send_response(404) 192 | self.server.log("Error with processing request for asset states: "+str(e)) 193 | else: 194 | fileName="web/view.html" 195 | contentType="text/html" 196 | if("style.css" in dirs): 197 | fileName="web/style.css" 198 | contentType="text/css" 199 | elif("coords.cfg" in dirs): 200 | fileName="web/coords.cfg" 201 | contentType="application/json" 202 | elif("blueprint.png" in dirs): 203 | fileName="web/blueprint.png" 204 | contentType="image/png" 205 | elif("coord.html" in dirs): 206 | fileName="web/coord.html" 207 | contentType="text/html" 208 | elif("logo.png" in dirs): 209 | fileName="web/logo.png" 210 | contentType="image/png" 211 | f = open(fileName) 212 | self.send_response(200) 213 | self.send_header('Content-type',contentType) 214 | self.end_headers() 215 | self.wfile.write(f.read()) 216 | f.close() 217 | else: 218 | self.send_response(404) 219 | self.server.log("Error with request. No resource specified") 220 | 221 | def do_POST(self): 222 | try: 223 | self.server.log("Asset data recieved from a reader.") 224 | content_len = int(self.headers.getheader('content-length')) 225 | request_data = self.rfile.read(content_len) 226 | self.send_response(200) 227 | self.server.log("Attempting to parse request content.") 228 | payload=json.loads(request_data) 229 | self.server.log("Payload recieved: ") 230 | self.server.log(json.dumps(payload,indent=4)) 231 | self.server.processPayload(payload) 232 | except Exception, error: 233 | self.send_response(404) 234 | self.server.log("Error with payload request from reader: "+str(error)) 235 | 236 | def log_message(self, format, *args): 237 | return --------------------------------------------------------------------------------