Mercurial > code > home > repos > homeauto
comparison service/arduinoNode/arduinoNode.py @ 169:d228105749ac
new /output to post statements which devices can handle. led and lcd output working
Ignore-this: afa16b081869a52380b04271a35c53c7
author | drewp@bigasterisk.com |
---|---|
date | Sun, 12 Apr 2015 03:44:14 -0700 |
parents | c0180bd2b33a |
children | 376599552a4c |
comparison
equal
deleted
inserted
replaced
168:b2f909325bb2 | 169:d228105749ac |
---|---|
1 """ | 1 """ |
2 depends on arduino-mk | 2 depends on packages: |
3 arduino-mk | |
4 indent | |
3 """ | 5 """ |
6 from __future__ import division | |
7 import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile | |
4 import shutil | 8 import shutil |
5 import tempfile | 9 import serial |
6 import glob, sys, logging, subprocess, socket, os, hashlib, time | |
7 import cyclone.web | 10 import cyclone.web |
8 from rdflib import Graph, Namespace, URIRef, Literal, RDF | 11 from rdflib import Graph, Namespace, URIRef, Literal, RDF |
12 from rdflib.parser import StringInputSource | |
9 from twisted.internet import reactor, task | 13 from twisted.internet import reactor, task |
14 | |
10 import devices | 15 import devices |
11 import dotrender | 16 import dotrender |
17 import rdflib_patch | |
18 rdflib_patch.fixQnameOfUriWithTrailingSlash() | |
12 | 19 |
13 logging.basicConfig(level=logging.DEBUG) | 20 logging.basicConfig(level=logging.DEBUG) |
14 | 21 |
15 from loggingserial import LoggingSerial | 22 from loggingserial import LoggingSerial |
16 | 23 |
18 from stategraph import StateGraph | 25 from stategraph import StateGraph |
19 | 26 |
20 log = logging.getLogger() | 27 log = logging.getLogger() |
21 logging.getLogger('serial').setLevel(logging.WARN) | 28 logging.getLogger('serial').setLevel(logging.WARN) |
22 | 29 |
23 import rdflib.namespace | |
24 old_split = rdflib.namespace.split_uri | |
25 def new_split(uri): | |
26 try: | |
27 return old_split(uri) | |
28 except Exception: | |
29 return uri, '' | |
30 rdflib.namespace.split_uri = new_split | |
31 | |
32 ROOM = Namespace('http://projects.bigasterisk.com/room/') | 30 ROOM = Namespace('http://projects.bigasterisk.com/room/') |
33 HOST = Namespace('http://bigasterisk.com/ruler/host/') | 31 HOST = Namespace('http://bigasterisk.com/ruler/host/') |
32 | |
33 ACTION_BASE = 10 # higher than any of the fixed command numbers | |
34 | 34 |
35 class Config(object): | 35 class Config(object): |
36 def __init__(self): | 36 def __init__(self): |
37 self.graph = Graph() | 37 self.graph = Graph() |
38 log.info('read config') | 38 log.info('read config') |
62 self.dev = dev | 62 self.dev = dev |
63 | 63 |
64 # The order of this list needs to be consistent between the | 64 # The order of this list needs to be consistent between the |
65 # deployToArduino call and the poll call. | 65 # deployToArduino call and the poll call. |
66 self._devs = devices.makeDevices(graph, self.uri) | 66 self._devs = devices.makeDevices(graph, self.uri) |
67 self._devCommandNum = dict((dev.uri, ACTION_BASE + devIndex) | |
68 for devIndex, dev in enumerate(self._devs)) | |
67 self._polledDevs = [d for d in self._devs if d.generatePollCode()] | 69 self._polledDevs = [d for d in self._devs if d.generatePollCode()] |
68 | 70 |
69 self._statementsFromInputs = {} # input uri: latest statements | 71 self._statementsFromInputs = {} # input uri: latest statements |
70 | 72 |
71 self.open() | 73 self.open() |
82 even boards with no inputs need some polling to see if they're | 84 even boards with no inputs need some polling to see if they're |
83 still ok | 85 still ok |
84 """ | 86 """ |
85 try: | 87 try: |
86 self._pollWork() | 88 self._pollWork() |
89 except serial.SerialException: | |
90 reactor.crash() | |
91 raise | |
87 except Exception as e: | 92 except Exception as e: |
88 log.warn("poll: %r" % e) | 93 log.warn("poll: %r" % e) |
89 | 94 |
90 def _pollWork(self): | 95 def _pollWork(self): |
96 t1 = time.time() | |
91 self.ser.write("\x60\x00") | 97 self.ser.write("\x60\x00") |
92 for i in self._polledDevs: | 98 for i in self._polledDevs: |
93 self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) | 99 self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) |
94 #plus statements about succeeding or erroring on the last poll | 100 #plus statements about succeeding or erroring on the last poll |
101 byte = self.ser.read(1) | |
102 if byte != 'x': | |
103 raise ValueError("after poll, got %x instead of 'x'" % byte) | |
104 elapsed = time.time() - t1 | |
105 if elapsed > 1.0: | |
106 log.warn('poll took %.1f seconds' % elapsed) | |
107 | |
95 | 108 |
96 def currentGraph(self): | 109 def currentGraph(self): |
97 g = Graph() | 110 g = Graph() |
98 | 111 |
99 g.add((HOST[socket.gethostname()], ROOM['connectedTo'], self.uri)) | 112 g.add((HOST[socket.gethostname()], ROOM['connectedTo'], self.uri)) |
100 | 113 |
101 for si in self._statementsFromInputs.values(): | 114 for si in self._statementsFromInputs.values(): |
102 for s in si: | 115 for s in si: |
103 g.add(s) | 116 g.add(s) |
104 return g | 117 return g |
105 | 118 |
119 def outputStatements(self, stmts): | |
120 unused = set(stmts) | |
121 for dev in self._devs: | |
122 stmtsForDev = [] | |
123 for pat in dev.outputPatterns(): | |
124 if [term is None for term in pat] != [False, False, True]: | |
125 raise NotImplementedError | |
126 for stmt in stmts: | |
127 if stmt[:2] == pat[:2]: | |
128 stmtsForDev.append(stmt) | |
129 unused.discard(stmt) | |
130 if stmtsForDev: | |
131 log.info("output goes to action handler for %s" % dev.uri) | |
132 self.ser.write("\x60" + chr(self._devCommandNum[dev.uri])) | |
133 dev.sendOutput(stmtsForDev, self.ser.write, self.ser.read) | |
134 if self.ser.read(1) != 'k': | |
135 raise ValueError( | |
136 "%s sendOutput/generateActionCode didn't use " | |
137 "matching output bytes" % dev.__class__) | |
138 log.info("success") | |
139 if unused: | |
140 log.warn("No devices cared about these statements:") | |
141 for s in unused: | |
142 log.warn(repr(s)) | |
143 | |
106 def generateArduinoCode(self): | 144 def generateArduinoCode(self): |
107 generated = { | 145 generated = { |
108 'baudrate': self.baudrate, | 146 'baudrate': self.baudrate, |
109 'includes': '', | 147 'includes': '', |
110 'global': '', | 148 'global': '', |
111 'setups': '', | 149 'setups': '', |
112 'polls': '' | 150 'polls': '', |
151 'actions': '', | |
113 } | 152 } |
114 for attr in ['includes', 'global', 'setups', 'polls']: | 153 for attr in ['includes', 'global', 'setups', 'polls', 'actions']: |
115 for i in self._devs: | 154 for dev in self._devs: |
116 if attr == 'includes': | 155 if attr == 'includes': |
117 gen = '\n'.join('#include "%s"\n' % inc | 156 gen = '\n'.join('#include "%s"\n' % inc |
118 for inc in i.generateIncludes()) | 157 for inc in dev.generateIncludes()) |
119 elif attr == 'global': gen = i.generateGlobalCode() | 158 elif attr == 'global': gen = dev.generateGlobalCode() |
120 elif attr == 'setups': gen = i.generateSetupCode() | 159 elif attr == 'setups': gen = dev.generateSetupCode() |
121 elif attr == 'polls': gen = i.generatePollCode() | 160 elif attr == 'polls': gen = dev.generatePollCode() |
122 else: raise NotImplementedError | 161 elif attr == 'actions': |
162 code = dev.generateActionCode() | |
163 if code: | |
164 gen = '''else if (cmd == %(cmdNum)s) { | |
165 %(code)s | |
166 Serial.write('k'); | |
167 } | |
168 ''' % dict(cmdNum=self._devCommandNum[dev.uri], | |
169 code=code) | |
170 else: | |
171 gen = '' | |
172 else: | |
173 raise NotImplementedError | |
123 | 174 |
124 if gen: | 175 if gen: |
125 generated[attr] += '// for %s\n%s\n' % (i.uri, gen) | 176 generated[attr] += '// for %s\n%s\n' % (dev.uri, gen) |
126 | 177 |
127 return ''' | 178 code = ''' |
128 %(includes)s | 179 %(includes)s |
129 | 180 |
130 %(global)s | 181 %(global)s |
131 | 182 |
132 void setup() { | 183 void setup() { |
133 Serial.begin(%(baudrate)d); | 184 Serial.begin(%(baudrate)d); |
134 Serial.flush(); | 185 Serial.flush(); |
135 %(setups)s | 186 %(setups)s |
136 } | 187 } |
137 | 188 |
138 void loop() { | 189 void loop() { |
139 byte head, cmd; | 190 byte head, cmd; |
140 if (Serial.available() >= 2) { | 191 if (Serial.available() >= 2) { |
142 if (head != 0x60) { | 193 if (head != 0x60) { |
143 Serial.flush(); | 194 Serial.flush(); |
144 return; | 195 return; |
145 } | 196 } |
146 cmd = Serial.read(); | 197 cmd = Serial.read(); |
147 if (cmd == 0x00) { | 198 if (cmd == 0x00) { // poll |
148 %(polls)s; | 199 %(polls)s |
149 } else if (cmd == 0x01) { | 200 Serial.write('x'); |
201 } else if (cmd == 0x01) { // get code checksum | |
150 Serial.write("CODE_CHECKSUM"); | 202 Serial.write("CODE_CHECKSUM"); |
151 } | 203 } |
204 %(actions)s | |
152 } | 205 } |
153 } | 206 } |
154 ''' % generated | 207 ''' % generated |
155 | 208 try: |
156 | 209 with tempfile.SpooledTemporaryFile() as codeFile: |
157 def codeChecksum(self, code): | 210 codeFile.write(code) |
158 # this is run on the code without CODE_CHECKSUM replaced yet | 211 codeFile.seek(0) |
159 return hashlib.sha1(code).hexdigest() | 212 code = subprocess.check_output([ |
160 | 213 'indent', |
161 def readBoardChecksum(self, length): | 214 '-linux', |
215 '-fc1', # ok to indent comments | |
216 '-i4', # 4-space indent | |
217 '-sob' # swallow blanks (not working) | |
218 ], stdin=codeFile) | |
219 except OSError as e: | |
220 log.warn("indent failed (%r)", e) | |
221 cksum = hashlib.sha1(code).hexdigest() | |
222 code = code.replace('CODE_CHECKSUM', cksum) | |
223 return code, cksum | |
224 | |
225 def _readBoardChecksum(self, length): | |
162 # this is likely right after reset, so it might take 2 seconds | 226 # this is likely right after reset, so it might take 2 seconds |
163 for tries in range(6): | 227 for tries in range(6): |
164 self.ser.write("\x60\x01") | 228 self.ser.write("\x60\x01") |
165 try: | 229 try: |
166 return self.ser.read(length) | 230 return self.ser.read(length) |
168 if tries == 5: | 232 if tries == 5: |
169 raise | 233 raise |
170 time.sleep(.5) | 234 time.sleep(.5) |
171 raise ValueError | 235 raise ValueError |
172 | 236 |
173 def boardIsCurrent(self, currentChecksum): | 237 def _boardIsCurrent(self, currentChecksum): |
174 try: | 238 try: |
175 boardCksum = self.readBoardChecksum(len(currentChecksum)) | 239 boardCksum = self._readBoardChecksum(len(currentChecksum)) |
176 if boardCksum == currentChecksum: | 240 if boardCksum == currentChecksum: |
177 log.info("board has current code (%s)" % currentChecksum) | 241 log.info("board has current code (%s)" % currentChecksum) |
178 return True | 242 return True |
179 else: | 243 else: |
180 log.info("board responds with incorrect code version") | 244 log.info("board responds with incorrect code version") |
181 except Exception as e: | 245 except Exception as e: |
182 log.info("can't get code version from board: %r" % e) | 246 log.info("can't get code version from board: %r" % e) |
183 return False | 247 return False |
184 | 248 |
185 def deployToArduino(self): | 249 def deployToArduino(self): |
186 code = self.generateArduinoCode() | 250 code, cksum = self.generateArduinoCode() |
187 cksum = self.codeChecksum(code) | 251 |
188 code = code.replace('CODE_CHECKSUM', cksum) | 252 if self._boardIsCurrent(cksum): |
189 | |
190 if self.boardIsCurrent(cksum): | |
191 return | 253 return |
192 | 254 |
193 try: | 255 try: |
194 if hasattr(self, 'ser'): | 256 if hasattr(self, 'ser'): |
195 self.ser.close() | 257 self.ser.close() |
198 self._arduinoMake(workDir, code) | 260 self._arduinoMake(workDir, code) |
199 finally: | 261 finally: |
200 shutil.rmtree(workDir) | 262 shutil.rmtree(workDir) |
201 finally: | 263 finally: |
202 self.open() | 264 self.open() |
203 | 265 |
204 def _arduinoMake(self, workDir, code): | 266 def _arduinoMake(self, workDir, code): |
205 with open(workDir + '/makefile', 'w') as makefile: | 267 with open(workDir + '/makefile', 'w') as makefile: |
206 makefile.write(''' | 268 makefile.write(''' |
207 BOARD_TAG = %(tag)s | 269 BOARD_TAG = %(tag)s |
208 USER_LIB_PATH := %(libs)s | 270 USER_LIB_PATH := %(libs)s |
253 class ArduinoCode(cyclone.web.RequestHandler): | 315 class ArduinoCode(cyclone.web.RequestHandler): |
254 def get(self): | 316 def get(self): |
255 board = [b for b in self.settings.boards if | 317 board = [b for b in self.settings.boards if |
256 b.uri == URIRef(self.get_argument('board'))][0] | 318 b.uri == URIRef(self.get_argument('board'))][0] |
257 self.set_header('Content-type', 'text/plain') | 319 self.set_header('Content-type', 'text/plain') |
258 self.write(board.generateArduinoCode()) | 320 code, cksum = board.generateArduinoCode() |
259 | 321 self.write(code) |
322 | |
323 def rdfGraphBody(body, headers): | |
324 g = Graph() | |
325 g.parse(StringInputSource(body), format='nt') | |
326 return g | |
327 | |
328 class OutputPage(cyclone.web.RequestHandler): | |
329 def post(self): | |
330 stmts = list(rdfGraphBody(self.request.body, self.request.headers)) | |
331 for b in self.settings.boards: | |
332 b.outputStatements(stmts) | |
260 | 333 |
261 def currentSerialDevices(): | 334 def currentSerialDevices(): |
262 log.info('find connected boards') | 335 log.info('find connected boards') |
263 return glob.glob('/dev/serial/by-id/*') | 336 return glob.glob('/dev/serial/by-id/*') |
264 | 337 |
289 | 362 |
290 log.setLevel(logging.DEBUG) | 363 log.setLevel(logging.DEBUG) |
291 reactor.listenTCP(9059, cyclone.web.Application([ | 364 reactor.listenTCP(9059, cyclone.web.Application([ |
292 (r"/", Index), | 365 (r"/", Index), |
293 (r"/graph", GraphPage), | 366 (r"/graph", GraphPage), |
367 (r'/output', OutputPage), | |
294 (r'/arduinoCode', ArduinoCode), | 368 (r'/arduinoCode', ArduinoCode), |
295 (r'/dot', Dot), | 369 (r'/dot', Dot), |
296 ], config=config, boards=boards)) | 370 ], config=config, boards=boards)) |
297 reactor.run() | 371 reactor.run() |
298 | 372 |