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)