Mercurial > code > home > repos > homeauto
changeset 971:fbe72d44f15a
only recompile if the C code is new. redo Device class api. single temperature sensor is working
Ignore-this: e78106d25dbb2ac8c5e5d8a81d576358
darcs-hash:20150411084359-312f9-7c50d2f4f21dd9d5a0fa8913873a1e6d7d325118
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Sat, 11 Apr 2015 01:43:59 -0700 |
parents | 4f5825a9fc47 |
children | 5f1bbec24d45 |
files | service/arduinoNode/arduinoNode.py service/arduinoNode/devices.py service/arduinoNode/dotrender.py |
diffstat | 3 files changed, 263 insertions(+), 89 deletions(-) [+] |
line wrap: on
line diff
--- a/service/arduinoNode/arduinoNode.py Sat Apr 11 01:43:14 2015 -0700 +++ b/service/arduinoNode/arduinoNode.py Sat Apr 11 01:43:59 2015 -0700 @@ -1,10 +1,14 @@ +""" +depends on arduino-mk +""" import shutil import tempfile -import glob, sys, logging, subprocess +import glob, sys, logging, subprocess, socket, os, hashlib, time import cyclone.web from rdflib import Graph, Namespace, URIRef, Literal, RDF from twisted.internet import reactor, task import devices +import dotrender logging.basicConfig(level=logging.DEBUG) @@ -16,16 +20,25 @@ log = logging.getLogger() logging.getLogger('serial').setLevel(logging.WARN) +import rdflib.namespace +old_split = rdflib.namespace.split_uri +def new_split(uri): + try: + return old_split(uri) + except Exception: + return uri, '' +rdflib.namespace.split_uri = new_split ROOM = Namespace('http://projects.bigasterisk.com/room/') +HOST = Namespace('http://bigasterisk.com/ruler/host/') class Config(object): def __init__(self): self.graph = Graph() log.info('read config') - self.graph.bind('', ROOM) + self.graph.parse('config.n3', format='n3') + self.graph.bind('', ROOM) # not working self.graph.bind('rdf', RDF) - self.graph.parse('config.n3', format='n3') def serialDevices(self): return dict([(row.dev, row.board) for row in self.graph.query( @@ -33,7 +46,7 @@ ?board :device ?dev; a :ArduinoBoard . }""", initNs={'': ROOM})]) - + class Board(object): """an arduino connected to this computer""" baudrate = 115200 @@ -50,20 +63,16 @@ # The order of this list needs to be consistent between the # deployToArduino call and the poll call. - self._inputs = [devices.PingInput(graph, self.uri)] - for row in graph.query("""SELECT ?dev WHERE { - ?board :hasPin ?pin . - ?pin :connectedTo ?dev . - } ORDER BY ?dev""", - initBindings=dict(board=self.uri), - initNs={'': ROOM}): - self._inputs.append(devices.makeBoardInput(graph, row.dev)) + self._devs = devices.makeDevices(graph, self.uri) + self._polledDevs = [d for d in self._devs if d.generatePollCode()] self._statementsFromInputs = {} # input uri: latest statements - + + self.open() def open(self): - self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate, timeout=2) + self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate, + timeout=2) def startPolling(self): task.LoopingCall(self._poll).start(.5) @@ -80,26 +89,46 @@ def _pollWork(self): self.ser.write("\x60\x00") - for i in self._inputs: + for i in self._polledDevs: self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) #plus statements about succeeding or erroring on the last poll def currentGraph(self): g = Graph() + + g.add((HOST[socket.gethostname()], ROOM['connectedTo'], self.uri)) + for si in self._statementsFromInputs.values(): for s in si: g.add(s) return g def generateArduinoCode(self): - generated = {'baudrate': self.baudrate, 'setups': '', 'polls': ''} - for attr in ['setups', 'polls']: - for i in self._inputs: - gen = (i.generateSetupCode() if attr == 'setups' - else i.generatePollCode()) - generated[attr] += '// for %s\n%s\n' % (i.uri, gen) + generated = { + 'baudrate': self.baudrate, + 'includes': '', + 'global': '', + 'setups': '', + 'polls': '' + } + for attr in ['includes', 'global', 'setups', 'polls']: + for i in self._devs: + if attr == 'includes': + gen = '\n'.join('#include "%s"\n' % inc + for inc in i.generateIncludes()) + elif attr == 'global': gen = i.generateGlobalCode() + elif attr == 'setups': gen = i.generateSetupCode() + elif attr == 'polls': gen = i.generatePollCode() + else: raise NotImplementedError + + if gen: + generated[attr] += '// for %s\n%s\n' % (i.uri, gen) return ''' +%(includes)s + +%(global)s + void setup() { Serial.begin(%(baudrate)d); Serial.flush(); @@ -117,13 +146,50 @@ cmd = Serial.read(); if (cmd == 0x00) { %(polls)s; + } else if (cmd == 0x01) { + Serial.write("CODE_CHECKSUM"); } } } ''' % generated + + def codeChecksum(self, code): + # this is run on the code without CODE_CHECKSUM replaced yet + return hashlib.sha1(code).hexdigest() + + def readBoardChecksum(self, length): + # this is likely right after reset, so it might take 2 seconds + for tries in range(6): + self.ser.write("\x60\x01") + try: + return self.ser.read(length) + except ValueError: + if tries == 5: + raise + time.sleep(.5) + raise ValueError + + def boardIsCurrent(self, currentChecksum): + try: + boardCksum = self.readBoardChecksum(len(currentChecksum)) + if boardCksum == currentChecksum: + log.info("board has current code (%s)" % currentChecksum) + return True + else: + log.info("board responds with incorrect code version") + except Exception as e: + log.info("can't get code version from board: %r" % e) + return False + def deployToArduino(self): code = self.generateArduinoCode() + cksum = self.codeChecksum(code) + code = code.replace('CODE_CHECKSUM', cksum) + + if self.boardIsCurrent(cksum): + return + try: if hasattr(self, 'ser'): self.ser.close() @@ -139,14 +205,17 @@ with open(workDir + '/makefile', 'w') as makefile: makefile.write(''' BOARD_TAG = %(tag)s -USER_LIB_PATH := -ARDUINO_LIBS = +USER_LIB_PATH := %(libs)s +ARDUINO_LIBS = %(arduinoLibs)s MONITOR_PORT = %(dev)s include /usr/share/arduino/Arduino.mk ''' % { 'dev': self.dev, 'tag': self.graph.value(self.uri, ROOM['boardTag']), + 'libs': os.path.abspath('arduino-libraries'), + 'arduinoLibs': ' '.join(sum((d.generateArduinoLibs() + for d in self._devs), [])), }) with open(workDir + '/main.ino', 'w') as main: @@ -177,46 +246,8 @@ class Dot(cyclone.web.RequestHandler): def get(self): - nodes = {} # uri: nodeline - edges = [] - - serial = [0] - def addNode(node): - if node not in nodes or isinstance(node, Literal): - id = 'node%s' % serial[0] - if isinstance(node, URIRef): - short = self.settings.config.graph.qname(node) - else: - short = str(node) - nodes[node] = ( - id, - '%s [ label="%s", shape = record, color = blue ];' % ( - id, short)) - serial[0] += 1 - else: - id = nodes[node][0] - return id - def addStmt(stmt): - ns = addNode(stmt[0]) - no = addNode(stmt[2]) - edges.append('%s -> %s [ label="%s" ];' % (ns, no, stmt[1])) - for b in self.settings.boards: - for stmt in b.currentGraph(): - # color these differently from config ones - addStmt(stmt) - for stmt in self.settings.config.graph: - addStmt(stmt) - - nodes = '\n'.join(line for _, line in nodes.values()) - edges = '\n'.join(edges) - dot = ''' - digraph { - rankdir = TB; - charset="utf-8"; - %(nodes)s - %(edges)s - } - ''' % dict(nodes=nodes, edges=edges) + configGraph = self.settings.config.graph + dot = dotrender.render(configGraph, self.settings.boards) self.write(dot) class ArduinoCode(cyclone.web.RequestHandler): @@ -247,11 +278,10 @@ b = Board(dev, config.graph, board, onChange) boards.append(b) - #boards[0].deployToArduino() + boards[0].deployToArduino() log.info('open boards') for b in boards: - b.open() b.startPolling() from twisted.python import log as twlog
--- a/service/arduinoNode/devices.py Sat Apr 11 01:43:14 2015 -0700 +++ b/service/arduinoNode/devices.py Sat Apr 11 01:43:59 2015 -0700 @@ -1,14 +1,40 @@ -from rdflib import Namespace, RDF, URIRef +from rdflib import Namespace, RDF, URIRef, Literal ROOM = Namespace('http://projects.bigasterisk.com/room/') -class BoardInput(object): - """ - one device that gives us input. this includes processing to make - statements, but this object doesn't store state - """ - def __init__(self, graph, uri): +def readLine(read): + buf = '' + for c in iter(lambda: read(1), '\n'): + buf += c + return buf + +class DeviceType(object): + deviceType = None + @classmethod + def findInstances(cls, graph, board): + """ + return any number of instances of this class for all the separately + controlled devices on the board. Two LEDS makes two instances, + but two sensors on the same onewire bus makes only one device + (which yields more statements). + """ + instances = [] + for row in graph.query("""SELECT ?dev ?pinNumber WHERE { + ?board :hasPin ?pin . + ?pin :pinNumber ?pinNumber; + :connectedTo ?dev . + ?dev a ?thisType . + } ORDER BY ?dev""", + initBindings=dict(board=board, + thisType=cls.deviceType), + initNs={'': ROOM}): + instances.append(cls(graph, row.dev, int(row.pinNumber))) + return instances + + # subclasses may add args to this + def __init__(self, graph, uri, pinNumber): self.graph, self.uri = graph, uri + self.pinNumber = pinNumber def readFromPoll(self, read): """ @@ -17,26 +43,42 @@ input (e.g. IR receiver). Returns rdf statements. """ - raise NotImplementedError + raise NotImplementedError('readFromPoll in %s' % self.__class__) + + def generateIncludes(self): + return [] + def generateArduinoLibs(self): + return [] + + def generateGlobalCode(self): + return '' + def generateSetupCode(self): return '' def generatePollCode(self): + """if this returns nothing, we don't try to poll this device""" return '' - def pinNumber(self, pred=ROOM['pin']): - pinUri = self.graph.value(self.uri, pred) - return int(self.graph.value(pinUri, ROOM['pinNumber'])) + def generateActionCode(self): + """ + if you get called to do your action, this code reads the args you + need and do the right action + """ + return '' + +_knownTypes = set() +def register(deviceType): + _knownTypes.add(deviceType) + return deviceType -_inputForType = {} -def registerInput(deviceType): - def newcls(cls): - _inputForType[deviceType] = cls - return cls - return newcls - -class PingInput(BoardInput): +@register +class PingInput(DeviceType): + @classmethod + def findInstances(cls, graph, board): + return [cls(graph, board, None)] + def generatePollCode(self): return "Serial.write('k');" def readFromPoll(self, read): @@ -44,8 +86,9 @@ raise ValueError('invalid ping response') return [(self.uri, ROOM['ping'], ROOM['ok'])] -@registerInput(deviceType=ROOM['MotionSensor']) -class MotionSensorInput(BoardInput): +@register +class MotionSensorInput(DeviceType): + deviceType = ROOM['MotionSensor'] def generateSetupCode(self): return 'pinMode(%(pin)d, INPUT); digitalWrite(%(pin)d, LOW);' % { 'pin': self.pinNumber(), @@ -64,7 +107,61 @@ return [(self.uri, ROOM['sees'], ROOM['motion'] if motion else ROOM['noMotion'])] -def makeBoardInput(graph, uri): - deviceType = graph.value(uri, RDF.type) - return _inputForType[deviceType](graph, uri) +@register +class OneWire(DeviceType): + deviceType = ROOM['OneWire'] + + def generateIncludes(self): + return ['OneWire.h', 'DallasTemperature.h'] + + def generateArduinoLibs(self): + return ['OneWire', 'DallasTemperature'] + + def generateGlobalCode(self): + # not yet isolated to support multiple OW buses + return ''' +OneWire oneWire(%(pinNumber)s); +DallasTemperature sensors(&oneWire); +DeviceAddress tempSensorAddress; +#define NUM_TEMPERATURE_RETRIES 5 + +void initSensors() { + sensors.begin(); + sensors.getAddress(tempSensorAddress, 0); + sensors.setResolution(tempSensorAddress, 12); +} + ''' % dict(pinNumber=self.pinNumber) + def generatePollCode(self): + return r''' +for (int i=0; i<NUM_TEMPERATURE_RETRIES; i++) { + sensors.requestTemperatures(); + float newTemp = sensors.getTempF(tempSensorAddress); + if (i < NUM_TEMPERATURE_RETRIES-1 && + (newTemp < -100 || newTemp > 180)) { + // too many errors that were fixed by restarting arduino. + // trying repeating this much init + initSensors(); + continue; + } + Serial.print(newTemp); + Serial.print('\n'); + Serial.print((char)i); + break; +} + ''' + + def readFromPoll(self, read): + newTemp = readLine(read) + retries = ord(read(1)) + return [ + (self.uri, ROOM['temperatureF'], Literal(newTemp)), + (self.uri, ROOM['temperatureRetries'], Literal(retries)), + ] + +def makeDevices(graph, board): + out = [] + for dt in sorted(_knownTypes, key=lambda cls: cls.__name__): + out.extend(dt.findInstances(graph, board)) + return out +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/dotrender.py Sat Apr 11 01:43:59 2015 -0700 @@ -0,0 +1,47 @@ +from rdflib import Literal, URIRef + +def render(configGraph, boards): + nodes = {} # uri: (nodeid, nodeline) + edges = [] + + serial = [0] + def addNode(node): + if node not in nodes or isinstance(node, Literal): + id = 'node%s' % serial[0] + if isinstance(node, URIRef): + short = configGraph.qname(node) + else: + short = str(node) + nodes[node] = ( + id, + '%s [ label="%s", shape = record, color = blue ];' % ( + id, short)) + serial[0] += 1 + else: + id = nodes[node][0] + return id + def addStmt(stmt): + ns = addNode(stmt[0]) + no = addNode(stmt[2]) + edges.append('%s -> %s [ label="%s" ];' % ( + ns, no, configGraph.qname(stmt[1]))) + for b in boards: + for stmt in b.currentGraph(): + # color these differently from config ones + addStmt(stmt) + for stmt in configGraph: + addStmt(stmt) + + nodes = '\n'.join(line for _, line in nodes.values()) + edges = '\n'.join(edges) + return ''' + digraph { + graph [ranksep=0.4]; + node [fontsize=8, margin=0]; + edge[weight=1.2, fontsize=8, fontcolor="gray"]; + rankdir = LR; + charset="utf-8"; + %(nodes)s + %(edges)s + } + ''' % dict(nodes=nodes, edges=edges)