changeset 169:d228105749ac

new /output to post statements which devices can handle. led and lcd output working Ignore-this: afa16b081869a52380b04271a35c53c7
author drewp@bigasterisk.com
date Sun, 12 Apr 2015 03:44:14 -0700
parents b2f909325bb2
children 376599552a4c
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