comparison service/arduinoNode/arduinoNode.py @ 1035:f01d9892ed79

update arduinoNode to support streamed graph output Ignore-this: fa35d1fae5b0e411b167650550c3e77d darcs-hash:c22c59acf2d4bbabfc467085f146a5de72b19f03
author drewp <drewp@bigasterisk.com>
date Thu, 28 Jan 2016 02:24:32 -0800
parents 43a2170bbdb8
children 4ebb5cc30002
comparison
equal deleted inserted replaced
1034:43a2170bbdb8 1035:f01d9892ed79
6 from __future__ import division 6 from __future__ import division
7 import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile 7 import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile
8 import shutil, json 8 import shutil, json
9 import serial 9 import serial
10 import cyclone.web 10 import cyclone.web
11 from rdflib import Graph, Namespace, URIRef, Literal, RDF 11 from rdflib import Graph, Namespace, URIRef, Literal, RDF, ConjunctiveGraph
12 from rdflib.parser import StringInputSource 12 from rdflib.parser import StringInputSource
13 from twisted.internet import reactor, task 13 from twisted.internet import reactor, task
14 from docopt import docopt 14 from docopt import docopt
15 15
16 import devices 16 import devices
21 21
22 logging.basicConfig(level=logging.DEBUG) 22 logging.basicConfig(level=logging.DEBUG)
23 23
24 from loggingserial import LoggingSerial 24 from loggingserial import LoggingSerial
25 25
26 sys.path.append("/my/site/magma") 26 sys.path.append("../../lib")
27 from stategraph import StateGraph 27 from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
28
29 sys.path.append("/my/proj/light9")
30 from light9.rdfdb.patch import Patch
31 from light9.rdfdb.rdflibpatch import inContext
28 32
29 sys.path.append("/my/proj/room") 33 sys.path.append("/my/proj/room")
30 from carbondata import CarbonClient 34 from carbondata import CarbonClient
31 35
32 log = logging.getLogger() 36 log = logging.getLogger()
35 ROOM = Namespace('http://projects.bigasterisk.com/room/') 39 ROOM = Namespace('http://projects.bigasterisk.com/room/')
36 HOST = Namespace('http://bigasterisk.com/ruler/host/') 40 HOST = Namespace('http://bigasterisk.com/ruler/host/')
37 41
38 ACTION_BASE = 10 # higher than any of the fixed command numbers 42 ACTION_BASE = 10 # higher than any of the fixed command numbers
39 43
44 CTX = ROOM['arduinosOn%s' % socket.gethostname()]
45
40 class Config(object): 46 class Config(object):
41 def __init__(self): 47 def __init__(self, masterGraph):
42 self.graph = Graph() 48 self.graph = ConjunctiveGraph()
43 log.info('read config') 49 log.info('read config')
44 for f in os.listdir('config'): 50 for f in os.listdir('config'):
45 if f.startswith('.'): continue 51 if f.startswith('.'): continue
46 self.graph.parse('config/%s' % f, format='n3') 52 self.graph.parse('config/%s' % f, format='n3')
47 self.graph.bind('', ROOM) # not working 53 self.graph.bind('', ROOM) # not working
48 self.graph.bind('rdf', RDF) 54 self.graph.bind('rdf', RDF)
55 # config graph is too noisy; maybe make it a separate resource
56 #masterGraph.patch(Patch(addGraph=self.graph))
49 57
50 def serialDevices(self): 58 def serialDevices(self):
51 return dict([(row.dev, row.board) for row in self.graph.query( 59 return dict([(row.dev, row.board) for row in self.graph.query(
52 """SELECT ?board ?dev WHERE { 60 """SELECT ?board ?dev WHERE {
53 ?board :device ?dev; 61 ?board :device ?dev;
55 }""", initNs={'': ROOM})]) 63 }""", initNs={'': ROOM})])
56 64
57 class Board(object): 65 class Board(object):
58 """an arduino connected to this computer""" 66 """an arduino connected to this computer"""
59 baudrate = 115200 67 baudrate = 115200
60 def __init__(self, dev, graph, uri, onChange): 68 def __init__(self, dev, configGraph, masterGraph, uri):
61 """ 69 """
62 each connected thing has some pins. 70 each connected thing has some pins.
63
64 We'll call onChange when we know the currentGraph() has
65 changed (and not just in creation time).
66 """ 71 """
67 self.uri = uri 72 self.uri = uri
68 self.graph = graph 73 self.configGraph = configGraph
74 self.masterGraph = masterGraph
69 self.dev = dev 75 self.dev = dev
76
77 self.masterGraph.patch(Patch(addQuads=[
78 (HOST[socket.gethostname()], ROOM['connectedTo'], self.uri, CTX),
79 ]))
70 80
71 # The order of this list needs to be consistent between the 81 # The order of this list needs to be consistent between the
72 # deployToArduino call and the poll call. 82 # deployToArduino call and the poll call.
73 self._devs = devices.makeDevices(graph, self.uri) 83 self._devs = devices.makeDevices(configGraph, self.uri)
74 self._devCommandNum = dict((dev.uri, ACTION_BASE + devIndex) 84 self._devCommandNum = dict((dev.uri, ACTION_BASE + devIndex)
75 for devIndex, dev in enumerate(self._devs)) 85 for devIndex, dev in enumerate(self._devs))
76 self._polledDevs = [d for d in self._devs if d.generatePollCode()] 86 self._polledDevs = [d for d in self._devs if d.generatePollCode()]
77 87
78 self._statementsFromInputs = {} # input device uri: latest statements 88 self._statementsFromInputs = {} # input device uri: latest statements
79 self._carbon = CarbonClient(serverHost='bang') 89 self._carbon = CarbonClient(serverHost='bang')
80 self.open() 90 self.open()
91 for d in self._devs:
92 self.syncMasterGraphToHostStatements(d)
81 93
82 def description(self): 94 def description(self):
83 """for web page""" 95 """for web page"""
84 return { 96 return {
85 'uri': self.uri, 97 'uri': self.uri,
105 self._pollWork() 117 self._pollWork()
106 except serial.SerialException: 118 except serial.SerialException:
107 reactor.crash() 119 reactor.crash()
108 raise 120 raise
109 except Exception as e: 121 except Exception as e:
122 import traceback; traceback.print_exc()
110 log.warn("poll: %r" % e) 123 log.warn("poll: %r" % e)
111 124
112 def _pollWork(self): 125 def _pollWork(self):
113 t1 = time.time() 126 t1 = time.time()
114 self.ser.write("\x60\x00") 127 self.ser.write("\x60\x00")
115 for i in self._polledDevs: 128 for i in self._polledDevs:
116 self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) 129 prev = self._statementsFromInputs.get(i.uri, [])
130 new = self._statementsFromInputs[i.uri] = inContext(
131 i.readFromPoll(self.ser.read), i.uri)
132 self.masterGraph.patch(Patch.fromDiff(prev, new))
133
117 #plus statements about succeeding or erroring on the last poll 134 #plus statements about succeeding or erroring on the last poll
118 byte = self.ser.read(1) 135 byte = self.ser.read(1)
119 if byte != 'x': 136 if byte != 'x':
120 raise ValueError("after poll, got %x instead of 'x'" % byte) 137 raise ValueError("after poll, got %x instead of 'x'" % byte)
121 elapsed = time.time() - t1 138 elapsed = time.time() - t1
126 def _exportToGraphite(self): 143 def _exportToGraphite(self):
127 # note this is writing way too often- graphite is storing at a lower res 144 # note this is writing way too often- graphite is storing at a lower res
128 now = time.time() 145 now = time.time()
129 # objects of these statements are suitable as graphite values. 146 # objects of these statements are suitable as graphite values.
130 graphitePredicates = {ROOM['temperatureF']} 147 graphitePredicates = {ROOM['temperatureF']}
131 for s, graphiteName in self.graph.subject_objects(ROOM['graphiteName']): 148 for s, graphiteName in self.configGraph.subject_objects(ROOM['graphiteName']):
132 for group in self._statementsFromInputs.values(): 149 for group in self._statementsFromInputs.values():
133 for stmt in group: 150 for stmt in group:
134 if stmt[0] == s and stmt[1] in graphitePredicates: 151 if stmt[0] == s and stmt[1] in graphitePredicates:
135 self._carbon.send(graphiteName, stmt[2].toPython(), now) 152 self._carbon.send(graphiteName, stmt[2].toPython(), now)
136 153
137 def currentGraph(self):
138 g = Graph()
139
140 g.add((HOST[socket.gethostname()], ROOM['connectedTo'], self.uri))
141
142 for si in self._statementsFromInputs.values():
143 for s in si:
144 g.add(s)
145 for dev in self._devs:
146 for stmt in dev.hostStatements():
147 g.add(stmt)
148 return g
149
150 def outputStatements(self, stmts): 154 def outputStatements(self, stmts):
151 unused = set(stmts) 155 unused = set(stmts)
152 for dev in self._devs: 156 for dev in self._devs:
153 stmtsForDev = [] 157 stmtsForDev = []
154 for pat in dev.outputPatterns(): 158 for pat in dev.outputPatterns():
164 dev.sendOutput(stmtsForDev, self.ser.write, self.ser.read) 168 dev.sendOutput(stmtsForDev, self.ser.write, self.ser.read)
165 if self.ser.read(1) != 'k': 169 if self.ser.read(1) != 'k':
166 raise ValueError( 170 raise ValueError(
167 "%s sendOutput/generateActionCode didn't use " 171 "%s sendOutput/generateActionCode didn't use "
168 "matching output bytes" % dev.__class__) 172 "matching output bytes" % dev.__class__)
173 # Dev *could* change hostStatements at any time, and
174 # we're not currently tracking that, but the usual is
175 # to change them in response to sendOutput so this
176 # should be good enough. The right answer is to give
177 # each dev the masterGraph for it to write to.
178 self.syncMasterGraphToHostStatements(dev)
169 log.info("success") 179 log.info("success")
170 if unused: 180 if unused:
171 log.info("Board %s doesn't care about these statements:", self.uri) 181 log.info("Board %s doesn't care about these statements:", self.uri)
172 for s in unused: 182 for s in unused:
173 log.info("%r", s) 183 log.info("%r", s)
174 184
185 def syncMasterGraphToHostStatements(self, dev):
186 hostStmtCtx = URIRef(dev.uri + '/host')
187 newQuads = inContext(dev.hostStatements(), hostStmtCtx)
188 self.masterGraph.patchSubgraph(hostStmtCtx, newQuads)
189
175 def generateArduinoCode(self): 190 def generateArduinoCode(self):
176 code = write_arduino_code.writeCode(self.baudrate, self._devs, self._devCommandNum) 191 code = write_arduino_code.writeCode(self.baudrate, self._devs, self._devCommandNum)
177 code = write_arduino_code.indent(code) 192 code = write_arduino_code.indent(code)
178 cksum = hashlib.sha1(code).hexdigest() 193 cksum = hashlib.sha1(code).hexdigest()
179 code = code.replace('CODE_CHECKSUM', cksum) 194 code = code.replace('CODE_CHECKSUM', cksum)
222 237
223 def _arduinoMake(self, workDir, code): 238 def _arduinoMake(self, workDir, code):
224 with open(workDir + '/makefile', 'w') as makefile: 239 with open(workDir + '/makefile', 'w') as makefile:
225 makefile.write(write_arduino_code.writeMakefile( 240 makefile.write(write_arduino_code.writeMakefile(
226 dev=self.dev, 241 dev=self.dev,
227 tag=self.graph.value(self.uri, ROOM['boardTag']), 242 tag=self.configGraph.value(self.uri, ROOM['boardTag']),
228 allLibs=sum((d.generateArduinoLibs() for d in self._devs), []))) 243 allLibs=sum((d.generateArduinoLibs() for d in self._devs), [])))
229 244
230 with open(workDir + '/main.ino', 'w') as main: 245 with open(workDir + '/main.ino', 'w') as main:
231 main.write(code) 246 main.write(code)
232 247
233 subprocess.check_call(['make', 'upload'], cwd=workDir) 248 subprocess.check_call(['make', 'upload'], cwd=workDir)
234 249
235 250
236 class GraphPage(cyclone.web.RequestHandler): 251 def currentGraph(self):
237 def get(self): 252 g = Graph()
238 g = StateGraph(ctx=ROOM['arduinosOn%s' % 'host']) 253
239 254
240 for b in self.settings.boards: 255 for dev in self._devs:
241 for stmt in b.currentGraph(): 256 for stmt in dev.hostStatements():
242 g.add(stmt) 257 g.add(stmt)
243 258 return g
244 if self.get_argument('config', 'no') == 'yes':
245 for stmt in self.settings.config.graph:
246 g.add(stmt)
247
248 if self.request.headers.get('accept') == 'application/ld+json':
249 self.set_header('Content-type', 'application/ld+json')
250 self.write(g.asJsonLd())
251 return
252
253 self.set_header('Content-type', 'application/x-trig')
254 self.write(g.asTrig())
255 259
256 class Dot(cyclone.web.RequestHandler): 260 class Dot(cyclone.web.RequestHandler):
257 def get(self): 261 def get(self):
258 configGraph = self.settings.config.graph 262 configGraph = self.settings.config.graph
259 dot = dotrender.render(configGraph, self.settings.boards) 263 dot = dotrender.render(configGraph, self.settings.boards)
304 }, indent=2)) 308 }, indent=2))
305 309
306 def currentSerialDevices(): 310 def currentSerialDevices():
307 log.info('find connected boards') 311 log.info('find connected boards')
308 return glob.glob('/dev/serial/by-id/*') 312 return glob.glob('/dev/serial/by-id/*')
309 313
310 def main(): 314 def main():
311 arg = docopt(""" 315 arg = docopt("""
312 Usage: arduinoNode.py [options] 316 Usage: arduinoNode.py [options]
313 317
314 -v Verbose 318 -v Verbose
317 if arg['-v']: 321 if arg['-v']:
318 from twisted.python import log as twlog 322 from twisted.python import log as twlog
319 twlog.startLogging(sys.stdout) 323 twlog.startLogging(sys.stdout)
320 324
321 log.setLevel(logging.DEBUG) 325 log.setLevel(logging.DEBUG)
322 326
323 config = Config() 327 masterGraph = PatchableGraph()
328 config = Config(masterGraph)
324 current = currentSerialDevices() 329 current = currentSerialDevices()
325 330
326 def onChange():
327 # notify reasoning
328 pass
329
330 boards = [] 331 boards = []
331 for dev, board in config.serialDevices().items(): 332 for dev, board in config.serialDevices().items():
332 if str(dev) not in current: 333 if str(dev) not in current:
333 continue 334 continue
334 log.info("we have board %s connected at %s" % (board, dev)) 335 log.info("we have board %s connected at %s" % (board, dev))
335 b = Board(dev, config.graph, board, onChange) 336 b = Board(dev, config.graph, masterGraph, board)
336 boards.append(b) 337 boards.append(b)
337 338
338 for b in boards: 339 for b in boards:
339 b.deployToArduino() 340 b.deployToArduino()
340 341
346 app = cyclone.web.Application([ 347 app = cyclone.web.Application([
347 (r"/()", cyclone.web.StaticFileHandler, { 348 (r"/()", cyclone.web.StaticFileHandler, {
348 "path": "static", "default_filename": "index.html"}), 349 "path": "static", "default_filename": "index.html"}),
349 (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}), 350 (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}),
350 (r'/boards', Boards), 351 (r'/boards', Boards),
351 (r"/graph", GraphPage), 352 (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}),
353 (r"/graph/events", CycloneGraphEventsHandler, {'masterGraph': masterGraph}),
352 (r'/output', OutputPage), 354 (r'/output', OutputPage),
353 (r'/arduinoCode', ArduinoCode), 355 (r'/arduinoCode', ArduinoCode),
354 (r'/dot', Dot), 356 (r'/dot', Dot),
355 ], config=config, boards=boards) 357 ], config=config, boards=boards)
356 reactor.listenTCP(9059, app, interface='::') 358 reactor.listenTCP(9059, app, interface='::')