Mercurial > code > home > repos > homeauto
changeset 974:f707210c13bd
new /output to post statements which devices can handle. led and lcd output working
Ignore-this: afa16b081869a52380b04271a35c53c7
darcs-hash:20150412104414-312f9-f0e67d9f939025fd0a67e78463e6902ddcf0e6d9
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Sun, 12 Apr 2015 03:44:14 -0700 |
parents | d7e8227fefc5 |
children | f3023410d875 |
files | service/arduinoNode/arduinoNode.py service/arduinoNode/config.n3 service/arduinoNode/devices.py service/arduinoNode/rdflib_patch.py |
diffstat | 4 files changed, 265 insertions(+), 54 deletions(-) [+] |
line wrap: on
line diff
--- a/service/arduinoNode/arduinoNode.py Sun Apr 12 03:43:20 2015 -0700 +++ b/service/arduinoNode/arduinoNode.py Sun Apr 12 03:44:14 2015 -0700 @@ -1,14 +1,21 @@ """ -depends on arduino-mk +depends on packages: + arduino-mk + indent """ +from __future__ import division +import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile import shutil -import tempfile -import glob, sys, logging, subprocess, socket, os, hashlib, time +import serial import cyclone.web from rdflib import Graph, Namespace, URIRef, Literal, RDF +from rdflib.parser import StringInputSource from twisted.internet import reactor, task + import devices import dotrender +import rdflib_patch +rdflib_patch.fixQnameOfUriWithTrailingSlash() logging.basicConfig(level=logging.DEBUG) @@ -20,18 +27,11 @@ 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/') +ACTION_BASE = 10 # higher than any of the fixed command numbers + class Config(object): def __init__(self): self.graph = Graph() @@ -64,6 +64,8 @@ # The order of this list needs to be consistent between the # deployToArduino call and the poll call. self._devs = devices.makeDevices(graph, self.uri) + self._devCommandNum = dict((dev.uri, ACTION_BASE + devIndex) + for devIndex, dev in enumerate(self._devs)) self._polledDevs = [d for d in self._devs if d.generatePollCode()] self._statementsFromInputs = {} # input uri: latest statements @@ -84,14 +86,25 @@ """ try: self._pollWork() + except serial.SerialException: + reactor.crash() + raise except Exception as e: log.warn("poll: %r" % e) def _pollWork(self): + t1 = time.time() self.ser.write("\x60\x00") for i in self._polledDevs: self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) #plus statements about succeeding or erroring on the last poll + byte = self.ser.read(1) + if byte != 'x': + raise ValueError("after poll, got %x instead of 'x'" % byte) + elapsed = time.time() - t1 + if elapsed > 1.0: + log.warn('poll took %.1f seconds' % elapsed) + def currentGraph(self): g = Graph() @@ -102,29 +115,67 @@ for s in si: g.add(s) return g - + + def outputStatements(self, stmts): + unused = set(stmts) + for dev in self._devs: + stmtsForDev = [] + for pat in dev.outputPatterns(): + if [term is None for term in pat] != [False, False, True]: + raise NotImplementedError + for stmt in stmts: + if stmt[:2] == pat[:2]: + stmtsForDev.append(stmt) + unused.discard(stmt) + if stmtsForDev: + log.info("output goes to action handler for %s" % dev.uri) + self.ser.write("\x60" + chr(self._devCommandNum[dev.uri])) + dev.sendOutput(stmtsForDev, self.ser.write, self.ser.read) + if self.ser.read(1) != 'k': + raise ValueError( + "%s sendOutput/generateActionCode didn't use " + "matching output bytes" % dev.__class__) + log.info("success") + if unused: + log.warn("No devices cared about these statements:") + for s in unused: + log.warn(repr(s)) + def generateArduinoCode(self): generated = { 'baudrate': self.baudrate, 'includes': '', 'global': '', 'setups': '', - 'polls': '' + 'polls': '', + 'actions': '', } - for attr in ['includes', 'global', 'setups', 'polls']: - for i in self._devs: + for attr in ['includes', 'global', 'setups', 'polls', 'actions']: + for dev 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 + for inc in dev.generateIncludes()) + elif attr == 'global': gen = dev.generateGlobalCode() + elif attr == 'setups': gen = dev.generateSetupCode() + elif attr == 'polls': gen = dev.generatePollCode() + elif attr == 'actions': + code = dev.generateActionCode() + if code: + gen = '''else if (cmd == %(cmdNum)s) { + %(code)s + Serial.write('k'); + } + ''' % dict(cmdNum=self._devCommandNum[dev.uri], + code=code) + else: + gen = '' + else: + raise NotImplementedError if gen: - generated[attr] += '// for %s\n%s\n' % (i.uri, gen) + generated[attr] += '// for %s\n%s\n' % (dev.uri, gen) - return ''' + code = ''' %(includes)s %(global)s @@ -132,7 +183,7 @@ void setup() { Serial.begin(%(baudrate)d); Serial.flush(); -%(setups)s + %(setups)s } void loop() { @@ -144,21 +195,34 @@ return; } cmd = Serial.read(); - if (cmd == 0x00) { -%(polls)s; - } else if (cmd == 0x01) { + if (cmd == 0x00) { // poll + %(polls)s + Serial.write('x'); + } else if (cmd == 0x01) { // get code checksum Serial.write("CODE_CHECKSUM"); } + %(actions)s } } ''' % generated - + try: + with tempfile.SpooledTemporaryFile() as codeFile: + codeFile.write(code) + codeFile.seek(0) + code = subprocess.check_output([ + 'indent', + '-linux', + '-fc1', # ok to indent comments + '-i4', # 4-space indent + '-sob' # swallow blanks (not working) + ], stdin=codeFile) + except OSError as e: + log.warn("indent failed (%r)", e) + cksum = hashlib.sha1(code).hexdigest() + code = code.replace('CODE_CHECKSUM', cksum) + return code, cksum - def codeChecksum(self, code): - # this is run on the code without CODE_CHECKSUM replaced yet - return hashlib.sha1(code).hexdigest() - - def readBoardChecksum(self, length): + 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") @@ -170,9 +234,9 @@ time.sleep(.5) raise ValueError - def boardIsCurrent(self, currentChecksum): + def _boardIsCurrent(self, currentChecksum): try: - boardCksum = self.readBoardChecksum(len(currentChecksum)) + boardCksum = self._readBoardChecksum(len(currentChecksum)) if boardCksum == currentChecksum: log.info("board has current code (%s)" % currentChecksum) return True @@ -183,11 +247,9 @@ return False def deployToArduino(self): - code = self.generateArduinoCode() - cksum = self.codeChecksum(code) - code = code.replace('CODE_CHECKSUM', cksum) + code, cksum = self.generateArduinoCode() - if self.boardIsCurrent(cksum): + if self._boardIsCurrent(cksum): return try: @@ -200,7 +262,7 @@ shutil.rmtree(workDir) finally: self.open() - + def _arduinoMake(self, workDir, code): with open(workDir + '/makefile', 'w') as makefile: makefile.write(''' @@ -255,8 +317,19 @@ board = [b for b in self.settings.boards if b.uri == URIRef(self.get_argument('board'))][0] self.set_header('Content-type', 'text/plain') - self.write(board.generateArduinoCode()) + code, cksum = board.generateArduinoCode() + self.write(code) + +def rdfGraphBody(body, headers): + g = Graph() + g.parse(StringInputSource(body), format='nt') + return g +class OutputPage(cyclone.web.RequestHandler): + def post(self): + stmts = list(rdfGraphBody(self.request.body, self.request.headers)) + for b in self.settings.boards: + b.outputStatements(stmts) def currentSerialDevices(): log.info('find connected boards') @@ -291,6 +364,7 @@ reactor.listenTCP(9059, cyclone.web.Application([ (r"/", Index), (r"/graph", GraphPage), + (r'/output', OutputPage), (r'/arduinoCode', ArduinoCode), (r'/dot', Dot), ], config=config, boards=boards))
--- a/service/arduinoNode/config.n3 Sun Apr 12 03:43:20 2015 -0700 +++ b/service/arduinoNode/config.n3 Sun Apr 12 03:44:14 2015 -0700 @@ -46,7 +46,7 @@ board1pin:d12 :pinNumber 12 . board1pin:d10 :connectedTo board1lcd:backlight . -board1lcd:backlight a :LedOutput . +board1lcd:backlight a :LedOutput, :ActiveLowOutput . board1pin:d3 :connectedTo board1ow: . board1ow: a :OneWire; @@ -55,14 +55,14 @@ :position :office . board1pin:d9 :pinNumber 9; :connectedTo board1lcd:SID . -board1pin:d8 :pinNumber 8; :connectedTo board1lcd:CLK . +board1pin:d8 :pinNumber 8; :connectedTo board1lcd:SCLK . board1pin:d7 :pinNumber 7; :connectedTo board1lcd:A0 . board1pin:d6 :pinNumber 6; :connectedTo board1lcd:RST . board1pin:d5 :pinNumber 5; :connectedTo board1lcd:CS . board1lcd: a :ST7565Lcd; :lcdSID board1lcd:SID; - :lcdCLK board1lcd:CLK; + :lcdSCLK board1lcd:SCLK; :lcdA0 board1lcd:A0; :lcdRST board1lcd:RST; :lcdCS board1lcd:CS .
--- a/service/arduinoNode/devices.py Sun Apr 12 03:43:20 2015 -0700 +++ b/service/arduinoNode/devices.py Sun Apr 12 03:44:14 2015 -0700 @@ -1,6 +1,9 @@ +from __future__ import division +import itertools from rdflib import Namespace, RDF, URIRef, Literal ROOM = Namespace('http://projects.bigasterisk.com/room/') +XSD = Namespace('http://www.w3.org/2001/XMLSchema#') def readLine(read): buf = '' @@ -18,7 +21,6 @@ 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; @@ -28,8 +30,7 @@ initBindings=dict(board=board, thisType=cls.deviceType), initNs={'': ROOM}): - instances.append(cls(graph, row.dev, int(row.pinNumber))) - return instances + yield cls(graph, row.dev, int(row.pinNumber)) # subclasses may add args to this def __init__(self, graph, uri, pinNumber): @@ -63,11 +64,32 @@ def generateActionCode(self): """ - if you get called to do your action, this code reads the args you - need and do the right action + If the host side runs sendOutput, this C code will be run on the + board to receive whatever sendOutput writes. Each sendOutput + write(buf) call should be matched with len(buf) Serial.read() + calls in here. """ return '' + def outputPatterns(self): + """ + Triple patterns, using None as a wildcard, that should be routed + to sendOutput + """ + return [] + + def sendOutput(self, statements, write, read): + """ + If we got statements that match this class's outputPatterns, this + will be called with the statements that matched, and a serial + write method. What you write here will be available as + Serial.read in the generateActionCode C code. + + Todo: it would be fine to read back confirmations or + whatever. Just need a way to collect them into graph statements. + """ + raise NotImplementedError + _knownTypes = set() def register(deviceType): _knownTypes.add(deviceType) @@ -91,12 +113,12 @@ deviceType = ROOM['MotionSensor'] def generateSetupCode(self): return 'pinMode(%(pin)d, INPUT); digitalWrite(%(pin)d, LOW);' % { - 'pin': self.pinNumber(), + 'pin': self.pinNumber, } def generatePollCode(self): return "Serial.write(digitalRead(%(pin)d) ? 'y' : 'n');" % { - 'pin': self.pinNumber() + 'pin': self.pinNumber } def readFromPoll(self, read): @@ -123,15 +145,15 @@ OneWire oneWire(%(pinNumber)s); DallasTemperature sensors(&oneWire); DeviceAddress tempSensorAddress; -#define NUM_TEMPERATURE_RETRIES 5 +#define NUM_TEMPERATURE_RETRIES 2 void initSensors() { sensors.begin(); sensors.getAddress(tempSensorAddress, 0); sensors.setResolution(tempSensorAddress, 12); } + ''' % dict(pinNumber=self.pinNumber) - ''' % dict(pinNumber=self.pinNumber) def generatePollCode(self): return r''' for (int i=0; i<NUM_TEMPERATURE_RETRIES; i++) { @@ -155,10 +177,115 @@ newTemp = readLine(read) retries = ord(read(1)) return [ - (self.uri, ROOM['temperatureF'], Literal(newTemp)), + (self.uri, ROOM['temperatureF'], + Literal(newTemp, datatype=XSD['decimal'])), (self.uri, ROOM['temperatureRetries'], Literal(retries)), ] +def byteFromFloat(f): + return chr(int(min(255, max(0, f * 255)))) + +@register +class LedOutput(DeviceType): + deviceType = ROOM['LedOutput'] + def generateSetupCode(self): + return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % { + 'pin': self.pinNumber, + } + + def outputPatterns(self): + return [(self.uri, ROOM['brightness'], None)] + + def sendOutput(self, statements, write, read): + assert len(statements) == 1 + assert statements[0][:2] == (self.uri, ROOM['brightness']) + value = float(statements[0][2]) + if (self.uri, RDF.type, ROOM['ActiveLowOutput']): + value = 1 - value + write(byteFromFloat(value)) + + def generateActionCode(self): + return r''' + while(Serial.available() < 1) NULL; + analogWrite(%(pin)d, Serial.read()); + ''' % dict(pin=self.pinNumber) + +@register +class ST7576Lcd(DeviceType): + deviceType = ROOM['ST7565Lcd'] + @classmethod + def findInstances(cls, graph, board): + grouped = itertools.groupby( + graph.query("""SELECT DISTINCT ?dev ?pred ?pinNumber WHERE { + ?board :hasPin ?pin . + ?pin :pinNumber ?pinNumber; + :connectedTo ?devPin . + ?dev a :ST7565Lcd . + ?dev ?pred ?devPin . + } ORDER BY ?dev""", + initBindings=dict(board=board, + thisType=cls.deviceType), + initNs={'': ROOM}), + lambda row: row.dev) + for dev, connections in grouped: + connections = dict((role, int(num)) for unused_dev, role, num + in connections) + yield cls(graph, dev, connections=connections) + + def __init__(self, graph, dev, connections): + super(ST7576Lcd, self).__init__(graph, dev, pinNumber=None) + self.connections = connections + + def generateIncludes(self): + return ['ST7565.h'] + + def generateArduinoLibs(self): + return ['ST7565'] + + def generateGlobalCode(self): + return ''' + ST7565 glcd(%(SID)d, %(SCLK)d, %(A0)d, %(RST)d, %(CS)d); + char newtxt[21*8+1]; + unsigned int written; + ''' % dict(SID=self.connections[ROOM['lcdSID']], + SCLK=self.connections[ROOM['lcdSCLK']], + A0=self.connections[ROOM['lcdA0']], + RST=self.connections[ROOM['lcdRST']], + CS=self.connections[ROOM['lcdCS']]) + + def generateSetupCode(self): + return ''' + glcd.st7565_init(); + glcd.st7565_command(CMD_DISPLAY_ON); + glcd.st7565_command(CMD_SET_ALLPTS_NORMAL); + glcd.st7565_set_brightness(0x18); + + glcd.display(); // show splashscreen + ''' + + def outputPatterns(self): + return [(self.uri, ROOM['text'], None)] + + def sendOutput(self, statements, write, read): + assert len(statements) == 1 + assert statements[0][:2] == (self.uri, ROOM['text']) + value = str(statements[0][2]) + assert len(value) < 254, repr(value) + write(chr(len(value)) + value) + + def generateActionCode(self): + return ''' + while(Serial.available() < 1) NULL; + byte bufSize = Serial.read(); + for (byte i = 0; i < bufSize; i++) { + while(Serial.available() < 1) NULL; + newtxt[i] = Serial.read(); + } + glcd.clear(); + glcd.drawstring(0,0, newtxt); + glcd.display(); + ''' + def makeDevices(graph, board): out = [] for dt in sorted(_knownTypes, key=lambda cls: cls.__name__):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/rdflib_patch.py Sun Apr 12 03:44:14 2015 -0700 @@ -0,0 +1,10 @@ + +def fixQnameOfUriWithTrailingSlash(): + 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