changeset 182:9fff29ebca71

start pinode Ignore-this: bfafd9994f7f9a61919d49274417ebbb
author drewp@bigasterisk.com
date Sun, 31 May 2015 00:56:55 -0700
parents 71577a849628
children 634d6e477953
files service/piNode/config.n3 service/piNode/devices.py service/piNode/piNode.py
diffstat 3 files changed, 287 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/piNode/config.n3	Sun May 31 00:56:55 2015 -0700
@@ -0,0 +1,43 @@
+@prefix rdfs:     <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix :         <http://projects.bigasterisk.com/room/> .
+@prefix ha:       <http://bigasterisk.com/homeauto/> .
+@prefix sensor:   <http://bigasterisk.com/homeauto/sensor/> .
+@prefix houseLoc: <http://bigasterisk.com/homeauto/houseLoc/> .
+
+@prefix board2pin: <http://bigasterisk.com/homeauto/board2/pin/> .
+@prefix board2ow: <http://bigasterisk.com/homeauto/board2/oneWire/> .
+
+ha:node2 a :PiBoard;
+  :hostname "sticker";
+  :hasPin
+    board2pin:GPIO2,
+    board2pin:GPIO3,
+    board2pin:GPIO4,
+    board2pin:GPIO17,
+    board2pin:GPIO27
+    .
+
+board2pin:GPIO2 :gpioNumber 2 .
+board2pin:GPIO3 :gpioNumber 3 .
+board2pin:GPIO4 :gpioNumber 4 .
+board2pin:GPIO17 :gpioNumber 17 .
+board2pin:GPIO27 :gpioNumber 27 .
+
+board2pin:GPIO17 :connectedTo sensor:motion1 .
+sensor:motion1 a :MotionSensor;
+  :sees houseLoc:bed .
+
+:bedLedStrip a :RgbStrip;
+  :redChannel board2pin:GPIO2;
+  :greenChannel board2pin:GPIO3;
+  :blueChannel board2pin:GPIO4 .
+    
+board2pin:GPIO27 :connectedTo :headboardWhite .
+:headboardWhite a :LedOutput .
+  
+#board2pin:b29 :connectedTo board2ow: .
+#board2ow: a :OneWire;
+#  :connectedTo board2ow:temperatureSensor .
+#board2ow:temperatureSensor a :TemperatureSensor;
+#  :position :bed .
+  
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/piNode/devices.py	Sun May 31 00:56:55 2015 -0700
@@ -0,0 +1,122 @@
+from __future__ import division
+import pigpio
+import time
+from rdflib import Namespace, RDF, URIRef, Literal
+
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+
+class DeviceType(object):
+    deviceType = None
+    @classmethod
+    def findInstances(cls, graph, board, pi):
+        """
+        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).
+        """
+        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}):
+            yield cls(graph, row.dev, pi, int(row.pinNumber))
+
+    def __init__(self, graph, uri, pi, pinNumber):
+        self.graph, self.uri, self.pi = graph, uri, pi
+        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 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 poll(self):
+        return [] # statements
+    
+    def outputPatterns(self):
+        """
+        Triple patterns, using None as a wildcard, that should be routed
+        to sendOutput
+        """
+        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):
+        """
+        If we got statements that match this class's outputPatterns, this
+        will be called with the statements that matched. 
+
+        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)
+    return deviceType
+
+@register
+class MotionSensorInput(DeviceType):
+    deviceType = ROOM['MotionSensor']
+
+    def setup(self):
+        self.pi.set_mode(17, pigpio.INPUT)
+        self.pi.set_pull_up_down(17, pigpio.PUD_DOWN)
+
+    def poll(self):
+        motion = self.pi.read(17)
+        
+        return [
+            (self.uri, ROOM['sees'],
+             ROOM['motion'] if motion else ROOM['noMotion']),
+            self.recentMotionStatement(motion),
+        ]
+
+    def recentMotionStatement(self, motion):
+        if not hasattr(self, 'lastMotionTime'):
+            self.lastMotionTime = 0
+        now = time.time()
+        if motion:
+            self.lastMotionTime = now
+        recentMotion = now - self.lastMotionTime < 60 * 10
+        return (self.uri, ROOM['seesRecently'],
+                ROOM['motion'] if recentMotion else ROOM['noMotion'])        
+    
+    def watchPrefixes(self):
+        return [
+            (self.uri, ROOM['sees']),
+            (self.uri, ROOM['seesRecently']),
+        ]
+
+def makeDevices(graph, board, pi):
+    out = []
+    for dt in sorted(_knownTypes, key=lambda cls: cls.__name__):
+        out.extend(dt.findInstances(graph, board, pi))
+    return out
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/piNode/piNode.py	Sun May 31 00:56:55 2015 -0700
@@ -0,0 +1,122 @@
+from __future__ import division
+import glob, sys, logging, subprocess, socket, os, hashlib, time, tempfile
+import shutil, json, socket
+import cyclone.web
+from rdflib import Graph, Namespace, URIRef, Literal, RDF
+from rdflib.parser import StringInputSource
+from twisted.internet import reactor, task
+from docopt import docopt
+logging.basicConfig(level=logging.DEBUG)
+sys.path.append("/my/site/magma")
+from stategraph import StateGraph
+sys.path.append('/home/pi/dim/PIGPIO')
+import pigpio
+
+import devices
+
+log = logging.getLogger()
+logging.getLogger('serial').setLevel(logging.WARN)
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+HOST = Namespace('http://bigasterisk.com/ruler/host/')
+
+hostname = socket.gethostname()
+
+class GraphPage(cyclone.web.RequestHandler):
+    def get(self):
+        g = StateGraph(ctx=ROOM['pi/%s' % hostname])
+
+        for stmt in self.settings.board.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 Board(object):
+    """similar to arduinoNode.Board but without the communications stuff"""
+    def __init__(self, graph, uri, onChange):
+        self.graph, self.uri = graph, uri
+        self.pi = pigpio.pi()
+        self._devs = devices.makeDevices(graph, self.uri, self.pi)
+        self._statementsFromInputs = {} # input device uri: latest statements
+
+    def startPolling(self):
+        task.LoopingCall(self._poll).start(.5)
+
+    def _poll(self):
+        for i in self._devs:
+            self._statementsFromInputs[i.uri] = i.poll()
+        
+    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)
+                dev.sendOutput(stmtsForDev)
+                log.info("success")
+        if unused:
+            log.warn("No devices cared about these statements:")
+            for s in unused:
+                log.warn(repr(s))
+        
+class OutputPage(cyclone.web.RequestHandler):
+   
+    def put(self):
+        subj = URIRef(self.get_argument('s'))
+        pred = URIRef(self.get_argument('p'))
+
+        turtleLiteral = self.request.body
+        try:
+            obj = Literal(float(turtleLiteral))
+        except TypeError:
+            obj = Literal(turtleLiteral)
+
+        stmt = (subj, pred, obj)
+        self.settings.board.outputStatements([stmt])
+
+
+def main():
+    arg = docopt("""
+    Usage: piNode.py [options]
+
+    -v   Verbose
+    """)
+    log.setLevel(logging.WARN)
+    if arg['-v']:
+        from twisted.python import log as twlog
+        twlog.startLogging(sys.stdout)
+
+        log.setLevel(logging.DEBUG)
+    
+    config = Config()
+
+    def onChange():
+        # notify reasoning
+        pass
+
+    thisBoard = URIRef('http://bigasterisk.com/homeauto/node2')
+    
+    board = Board(config.graph, thisBoard, onChange)
+    
+    reactor.listenTCP(9059, cyclone.web.Application([
+        (r"/()", cyclone.web.StaticFileHandler, {
+            "path": "static", "default_filename": "index.html"}),
+        (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}),
+        (r"/graph", GraphPage),
+        (r'/output', OutputPage),
+        (r'/dot', Dot),
+        ], config=config, board=board))
+    reactor.run()
+
+main()