changeset 969:70a5392b24d3

start arduinonode Ignore-this: 6ddc4d3af9ab8468e25b346bddf15835 darcs-hash:20150406091339-312f9-bcbca91eff6dc85a402d341248e7ce6128e71723
author drewp <drewp@bigasterisk.com>
date Mon, 06 Apr 2015 02:13:39 -0700
parents d68cb03ce575
children 4f5825a9fc47
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