changeset 170:376599552a4c

polymer board debug page with working output widgets Ignore-this: 3157d0c47a91afe47b30a5f182629d93
author drewp@bigasterisk.com
date Mon, 13 Apr 2015 23:30:12 -0700
parents d228105749ac
children 4d2df276baae
files service/arduinoNode/arduinoNode.py service/arduinoNode/devices.py service/arduinoNode/static/index.html service/arduinoNode/static/output-widgets.html
diffstat 4 files changed, 314 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/service/arduinoNode/arduinoNode.py	Sun Apr 12 03:44:14 2015 -0700
+++ b/service/arduinoNode/arduinoNode.py	Mon Apr 13 23:30:12 2015 -0700
@@ -5,7 +5,7 @@
 """
 from __future__ import division
 import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile
-import shutil
+import shutil, json
 import serial
 import cyclone.web
 from rdflib import Graph, Namespace, URIRef, Literal, RDF
@@ -72,6 +72,15 @@
 
         self.open()
 
+    def description(self):
+        """for web page"""
+        return {
+            'uri': self.uri,
+            'dev': self.dev,
+            'baudrate': self.baudrate,
+            'devices': [d.description() for d in self._devs],
+            }
+        
     def open(self):
         self.ser = LoggingSerial(port=self.dev, baudrate=self.baudrate,
                                  timeout=2)
@@ -285,11 +294,6 @@
 
         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):
@@ -330,7 +334,15 @@
         stmts = list(rdfGraphBody(self.request.body, self.request.headers))
         for b in self.settings.boards:
             b.outputStatements(stmts)
+
+class Boards(cyclone.web.RequestHandler):
+    def get(self):
         
+        self.set_header('Content-type', 'application/json')
+        self.write(json.dumps({
+            'boards': [b.description() for b in self.settings.boards]
+        }, indent=2))
+            
 def currentSerialDevices():
     log.info('find connected boards')
     return glob.glob('/dev/serial/by-id/*')
@@ -362,7 +374,10 @@
 
     log.setLevel(logging.DEBUG)
     reactor.listenTCP(9059, cyclone.web.Application([
-        (r"/", Index),
+        (r"/()", cyclone.web.StaticFileHandler, {
+            "path": "static", "default_filename": "index.html"}),
+        (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}),
+        (r'/boards', Boards),
         (r"/graph", GraphPage),
         (r'/output', OutputPage),
         (r'/arduinoCode', ArduinoCode),
--- a/service/arduinoNode/devices.py	Sun Apr 12 03:44:14 2015 -0700
+++ b/service/arduinoNode/devices.py	Mon Apr 13 23:30:12 2015 -0700
@@ -36,6 +36,16 @@
     def __init__(self, graph, uri, pinNumber):
         self.graph, self.uri = graph, uri
         self.pinNumber = pinNumber
+
+    def description(self):
+        return {
+            'uri': self.uri,
+            'className': self.__class__.__name__,
+            'pinNumber': self.pinNumber,
+            'outputPatterns': self.outputPatterns(),
+            'watchPrefixes': self.watchPrefixes(),
+            'outputWidgets': self.outputWidgets(),
+        }
         
     def readFromPoll(self, read):
         """
@@ -46,20 +56,37 @@
         """
         raise NotImplementedError('readFromPoll in %s' % self.__class__)
 
+    def watchPrefixes(self):
+        """
+        subj,pred pairs of the statements that might be returned from
+        readFromPoll, so the dashboard knows what it should
+        watch. This should be eliminated, as the dashboard should just
+        always watch the whole tree of statements starting self.uri
+        """
+        return []
+        
     def generateIncludes(self):
+        """filenames of .h files to #include"""
         return []
 
     def generateArduinoLibs(self):
+        """names of libraries for the ARDUINO_LIBS line in the makefile"""
         return []
         
     def generateGlobalCode(self):
+        """C code to emit in the global section"""
         return ''
         
     def generateSetupCode(self):
+        """C code to emit in setup()"""
         return ''
         
     def generatePollCode(self):
-        """if this returns nothing, we don't try to poll this device"""
+        """
+        C code to run a poll update. This should Serial.write its output
+        for readFromPoll to consume. If this returns nothing, we don't
+        try to poll this device.
+        """
         return ''
 
     def generateActionCode(self):
@@ -78,6 +105,13 @@
         """
         return []
 
+    def outputWidgets(self):
+        """
+        structs to make output widgets on the dashboard. ~1 of these per
+        handler you have in sendOutput
+        """
+        return []
+        
     def sendOutput(self, statements, write, read):
         """
         If we got statements that match this class's outputPatterns, this
@@ -103,11 +137,15 @@
     
     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'])]
 
+    def watchPrefixes(self):
+        return [(self.uri, ROOM['ping'])]
+
 @register
 class MotionSensorInput(DeviceType):
     deviceType = ROOM['MotionSensor']
@@ -129,8 +167,15 @@
         return [(self.uri, ROOM['sees'],
                  ROOM['motion'] if motion else ROOM['noMotion'])]
 
+    def watchPrefixes(self):
+        return [(self.uri, ROOM['sees'])]
+
 @register
 class OneWire(DeviceType):
+    """
+    A OW bus with temperature sensors (and maybe other devices, which
+    are also to be handled under this object)
+    """
     deviceType = ROOM['OneWire']
    
     def generateIncludes(self):
@@ -176,12 +221,20 @@
     def readFromPoll(self, read):
         newTemp = readLine(read)
         retries = ord(read(1))
+        # uri will change; there could (likely) be multiple connected sensors
         return [
             (self.uri, ROOM['temperatureF'],
              Literal(newTemp, datatype=XSD['decimal'])),
             (self.uri, ROOM['temperatureRetries'], Literal(retries)),
             ]
 
+    def watchPrefixes(self):
+        # these uris will become dynamic! see note on watchPrefixes
+        # about eliminating it.
+        return [(self.uri, ROOM['temperatureF']),
+                (self.uri, ROOM['temperatureRetries']),
+                ]
+
 def byteFromFloat(f):
     return chr(int(min(255, max(0, f * 255))))
         
@@ -209,6 +262,46 @@
           while(Serial.available() < 1) NULL;
           analogWrite(%(pin)d, Serial.read());
         ''' % dict(pin=self.pinNumber)
+
+    def outputWidgets(self):
+        return [{
+            'element': 'output-slider',
+            'min': 0,
+            'max': 1,
+            'step': 1 / 255,
+            'subj': self.uri,
+            'pred': ROOM['brightness'],
+        }]
+
+@register
+class DigitalOutput(DeviceType):
+    deviceType = ROOM['DigitalOutput']
+    def generateSetupCode(self):
+        return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % {
+            'pin': self.pinNumber,
+        }
+ 
+    def outputPatterns(self):
+        return [(self.uri, ROOM['level'], None)]
+
+    def sendOutput(self, statements, write, read):
+        assert len(statements) == 1
+        assert statements[0][:2] == (self.uri, ROOM['level'])
+        value = {"high": 1, "low": 0}[str(statements[0][2])]
+        write(chr(value))
+        
+    def generateActionCode(self):
+        return r'''
+          while(Serial.available() < 1) NULL;
+          digitalWrite(%(pin)d, Serial.read());
+        ''' % dict(pin=self.pinNumber)
+        
+    def outputWidgets(self):
+        return [{
+            'element': 'output-switch',
+            'subj': self.uri,
+            'pred': ROOM['level'],
+        }]
         
 @register
 class ST7576Lcd(DeviceType):
@@ -273,14 +366,26 @@
         assert len(value) < 254, repr(value)
         write(chr(len(value)) + value)
 
+    def outputWidgets(self):
+        return [{
+                'element': 'output-fixed-text',
+                'cols': 21,
+                'rows': 8,
+                'subj': self.uri,
+                'pred': ROOM['text'],
+            }]
+        
     def generateActionCode(self):
         return '''
           while(Serial.available() < 1) NULL;
           byte bufSize = Serial.read();
-          for (byte i = 0; i < bufSize; i++) {
+          for (byte i = 0; i < bufSize; ++i) {
             while(Serial.available() < 1) NULL;
             newtxt[i] = Serial.read();
           }
+          for (byte i = bufSize; i < sizeof(newtxt); ++i) {
+            newtxt[i] = 0;
+          }
           glcd.clear();
           glcd.drawstring(0,0, newtxt); 
           glcd.display();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/arduinoNode/static/index.html	Mon Apr 13 23:30:12 2015 -0700
@@ -0,0 +1,115 @@
+<!doctype html>
+<html>
+  <head>
+    <title>arduinoNode</title>
+    <meta charset="utf-8" />
+    <link rel="import" href="/lib/polymer/0.5.2/polymer/polymer.html">
+    <link rel="import" href="/lib/polymer/0.5.2/core-ajax/core-ajax.html">
+    <link rel="import" href="/lib/polymer/0.5.2/paper-button/paper-button.html">
+    <link rel="import" href="/lib/polymer/0.5.2/core-resizable/core-resizable.html">
+    <link rel="import" href="/room/ari/static/rdf-observe.html">
+    <link rel="import" href="/room/ari/static/rdf-oneshot.html">
+    <link rel="import" href="static/output-widgets.html">
+    
+    <style>
+     body {
+       font-family: monospace;
+     }
+    </style>
+  </head>
+  <body layout vertical fullbleed>
+    <script>
+     window.NS = {
+       dev: 'http://projects.bigasterisk.com/device/',
+       room: 'http://projects.bigasterisk.com/room/',
+       rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
+     };
+    </script>
+
+    <polymer-element name="linked-uri" noscript attributes="href">
+      <template>
+        <a href="{{href}}">{{href}}</a>
+      </template>
+    </polymer-element>
+    
+    <polymer-element name="arduinonode-boards" noscript>
+      <template>
+        <style>
+         h1 {
+           margin: 0;
+           font-size: 130%;
+         }
+         ul {
+           padding-left: 5px;
+         }
+         .board, .device {
+           border: 1px solid gray;
+           border-radius: 10px;
+           margin: 13px;
+           padding: 7px;
+           box-shadow: 2px 5px 5px rgba(0, 0, 0, 0.14);
+         }
+         .board {
+           background: rgb(244, 244, 244);
+         }
+         .device {
+           background: #fff;
+         }
+        </style>
+        <core-ajax
+          url="boards"
+          auto="true"
+          handleAs="json"
+          response="{{ret}}"></core-ajax>
+        <template repeat="{{board in ret.boards}}">
+          <div class="board">
+            <h1>Board <linked-uri href="{{board.uri}}"></linked-uri></h1>
+            <h2>Devices</h2>
+            <ul>
+              <template repeat="{{dev in board.devices}}">
+                <div class="device">
+                  <h1>
+                    Device <linked-uri href="{{dev.uri}}"></linked-uri>
+                    (class {{dev.className}})
+                  </h1>
+                  <div>watching:</div>
+                  <ul>
+                    <template repeat="{{prefix in dev.watchPrefixes}}">
+                      <div>{{prefix[0]}}, {{prefix[1]}}, ?obj</div>
+                    </template>
+                  </ul>
+
+                  <div>send output:</div>
+                  <ul>
+                    <template repeat="{{out in dev.outputWidgets}}">
+                      <div>
+                        <output-widget-any desc="{{out}}"></output-widget-any>
+                      </div>
+                    </template>
+                  </ul>
+                </div>
+              </template>
+            </ul>
+              
+          </div>
+        </template>
+      </template>
+
+    </polymer-element>
+    <arduinonode-boards></arduinonode-boards>
+
+    
+    <polymer-element name="data-dump" noscript>
+      <template>
+        <rdf-observe
+          graph="http://bang:9059/graph"
+          subject="sensor:motion0"
+          predicate="room:sees"
+          out="{{out}}">
+        </rdf-observe>
+        <div>sees: {{out['room:sees']}}</div>
+      </template>
+    </polymer-element>
+    <!-- <data-dump></data-dump> -->
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/arduinoNode/static/output-widgets.html	Mon Apr 13 23:30:12 2015 -0700
@@ -0,0 +1,70 @@
+<link rel="import" href="/lib/polymer/0.5.2/core-ajax/core-ajax.html">
+
+<polymer-element name="output-widget" attributes="subj pred">
+  <template>
+    <core-ajax id="output" url="../output" method="POST"></core-ajax>
+    {{subj}} set {{pred}} to
+  </template>
+  <script>
+   function ntriple(s, p, o) {
+     // incomplete
+     o = o.replace('\n', '\\n');
+     return '<'+s+'> <'+p+'> "'+o+'".';
+   }
+   Polymer({
+     valueChanged: function() {
+       this.$.output.body = ntriple(this.subj, this.pred, this.value);
+       this.$.output.go();
+     }
+   });
+  </script>
+</polymer-element>
+
+<polymer-element name="output-slider" extends="output-widget"
+                 attributes="min max step"
+                 noscript>
+  <template>
+    <shadow></shadow>
+    <input type="range"
+           min="{{min}}" max="{{max}}" step="{{step}}"
+           value="{{value}}"> {{value}}
+  </template>
+</polymer-element>
+
+<polymer-element name="output-fixed-text" extends="output-widget"
+                 attributes="rows cols"
+                 noscript>
+  <template>
+    <textarea rows="{{rows}}" cols="{{cols}}" value="{{value}}"></textarea>
+  </template>
+</polymer-element>
+
+<polymer-element name="output-switch" extends="output-widget">
+  <template>
+    <shadow></shadow>
+    <input type="checkbox" checked="{{check}}"> {{value}}
+  </template>
+  <script>
+   Polymer({
+     check: false,
+     checkChanged: function() {
+       this.value = this.check ? "high" : "low";
+     }
+   });
+  </script>
+</polymer-element>
+
+<polymer-element name="output-widget-any" attributes="desc">
+  <template></template>
+  <script>
+   Polymer({
+     domReady: function() {
+       var elem = document.createElement(this.desc.element);
+       this.shadowRoot.appendChild(elem);
+       for (var k of Object.keys(this.desc)) {
+         elem.setAttribute(k, this.desc[k]);
+       }
+     }
+   });
+  </script>
+</polymer-element>