comparison service/arduinoNode/arduinoNode.py @ 1139:db955e7943af

arduinonode reads config from etcd. use pushConfig.py to inform all nodes Ignore-this: 3155d873bb30ca82b48ace7531a550e5 darcs-hash:0b2be13fbb4dc2571bb1bb8ac97f6a1350f36e77
author drewp <drewp@bigasterisk.com>
date Sat, 03 Mar 2018 17:53:37 -0800
parents 1e43ec4a5f23
children 3010238b94a0
comparison
equal deleted inserted replaced
1138:08615804ee0e 1139:db955e7943af
1 """
2 depends on packages:
3 arduino-mk
4 indent
5 """
6 from __future__ import division 1 from __future__ import division
7 import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile 2 import glob, sys, logging, subprocess, socket, hashlib, time, tempfile
8 import shutil, json 3 import shutil, json
9 import serial 4 import serial
10 import cyclone.web 5 import cyclone.web
11 from cyclone.httpclient import fetch 6 from cyclone.httpclient import fetch
12 from rdflib import Graph, Namespace, URIRef, Literal, RDF, ConjunctiveGraph 7 from rdflib import Graph, Namespace, URIRef, Literal, RDF, ConjunctiveGraph
13 from rdflib.parser import StringInputSource 8 from rdflib.parser import StringInputSource
14 from twisted.internet import reactor, task 9 from twisted.internet import reactor, task
10 from twisted.internet.defer import inlineCallbacks
11 from twisted.internet.threads import deferToThread
15 from docopt import docopt 12 from docopt import docopt
13 import etcd3
16 14
17 import devices 15 import devices
18 import write_arduino_code 16 import write_arduino_code
19 import dotrender 17 import dotrender
20 import rdflib_patch 18 import rdflib_patch
41 39
42 ACTION_BASE = 10 # higher than any of the fixed command numbers 40 ACTION_BASE = 10 # higher than any of the fixed command numbers
43 41
44 hostname = socket.gethostname() 42 hostname = socket.gethostname()
45 CTX = ROOM['arduinosOn%s' % hostname] 43 CTX = ROOM['arduinosOn%s' % hostname]
44 etcd = etcd3.client(host='bang6')
46 45
47 class Config(object): 46 class Config(object):
48 def __init__(self, masterGraph): 47 def __init__(self, masterGraph, slowMode=False):
49 self.graph = ConjunctiveGraph() 48 self.masterGraph = masterGraph
49 self.slowMode = slowMode
50 self.configGraph = ConjunctiveGraph()
51
52 self.etcPrefix = 'arduino/'
53
54 self.boards = []
55 self.reread()
56
57 self.rereadLater = None
58 deferToThread(self.watchEtcd)
59
60 def watchEtcd(self):
61 events, cancel = etcd.watch_prefix(self.etcPrefix)
62 reactor.addSystemEventTrigger('before', 'shutdown', cancel)
63 for ev in events:
64 log.info('%s changed', ev.key)
65 reactor.callFromThread(self.configChanged)
66
67 def configChanged(self):
68 if self.rereadLater:
69 self.rereadLater.cancel()
70 self.rereadLater = reactor.callLater(.1, self.reread)
71
72 def reread(self):
73 self.rereadLater = None
50 log.info('read config') 74 log.info('read config')
51 for f in os.listdir('config'): 75 self.configGraph = ConjunctiveGraph()
52 if f.startswith('.'): continue 76 for v, md in etcd.get_prefix(self.etcPrefix):
53 self.graph.parse('config/%s' % f, format='n3') 77 log.info(' read file %r', md.key)
54 self.graph.bind('', ROOM) # not working 78 self.configGraph.parse(StringInputSource(v), format='n3')
55 self.graph.bind('rdf', RDF) 79 self.configGraph.bind('', ROOM) # not working
80 self.configGraph.bind('rdf', RDF)
56 # config graph is too noisy; maybe make it a separate resource 81 # config graph is too noisy; maybe make it a separate resource
57 #masterGraph.patch(Patch(addGraph=self.graph)) 82 #masterGraph.patch(Patch(addGraph=self.configGraph))
83 self.setupBoards()
58 84
59 def serialDevices(self): 85 def serialDevices(self):
60 return dict([(row.dev, row.board) for row in self.graph.query( 86 return dict([(row.dev, row.board) for row in self.configGraph.query(
61 """SELECT ?board ?dev WHERE { 87 """SELECT ?board ?dev WHERE {
62 ?board :device ?dev; 88 ?board :device ?dev;
63 a :ArduinoBoard . 89 a :ArduinoBoard .
64 }""", initNs={'': ROOM})]) 90 }""", initNs={'': ROOM})])
91
92 def setupBoards(self):
93 current = currentSerialDevices()
94
95 self.boards = []
96 for dev, board in self.serialDevices().items():
97 if str(dev) not in current:
98 continue
99 log.info("we have board %s connected at %s" % (board, dev))
100 b = Board(dev, self.configGraph, self.masterGraph, board)
101 self.boards.append(b)
102
103 for b in self.boards:
104 b.deployToArduino()
105
106 log.info('open boards')
107 for b in self.boards:
108 b.startPolling(period=.1 if not self.slowMode else 10)
109
65 110
66 class Board(object): 111 class Board(object):
67 """an arduino connected to this computer""" 112 """an arduino connected to this computer"""
68 baudrate = 115200 113 baudrate = 115200
69 def __init__(self, dev, configGraph, masterGraph, uri): 114 def __init__(self, dev, configGraph, masterGraph, uri):
165 self._influx.exportToInflux(stmts) 210 self._influx.exportToInflux(stmts)
166 211
167 def _sendOneshot(self, oneshot): 212 def _sendOneshot(self, oneshot):
168 body = (' '.join('%s %s %s .' % (s.n3(), p.n3(), o.n3()) 213 body = (' '.join('%s %s %s .' % (s.n3(), p.n3(), o.n3())
169 for s,p,o in oneshot)).encode('utf8') 214 for s,p,o in oneshot)).encode('utf8')
170 bang6 = 'fcb8:4119:fb46:96f8:8b07:1260:0f50:fcfa'
171 fetch(method='POST', 215 fetch(method='POST',
172 url='http://[%s]:9071/oneShot' % bang6, 216 url='http://bang6:9071/oneShot',
173 headers={'Content-Type': ['text/n3']}, postdata=body, 217 headers={'Content-Type': ['text/n3']}, postdata=body,
174 timeout=5) 218 timeout=5)
175 219
176 def outputStatements(self, stmts): 220 def outputStatements(self, stmts):
177 unused = set(stmts) 221 unused = set(stmts)
285 return g 329 return g
286 330
287 class Dot(cyclone.web.RequestHandler): 331 class Dot(cyclone.web.RequestHandler):
288 def get(self): 332 def get(self):
289 configGraph = self.settings.config.graph 333 configGraph = self.settings.config.graph
290 dot = dotrender.render(configGraph, self.settings.boards) 334 dot = dotrender.render(configGraph, self.settings.config.boards)
291 self.write(dot) 335 self.write(dot)
292 336
293 class ArduinoCode(cyclone.web.RequestHandler): 337 class ArduinoCode(cyclone.web.RequestHandler):
294 def get(self): 338 def get(self):
295 board = [b for b in self.settings.boards if 339 board = [b for b in self.settings.config.boards if
296 b.uri == URIRef(self.get_argument('board'))][0] 340 b.uri == URIRef(self.get_argument('board'))][0]
297 self.set_header('Content-Type', 'text/plain') 341 self.set_header('Content-Type', 'text/plain')
298 code, cksum = board.generateArduinoCode() 342 code, cksum = board.generateArduinoCode()
299 self.write(code) 343 self.write(code)
300 344
305 349
306 class OutputPage(cyclone.web.RequestHandler): 350 class OutputPage(cyclone.web.RequestHandler):
307 def post(self): 351 def post(self):
308 # for old ui; use PUT instead 352 # for old ui; use PUT instead
309 stmts = list(rdfGraphBody(self.request.body, self.request.headers)) 353 stmts = list(rdfGraphBody(self.request.body, self.request.headers))
310 for b in self.settings.boards: 354 for b in self.settings.config.boards:
311 b.outputStatements(stmts) 355 b.outputStatements(stmts)
312 356
313 def put(self): 357 def put(self):
314 subj = URIRef(self.get_argument('s')) 358 subj = URIRef(self.get_argument('s'))
315 pred = URIRef(self.get_argument('p')) 359 pred = URIRef(self.get_argument('p'))
319 obj = Literal(float(turtleLiteral)) 363 obj = Literal(float(turtleLiteral))
320 except ValueError: 364 except ValueError:
321 obj = Literal(turtleLiteral) 365 obj = Literal(turtleLiteral)
322 366
323 stmt = (subj, pred, obj) 367 stmt = (subj, pred, obj)
324 for b in self.settings.boards: 368 for b in self.settings.config.boards:
325 b.outputStatements([stmt]) 369 b.outputStatements([stmt])
326 370
327 371
328 class Boards(cyclone.web.RequestHandler): 372 class Boards(cyclone.web.RequestHandler):
329 def get(self): 373 def get(self):
330 self.set_header('Content-type', 'application/json') 374 self.set_header('Content-type', 'application/json')
331 self.write(json.dumps({ 375 self.write(json.dumps({
332 'host': hostname, 376 'host': hostname,
333 'boards': [b.description() for b in self.settings.boards] 377 'boards': [b.description() for b in self.settings.config.boards]
334 }, indent=2)) 378 }, indent=2))
335 379
336 def currentSerialDevices(): 380 def currentSerialDevices():
337 log.info('find connected boards') 381 log.info('find connected boards')
338 return glob.glob('/dev/serial/by-id/*') 382 return glob.glob('/dev/serial/by-id/*')
340 def main(): 384 def main():
341 arg = docopt(""" 385 arg = docopt("""
342 Usage: arduinoNode.py [options] 386 Usage: arduinoNode.py [options]
343 387
344 -v Verbose 388 -v Verbose
389 -s serial logging
390 -l slow polling
345 """) 391 """)
346 log.setLevel(logging.WARN) 392 log.setLevel(logging.WARN)
347 if arg['-v']: 393 if arg['-v']:
348 from twisted.python import log as twlog 394 from twisted.python import log as twlog
349 twlog.startLogging(sys.stdout) 395 twlog.startLogging(sys.stdout)
350 396
351 log.setLevel(logging.DEBUG) 397 log.setLevel(logging.DEBUG)
398 if arg['-s']:
399 logging.getLogger('serial').setLevel(logging.INFO)
352 400
353 masterGraph = PatchableGraph() 401 masterGraph = PatchableGraph()
354 config = Config(masterGraph) 402 config = Config(masterGraph, slowMode=arg['-l'])
355 current = currentSerialDevices()
356
357 boards = []
358 for dev, board in config.serialDevices().items():
359 if str(dev) not in current:
360 continue
361 log.info("we have board %s connected at %s" % (board, dev))
362 b = Board(dev, config.graph, masterGraph, board)
363 boards.append(b)
364
365 for b in boards:
366 b.deployToArduino()
367
368 log.info('open boards')
369 for b in boards:
370 b.startPolling(period=.1 if not arg['-v'] else 10)
371
372 403
373 reactor.listenTCP(9059, cyclone.web.Application([ 404 reactor.listenTCP(9059, cyclone.web.Application([
374 (r"/()", cyclone.web.StaticFileHandler, { 405 (r"/()", cyclone.web.StaticFileHandler, {
375 "path": "static", "default_filename": "index.html"}), 406 "path": "static", "default_filename": "index.html"}),
376 (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}), 407 (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}),
378 (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}), 409 (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}),
379 (r"/graph/events", CycloneGraphEventsHandler, {'masterGraph': masterGraph}), 410 (r"/graph/events", CycloneGraphEventsHandler, {'masterGraph': masterGraph}),
380 (r'/output', OutputPage), 411 (r'/output', OutputPage),
381 (r'/arduinoCode', ArduinoCode), 412 (r'/arduinoCode', ArduinoCode),
382 (r'/dot', Dot), 413 (r'/dot', Dot),
383 ], config=config, boards=boards), interface='::') 414 ], config=config), interface='::')
384 reactor.run() 415 reactor.run()
385 416
386 main() 417 main()