Mercurial > code > home > repos > homeauto
changeset 164:49c1756b2edb
start arduinonode
Ignore-this: 6ddc4d3af9ab8468e25b346bddf15835
author | drewp@bigasterisk.com |
---|---|
date | Mon, 06 Apr 2015 02:13:39 -0700 |
parents | 4b0f221d790c |
children | af4e9d9f0bd8 |
files | service/arduinoNode/arduinoNode.py service/arduinoNode/config.n3 service/arduinoNode/devices.py service/arduinoNode/loggingserial.py service/arduinoNode/ns.n3 service/arduinoNode/pydeps service/arduinoNode/readme |
diffstat | 7 files changed, 481 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/arduinoNode.py Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,269 @@ +import shutil +import tempfile +import glob, sys, logging, subprocess +import cyclone.web +from rdflib import Graph, Namespace, URIRef, Literal, RDF +from twisted.internet import reactor, task +import devices + +logging.basicConfig(level=logging.DEBUG) + +from loggingserial import LoggingSerial + +sys.path.append("/my/site/magma") +from stategraph import StateGraph + +log = logging.getLogger() +logging.getLogger('serial').setLevel(logging.WARN) + + +ROOM = Namespace('http://projects.bigasterisk.com/room/') + +class Config(object): + def __init__(self): + self.graph = Graph() + log.info('read config') + self.graph.bind('', ROOM) + 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( + """SELECT ?board ?dev WHERE { + ?board :device ?dev; + a :ArduinoBoard . + }""", initNs={'': ROOM})]) + +class Board(object): + """an arduino connected to this computer""" + baudrate = 115200 + def __init__(self, dev, graph, uri, onChange): + """ + each connected thing has some pins. + + We'll call onChange when we know the currentGraph() has + changed (and not just in creation time). + """ + self.uri = uri + self.graph = graph + self.dev = dev + + # 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._statementsFromInputs = {} # input uri: latest statements + + + def open(self): + self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate, timeout=2) + + def startPolling(self): + task.LoopingCall(self._poll).start(.5) + + def _poll(self): + """ + even boards with no inputs need some polling to see if they're + still ok + """ + try: + self._pollWork() + except Exception as e: + log.warn("poll: %r" % e) + + def _pollWork(self): + self.ser.write("\x60\x00") + for i in self._inputs: + self._statementsFromInputs[i.uri] = i.readFromPoll(self.ser.read) + #plus statements about succeeding or erroring on the last poll + + def currentGraph(self): + g = Graph() + 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) + + return ''' +void setup() { + Serial.begin(%(baudrate)d); + Serial.flush(); +%(setups)s +} + +void loop() { + byte head, cmd; + if (Serial.available() >= 2) { + head = Serial.read(); + if (head != 0x60) { + Serial.flush(); + return; + } + cmd = Serial.read(); + if (cmd == 0x00) { +%(polls)s; + } + } +} + ''' % generated + + def deployToArduino(self): + code = self.generateArduinoCode() + try: + if hasattr(self, 'ser'): + self.ser.close() + workDir = tempfile.mkdtemp(prefix='arduinoNode_board_deploy') + try: + self._arduinoMake(workDir, code) + finally: + shutil.rmtree(workDir) + finally: + self.open() + + def _arduinoMake(self, workDir, code): + with open(workDir + '/makefile', 'w') as makefile: + makefile.write(''' +BOARD_TAG = %(tag)s +USER_LIB_PATH := +ARDUINO_LIBS = +MONITOR_PORT = %(dev)s + +include /usr/share/arduino/Arduino.mk + ''' % { + 'dev': self.dev, + 'tag': self.graph.value(self.uri, ROOM['boardTag']), + }) + + with open(workDir + '/main.ino', 'w') as main: + main.write(code) + + subprocess.check_call(['make', 'upload'], cwd=workDir) + + +class Index(cyclone.web.RequestHandler): + def get(self): + self.set_header("Content-Type", "text/html") + self.write(open("index.html").read()) + +class GraphPage(cyclone.web.RequestHandler): + def get(self): + g = StateGraph(ctx=ROOM['arduinosOn%s' % 'host']) + + for b in self.settings.boards: + for stmt in b.currentGraph(): + g.add(stmt) + + if self.get_argument('config', 'no') == 'yes': + for stmt in self.settings.config.graph: + g.add(stmt) + + self.set_header('Content-type', 'application/x-trig') + self.write(g.asTrig()) + +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) + self.write(dot) + +class ArduinoCode(cyclone.web.RequestHandler): + def get(self): + 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()) + + +def currentSerialDevices(): + log.info('find connected boards') + return glob.glob('/dev/serial/by-id/*') + +def main(): + config = Config() + current = currentSerialDevices() + + def onChange(): + # notify reasoning + pass + + boards = [] + for dev, board in config.serialDevices().items(): + if str(dev) not in current: + continue + log.info("we have board %s connected at %s" % (board, dev)) + b = Board(dev, config.graph, board, onChange) + boards.append(b) + + #boards[0].deployToArduino() + + log.info('open boards') + for b in boards: + b.open() + b.startPolling() + + from twisted.python import log as twlog + twlog.startLogging(sys.stdout) + + log.setLevel(logging.DEBUG) + reactor.listenTCP(9059, cyclone.web.Application([ + (r"/", Index), + (r"/graph", GraphPage), + (r'/arduinoCode', ArduinoCode), + (r'/dot', Dot), + ], config=config, boards=boards)) + reactor.run() + +main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/config.n3 Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,19 @@ +@prefix : <http://projects.bigasterisk.com/room/> . +@prefix board0: <http://bigasterisk.com/homeauto/board0> . +@prefix board0pin: <http://bigasterisk.com/homeauto/board0/pin/> . +@prefix sensor: <http://bigasterisk.com/homeauto/sensor/> . +@prefix houseLoc: <http://bigasterisk.com/homeauto/houseLoc/> . + +board0: a :ArduinoBoard; + :device "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A900cepU-if00-port0"; + :boardTag "atmega328"; + :hasPin board0pin:d3 . + +board0pin:d3 :pinNumber 3 . +board0pin:d4 :pinNumber 4 . + +board0pin:d3 :connectedTo sensor:motion0 . +sensor:motion0 a :MotionSensor; + :sees houseLoc:storage . + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/devices.py Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,70 @@ +from rdflib import Namespace, RDF, URIRef + +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): + self.graph, self.uri = graph, uri + + def readFromPoll(self, read): + """ + read an update message returned as part of a poll bundle. This may + consume a varying number of bytes depending on the type of + input (e.g. IR receiver). + Returns rdf statements. + """ + raise NotImplementedError + + def generateSetupCode(self): + return '' + + def generatePollCode(self): + return '' + + def pinNumber(self, pred=ROOM['pin']): + pinUri = self.graph.value(self.uri, pred) + return int(self.graph.value(pinUri, ROOM['pinNumber'])) + +_inputForType = {} +def registerInput(deviceType): + def newcls(cls): + _inputForType[deviceType] = cls + return cls + return newcls + +class PingInput(BoardInput): + def generatePollCode(self): + return "Serial.write('k');" + def readFromPoll(self, read): + if read(1) != 'k': + raise ValueError('invalid ping response') + return [(self.uri, ROOM['ping'], ROOM['ok'])] + +@registerInput(deviceType=ROOM['MotionSensor']) +class MotionSensorInput(BoardInput): + def generateSetupCode(self): + return 'pinMode(%(pin)d, INPUT); digitalWrite(%(pin)d, LOW);' % { + 'pin': self.pinNumber(), + } + + def generatePollCode(self): + return "Serial.write(digitalRead(%(pin)d) ? 'y' : 'n');" % { + 'pin': self.pinNumber() + } + + def readFromPoll(self, read): + b = read(1) + if b not in 'yn': + raise ValueError('unexpected response %r' % b) + motion = b == 'y' + 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) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/loggingserial.py Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,40 @@ +# forked from /my/proj/house/frontdoor/loggingserial.py + +import serial, logging + +log = logging.getLogger('serial') + +class LoggingSerial(object): + """like serial.Serial, but logs all data""" + + def __init__(self, port=None, ports=None, baudrate=9600, timeout=10): + if ports is None: + ports = [port] + + for port in ports: + try: + log.info("trying port: %s" % port) + self.ser = serial.Serial(port=port, baudrate=baudrate, + timeout=timeout, + xonxoff=0, rtscts=0) + except serial.SerialException: + pass + if not hasattr(self, 'ser'): + raise IOError("no port found") + + def flush(self): + self.ser.flush() + + def close(self): + self.ser.close() + + def write(self, s): + log.info("Serial write: %r" % s) + self.ser.write(s) + + def read(self, n, errorOnTimeout=True): + buf = self.ser.read(n) + log.info("Serial read: %r" % buf) + if errorOnTimeout and n > 0 and len(buf) == 0: + raise ValueError("timed out") + return buf
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/ns.n3 Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,11 @@ +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix xs: <http://www.w3.org/2001/XMLSchema#> . +@prefix dc: <http://purl.org/dc/terms/> . + +@prefix : <http://projects.bigasterisk.com/room/> . +@prefix board0: <http://bigasterisk.com/homeauto/board0> . +@prefix board0pin: <http://bigasterisk.com/homeauto/board0/pin/> . +@prefix sensor: <http://bigasterisk.com/homeauto/sensor/> . +@prefix houseLoc: <http://bigasterisk.com/homeauto/houseLoc/> . + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/pydeps Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,5 @@ +cyclone==1.1 +isodate==0.5.1 +pyserial==2.7 +python-dateutil==2.4.2 +rdflib==4.2.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/arduinoNode/readme Mon Apr 06 02:13:39 2015 -0700 @@ -0,0 +1,67 @@ +node config file: + + board:b0 :device "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A900cepU-if00-port0"; + :boardTag "diecimila"; + :connectedTo sensor:motion0 . + + sensor:motion0 a :MotionSensor; + :pin board0:pin3 + :sees :downhallway . + + output:out0 a :GeneralOutput ; + :pin board0:pin3; + :controls :heater . + + output:out1 a :ShiftBrite; + :shiftbriteL board0:pin5; + :shiftbriteD board0:pin11; + :shiftbriteC board0:pin12 . + + output:out2 a :IrEmitter; + :pin board0:pin4 + . + + output:out3 a :RgbStrip; + :ledCount 10 + . + + output:out4 a :OneWireBus; + :pin board0:pin5 + :connectedTo sensor:temp0, sensor:temp1 . + + sensor:temp0 a :TemperatureSensor; + :oneWireAddress "12:14:35:23"; + :feels :downhallway; + :location house:wall31; + :height "80cm" + :pollPeriod "60sec" + . + + sensor:ir1 a :PowerMeterMonitor; + :reads :housePower; + . + + +linux side host: + read config. we may have any number of arduinos. + serve the complete arduino code to run. + poll inputs. + serve outputs. + upon request, build arduino code and deploy it. + +emits this graph: + board:b0 a :connectedBoard; + :host :bang; + :lastSeen "-1sec" . + sensor:motion0 + :sees :downhallway; + :motionState :noMotion; + :lastRead "16:30:00"; + :lastMotion "16:02:00" . + sensor:theaterIrDetect + :sawCode "0e55cc" . + sensor:ir1 a :PowerMeterMonitor; + :currentWatts 462; + :lastPulseTime "16:09:00"; + :kwhPerBlink 1.0; + \ No newline at end of file