Mercurial > code > home > repos > homeauto
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='::') |