serve.py 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549
  1. # -*- coding: utf-8 -*-
  2. #---Standard library imports
  3. import string
  4. import sys
  5. import logging
  6. import time
  7. from datetime import datetime
  8. from collections import namedtuple, defaultdict, OrderedDict
  9. import json
  10. import traceback
  11. from functools import wraps
  12. import Queue
  13. import threading
  14. import hashlib
  15. from UserDict import UserDict
  16. import random
  17. #---Third-party imports
  18. from utils.path import path
  19. if sys.platform == 'win32':
  20. import win32api
  21. import win32con
  22. from configobj import ConfigObj, flatten_errors
  23. import validate
  24. import cherrypy
  25. from cherrypy.lib.static import serve_file
  26. from cherrypy.lib.http import HTTPDate
  27. from dateutil import parser
  28. import scipy
  29. from runphoton import BBox, Resolution
  30. from utils.materials import materialsAsXml, Brdf, loadMaterialsFromString
  31. from utils import daemonthread, loadLocations
  32. from utils.xml import StringToTree, TreeToString, subelement
  33. import evaluate
  34. import render
  35. import optimize as optimizer
  36. import simulate as simulator
  37. import plotter
  38. import localize
  39. #---Globals
  40. CONFIG_SPEC = path(__file__).parent.joinpath('config/configspec.ini')
  41. PATH_STATIC = path(__file__).parent.joinpath('static').abspath()
  42. LOCALIZATION_ON = True
  43. LOCALIZATION_ON = False
  44. LOGFILE = path('.').joinpath('out.log')
  45. MEASUREMENTS_STORAGE = path('.').joinpath('measurements.txt')
  46. if not MEASUREMENTS_STORAGE.exists():
  47. MEASUREMENTS_STORAGE.touch()
  48. # first substitution is station name, second is apid
  49. OPTIMIZE_FILES = '/home/dirk/loco/maps/UMIC/experiment/measure_%s_%s.txt'
  50. log = logging.getLogger('lws')
  51. app = None
  52. def _splitBaseurl(baseurl):
  53. if baseurl is None:
  54. return None, None
  55. parts = baseurl.split(':')
  56. if len(parts) == 1:
  57. parts.append(None)
  58. return parts[0], parts[1]
  59. def getHTTPConfig():
  60. c = urlparse.urlsplit(cherrypy.request.base)
  61. host, port = _splitBaseurl(c[1])
  62. if app.config['behindProxy']:
  63. port = app.config['proxyHttpPort']
  64. host = _splitBaseurl(urlparse.urlsplit(app.config['proxyBase'])[1])[0]
  65. else:
  66. port = app.config['httpPort']
  67. return host, port
  68. def setupLogging():
  69. log.handlers = []
  70. log.setLevel(logging.DEBUG)
  71. sh = logging.StreamHandler()
  72. formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s", "%m-%d %H:%M:%S")
  73. sh.setFormatter(formatter)
  74. log.addHandler(sh)
  75. logfile = LOGFILE
  76. log.info('storing logfile to %s' % logfile.normpath())
  77. fh = logging.FileHandler(logfile, 'a')
  78. fh.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s", '%m-%d %X'))
  79. log.addHandler(fh)
  80. logging.getLogger('cherrypy.error').addHandler(fh)
  81. def setForceNoCache(expires_past=HTTPDate(1169942400.0)):
  82. headers = cherrypy.response.headers
  83. headers['Expires'] = expires_past
  84. headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
  85. headers['Pragma'] = 'no-cache'
  86. def nocache(func):
  87. @wraps(func)
  88. def wrapper(*args, **kwargs):
  89. setForceNoCache()
  90. return func(*args, **kwargs)
  91. return wrapper
  92. Measurement = namedtuple('Measurement', 'x y z avgrssi rssis')
  93. APInfo = namedtuple('AP', 'rssi remotets localts delta')
  94. class MobileStation(object):
  95. def __init__(self, name):
  96. self.name = name
  97. self.data = defaultdict(list) # key: (bssid, ssid), values: list of apinfo
  98. class LWS(object):
  99. def __init__(self, configfile, cfgoverlay=None):
  100. self.mstations = {} # key: station name, value: MobileStation()
  101. self.mac2cfg = {} # key: mac address, value: config section for corresponding access point id
  102. self.aliasedmac2apid = {} # key: mac, value corresponding apid
  103. self.activeScene = None # points to sctive scene section in config
  104. self.configfile = path(configfile)
  105. self.cfgoverlay = cfgoverlay # nested dict to overwrite config values
  106. self._loadConfig()
  107. self.measure_enabled = None # beocmes tuple (station name, locid, startts, duration)
  108. self.measurement_stopped = None
  109. self.pathtrack_enabled = None
  110. self.lastCollectedPath = None
  111. # becomes a dict with key: station name and value a dict with key: locid and value: {(bssid, ssid): APInfo()}
  112. self.tracked_measurements = defaultdict(dict)
  113. # dict with key stationname, and value: dict with key pathid, and value: list (locid, timestamp)
  114. self.tracked_positions = defaultdict(dict)
  115. # in memory storage of measurements (from data in MEASUREMENTS_STORAGE) - like self.tracked_measurements
  116. self.stored_measurements = defaultdict(lambda: defaultdict(list))
  117. self.loadMeasurementDump()
  118. if not self.activeScene['locationfile'].exists():
  119. print 'creating locfile at %s' % self.activeScene['locationfile'].abspath()
  120. self.activeScene['locationfile'].touch()
  121. self.known_locations = loadLocations(self.activeScene['locationfile']) # key: locid, value: measure.Location()
  122. daemonthread(target=self._checkConfig, name="configReloadLoop")
  123. self.simulator = simulator.Simulator(self.config['tmp'])
  124. self.hash2objfile = {}
  125. self.loadobjfiles()
  126. optimizer.app = self
  127. self.optimizer = optimizer.Optimizer()
  128. self.plotLock = threading.Lock()
  129. self.writeLock = threading.Lock()
  130. # measurements for different APs
  131. self._apid2measurements = {}
  132. self.localization_data = defaultdict(lambda: Queue.Queue())
  133. self.last_result = defaultdict(lambda: Queue.Queue())
  134. self.device2localizationenv = {}
  135. self.localizeSessions = defaultdict(set)
  136. self.localizeSessionInactiveAPs = defaultdict(set) # key: device, value: set of deactivated APs
  137. self.draw_aptiles_lock = threading.Lock()
  138. def loadobjfiles(self):
  139. objpath = self.config['tmp'].joinpath('obj')
  140. if not objpath.exists():
  141. objpath.mkdir()
  142. for f in objpath.files():
  143. h = hashlib.sha1(f.bytes()).hexdigest()
  144. self.hash2objfile[h] = f
  145. def _loadConfig(self):
  146. # check file exists and is readable
  147. f = open(self.configfile)
  148. f.read(1024)
  149. f.close()
  150. log.info('loading configfile %s' % self.configfile)
  151. configSpec = ConfigObj(CONFIG_SPEC, encoding='utf-8', list_values=False)
  152. validator = validate.Validator()
  153. self.config = ConfigObj(self.configfile, configspec=configSpec, interpolation='template', encoding='UTF-8')
  154. res = self.config.validate(validator, preserve_errors=True)
  155. #check for errors
  156. for entry in flatten_errors(self.config, res):
  157. # each entry is a tuple
  158. section_list, key, error = entry
  159. section_list.append('[missing section]') if key is None else section_list.append(key)
  160. section_string = ', '.join(section_list)
  161. if not error:
  162. error = 'Missing value or section.'
  163. raise Exception, '%s=%s' % (section_string, error)
  164. def _applyOverlay(section, d):
  165. for key, value in d.items():
  166. if isinstance(d[key], dict):
  167. _applyOverlay(section[key], d[key])
  168. else:
  169. section[key] = value
  170. if self.cfgoverlay:
  171. _applyOverlay(self.config, self.cfgoverlay)
  172. for k in 'tmp', 'tmp_apdata', 'tmp_optruns', 'tmp_tracked':
  173. self.config[k] = path(self.config[k]).abspath()
  174. if not self.config[k].exists():
  175. self.config[k].mkdir()
  176. # check objfile fs paths
  177. for secname in self.config['scenes'].sections:
  178. for fname in ('objfile', 'locationfile', 'measurementsfile'):
  179. if secname == 'onewall':
  180. # special default lookup under ./HERE/static
  181. f = path(__file__).parent.joinpath('setups', secname, self.config['scenes'][secname][fname])
  182. else:
  183. f = path(self.config['scenes'][secname][fname])
  184. self.config['scenes'][secname][fname] = f
  185. if not fname in ('locationfile', 'measurementsfile') and not f.exists():
  186. log.warn('configured %s %s for scene %s does not exist' % (fname, f.abspath(), secname))
  187. sys.exit()
  188. active = self.config['scenes']['active']
  189. self.activeScene = self.config['scenes'][active]
  190. if active == 'onewall' and len(self.config['aps'].sections) == 0:
  191. # add a default AP for onewall scene
  192. class FAKE(UserDict): pass
  193. for i, (name, x, y, z) in enumerate([('ap1', -6.0, -1.0, -2.0), ('ap2', 0.0, 0.0, 0.0)]):
  194. faked_ap = FAKE()
  195. self.config['aps'][name] = faked_ap
  196. faked_ap.name = name
  197. faked_ap['mac'] = '00:00:00:00:00:0' + str(i)
  198. faked_ap['powerid'] = 'default'
  199. faked_ap['power'] = 0.1
  200. faked_ap['x'] = x
  201. faked_ap['y'] = y
  202. faked_ap['z'] = z
  203. faked_ap['on'] = True
  204. faked_ap['optimize'] = True
  205. self.config['aps'].sections.append(name)
  206. self.aliasedmac2apid.clear()
  207. apcfgs = self.config['aps']
  208. for apid in apcfgs.sections:
  209. assert apcfgs[apid]['mac'] is not None, 'invalid cfg for %s: %r' % (apid, apcfgs[apid])
  210. self.mac2cfg[apcfgs[apid]['mac']] = apcfgs[apid]
  211. for mac in apcfgs[apid]['aliases']:
  212. self.aliasedmac2apid[mac] = apid
  213. #~ if apid != '107':
  214. #~ self.config['aps'][apid]['optimize'] = False
  215. # add reverse paths
  216. for pathid in list(self.activeScene['paths'].scalars):
  217. if not pathid.startswith('still_'):
  218. self.activeScene['paths']['%s_r' % pathid] = list(reversed(self.activeScene['paths'][pathid]))
  219. def _checkConfig(self):
  220. last_mtime = self.configfile.mtime
  221. while True:
  222. try:
  223. if self.configfile.mtime > last_mtime:
  224. self._loadConfig()
  225. last_mtime = self.configfile.mtime
  226. except Exception:
  227. log.exception('error during loading config from %s' % self.configfile.normpath())
  228. time.sleep(2)
  229. @cherrypy.expose
  230. def index(self, **kwargs):
  231. reload(render)
  232. return render.renderIndex(self)
  233. @cherrypy.expose
  234. def localize(self, name, **kwargs):
  235. reload(render)
  236. return render.renderLocalize(self, name, refresh='refresh' in kwargs)
  237. @cherrypy.expose
  238. def view(self, name):
  239. if not name in self.mstations:
  240. return ''' unknown station: "%s"''' % name
  241. reload(render)
  242. return render.renderView(self, self.mstations[name])
  243. @cherrypy.expose
  244. def measure(self, name):
  245. ''' collect measurements for training raytracer params '''
  246. reload(render)
  247. return render.renderMeasure(self, self.mstations.get(name) )
  248. @cherrypy.expose
  249. def locate(self, name):
  250. if not name in self.mstations:
  251. return ''' unknown station: "%s"''' % name
  252. reload(render)
  253. return render.renderLocate(self, self.mstations[name])
  254. @cherrypy.expose
  255. def cluster(self):
  256. reload(render)
  257. return render.renderCluster(self)
  258. @cherrypy.expose
  259. def measurements(self, view=None, stations=None):
  260. reload(render)
  261. if stations is None:
  262. stations = self.config['knownStations']
  263. else:
  264. stations = [e.strip() for e in stations.split(',') if e.strip()]
  265. if view is not None:
  266. view = view.split(',')
  267. else:
  268. view = ['mean', 'median', 'stddev']
  269. return render.renderMeasurements(self, stations=stations, view=view)
  270. @cherrypy.expose
  271. def optimize(self):
  272. reload(render)
  273. return render.renderOptimizer(self)
  274. @cherrypy.expose
  275. def optruns(self, optruns='', **kwargs):
  276. reload(render)
  277. return render.renderEvaluteOptruns(self, optruns, compact='compact' in kwargs, **kwargs)
  278. @cherrypy.expose
  279. def plotHistogram(self, **kwargs):
  280. plotfile = self.config['tmp'].joinpath('_hist.png')
  281. reload(plotter)
  282. with self.plotLock:
  283. plotter.plotHistogram(plotfile, **kwargs)
  284. return serve_file(plotfile)
  285. @cherrypy.expose
  286. def collect(self, station):
  287. ''' collect a path for using in locatization'''
  288. reload(render)
  289. return render.renderCollectPath(self, station)
  290. @cherrypy.expose
  291. def evaluate(self, station, optrun=None, cubewidth=3, errtype='2d', filter=None, algo='hmm', **kwargs):
  292. ''' evalute collected measurements'''
  293. reload(render)
  294. return render.renderEvaluate(self, optrun, station, int(cubewidth), errtype=errtype,
  295. refresh='refresh' in kwargs and not kwargs['refresh'] == False,
  296. filter=filter, algo=algo)
  297. @cherrypy.expose
  298. def evaluatePath(self, station, optrun, pathid, runid, setting, cubewidth=3, algo='hmm', **kwargs):
  299. ''' evalute collected measurements'''
  300. reload(render)
  301. if pathid == 'LASTCOLLECTED':
  302. pathid = self.lastCollectedPath[1]
  303. if runid == 'LASTCOLLECTED':
  304. runid = self.lastCollectedPath[2]
  305. return render.renderEvaluatePath(self, optrun, station, pathid, runid, setting,
  306. int(cubewidth), algo, refresh='refresh' in kwargs and not kwargs['refresh'] == False)
  307. @cherrypy.expose
  308. def startOptimizer(self, optsession, station, power, density, numphotons, resolution, bbox, scene,
  309. startpop, childcount, childcull, mutprob=0.1, mutamt=0.1, resume='',
  310. neworgs=0, timelimit=0, genlimit=0, **kwds):
  311. # forbid these two chars in name as they have some internal semantics
  312. optsession = optsession.replace('_', '.').replace(',', '.')
  313. power = float(power)
  314. density = float(density)
  315. numphotons = int(numphotons)
  316. bbox = [float(e) for e in bbox.split(',') if e.strip()]
  317. bbox = BBox(x1=bbox[0], x2=bbox[1], y1=bbox[2], y2=bbox[3], z1=bbox[4], z2=bbox[5])
  318. if ',' in resolution: # its a triple
  319. res = [int(e) for e in resolution.split(',')]
  320. resolution = Resolution(res[0], res[1], res[2])
  321. else: # its only the x value - calculate the rest to ensure the other two dims have the same pixel/m ratio
  322. xres = int(resolution)
  323. xr, yr, zr = bbox.x2 - bbox.x1, bbox.y2 - bbox.y1, bbox.z2 - bbox.z1
  324. resolution = Resolution(x=xres, y=int(float(xres) * yr / xr), z=int(float(xres) * zr / xr))
  325. startpop = int(startpop)
  326. childcount = int(childcount)
  327. childcull = int(childcull)
  328. self.optimizer.startOptimizeRun(optsession, station, power, density, numphotons, resolution, bbox, scene,
  329. startpop, childcount, childcull, float(mutprob),
  330. float(mutamt), resume=resume.strip(), neworgs=int(neworgs),
  331. timelimit=timelimit, genlimit=genlimit)
  332. raise cherrypy.HTTPRedirect(cherrypy.request.headers['Referer'])
  333. @cherrypy.expose
  334. def stopOptimizer(self):
  335. self.optimizer.stopOptimizeRun()
  336. raise cherrypy.HTTPRedirect(cherrypy.request.headers['Referer'])
  337. @cherrypy.expose
  338. def getAPInfo(self, name, jsid, filter=None):
  339. cherrypy.response.headers['Content-Type'] = 'application/json'
  340. if filter is not None:
  341. filter = {e for e in filter.split(',') if e.strip()}
  342. if not name in self.mstations:
  343. return json.dumps({})
  344. if not 'lastdelivered' in cherrypy.session:
  345. cherrypy.session['lastdelivered'] = {} #key: jsid, value: last delivered ts
  346. lastdelivered = cherrypy.session['lastdelivered']
  347. ld = lastdelivered.get(jsid)
  348. lastdelivered[jsid] = datetime.now()
  349. resdata = {}
  350. for (mac, apname), infolist in self.mstations[name].data.items():
  351. if filter is not None:
  352. if not mac in self.mac2cfg:
  353. continue
  354. apid = self.mac2cfg[mac].name
  355. if not apid in filter:
  356. continue
  357. if ld is not None:
  358. _infolist = [apinfo for apinfo in infolist if apinfo.localts > ld]
  359. else:
  360. _infolist = infolist[-5:]
  361. resdata[mac.replace(':', '_')] = _infolist
  362. dthandler = lambda obj: obj.strftime('%H:%M:%S') if isinstance(obj, datetime) else None
  363. return json.dumps(resdata, default=dthandler)
  364. @cherrypy.expose
  365. def startMeasureLoc(self, name, locid, duration):
  366. cherrypy.response.headers['Content-Type'] = 'application/json'
  367. if not locid.isdigit() or not int(locid) in self.known_locations:
  368. valid = ','.join(str(e) for e in self.known_locations)
  369. return json.dumps({'started': '', 'error': 'invalid location %s [valid: %s]' % (locid, valid)})
  370. duration = float(duration)
  371. startts = time.time()
  372. with self.writeLock, open(MEASUREMENTS_STORAGE, 'a') as f:
  373. f.write('start: %s %s %s\n' % (name, locid, datetime.fromtimestamp(startts)))
  374. self.measure_enabled = (name, locid, startts, duration)
  375. # setup list
  376. self.tracked_measurements[name][locid] = defaultdict(list)
  377. def waitForEnd():
  378. while time.time() < startts + duration:
  379. time.sleep(0.1)
  380. self.measure_enabled = None
  381. self.measurement_stopped = datetime.now()
  382. with self.writeLock, open(MEASUREMENTS_STORAGE, 'a') as f:
  383. aps = self.tracked_measurements[name][locid].items()
  384. log.info('storing measurements for %s access points' % len(aps))
  385. run = {}
  386. for (bssid, ssid), data in aps:
  387. if any(not c in string.printable for c in ssid):
  388. log.warn('got corrupt ssid: %s' % repr(ssid))
  389. continue
  390. apid = '???' if not bssid in self.mac2cfg else self.mac2cfg[bssid].name
  391. s = '%s %s %s %s %s rssi: ' % (name, locid, apid, bssid, ssid)
  392. f.write(s)
  393. f.write(','.join(str(apinfo.rssi) for apinfo in data) + '\n')
  394. run[bssid] = [apinfo.rssi for apinfo in data]
  395. f.write('end: %s %s %s\n' % (name, locid, self.measurement_stopped))
  396. self.stored_measurements[name][int(locid)].append((self.measurement_stopped, run))
  397. daemonthread(target=waitForEnd, name="waitForEnd_%s" % name)
  398. return json.dumps({'started': str(datetime.now()).split()[1]})
  399. def getMeasurements(self, station, apid, fromMeasurementStore=True):
  400. if fromMeasurementStore:
  401. if not self.activeScene['measurementsfile']:
  402. return None
  403. key = (station, apid)
  404. measurements, ts = self._apid2measurements.get(key, (None, None))
  405. f = path(self.activeScene['measurementsfile'] % (station, apid))
  406. if measurements is None or (f.exists() and f.mtime > ts):
  407. measurements = {}
  408. try:
  409. if not f.exists():
  410. log.warn('no measurement data found at %s' % f.abspath())
  411. self._apid2measurements[key] = (None, None)
  412. return None
  413. else:
  414. log.info('loading measurement data from %s' % f.abspath())
  415. for line in f.lines():
  416. if not line.strip():
  417. continue
  418. first, all_rssis = line.split(':')
  419. locid, x, y, z, avgrssi = first.split()
  420. rssis = [float(e) for e in all_rssis.split(',')]
  421. measurements[locid] = Measurement(float(x), float(y), float(z), float(avgrssi), rssis)
  422. self._apid2measurements[key] = (measurements, f.mtime)
  423. except Exception:
  424. log.error('cannot load measurements from %s' % f.abspath())
  425. measurements = {}
  426. return measurements
  427. else:
  428. measurements = {}
  429. MIN_DIST = 0.2 # only use measurements that are near a location
  430. # from tracked path
  431. pathids2runids = evaluate.getCollectedPathids2runids(station, self.activeScene['paths'],
  432. self.config['tmp_tracked'])
  433. for pathid, runs in pathids2runids.items():
  434. log.debug('extracting measurements from %s/%s/%s' % (apid, pathid, ','.join(runs)))
  435. locid2rssis = defaultdict(list)
  436. locids = [int(e) for e in self.activeScene['paths'][pathid]]
  437. for runid in runs:
  438. try:
  439. ms = localize.Measurements.fromTrackedMeasurements(self.known_locations, self.config['tmp_tracked'],
  440. station, pathid, runid)
  441. except Exception:
  442. log.error('cannot read path %s/%s' % (pathid, runid))
  443. continue
  444. nearestMeasurements = defaultdict(lambda : (None, None))
  445. for m in ms:
  446. for locid in locids:
  447. loc = self.known_locations[locid]
  448. _m, distance = nearestMeasurements[locid]
  449. curr_dist = ((m.pos.x - loc.x)**2 + (m.pos.y - loc.y)**2 + + (m.pos.z - loc.z)**2)**0.5
  450. if distance is None or curr_dist < distance:
  451. nearestMeasurements[locid] = (m, curr_dist)
  452. for locid in locids:
  453. m, distance = nearestMeasurements[locid]
  454. rssi = m.apdata.get(apid)
  455. if distance < MIN_DIST and rssi is not None:
  456. locid2rssis[locid].append(rssi)
  457. # now proces all collected rssis for the given path
  458. for locid, rssis in locid2rssis.items():
  459. loc = self.known_locations[locid]
  460. if len(rssis) > 0:
  461. avgrssi = sum(rssis) / len(rssis)
  462. measurements[str(locid)] = Measurement(float(loc.x), float(loc.y), float(loc.z), float(avgrssi), rssis)
  463. return measurements
  464. def loadMeasurementDump(self):
  465. ''' load measurements from file that includes all stations '''
  466. self.stored_measurements.clear()
  467. run = {}
  468. for i, l in enumerate(MEASUREMENTS_STORAGE.lines()):
  469. try:
  470. if l.startswith('start:'):
  471. l2 = l.split()
  472. _, station, locid, d1, d2 = l2
  473. run[(station, locid)] = {}
  474. elif l.startswith('end:'):
  475. l2 = l.split()
  476. if len(l2) == 5:
  477. _, station, locid, d1, d2 = l2
  478. if (station, locid) in run:
  479. dt = parser.parse('%s %s' % (d1, d2))
  480. self.stored_measurements[station][int(locid)].append((dt, run[(station, locid)]))
  481. #~ self.stored_measurements[self.active_station]
  482. run.pop((station, locid), None)
  483. else:
  484. if 'rssi:' in l:
  485. meta, rssis = l.split('rssi:')
  486. meta = meta.split()
  487. station, locid, apid, mac = meta[:4]
  488. rssis = [float(e) for e in rssis.split(',') if e.strip()]
  489. if (station, locid) in run:
  490. run[(station, locid)][apid] = rssis
  491. except Exception:
  492. log.exception('error during parsing line %s: %s]' % (i+1, l))
  493. @cherrypy.expose
  494. def getMeasurementResults(self, name, locid):
  495. cherrypy.response.headers['Content-Type'] = 'application/json'
  496. apdata = {}
  497. if self.measure_enabled is None:
  498. aps = self.tracked_measurements[name][locid].items()
  499. for (bssid, ssid), data in aps:
  500. rssis = [apinfo.rssi for apinfo in data]
  501. avg_rssi = sum(rssis) / float(len(rssis)) if len(rssis) > 0 else 0
  502. apdata[bssid.replace(':', '_')] = {'rssis': rssis, 'avg': '%.1f' % avg_rssi}
  503. # signal still running measurement with endts = ""
  504. endts = str(self.measurement_stopped).split()[1] if self.measurement_stopped is not None else ''
  505. return json.dumps({'data': apdata,
  506. 'stopped': endts,
  507. })
  508. @nocache
  509. @cherrypy.expose
  510. def buildOptimizefile(self, station):
  511. cherrypy.response.headers['Content-Type'] = 'text/plain'
  512. self.loadMeasurementDump()
  513. log.info('known locids: %s' % ','.join(str(e) for e in self.known_locations.keys()))
  514. apid2locs = defaultdict(lambda: defaultdict(list))
  515. for locid in self.stored_measurements[station]:
  516. for dt, runs in self.stored_measurements[station][locid]:
  517. for apid in runs:
  518. # ignore all mac addresses, that are not configured
  519. if not apid in self.config['aps']:
  520. continue
  521. apid2locs[apid][locid].extend(runs[apid])
  522. res = ''
  523. for apid, locs in apid2locs.items():
  524. outfile = OPTIMIZE_FILES % (station, apid)
  525. res += 'write to %s\n' % path(outfile).abspath()
  526. with open(outfile, 'w') as f:
  527. for locid, rssis in locs.items():
  528. if not locid in self.known_locations:
  529. log.warn('skipping unknown location %s' % locid)
  530. continue
  531. loc = self.known_locations[locid]
  532. avg_sig = sum(rssis) / float(len(rssis))
  533. _str_rssis = ','.join(str(e) for e in rssis)
  534. f.write('%s %s %s %s %.2f : %s\n' % (locid, loc.x, loc.y, loc.z, avg_sig, _str_rssis))
  535. res += '%s\n' % path(outfile).text()
  536. return res
  537. @cherrypy.expose
  538. def initDevice(self, name):
  539. cherrypy.response.headers['Content-Type'] = 'text/plain'
  540. return 'true 0.5'
  541. def _localize(self, device):
  542. SHUTDOWN_TIMEOUT = 10 #seconds
  543. CUBE_WIDTH = 3
  544. COUNT = 2000
  545. try:
  546. # init stuff
  547. env = self.getLocalizationEnv(device)
  548. last_ts = time.time()
  549. lmse = localize.LMSELocalizer(env, cubewidth=CUBE_WIDTH, verbose=False, with_history=True)
  550. hmm = localize.HMMLocalizer(env, cubewidth=CUBE_WIDTH, verbose=False)
  551. #~ localizers = [('lmse', lmse), ('hmm', hmm)]
  552. localizers = [('hmm', hmm)]
  553. #~ localizers = [('lmse', lmse)]
  554. #~ localizers = []
  555. while True:
  556. try:
  557. # get from Queue and raises Queue.empty exception
  558. data = self.localization_data[device].get(timeout=0.03)
  559. ts2apdata = defaultdict(dict)
  560. for info in data:
  561. cfg = self.mac2cfg.get(info['bssid'])
  562. if not cfg:
  563. continue
  564. apid = cfg.name
  565. if apid in self.localizeSessionInactiveAPs[device]:
  566. continue
  567. ts = round(info['ts'], 1) # round to 100ms granularity
  568. if 'pos' in info:
  569. pos = tuple(info['pos'])
  570. else:
  571. pos = (0,0,0)
  572. ts2apdata[(pos, ts)][apid] = info['rssi']
  573. measurements = []
  574. for (pos, ts), apdata in sorted(ts2apdata.items()):
  575. remotets = datetime.fromtimestamp(ts)
  576. measurements.append(localize.Measurement(remotets, apdata, pos))
  577. tt = time.time()
  578. for curr_m in measurements:
  579. for algo, localizer in localizers:
  580. paths, measurements_modified = localizer.evaluateNext(localize.Measurements([curr_m]))
  581. measurements_a = measurements_modified.asArray(localizer.apids)
  582. #~ xyz = paths['seq'][0]
  583. for mm, ma, i in zip(measurements_modified, measurements_a, reversed(range(1, len(paths.values()[0])+1))):
  584. xyzs = [(env.cube2meter(x, y ,z , cubewidth=CUBE_WIDTH), costs) for x, y, z, costs in localizer.decoder.history[-i][-COUNT:]]
  585. res = [(x, y, z, costs, ma, mm) for (x, y, z), costs in xyzs]
  586. for sessionid in list(self.localizeSessions[device]):
  587. self.last_result[(device, sessionid, algo)].put(res)
  588. #~ log.debug('both in %.3f secs' % (time.time() - tt)
  589. except Queue.Empty:
  590. if time.time() - last_ts > SHUTDOWN_TIMEOUT:
  591. log.info('shutdown localizer worker')
  592. break
  593. time.sleep(0.02)
  594. continue
  595. else:
  596. last_ts = time.time()
  597. finally:
  598. self.localization_data.pop(device)
  599. self.localizeSessions[device].clear()
  600. self.localizeSessionInactiveAPs.clear()
  601. def getLocalizationEnv(self, device):
  602. if not device in self.device2localizationenv:
  603. locfile = self.activeScene['locationfile']
  604. objfile = self.activeScene['objfile']
  605. aps = []
  606. for apid in self.config['aps'].sections:
  607. apcfg = self.config['aps'][apid]
  608. aps.append((apid, apcfg['x'], apcfg['y'], apcfg['z']))
  609. # HARDCODE
  610. #optrun = 'iconia.basic5mat_1'
  611. #optrun = 'optsession_1_swaped'
  612. #optrun = 'optnetwork4_1'
  613. optrun = self.config['serve_localize_apdata']
  614. if not optrun:
  615. print "Error: No optrun"
  616. else:
  617. print "optrun: ", optrun
  618. optrundir = self.config['tmp_apdata'].joinpath(optrun)
  619. vip = optrundir.joinpath(self.activeScene.name + '_%s.dat')
  620. env = localize.Environment(objfile=objfile, aps=aps, locationfile=locfile, tmpdir=self.config['tmp'],
  621. vi_path=vip, bbox2d=self.activeScene['bbox'][:4],
  622. davalues=[])
  623. self.device2localizationenv[device] = env
  624. return self.device2localizationenv[device]
  625. @cherrypy.expose
  626. def newData(self, name, data):
  627. ''' receive new data from a measuring device '''
  628. try:
  629. localts = datetime.now()
  630. if isinstance(data, basestring):
  631. #TODO: use json?
  632. data = eval(data)
  633. if not name in self.mstations:
  634. self.mstations[name] = MobileStation(name)
  635. unique_aps = len({e['bssid'] for e in data})
  636. unique_ts = len({e['ts'] for e in data})
  637. log.info('receiving data from %s with %s aps [%s unique ts]' % (name, unique_aps, unique_ts))
  638. station = self.mstations[name]
  639. # this is probably not threadsafe
  640. if not name in self.localization_data:
  641. queue = self.localization_data[name]
  642. daemonthread(target=self._localize, name="localize_%s" % name, args=(name,))
  643. else:
  644. queue = self.localization_data[name]
  645. queue.put(data)
  646. ## check aliases
  647. rssids_from_aliased = defaultdict(list)
  648. for info in data:
  649. mac = info['bssid']
  650. apid = self.aliasedmac2apid.get(mac)
  651. if apid is not None:
  652. found = False
  653. # first check if alias target is in data
  654. for info2 in data:
  655. if self.mac2cfg.get(info2['bssid']) == apid:
  656. found = True
  657. if not found:
  658. # change the mac of this info record
  659. info['bssid'] = self.config['aps'][apid]['mac']
  660. else:
  661. rssids_from_aliased[apid].append(info['rssi'])
  662. last_rssi = {}
  663. for info in data:
  664. # (mac, ssid)
  665. key = (info['bssid'], info['ssid'])
  666. if not key in last_rssi and key in station.data:
  667. last_rssi[key] = station.data[key][-1].rssi
  668. if not key in last_rssi:
  669. delta = 0
  670. else:
  671. delta = last_rssi[key] - info['rssi']
  672. rssi = info['rssi']
  673. if info['bssid'] in self.mac2cfg and self.mac2cfg[info['bssid']].name in rssids_from_aliased:
  674. rssis = rssids_from_aliased[self.mac2cfg[info['bssid']].name]
  675. rssi = sum(rssis + [rssi]) / (len(rssis) + 1)
  676. last_rssi[key] = rssi
  677. api = APInfo(rssi=rssi,
  678. remotets=datetime.fromtimestamp(info['ts']),
  679. localts=localts,
  680. delta=delta)
  681. #~ print api.localts - api.remotets
  682. if self.measure_enabled is not None:
  683. stationname, locid, _, _ = self.measure_enabled
  684. measurement_info = self.tracked_measurements[stationname][locid]
  685. measurement_info[key].append(api)
  686. elif self.pathtrack_enabled is not None:
  687. stationname, pathid, _ = self.pathtrack_enabled
  688. measurement_info = self.tracked_measurements[stationname][pathid]
  689. measurement_info[key].append(api)
  690. station.data[key].append(api)
  691. except Exception:
  692. log.exception('error during receiving data')
  693. @cherrypy.expose
  694. def uploadobjfile(self, expectedhash, objfile):
  695. ''' remote guis can upload their locally modified obj files over this interface'''
  696. objpath = self.config['tmp'].joinpath('obj')
  697. s = objfile.file.read()
  698. h = hashlib.sha1(s).hexdigest()
  699. assert h == expectedhash
  700. name = path(objfile.filename.replace('\\', '/').split('/')[-1]).namebase
  701. f = objpath.joinpath('%s_%s.obj' % (h, name))
  702. log.info('storing objfile [%.1f kb] to %s...' % (len(s) / 1024.0, f.abspath()))
  703. f.write_bytes(s)
  704. self.hash2objfile[h] = f
  705. @cherrypy.expose
  706. def queueJob(self, scenename, objhash, materials, ap, bbox, resolution, density, numphotons, disabledObjects=''):
  707. ''' queue a rendering job from a remote gui'''
  708. cherrypy.response.headers['Content-Type'] = 'text/xml'
  709. if not objhash in self.hash2objfile:
  710. log.warn('need new obj file')
  711. return '<error>unknown objfile</error>'
  712. objfile = self.hash2objfile[objhash]
  713. # eval is quite unsecure - see http://stackoverflow.com/questions/661084/security-of-pythons-eval-on-untrusted-strings
  714. resolution = Resolution(**eval(resolution, {'__builtins__':[]}, {}))
  715. bbox = BBox(**eval(bbox, {'__builtins__':[]}, {}))
  716. ap = simulator.AccessPoint(**eval(ap, {'__builtins__':[]}, {}))
  717. materials = loadMaterialsFromString(materials)
  718. density = float(density)
  719. numphotons = int(numphotons)
  720. disabledObjects = disabledObjects.split(',')
  721. run = simulator.Run(scenename, objfile, materials, ap, bbox, resolution,
  722. density, numphotons, code='transfer_result', disabledObjects=disabledObjects)
  723. self.simulator.queue(run, simulator.HIGH_PRIO)
  724. log.info('queued run %s' % run.id)
  725. return '<success state="queued" id="%s"/>' % run.id
  726. @cherrypy.expose
  727. def getJob(self, worker):
  728. cherrypy.response.headers['Content-Type'] = 'text/xml'
  729. ctrlfile = self.simulator.getWork(worker)
  730. if ctrlfile is None:
  731. return '<empty/>'
  732. return ctrlfile.text()
  733. @cherrypy.expose
  734. def transferFullResult(self, worker, runid, zippedfile):
  735. ''' called by worker_code_transfer_result - sends back .raw and .dat file'''
  736. zippeddata = zippedfile.file.read()
  737. self.simulator.storeTransferedResult(worker, runid, zippeddata)
  738. @cherrypy.expose
  739. def transferResultAtLocIDs(self, worker, runid, data):
  740. ''' called by worker_code_transfer_result - sends back .raw and .dat file
  741. data is a dict with key: locid, value: rssi
  742. '''
  743. data = eval(data, {'__builtins__':[]}, {})
  744. #~ t = time.time()
  745. self.simulator.storeResultAtLocIDs(worker, runid, data)
  746. #~ print time.time() - t
  747. @cherrypy.expose
  748. def queryJobs(self, runids):
  749. cherrypy.response.headers['Content-Type'] = 'text/xml'
  750. waiting, running, finished = self.simulator.queryRuns(runids.split(','))
  751. tree = StringToTree('<result/>')
  752. tree.getroot().attrib['waiting'] = ','.join(waiting)
  753. tree.getroot().attrib['running'] = ','.join(running)
  754. tree.getroot().attrib['finished'] = ','.join(finished)
  755. return TreeToString(tree)
  756. @cherrypy.expose
  757. def getJobZipData(self, runid):
  758. for run in self.simulator.finished_jobs:
  759. if runid == run.id:
  760. for f in run.resultfiles:
  761. if f.endswith('.zip'):
  762. return f.bytes()
  763. @cherrypy.expose
  764. def getObjData(self, worker, objfile, runid):
  765. cherrypy.response.headers['Content-Type'] = 'text/plain'
  766. #~ run = simulator.Run.get(runid)
  767. log.info('serving objfile %s from run %s to worker %s' % (objfile, runid, worker))
  768. of = self.simulator.tmpdir.joinpath(runid, objfile)
  769. return of.text()
  770. @cherrypy.expose
  771. def brokenJob(self, worker, runid):
  772. log.warn('signaling broken job [%s]' % cherrypy.request.headers.get('user-agent', ''))
  773. self.simulator.brokenRun(worker, runid)
  774. if 'Referer' in cherrypy.request.headers:
  775. # redirect to referer
  776. raise cherrypy.HTTPRedirect(cherrypy.request.headers['Referer'])
  777. @cherrypy.expose
  778. def optimizePlot(self, session, onlymins='false'):
  779. reload(plotter)
  780. sdir = self.optimizer.tmpdir.joinpath(session)
  781. if onlymins == 'true':
  782. lines = sdir.joinpath('mins.txt').lines()[:-1]
  783. plotfile = sdir.joinpath('_plot_mins.png')
  784. highlightmins = False
  785. else:
  786. lines = []
  787. for f in list(sorted(sdir.files('population_*.txt'), key=lambda x: x.mtime)):
  788. lines.extend(f.lines()[:-1])
  789. plotfile = sdir.joinpath('_plot_all.png')
  790. highlightmins = True
  791. max_mt = max([f.mtime for f in sdir.files('*.txt')] + [0])
  792. if not plotfile.exists() or max_mt > plotfile.mtime:
  793. with self.plotLock:
  794. plotter.plotOptimizeRun(lines, plotfile, highlightmins=highlightmins)
  795. return serve_file(plotfile)
  796. @cherrypy.expose
  797. def simulatorStatsPlot(self):
  798. reload(plotter)
  799. tmpdir = self.simulator.tmpdir
  800. statsfile = tmpdir.parent.joinpath('simulator.stats')
  801. assert statsfile.exists()
  802. plotfile = tmpdir.joinpath('simulatorStats.png')
  803. lines = statsfile.lines()
  804. with self.plotLock:
  805. plotter.plotSimulatorStats(lines, plotfile)
  806. return serve_file(plotfile)
  807. @cherrypy.expose
  808. def getAPConfig(self):
  809. cherrypy.response.headers['Content-Type'] = 'text/xml'
  810. tree = StringToTree('<aps/>')
  811. for apid in self.config['aps'].sections:
  812. cfg = self.config['aps'][apid]
  813. attribs = {'mac': cfg['mac'], 'x': cfg['x'], 'y': cfg['y'], 'z': cfg['z'],
  814. 'on': str(cfg['on']).lower(), 'powerid': cfg['powerid'], 'id': apid, 'power': cfg['power']}
  815. subelement(tree.getroot(), 'ap', attribs=attribs)
  816. return TreeToString(tree)
  817. @cherrypy.expose
  818. def getObjFile(self):
  819. cherrypy.response.headers['Content-Type'] = 'text/plain'
  820. return self.activeScene['objfile'].text()
  821. @cherrypy.expose
  822. def getMaterialsFile(self):
  823. cherrypy.response.headers['Content-Type'] = 'text/xml'
  824. materials = {}
  825. for matname in self.activeScene['materials'].scalars:
  826. reflect, alpha = self.activeScene['materials'][matname]
  827. materials[matname] = Brdf(reflect, alpha)
  828. return TreeToString(materialsAsXml(materials))
  829. @cherrypy.expose
  830. def getSceneConfig(self):
  831. cherrypy.response.headers['Content-Type'] = 'text/xml'
  832. tree = StringToTree('<scene/>')
  833. e = tree.getroot()
  834. e.attrib['bbox'] = ','.join(str(e) for e in self.activeScene['bbox'])
  835. e.attrib['resolution'] = ','.join(str(e) for e in self.activeScene['resolution'])
  836. e.attrib['numphotons'] = str(self.activeScene['numphotons'])
  837. e.attrib['density'] = str(self.activeScene['density'])
  838. e.attrib['name'] = self.activeScene.name
  839. e.attrib['device'] = self.config['defaultDevice']
  840. e.attrib['alldevices'] = ','.join(self.config['knownStations'])
  841. return TreeToString(tree)
  842. @cherrypy.expose
  843. def getOptimizeSessions(self):
  844. cherrypy.response.headers['Content-Type'] = 'text/xml'
  845. tree = StringToTree('<sessions/>')
  846. _key = lambda x: x.files('*.txt')[0].mtime if len(x.files('*.txt')) > 0 else 0
  847. for i, d in enumerate(reversed(sorted(self.optimizer.tmpdir.dirs(), key=_key))):
  848. f = d.joinpath('init.txt')
  849. if not f.exists():
  850. continue
  851. initparams = dict([(e.split(':')[0], e.split(':')[1].strip())
  852. for e in f.text().split('\n') if e.strip()])
  853. attribs = {'name': d.name, 'dt': str(datetime.fromtimestamp(d.mtime)).split('.')[0]}
  854. attribs.update(initparams)
  855. sessionEl = subelement(tree.getroot(), 'session', attribs=attribs)
  856. mindelta = -1
  857. f = d.joinpath('mins.txt')
  858. if f.exists():
  859. mindata = f.text().strip().split('\n')[-1].split()
  860. e = subelement(sessionEl, 'minimum')
  861. for s in mindata[1:]:
  862. name, value = s.split(':')
  863. e.attrib[name.replace('/', '-')] = value
  864. return TreeToString(tree)
  865. def _plot2dmap(self, level, refresh, customdraw=None):
  866. reload(plotter)
  867. tmpdir = self.simulator.tmpdir
  868. plotfile = tmpdir.joinpath('2dcut_%s.png' % level)
  869. aps = {e.name: e for e in self.mac2cfg.values()}
  870. plotter.plot2DMap(self.activeScene, level, self.known_locations, aps, plotfile, refresh, customdraw)
  871. return plotfile
  872. @cherrypy.expose
  873. def plot2DMap(self, level, **kwds):
  874. plotfile = self._plot2dmap(level, 'refresh' in kwds)
  875. return serve_file(plotfile)
  876. @cherrypy.expose
  877. def plotMeasurementCoverage(self, level, apfilter=None, **kwds):
  878. plotfile = self.simulator.tmpdir.joinpath('2dcut_%s_fancy.png' % level)
  879. def customdraw(img, draw, m2d, z_cut, aps, locations):
  880. for apid, apcfg in aps.items():
  881. if apfilter is not None and not apfilter in apid:
  882. continue
  883. for locid, loc in locations.items():
  884. if abs(loc.z - z_cut) < 2 and abs(apcfg['z'] - z_cut) < 2:
  885. x1, y1 = m2d.meter2pixel(apcfg['x'], apcfg['y'])
  886. x2, y2 = m2d.meter2pixel(loc.x, loc.y)
  887. draw.line((x1, y1, x2, y2), fill=(64,64,64))
  888. img.save(plotfile)
  889. self._plot2dmap(level, 'refresh' in kwds, customdraw)
  890. return serve_file(plotfile)
  891. @cherrypy.expose
  892. def getLocationsAndMeasurements(self, station, overlay_tracked='false'):
  893. cherrypy.response.headers['Content-Type'] = 'text/xml'
  894. tree = StringToTree('<root/>')
  895. locationsEl = subelement(tree.getroot(), 'locations')
  896. for locid, loc in self.known_locations.items():
  897. attribs = {'id': locid, 'x': str(loc.x), 'y': str(loc.y), 'z': str(loc.z)}
  898. subelement(locationsEl, 'location', attribs=attribs)
  899. measurementsEl = subelement(tree.getroot(), 'measurements')
  900. for mac, apcfg in self.mac2cfg.items():
  901. attribs = {'mac': mac, 'id': apcfg.name}
  902. apEl = subelement(measurementsEl, 'ap', attribs=attribs)
  903. measurements = self.getMeasurements(station, apcfg.name, fromMeasurementStore=overlay_tracked != 'true')
  904. if measurements is not None:
  905. for locid, measurement in measurements.items():
  906. attribs = {'id': locid, 'avgrssi': measurement.avgrssi}
  907. subelement(apEl, 'location', attribs=attribs).text = ','.join(str(e) for e in measurement.rssis)
  908. return TreeToString(tree)
  909. @cherrypy.expose
  910. def startCollectPath(self, station, pathid):
  911. log.info('starting collecting path for station: %s and path: %s' % (station, pathid))
  912. self.pathtrack_enabled = (station, pathid, time.time())
  913. self.tracked_measurements[station][pathid] = defaultdict(list)
  914. self.tracked_positions[station][pathid] = []
  915. def _getPathByRemoteTS(self, tracked_measurements):
  916. by_remotets = defaultdict(list)
  917. for (mac, ssid), values in tracked_measurements.items():
  918. for apinfo in values:
  919. #!!! HACK for eduroam multi personalities
  920. aps = [('eduegcorr1', '00:17:df:a8:19:e'),
  921. ('edueg031', '00:17:df:a8:1b:4'),
  922. ('eduegcorr2' ,'00:17:df:a7:e8:0'),
  923. ('eduegdemo', '00:17:df:a8:19:9'),
  924. ('eduog1108', '00:17:df:a8:03:2'),
  925. ('eduog1server', '00:17:df:a7:e8:e'),
  926. ('eduog2206', '00:17:df:a8:a4:f'),
  927. ('eduog2corr', '00:17:df:a7:ea:f'),
  928. ('eduog2kitchen', '00:17:df:a7:e9:3'),
  929. ]
  930. for apid, mac_prefix in aps:
  931. if mac.startswith(mac_prefix):
  932. mac = self.config['aps'][apid]['mac']
  933. break
  934. # kill last 5 microseconds digits
  935. ts = apinfo.remotets.replace(microsecond = apinfo.remotets.microsecond / 10**5 * 10**5)
  936. by_remotets[ts].append((mac, apinfo))
  937. return by_remotets
  938. @cherrypy.expose
  939. def stopCollectPath(self, station, pathid, **kwds):
  940. self.pathtrack_enabled = None
  941. if 'broken' in kwds:
  942. log.warn('stopping broken measurement')
  943. return
  944. d = self.config['tmp_tracked'].joinpath(station, pathid)
  945. if not d.exists():
  946. d.makedirs()
  947. for idx in range(1, 100):
  948. runid = '%02d' % idx
  949. if not d.joinpath('measurements_%s.txt' % runid).exists():
  950. break
  951. self.lastCollectedPath = (station, pathid, runid)
  952. outfile = d.joinpath('measurements_%s.txt' % runid)
  953. outfileLocations = d.joinpath('locations_%s.txt' % runid)
  954. tracked_measurements = self.tracked_measurements[station][pathid]
  955. tracked_positions = self.tracked_positions[station][pathid]
  956. log.info('collecting path for station: %s and pathid: %s finished' % (station, pathid))
  957. log.info('got %s measurements and %s location infos' % (len(tracked_measurements), len(tracked_positions)))
  958. by_remotets = self._getPathByRemoteTS(tracked_measurements)
  959. _mean = lambda l: float(sum(l)) / len(l)
  960. with open(outfile, 'w') as f:
  961. for ts, values in sorted(by_remotets.items()):
  962. mac2rssis = defaultdict(list)
  963. for mac, apinfo in values:
  964. if mac in self.mac2cfg:
  965. mac2rssis[mac].append(apinfo.rssi)
  966. s = ' '.join('%s:%s' % (self.mac2cfg[mac].name, '%.1f' % _mean(rssis)) for mac, rssis in mac2rssis.items() )
  967. if not s:
  968. continue
  969. f.write('%s ts:%s\n' % (s, str(ts).replace(' ', 'T')))
  970. with open(outfileLocations, 'w') as f:
  971. for locid, ts in tracked_positions:
  972. f.write('locid:%s ts:%s\n' % (locid, str(ts).replace(' ', 'T')))
  973. @cherrypy.expose
  974. def newCollectPathPosition(self, station, pathid, locid, remotets):
  975. cherrypy.response.headers['Content-Type'] = 'text/xml'
  976. log.info('got locid: %s for station: %s and pathid: %s' % (locid, station, pathid))
  977. self.tracked_positions[station][pathid].append((locid, datetime.fromtimestamp(float(remotets))))
  978. return '<root value="%s"/>' % len(self._getPathByRemoteTS(self.tracked_measurements[station][pathid]))
  979. def _getCollectedPathids2runids(self, station, filter=None):
  980. tree = self._getCollectedPaths(station)
  981. pathids2runids = defaultdict(list)
  982. for pathEl in tree.getroot().xpath('path'):
  983. if filter is not None and filter.endswith('_r'):
  984. filter = filter[:-2]
  985. if filter is not None and not filter in pathEl.attrib['id']:
  986. continue
  987. for runEl in pathEl.xpath('run'):
  988. pathids2runids[pathEl.attrib['id']].append(runEl.attrib['id'])
  989. return pathids2runids
  990. def _getCollectedPaths(self, station):
  991. tree = StringToTree('<paths/>')
  992. for pathid in self.activeScene['paths'].scalars:
  993. locs = ','.join(str(e) for e in self.activeScene['paths'][pathid])
  994. pel = subelement(tree.getroot(), 'path', attribs={'id': pathid, 'locations': locs})
  995. d = self.config['tmp_tracked'].joinpath(station, pathid)
  996. if not d.exists():
  997. continue
  998. fnames = sorted(d.files('measurements_*.txt'))
  999. for fname in fnames:
  1000. id = fname.name[len('measurements_'):-4]
  1001. rel = subelement(pel, 'run', attribs = {'id': id})
  1002. subelement(rel, 'measurements').text = fname.text()
  1003. locfile = d.joinpath('locations_%s.txt' % id)
  1004. subelement(rel, 'locations').text = locfile.text() if locfile.exists() else ''
  1005. return tree
  1006. @cherrypy.expose
  1007. def getCollectedPaths(self, station):
  1008. cherrypy.response.headers['Content-Type'] = 'text/xml'
  1009. tree = evaluate.getCollectedPaths(station, self.activeScene['paths'], self.config['tmp_tracked'])
  1010. return TreeToString(tree)
  1011. @cherrypy.expose
  1012. def plotEvaluatedPath(self, optrun, station, pathid, runid, setting, cubewidth, algo):
  1013. plotfile = self.config['tmp'].joinpath('evaluator', optrun, '%s_%s_%s_%s_%s_%s.png' % (station, cubewidth, algo, setting, pathid, runid))
  1014. return serve_file(plotfile)
  1015. @cherrypy.expose
  1016. def interestingOptrun(self, optrun):
  1017. f = self.optimizer.tmpdir.joinpath(optrun, '.interested')
  1018. if f.exists():
  1019. f.remove()
  1020. else:
  1021. f.touch()
  1022. raise cherrypy.HTTPRedirect(cherrypy.request.headers['Referer'])
  1023. @cherrypy.expose
  1024. def buildOptrun(self, optrun):
  1025. reload(evaluate)
  1026. evaluate.buildOptrun(self, optrun)
  1027. raise cherrypy.HTTPRedirect(cherrypy.request.headers['Referer'])
  1028. @cherrypy.expose
  1029. def tiles(self, dummy, level, zoom, x, y):
  1030. f = self.config['tmp'].joinpath('tiles_%s' % level, zoom, x, y)
  1031. return serve_file(f, content_type="image/jpeg")
  1032. @cherrypy.expose
  1033. def apdatatiles(self, dummy, device_apid, zoom, x, y):
  1034. device, apid = device_apid.split(',')
  1035. with self.draw_aptiles_lock:
  1036. d = self.config['tmp'].joinpath('apdatatiles_%s' % apid)
  1037. if not d.exists():
  1038. env = self.getLocalizationEnv(device)
  1039. reload(plotter)
  1040. plotter.plot2DMapAPData(self, self.activeScene, env, apid)
  1041. f = d.joinpath(zoom, x, y)
  1042. return serve_file(f, content_type="image/jpeg")
  1043. def intoMapSpace(self, x, y, z):
  1044. ''' from real space (m) into map space (px)'''
  1045. pixel_per_meter = self.activeScene['2dpixelpermeter']
  1046. x1, x2, y1, y2, z1, z2 = self.activeScene['bbox']
  1047. res_x = int(x * pixel_per_meter * 8 - x1 * pixel_per_meter * 8)
  1048. res_y = int(y * pixel_per_meter * 8 - y1 * pixel_per_meter * 8)
  1049. res_z = int(z * pixel_per_meter * 8 - z1 * pixel_per_meter * 8)
  1050. return res_x, res_y, res_z
  1051. def fromMapSpace(self, x, y, z):
  1052. ''' from map (px) into real space (m)'''
  1053. pixel_per_meter = self.activeScene['2dpixelpermeter']
  1054. x1, x2, y1, y2, z1, z2 = self.activeScene['bbox']
  1055. res_x = (x + x1 * pixel_per_meter * 8) / (pixel_per_meter * 8)
  1056. res_y = (y + y1 * pixel_per_meter * 8) / (pixel_per_meter * 8)
  1057. res_z = (z + z1 * pixel_per_meter * 8) / (pixel_per_meter * 8)
  1058. return res_x, res_y, res_z
  1059. @cherrypy.expose
  1060. def locinfo(self, device, sessionid, algo, limit=100, skip=0):
  1061. limit = int(limit)
  1062. skip = int(skip)
  1063. cherrypy.response.headers['Content-Type'] = 'application/json'
  1064. env = self.getLocalizationEnv(device)
  1065. queue = self.last_result[(device, sessionid, algo)]
  1066. while queue.qsize() > 100:
  1067. try:
  1068. queue.get(False)
  1069. except Queue.Empty:
  1070. continue
  1071. results = []
  1072. for i in range(100):
  1073. try:
  1074. results.append(queue.get(False))
  1075. except Queue.Empty:
  1076. break
  1077. #~ print len(results), (device, algo)
  1078. frames = []
  1079. for j, result in enumerate(results):
  1080. if skip > 0 and j % skip != 0:
  1081. continue
  1082. frame = {}
  1083. z_count = defaultdict(int)
  1084. x_best, y_best, z_best, costs, ma_best, mm_best = result[-1]
  1085. hight = z_best
  1086. m_best_dict = {}
  1087. for apid, aprssi in zip(env.aps.keys(), ma_best):
  1088. if aprssi < 0:
  1089. m_best_dict[apid] = aprssi
  1090. x_best, y_best, z_best = env.translateToVoxel(x_best, y_best, z_best)
  1091. for i, (x, y, z, costs, ma, mm) in enumerate(result):
  1092. x, y, z = self.intoMapSpace(x, y, z)
  1093. z_count[z] += 1
  1094. frame[(x, y, z)] = (costs, i)
  1095. top_z = list(sorted([(v, k) for k, v in z_count.items()], reverse=True))[0][1]
  1096. topn_z = {}
  1097. for (x, y, z), v in frame.items():
  1098. if top_z != z:
  1099. continue
  1100. topn_z[(x, y)] = (v[0], v[1])
  1101. topn = list(sorted(topn_z.items(), key=lambda e: e[1][1]))[-limit:]
  1102. topn = [(k[0], k[1], int(v[1])) for k, v in topn]
  1103. aps = env.aps.keys()
  1104. apdata = [(e, env.aps[e].dbdata[x_best, y_best, z_best], m_best_dict.get(e, -100)) for e in aps]
  1105. pos_x, pos_y, pos_z = self.intoMapSpace(*mm_best.pos)
  1106. frames.append({'pos': (pos_x, pos_y, pos_z), 'z': top_z, 'topn': topn, 'aps': apdata, 'hight':hight})
  1107. #~ print frames
  1108. return json.dumps({'data': frames, 'meta': {'queuesize': queue.qsize()}})
  1109. @cherrypy.expose
  1110. def spawnSyntheticSignal(self, device, lon, lat):
  1111. x, y, _ = self.fromMapSpace(float(lon), float(lat), 0)
  1112. env = self.getLocalizationEnv(device)
  1113. ts = time.time()
  1114. _x, _y, _z = env.translateToVoxel(x, y, 1)
  1115. _scandata = []
  1116. for apid, ap in env.aps.items():
  1117. bssid = self.config['aps'][apid]['mac']
  1118. rssi = ap.dbdata[_x, _y, _z]
  1119. _scandata.append({'bssid': bssid, 'rssi': rssi, 'ts': ts, 'ssid': apid})
  1120. self.newData(device, _scandata)
  1121. @cherrypy.expose
  1122. def startLocalizeSession(self, device, sessionid):
  1123. self.localizeSessions[device].add(sessionid)
  1124. #TODO: need clean up strategy for self.last_result
  1125. @cherrypy.expose
  1126. def stopLocalizeSession(self, device, sessionid):
  1127. self.localizeSessions[device].discard(sessionid)
  1128. for (_device, _sessionid, algo) in self.last_result.keys():
  1129. if _device == device and _sessionid == sessionid:
  1130. queue = self.last_result[(_device, _sessionid, algo)]
  1131. while queue.qsize() > 0:
  1132. try:
  1133. queue.get(False)
  1134. except Queue.Empty:
  1135. continue
  1136. @cherrypy.expose
  1137. def useAPforLocalizeDevice(self, device, apid, use):
  1138. if use == 'false':
  1139. self.localizeSessionInactiveAPs[device].add(apid)
  1140. else:
  1141. self.localizeSessionInactiveAPs[device].discard(apid)
  1142. def handleError():
  1143. '''cherrypy error handling function'''
  1144. cherrypy.response.headers['Content-Type'] = 'text/plain'
  1145. h = sorted(" %s: %s" % (k, v) for k, v in cherrypy.request.header_list)
  1146. headers = '\nRequest Headers:\n' + '\n'.join(h)
  1147. url = cherrypy.url(qs=cherrypy.request.query_string)
  1148. formattedparams = ', '.join('%s: %s' %
  1149. (k, v.encode('utf-8') if isinstance(v, basestring) else '?')
  1150. for k, v in cherrypy.request.params.items())
  1151. params = '\nRequest Parameters:\n ' + (formattedparams if formattedparams.strip() else '<no parameters>')
  1152. msg = '\nRequest Url:\n %s\n%s\n %s\n\n' % (url, params, headers)
  1153. log.error(msg, exc_info=True)
  1154. stackTrace = "".join(traceback.format_exc())
  1155. cherrypy.response.body = [''.join([msg, stackTrace, '\n'])]
  1156. def check_shutdown(sig):
  1157. if sig == win32con.CTRL_C_EVENT:
  1158. log.warn('received CTRL-C, inititating shutdown')
  1159. elif sig == win32con.CTRL_CLOSE_EVENT:
  1160. log.warn('received close, inititating shutdown')
  1161. elif sig in (win32con.CTRL_SHUTDOWN_EVENT, win32con.CTRL_LOGOFF_EVENT):
  1162. log.warn('received logoff/shutdown, inititating shutdown')
  1163. cherrypy.engine.stop()
  1164. cherrypy.engine.exit()
  1165. print 'LWS SHUTDOWN DONE'
  1166. def startServer(configfile=None, blocking=True):
  1167. setupLogging()
  1168. if sys.platform == 'win32':
  1169. win32api.SetConsoleCtrlHandler(check_shutdown, True)
  1170. if configfile is None:
  1171. configfile = path('./lws.ini').abspath()
  1172. if not path('./lws.ini').exists():
  1173. print 'no configfile given - autocreate at %s' % configfile.abspath()
  1174. configfile.touch()
  1175. else:
  1176. configfile = path(configfile).abspath()
  1177. global app
  1178. app = LWS(configfile)
  1179. globalconf = {
  1180. 'server.thread_pool' : 50,
  1181. 'server.socket_host' : app.config['httpHost'],
  1182. 'server.socket_port' : app.config['httpPort'],
  1183. 'engine.autoreload_on' : app.config['cpAutoRestart'],
  1184. 'log.screen' : app.config['cpLogScreen'],
  1185. 'tools.sessions.on' : True,
  1186. 'tools.sessions.storage_type' : 'ram',
  1187. 'checker.on' : False, # disable cherrypy checker
  1188. 'tools.decode.on' : True, # decode with utf-8 - fallback to latin-1
  1189. 'tools.gzip.on' : True,
  1190. 'tools.gzip.compress_level' : 6,
  1191. #gzip these mimetypes
  1192. 'tools.gzip.mime_types' : ['text/html', 'text/plain',
  1193. 'text/css', 'application/x-javascript',
  1194. 'text/javascript', 'text/xml', 'application/javascript',
  1195. 'application/json',
  1196. ],
  1197. 'tools.proxy.on' : app.config['behindProxy'],
  1198. 'tools.proxy.base' : app.config['proxyBase'],
  1199. 'tools.encode.on' : True,
  1200. 'tools.encode.encoding' : 'utf-8',
  1201. 'request.error_response' : handleError,
  1202. }
  1203. cherrypy.config.update(globalconf)
  1204. appconf = {}
  1205. appconf['/static'] = {
  1206. 'tools.etags.on' : True,
  1207. 'tools.etags.autotags' : True,
  1208. 'tools.staticdir.on' : True,
  1209. 'tools.staticdir.dir' : PATH_STATIC,
  1210. 'tools.sessions.on' : False,
  1211. }
  1212. cptree = cherrypy.tree.mount(app, '/', config=appconf)
  1213. log.info('mounting server at %s:%s' % (app.config['httpHost'], app.config['httpPort']))
  1214. cherrypy.engine.start()
  1215. if blocking:
  1216. cherrypy.engine.block()
  1217. def stopServer():
  1218. cherrypy.engine.exit()