diff service/cardReader/rfid.py @ 1526:c476543e761e

rename rfid_pn532_py Ignore-this: 89356f780bb5df4d9ec639c44d2a3a67 darcs-hash:212e7f3250cf67ce80922597188d33547d41b0f8
author drewp <drewp@bigasterisk.com>
date Wed, 05 Feb 2020 16:40:46 -0800
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/cardReader/rfid.py	Wed Feb 05 16:40:46 2020 -0800
@@ -0,0 +1,230 @@
+import os
+os.environ['LIBNFC_DEFAULT_DEVICE'] = "pn532_uart:/dev/ttyUSB0"
+
+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, defer
+import cyclone.web
+from cyclone.httpclient import fetch
+import cyclone
+import logging, time, json, random, string, traceback
+from logsetup import log, enableTwistedLog
+from greplin import scales
+from greplin.scales.cyclonehandler import StatsHandler
+from export_to_influxdb import InfluxExporter
+from tags import NfcDevice, FakeNfc, NfcError, AuthFailedError
+
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+
+ctx = ROOM['frontDoorWindowRfidCtx']
+
+STATS = scales.collection('/root',
+                          scales.PmfStat('cardReadPoll'),
+                          scales.IntStat('newCardReads'),
+)
+
+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 = .1
+        self.expireSecs = 5
+
+        # now=False avoids a serious bug where the first read error
+        # could happen before reactor.run() is called, and then the
+        # error fails to crash the reactor and get us restarted.
+        task.LoopingCall(self.poll).start(self.pollPeriodSecs, now=False)
+
+    @STATS.cardReadPoll.time()
+    def poll(self):
+        now = time.time()
+
+        self.flushOldReads(now)
+
+        try:
+            for tag in self.reader.getTags(): # blocks for a bit
+                uid = tag.uid()
+                log.debug('detected tag uid=%r', uid)
+                cardIdUri = uidUri(uid)
+
+                is_new = cardIdUri not in self.log
+                self.log[cardIdUri] = now
+                if is_new:
+                    STATS.newCardReads += 1
+                    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:
+                        # This might not be appropriate to call after
+                        # readBlock fails. I am getting double
+                        # exceptions.
+                        tag.disconnect()
+                    self.startCardRead(cardIdUri, textLit)
+        except AuthFailedError as e:
+            log.error(e)
+        except (NfcError, OSError) as e:
+            traceback.print_exc()
+            log.error(e)
+            reactor.stop()
+
+    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('%s :cardText %s .', cardUri.n3(), text.n3())
+        self._sendOneshot([(sensor, ROOM['startReading'], cardUri),
+                            (cardUri, ROOM['cardText'], text)])
+
+    def endCardRead(self, cardUri):
+        log.debug(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://bang:9071/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__}')
+        defer.setDebugging(True)
+
+    masterGraph = PatchableGraph()
+    reader = NfcDevice() if not arg['-n'] else FakeNfc()
+
+    ie=InfluxExporter(Graph())
+    ie.exportStats(STATS, ['root.cardReadPoll.count',
+                           'root.cardReadPoll.95percentile',
+                           'root.newCardReads',
+                       ],
+                    period_secs=10,
+                    retain_days=7,
+    )
+
+    loop = ReadLoop(reader, masterGraph, overwrite_any_tag=arg['--overwrite_any_tag'])
+
+    port = 10012
+    reactor.listenTCP(port, cyclone.web.Application([
+        (r"/(|.+\.html)", cyclone.web.StaticFileHandler,
+         {"path": ".", "default_filename": "index.html"}),
+        (r"/graph/rfid", CycloneGraphHandler, {'masterGraph': masterGraph}),
+        (r"/graph/rfid/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()