comparison service/arduinoNode/arduinoNode.py @ 166:c0180bd2b33a

only recompile if the C code is new. redo Device class api. single temperature sensor is working Ignore-this: e78106d25dbb2ac8c5e5d8a81d576358
author drewp@bigasterisk.com
date Sat, 11 Apr 2015 01:43:59 -0700
parents 49c1756b2edb
children d228105749ac
comparison
equal deleted inserted replaced
165:af4e9d9f0bd8 166:c0180bd2b33a
1 """
2 depends on arduino-mk
3 """
1 import shutil 4 import shutil
2 import tempfile 5 import tempfile
3 import glob, sys, logging, subprocess 6 import glob, sys, logging, subprocess, socket, os, hashlib, time
4 import cyclone.web 7 import cyclone.web
5 from rdflib import Graph, Namespace, URIRef, Literal, RDF 8 from rdflib import Graph, Namespace, URIRef, Literal, RDF
6 from twisted.internet import reactor, task 9 from twisted.internet import reactor, task
7 import devices 10 import devices
11 import dotrender
8 12
9 logging.basicConfig(level=logging.DEBUG) 13 logging.basicConfig(level=logging.DEBUG)
10 14
11 from loggingserial import LoggingSerial 15 from loggingserial import LoggingSerial
12 16
14 from stategraph import StateGraph 18 from stategraph import StateGraph
15 19
16 log = logging.getLogger() 20 log = logging.getLogger()
17 logging.getLogger('serial').setLevel(logging.WARN) 21 logging.getLogger('serial').setLevel(logging.WARN)
18 22
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
19 31
20 ROOM = Namespace('http://projects.bigasterisk.com/room/') 32 ROOM = Namespace('http://projects.bigasterisk.com/room/')
33 HOST = Namespace('http://bigasterisk.com/ruler/host/')
21 34
22 class Config(object): 35 class Config(object):
23 def __init__(self): 36 def __init__(self):
24 self.graph = Graph() 37 self.graph = Graph()
25 log.info('read config') 38 log.info('read config')
26 self.graph.bind('', ROOM) 39 self.graph.parse('config.n3', format='n3')
40 self.graph.bind('', ROOM) # not working
27 self.graph.bind('rdf', RDF) 41 self.graph.bind('rdf', RDF)
28 self.graph.parse('config.n3', format='n3')
29 42
30 def serialDevices(self): 43 def serialDevices(self):
31 return dict([(row.dev, row.board) for row in self.graph.query( 44 return dict([(row.dev, row.board) for row in self.graph.query(
32 """SELECT ?board ?dev WHERE { 45 """SELECT ?board ?dev WHERE {
33 ?board :device ?dev; 46 ?board :device ?dev;
34 a :ArduinoBoard . 47 a :ArduinoBoard .
35 }""", initNs={'': ROOM})]) 48 }""", initNs={'': ROOM})])
36 49
37 class Board(object): 50 class Board(object):
38 """an arduino connected to this computer""" 51 """an arduino connected to this computer"""
39 baudrate = 115200 52 baudrate = 115200
40 def __init__(self, dev, graph, uri, onChange): 53 def __init__(self, dev, graph, uri, onChange):
41 """ 54 """
48 self.graph = graph 61 self.graph = graph
49 self.dev = dev 62 self.dev = dev
50 63
51 # The order of this list needs to be consistent between the 64 # The order of this list needs to be consistent between the
52 # deployToArduino call and the poll call. 65 # deployToArduino call and the poll call.
53 self._inputs = [devices.PingInput(graph, self.uri)] 66 self._devs = devices.makeDevices(graph, self.uri)
54 for row in graph.query("""SELECT ?dev WHERE { 67 self._polledDevs = [d for d in self._devs if d.generatePollCode()]
55 ?board :hasPin ?pin .
56 ?pin :connectedTo ?dev .
57 } ORDER BY ?dev""",
58 initBindings=dict(board=self.uri),
59 initNs={'': ROOM}):
60 self._inputs.append(devices.makeBoardInput(graph, row.dev))
61 68
62 self._statementsFromInputs = {} # input uri: latest statements 69 self._statementsFromInputs = {} # input uri: latest statements
63 70
71 self.open()
64 72
65 def open(self): 73 def open(self):
66 self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate, timeout=2) 74 self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate,
75 timeout=2)
67 76
68 def startPolling(self): 77 def startPolling(self):
69 task.LoopingCall(self._poll).start(.5) 78 task.LoopingCall(self._poll).start(.5)
70 79
71 def _poll(self): 80 def _poll(self):
78 except Exception as e: 87 except Exception as e:
79 log.warn("poll: %r" % e) 88 log.warn("poll: %r" % e)
80 89
81 def _pollWork(self): 90 def _pollWork(self):
82 self.ser.write("\x60\x00") 91 self.ser.write("\x60\x00")
83 for i in self._inputs: 92 for i in self._polledDevs:
84 self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) 93 self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read)
85 #plus statements about succeeding or erroring on the last poll 94 #plus statements about succeeding or erroring on the last poll
86 95
87 def currentGraph(self): 96 def currentGraph(self):
88 g = Graph() 97 g = Graph()
98
99 g.add((HOST[socket.gethostname()], ROOM['connectedTo'], self.uri))
100
89 for si in self._statementsFromInputs.values(): 101 for si in self._statementsFromInputs.values():
90 for s in si: 102 for s in si:
91 g.add(s) 103 g.add(s)
92 return g 104 return g
93 105
94 def generateArduinoCode(self): 106 def generateArduinoCode(self):
95 generated = {'baudrate': self.baudrate, 'setups': '', 'polls': ''} 107 generated = {
96 for attr in ['setups', 'polls']: 108 'baudrate': self.baudrate,
97 for i in self._inputs: 109 'includes': '',
98 gen = (i.generateSetupCode() if attr == 'setups' 110 'global': '',
99 else i.generatePollCode()) 111 'setups': '',
100 generated[attr] += '// for %s\n%s\n' % (i.uri, gen) 112 'polls': ''
113 }
114 for attr in ['includes', 'global', 'setups', 'polls']:
115 for i in self._devs:
116 if attr == 'includes':
117 gen = '\n'.join('#include "%s"\n' % inc
118 for inc in i.generateIncludes())
119 elif attr == 'global': gen = i.generateGlobalCode()
120 elif attr == 'setups': gen = i.generateSetupCode()
121 elif attr == 'polls': gen = i.generatePollCode()
122 else: raise NotImplementedError
123
124 if gen:
125 generated[attr] += '// for %s\n%s\n' % (i.uri, gen)
101 126
102 return ''' 127 return '''
128 %(includes)s
129
130 %(global)s
131
103 void setup() { 132 void setup() {
104 Serial.begin(%(baudrate)d); 133 Serial.begin(%(baudrate)d);
105 Serial.flush(); 134 Serial.flush();
106 %(setups)s 135 %(setups)s
107 } 136 }
115 return; 144 return;
116 } 145 }
117 cmd = Serial.read(); 146 cmd = Serial.read();
118 if (cmd == 0x00) { 147 if (cmd == 0x00) {
119 %(polls)s; 148 %(polls)s;
149 } else if (cmd == 0x01) {
150 Serial.write("CODE_CHECKSUM");
120 } 151 }
121 } 152 }
122 } 153 }
123 ''' % generated 154 ''' % generated
124 155
156
157 def codeChecksum(self, code):
158 # this is run on the code without CODE_CHECKSUM replaced yet
159 return hashlib.sha1(code).hexdigest()
160
161 def readBoardChecksum(self, length):
162 # this is likely right after reset, so it might take 2 seconds
163 for tries in range(6):
164 self.ser.write("\x60\x01")
165 try:
166 return self.ser.read(length)
167 except ValueError:
168 if tries == 5:
169 raise
170 time.sleep(.5)
171 raise ValueError
172
173 def boardIsCurrent(self, currentChecksum):
174 try:
175 boardCksum = self.readBoardChecksum(len(currentChecksum))
176 if boardCksum == currentChecksum:
177 log.info("board has current code (%s)" % currentChecksum)
178 return True
179 else:
180 log.info("board responds with incorrect code version")
181 except Exception as e:
182 log.info("can't get code version from board: %r" % e)
183 return False
184
125 def deployToArduino(self): 185 def deployToArduino(self):
126 code = self.generateArduinoCode() 186 code = self.generateArduinoCode()
187 cksum = self.codeChecksum(code)
188 code = code.replace('CODE_CHECKSUM', cksum)
189
190 if self.boardIsCurrent(cksum):
191 return
192
127 try: 193 try:
128 if hasattr(self, 'ser'): 194 if hasattr(self, 'ser'):
129 self.ser.close() 195 self.ser.close()
130 workDir = tempfile.mkdtemp(prefix='arduinoNode_board_deploy') 196 workDir = tempfile.mkdtemp(prefix='arduinoNode_board_deploy')
131 try: 197 try:
137 203
138 def _arduinoMake(self, workDir, code): 204 def _arduinoMake(self, workDir, code):
139 with open(workDir + '/makefile', 'w') as makefile: 205 with open(workDir + '/makefile', 'w') as makefile:
140 makefile.write(''' 206 makefile.write('''
141 BOARD_TAG = %(tag)s 207 BOARD_TAG = %(tag)s
142 USER_LIB_PATH := 208 USER_LIB_PATH := %(libs)s
143 ARDUINO_LIBS = 209 ARDUINO_LIBS = %(arduinoLibs)s
144 MONITOR_PORT = %(dev)s 210 MONITOR_PORT = %(dev)s
145 211
146 include /usr/share/arduino/Arduino.mk 212 include /usr/share/arduino/Arduino.mk
147 ''' % { 213 ''' % {
148 'dev': self.dev, 214 'dev': self.dev,
149 'tag': self.graph.value(self.uri, ROOM['boardTag']), 215 'tag': self.graph.value(self.uri, ROOM['boardTag']),
216 'libs': os.path.abspath('arduino-libraries'),
217 'arduinoLibs': ' '.join(sum((d.generateArduinoLibs()
218 for d in self._devs), [])),
150 }) 219 })
151 220
152 with open(workDir + '/main.ino', 'w') as main: 221 with open(workDir + '/main.ino', 'w') as main:
153 main.write(code) 222 main.write(code)
154 223
175 self.set_header('Content-type', 'application/x-trig') 244 self.set_header('Content-type', 'application/x-trig')
176 self.write(g.asTrig()) 245 self.write(g.asTrig())
177 246
178 class Dot(cyclone.web.RequestHandler): 247 class Dot(cyclone.web.RequestHandler):
179 def get(self): 248 def get(self):
180 nodes = {} # uri: nodeline 249 configGraph = self.settings.config.graph
181 edges = [] 250 dot = dotrender.render(configGraph, self.settings.boards)
182
183 serial = [0]
184 def addNode(node):
185 if node not in nodes or isinstance(node, Literal):
186 id = 'node%s' % serial[0]
187 if isinstance(node, URIRef):
188 short = self.settings.config.graph.qname(node)
189 else:
190 short = str(node)
191 nodes[node] = (
192 id,
193 '%s [ label="%s", shape = record, color = blue ];' % (
194 id, short))
195 serial[0] += 1
196 else:
197 id = nodes[node][0]
198 return id
199 def addStmt(stmt):
200 ns = addNode(stmt[0])
201 no = addNode(stmt[2])
202 edges.append('%s -> %s [ label="%s" ];' % (ns, no, stmt[1]))
203 for b in self.settings.boards:
204 for stmt in b.currentGraph():
205 # color these differently from config ones
206 addStmt(stmt)
207 for stmt in self.settings.config.graph:
208 addStmt(stmt)
209
210 nodes = '\n'.join(line for _, line in nodes.values())
211 edges = '\n'.join(edges)
212 dot = '''
213 digraph {
214 rankdir = TB;
215 charset="utf-8";
216 %(nodes)s
217 %(edges)s
218 }
219 ''' % dict(nodes=nodes, edges=edges)
220 self.write(dot) 251 self.write(dot)
221 252
222 class ArduinoCode(cyclone.web.RequestHandler): 253 class ArduinoCode(cyclone.web.RequestHandler):
223 def get(self): 254 def get(self):
224 board = [b for b in self.settings.boards if 255 board = [b for b in self.settings.boards if
245 continue 276 continue
246 log.info("we have board %s connected at %s" % (board, dev)) 277 log.info("we have board %s connected at %s" % (board, dev))
247 b = Board(dev, config.graph, board, onChange) 278 b = Board(dev, config.graph, board, onChange)
248 boards.append(b) 279 boards.append(b)
249 280
250 #boards[0].deployToArduino() 281 boards[0].deployToArduino()
251 282
252 log.info('open boards') 283 log.info('open boards')
253 for b in boards: 284 for b in boards:
254 b.open()
255 b.startPolling() 285 b.startPolling()
256 286
257 from twisted.python import log as twlog 287 from twisted.python import log as twlog
258 twlog.startLogging(sys.stdout) 288 twlog.startLogging(sys.stdout)
259 289