changeset 412:91162a54553c

copy rest of rfid service from the first try. fix some crashes in tags.py Ignore-this: a3fdcc0a8494e358d3c0abc109e8ed4d
author drewp@bigasterisk.com
date Sat, 23 Mar 2019 04:26:03 -0700
parents 9fbd2d0193bf
children 5fc75de6b905
files service/rfid_pn532_py/Dockerfile.pi service/rfid_pn532_py/Dockerfile.x86 service/rfid_pn532_py/index.html service/rfid_pn532_py/makefile service/rfid_pn532_py/requirements.txt service/rfid_pn532_py/rfid.py service/rfid_pn532_py/tags.py
diffstat 7 files changed, 394 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/service/rfid_pn532_py/Dockerfile.pi	Sat Mar 16 18:22:57 2019 -0700
+++ b/service/rfid_pn532_py/Dockerfile.pi	Sat Mar 23 04:26:03 2019 -0700
@@ -1,18 +1,17 @@
 FROM bang6:5000/base_pi
 
-
-
-
 WORKDIR /opt
 RUN apt-get install -y libnfc5 libfreefare0 libnfc-dev libfreefare-dev 
 RUN apt-get install -y python3-nose2
 COPY pyfreefare-build-pi ./pyfreefare-build
 
 COPY requirements.txt .
-RUN pip install -r requirements.txt
+RUN pip3 install -r requirements.txt
+RUN pip3 install -v -U 'https://github.com/drewp/cyclone/archive/patch-1.zip'
 
 COPY *.py *.html  ./
 
 ENV PYTHONPATH=/opt/pyfreefare-build
 EXPOSE 10012
 
+CMD ["/usr/bin/python3", "rfid.py"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/rfid_pn532_py/Dockerfile.x86	Sat Mar 23 04:26:03 2019 -0700
@@ -0,0 +1,17 @@
+FROM bang6:5000/base_x86
+
+WORKDIR /opt
+RUN apt-get install -y libnfc5 libfreefare0 libnfc-dev libfreefare-dev 
+RUN apt-get install -y python3-nose2
+COPY pyfreefare-build-x86 ./pyfreefare-build
+
+COPY requirements.txt .
+RUN pip3 install -r requirements.txt
+RUN pip3 install -U 'https://github.com/drewp/cyclone/archive/patch-1.zip'
+
+COPY *.py *.html  ./
+
+ENV PYTHONPATH=/opt/pyfreefare-build
+EXPOSE 10012
+
+CMD ["/usr/bin/python3", "rfid.py", "-v"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/rfid_pn532_py/index.html	Sat Mar 23 04:26:03 2019 -0700
@@ -0,0 +1,128 @@
+<!doctype html>
+<html>
+  <head>
+    <title>rfid</title>
+    <meta charset="utf-8" />
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <script src="/lib/polymer/1.0.9/webcomponentsjs/webcomponents.min.js"></script>
+    <script src="/lib/require/require-2.3.3.js"></script>
+    <script>
+     requirejs.config({
+       paths: {
+         "streamed-graph": "/rdf/streamed-graph",
+         "quadstore": "/rdf/quadstore",
+         "async-module": "/lib/async/80f1793/async",
+         "async": "/lib/async/80f1793/async",
+         "jsonld-module": "/lib/jsonld.js/0.4.11/js/jsonld",
+         "jsonld": "/lib/jsonld.js/0.4.11/js/jsonld",
+         "rdfstore": "/lib/rdf_store/0.9.7/dist/rdfstore",
+         "moment": "/lib/moment.min",
+         "underscore": "/lib/underscore-1.5.2.min",
+       }
+     });
+    </script>
+    <script>
+     window.NS = {
+       dev: 'http://projects.bigasterisk.com/device/',
+       room: 'http://projects.bigasterisk.com/room/',
+       rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
+       sensor: 'http://bigasterisk.com/homeauto/sensor/',
+     };
+    </script>
+    <link rel="import" href="/lib/polymer/1.0.9/iron-ajax/iron-ajax.html">
+    <link rel="import" href="/rdf/streamed-graph.html">
+    <link rel="import" href="/lib/polymer/1.0.9/polymer/polymer.html">
+    <link rel="import" href="/rdf/rdf-oneshot.html">
+    <link rel="import" href="/rdf/rdf-uri.html">
+  </head>
+  <body>
+    <dom-module id="rfid-control">
+      <style>
+       button {
+           min-width: 60px;
+           min-height: 40px;
+       }
+       table {
+           border-collapse: collapse;
+       }
+       
+       td, th {
+           border: 1px solid gray;
+       }
+      </style>
+      <template>
+        <div>
+          <streamed-graph url="graph/events" graph="{{graph}}"></streamed-graph>
+          <!-- also get a graph of users so we can look up cards -->
+        </div>
+        
+        <iron-ajax id="rewrite" url="rewrite" method="POST"></iron-ajax>
+        
+        Current reads: 
+        <table>
+          <tr><th>Card UID</th><th>Card text</th><th></th></tr>
+          <template is="dom-repeat" items="{{currentReads}}">
+            <tr>
+              <td>{{item.uidDisplay}}</td>
+              <td>{{item.text}}</td>
+              <td>
+                <div id="form">
+                  <button on-click="rewrite">Rewrite</button>
+                </div>
+              </td>
+            </tr>
+          </template>
+        </table>
+        
+      </template>
+      <script>
+       HTMLImports.whenReady(function () {
+         Polymer({
+           is: 'rfid-control',
+           properties: {
+             graph: { type: Object, notify: true, observer: "_onGraph" },
+             currentReads: { type: Array, value: [] },
+           },
+           behaviors: [BigastUri],
+           _onGraph: function(graph) {
+             if (!graph.graph) return;
+             const env = graph.graph.store.rdf;
+
+             this.splice('currentReads', 0, this.currentReads.length);
+             graph.graph.quadStore.quads(
+               {subject: env.createNamedNode('room:frontDoorWindowRfid'),
+                predicate: env.createNamedNode('room:reading'),
+               },
+               (q) => {
+                 graph.graph.quadStore.quads(
+                   {subject: q.object,
+                    predicate: env.createNamedNode('room:cardText'),
+                   },
+                   (q2) => {
+                     this.push(
+                       'currentReads', {
+                         'cardUid': q.object,
+                         'uidDisplay': q.object.toString().replace(/.*\//, ""),
+                         'text': q2.object.toString()
+                       });
+                   });
+               });
+           },
+           rewrite: function(ev) {
+             const cardUid = ev.model.item.cardUid;
+
+             // ask for user first
+
+             this.$.rewrite.contentType = "application/json";
+             this.$.rewrite.body = {'cardUid': cardUid.toString(),
+                                    'user': "some foaf"};
+             this.$.rewrite.generateRequest();
+           }
+         });
+       });
+      </script>
+    </dom-module>
+    <rfid-control></rfid-control>
+  </body>
+</html>
--- a/service/rfid_pn532_py/makefile	Sat Mar 16 18:22:57 2019 -0700
+++ b/service/rfid_pn532_py/makefile	Sat Mar 23 04:26:03 2019 -0700
@@ -1,3 +1,8 @@
+SERVICE=rfid_pn532_py
+JOB=rfid
+PORT=10012
+RUNHOST=frontdoor
+
 pyfreefare-build-x86/nfc.py: Dockerfile.pyfreefare.x86 pyfreefare/nfc.h pyfreefare/freefare.h build_ctypes_modules.sh
 	docker build --file Dockerfile.pyfreefare.x86 -t pyfreefare_build_x86:latest .
 	docker run --rm --net=host \
@@ -17,9 +22,27 @@
 	PYTHONPATH=pyfreefare-build-x86 nose2-3 tags_test
 
 
-rfid_build_image_pi: pyfreefare-build-pi/nfc.py pyfreefare-build-pi/freefare.py
-	docker build --file Dockerfile.pi -t bang6:5000/rfid_pn532_py_pi:latest .
-	docker push bang6:5000/rfid_pn532_py_pi:latest
+build_image_x86: pyfreefare-build-x86/nfc.py pyfreefare-build-x86/freefare.py Dockerfile.x86
+	rm -rf tmp_ctx
+	mkdir -p tmp_ctx
+	cp -a Dockerfile.x86 ../../lib/*.py *.py *.txt *.html pyfreefare-build-x86 tmp_ctx
+	docker build --network=host --file Dockerfile.x86 -t bang6:5000/$(SERVICE)_x86:latest tmp_ctx
+	docker push bang6:5000/$(SERVICE)_x86:latest
+
+build_image_pi: pyfreefare-build-pi/nfc.py pyfreefare-build-pi/freefare.py Dockerfile.pi
+	rm -rf tmp_ctx
+	mkdir -p tmp_ctx
+	cp -a Dockerfile.pi ../../lib/*.py *.py *.txt *.html pyfreefare-build-pi tmp_ctx
+	docker build --network=host --file Dockerfile.pi -t bang6:5000/$(SERVICE)_pi:latest tmp_ctx
+	docker push bang6:5000/$(SERVICE)_pi:latest
+
+run_local_x86: build_image_x86
+	docker run -it --rm --privileged --net=host --cap-add=SYS_PTRACE --name $(JOB)_run bang6:5000/$(SERVICE)_x86:latest python3 rfid.py -v 
+
 
 # test on pi:
 # docker pull bang6:5000/rfid_pn532_py_pi:latest && docker run -it --rm --privileged --name rfid_shell bang6:5000/rfid_pn532_py_pi:latest nose2-3 tags_test
+
+redeploy: build_image_pi
+	sudo /my/proj/ansible/playbook -l $(RUNHOST) -t rfid
+	supervisorctl -s http://$(RUNHOST):9001/ restart $(JOB)_$(PORT)
--- a/service/rfid_pn532_py/requirements.txt	Sat Mar 16 18:22:57 2019 -0700
+++ b/service/rfid_pn532_py/requirements.txt	Sat Mar 23 04:26:03 2019 -0700
@@ -1,1 +1,7 @@
+cyclone
+docopt
+rdflib-jsonld==0.4.0
+rdflib==4.2.2
+https://projects.bigasterisk.com/rdfdb/rdfdb-0.7.0.tar.gz
+git+http://github.com/drewp/scales.git@448d59fb491b7631877528e7695a93553bfaaa93#egg=scales
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/rfid_pn532_py/rfid.py	Sat Mar 23 04:26:03 2019 -0700
@@ -0,0 +1,204 @@
+import os
+os.environ['LIBNFC_DEFAULT_DEVICE'] = "pn532_i2c:/dev/i2c-1"
+
+from docopt import docopt
+from rdfdb.patch import Patch
+from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
+from rdflib import Namespace, URIRef, Literal, Graph
+from rdflib.parser import StringInputSource
+from twisted.internet import reactor, task
+import cyclone.web
+from cyclone.httpclient import fetch
+import cyclone
+import logging, time, json, random, string
+from logsetup import log, enableTwistedLog
+from greplin import scales
+from greplin.scales.cyclonehandler import StatsHandler
+from tags import NfcDevice, FakeNfc
+
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+
+ctx = ROOM['frontDoorWindowRfidCtx']
+
+STATS = scales.collection('/web',
+                          scales.PmfStat('cardReadPoll'),
+)
+
+class OutputPage(cyclone.web.RequestHandler):
+    def put(self):
+        arg = self.request.arguments
+        if arg.get('s') and arg.get('p'):
+            self._onQueryStringStatement(arg['s'][-1], arg['p'][-1], self.request.body)
+        else:
+            self._onGraphBodyStatements(self.request.body, self.request.headers)
+    post = put
+    def _onQueryStringStatement(self, s, p, body):
+        subj = URIRef(s)
+        pred = URIRef(p)
+        turtleLiteral = self.request.body
+        try:
+            obj = Literal(float(turtleLiteral))
+        except ValueError:
+            obj = Literal(turtleLiteral)
+        self._onStatements([(subj, pred, obj)])
+        
+    def _onGraphBodyStatements(self, body, headers):
+        g = Graph()
+        g.parse(StringInputSource(body), format='nt')
+        if not g:
+            raise ValueError("expected graph body")
+        self._onStatements(list(g.triples((None, None, None))))
+    post = put
+    
+    def _onStatements(self, stmts):
+        # write rfid to new key, etc.
+        if len(stmts) > 0 and stmts[0][1] == ROOM['keyContents']:
+            return
+        log.warn("ignoring %s", stmts)
+
+def uidUri(card_id):
+    return URIRef('http://bigasterisk.com/rfidCard/%s' % card_id)
+
+BODY_VERSION = "1"
+def randomBody():
+    return BODY_VERSION + '*' + ''.join(random.choice(string.ascii_uppercase) for n in range(16 - 2))
+
+def looksLikeBigasterisk(text):
+    return text.startswith(BODY_VERSION + "*")
+    
+class Rewrite(cyclone.web.RequestHandler):
+    def post(self):
+        agent = URIRef(self.request.headers['x-foaf-agent'])
+        body = json.loads(self.request.body)
+
+        _, uid = reader.read_id()
+        log.info('current card id: %r %r', _, uid)
+        if uid is None:
+            self.set_status(404, "no card present")
+            # maybe retry a few more times since the card might be nearby
+            return
+            
+        text = randomBody()
+        log.info('%s rewrites %s to %s, to be owned by %s', 
+                 agent, uid, text, body['user'])
+        
+        #reader.KEY = private.rfid_key
+        reader.write(uid, text)
+        log.info('done with write')
+
+    
+sensor = ROOM['frontDoorWindowRfid']
+
+class ReadLoop(object):
+    def __init__(self, reader, masterGraph, overwrite_any_tag):
+        self.reader = reader
+        self.masterGraph = masterGraph
+        self.overwrite_any_tag = overwrite_any_tag
+        self.log = {} # cardIdUri : most recent seentime
+
+        self.pollPeriodSecs = 5.1
+        self.expireSecs = 5
+        
+        task.LoopingCall(self.poll).start(self.pollPeriodSecs)
+
+    @STATS.cardReadPoll.time()
+    def poll(self):
+        now = time.time()
+
+        self.flushOldReads(now)
+
+        for tag in self.reader.getTags(): # blocks for a bit
+            uid = tag.uid()
+            log.info('detected tag uid=%r', uid)
+            cardIdUri = uidUri(uid)
+
+            is_new = cardIdUri not in self.log
+            self.log[cardIdUri] = now
+            if is_new:
+                tag.connect()
+                try:
+                    textLit = Literal(tag.readBlock(1).rstrip('\x00'))
+                    if self.overwrite_any_tag and not looksLikeBigasterisk(textLit):
+                        log.info("block 1 was %r; rewriting it", textLit)
+                        tag.writeBlock(1, randomBody())
+                        textLit = Literal(tag.readBlock(1).rstrip('\x00'))
+                finally:
+                    tag.disconnect()
+                self.startCardRead(cardIdUri, textLit)
+        
+    def flushOldReads(self, now):
+        for uri in list(self.log):
+            if self.log[uri] < now - self.expireSecs:
+                self.endCardRead(uri)
+                del self.log[uri]
+
+    def startCardRead(self, cardUri, text):
+        self.masterGraph.patch(Patch(addQuads=[
+            (sensor, ROOM['reading'], cardUri, ctx),
+            (cardUri, ROOM['cardText'], text, ctx)],
+                                     delQuads=[]))
+        log.info('read card at id=%s %r', cardUri, str(text))
+        self._sendOneshot([(sensor, ROOM['startReading'], cardUri),
+                            (cardUri, ROOM['cardText'], text)])
+
+    def endCardRead(self, cardUri):
+        log.info(f'{cardUri} has been gone for {self.expireSecs} sec')
+        delQuads = []
+        for spo in self.masterGraph._graph.triples(
+                (sensor, ROOM['reading'], cardUri)):
+            delQuads.append(spo + (ctx,))
+        for spo in self.masterGraph._graph.triples(
+                (cardUri, ROOM['cardText'], None)):
+            delQuads.append(spo + (ctx,))
+            
+        self.masterGraph.patch(Patch(addQuads=[], delQuads=delQuads))
+        
+    def _sendOneshot(self, oneshot):
+        body = (' '.join('%s %s %s .' % (s.n3(), p.n3(), o.n3())
+                         for s,p,o in oneshot)).encode('utf8')
+        url = b'http://bang6:19071/oneShot'
+        d = fetch(method=b'POST',
+                  url=url,
+                  headers={b'Content-Type': [b'text/n3']},
+                  postdata=body,
+                  timeout=5)
+        def err(e):
+            log.info('oneshot post to %r failed:  %s',
+                     url, e.getErrorMessage())
+        d.addErrback(err)
+
+                                                              
+        
+if __name__ == '__main__':
+    arg = docopt("""
+    Usage: rfid.py [options]
+
+    -v                    Verbose
+    --overwrite_any_tag   Rewrite any unknown tag with a new random body
+    -n                    Fake reader
+    """)
+    log.setLevel(logging.INFO)
+    if arg['-v']:
+        enableTwistedLog()
+        log.setLevel(logging.DEBUG)
+        log.info(f'cyclone {cyclone.__version__}')
+        
+    masterGraph = PatchableGraph()
+    reader = NfcDevice() if not arg['-n'] else FakeNfc()
+
+    loop = ReadLoop(reader, masterGraph, overwrite_any_tag=arg['--overwrite_any_tag'])
+
+    port = 10012
+    reactor.listenTCP(port, cyclone.web.Application([
+        (r"/()", cyclone.web.StaticFileHandler,
+         {"path": ".", "default_filename": "index.html"}),
+        (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}),
+        (r"/graph/events", CycloneGraphEventsHandler,
+         {'masterGraph': masterGraph}),
+        (r'/output', OutputPage),
+        (r'/rewrite', Rewrite),
+        (r'/stats/(.*)', StatsHandler, {'serverName': 'rfid'}),
+        ], masterGraph=masterGraph, debug=arg['-v']), interface='::')
+    log.warn('serving on %s', port)
+
+    reactor.run()
--- a/service/rfid_pn532_py/tags.py	Sat Mar 16 18:22:57 2019 -0700
+++ b/service/rfid_pn532_py/tags.py	Sat Mar 23 04:26:03 2019 -0700
@@ -4,6 +4,10 @@
 import logging
 log = logging.getLogger('tags')
 
+class FakeNfc(object):
+    def getTags(self):
+        return []
+
     
 class NfcDevice(object):
     def __init__(self):
@@ -17,14 +21,14 @@
         devices_found = nfc.nfc_list_devices(self.context, conn_strings, 10)
         log.info(f'{devices_found} connection strings')
         for i in range(devices_found):
-            log.info(f'  dev {i}: {conn_strings[i]}')
+            log.info(f'  dev {i}: {cast(conn_strings[i], c_char_p).value}')
         if devices_found < 1:
             raise IOError("no devices")
             
         log.info("open dev")
         self.dev = nfc.nfc_open(self.context, conn_strings[0])
-        if nfc.nfc_device_get_last_error(self.dev):
-            raise IOError(f'nfc.open failed on {conn_strings[0]}')
+        if not self.dev or nfc.nfc_device_get_last_error(self.dev):
+            raise IOError(f'nfc.open failed on {cast(conn_strings[0], c_char_p).value}')
 
     def __del__(self):
         if self.dev:
@@ -40,6 +44,8 @@
         try:
             log.info(f"found tags in {time.time() - t0}")
             for t in ret:
+                if not t:
+                    break
                 yield NfcTag(t)
         finally:
             freefare.freefare_free_tags(ret)
@@ -91,4 +97,4 @@
         dataBlock = (c_ubyte*16)(*dataBytes)
         
         self._check(freefare.mifare_classic_write(self.tag, blocknum, dataBlock))
-        log.info("  wrote block {blocknum}: {dataBlock}")
+        log.info(f"  wrote block {blocknum}: {dataBlock}")