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