changeset 1193:08a6eb5edf3d

tinyscreen can flip images and render news Ignore-this: 72c89530e71d4c6f2550bb4deeccd807 darcs-hash:1e6cd5b4615297bd91d63e92951e90594ab98f25
author drewp <drewp@bigasterisk.com>
date Wed, 02 Jan 2019 18:40:16 -0800
parents 532942f6d405
children ee9cbe5817a4
files service/tinyScreen/Dockerfile service/tinyScreen/Dockerfile.pi service/tinyScreen/index.html service/tinyScreen/makefile service/tinyScreen/requirements.txt service/tinyScreen/requrements.txt service/tinyScreen/tiny_screen.py
diffstat 7 files changed, 234 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tinyScreen/Dockerfile	Wed Jan 02 18:40:16 2019 -0800
@@ -0,0 +1,14 @@
+FROM bang6:5000/base_x86
+
+WORKDIR /opt
+
+COPY requirements.txt ./
+RUN pip install -r requirements.txt
+
+COPY font ./font
+COPY anim ./anim
+COPY *.py *.html ./
+
+EXPOSE 11013
+
+CMD [ "python", "./tiny_screen.py" ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tinyScreen/Dockerfile.pi	Wed Jan 02 18:40:16 2019 -0800
@@ -0,0 +1,14 @@
+FROM bang6:5000/base_pi
+
+WORKDIR /opt
+
+COPY requirements.txt ./
+RUN pip install -r requirements.txt
+
+COPY font ./font
+COPY anim ./anim
+COPY *.py *.html ./
+
+EXPOSE 11013
+
+CMD [ "python", "./tiny_screen.py" ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tinyScreen/index.html	Wed Jan 02 18:40:16 2019 -0800
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+  <head>
+    <title></title>
+    <meta charset="utf-8" />
+  </head>
+  <body>
+
+    request state:   boot sleep locked lockedUnknownKey unlockNews
+    goal state:
+    current state:
+    news: ____
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tinyScreen/makefile	Wed Jan 02 18:40:16 2019 -0800
@@ -0,0 +1,32 @@
+JOB=tiny_screen
+PORT=10013
+
+TAG=bang6:5000/${JOB}_x86:latest
+TAG_PI=bang6:5000/${JOB}_pi:latest
+
+push:
+	docker push ${TAG}
+
+build_x86:
+	rm -rf tmp_ctx
+	mkdir -p tmp_ctx
+	cp -a Dockerfile ../../lib/*.py *.py *.txt *.html anim font tmp_ctx
+	docker build --network=host -t ${TAG} tmp_ctx
+	rm -rf tmp_ctx
+
+build_image: build_x86 push
+
+build_pi:
+	rm -rf tmp_ctx
+	mkdir -p tmp_ctx
+	cp -a Dockerfile.pi ../../lib/*.py *.py *.txt *.html anim font tmp_ctx
+	docker build -f Dockerfile.pi --network=host -t ${TAG_PI} tmp_ctx
+	rm -rf tmp_ctx
+
+build_image_pi: build_pi push
+
+shell:
+	docker run --rm -it --cap-add SYS_PTRACE --net=host $(TAG) /bin/sh
+
+local_run: build_x86
+	docker run --rm -it --net=host -e DISPLAY=$(DISPLAY) -e HOME=$(HOME) -v $(HOME):$(HOME) -v /tmp/.X11-unix:/tmp/.X11-unix -v `pwd`/index.html:/opt/index.html bang6:5000/tiny_screen_x86:latest python ./tiny_screen.py -v -x
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tinyScreen/requirements.txt	Wed Jan 02 18:40:16 2019 -0800
@@ -0,0 +1,6 @@
+luma.oled
+rdflib==4.2.2
+cyclone
+https://projects.bigasterisk.com/rdfdb/rdfdb-0.6.0.tar.gz
+rdflib-jsonld==0.3
+pygame
--- a/service/tinyScreen/requrements.txt	Wed Jan 02 18:39:35 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-luma.oled
--- a/service/tinyScreen/tiny_screen.py	Wed Jan 02 18:39:35 2019 -0800
+++ b/service/tinyScreen/tiny_screen.py	Wed Jan 02 18:40:16 2019 -0800
@@ -1,11 +1,161 @@
+from docopt import docopt
+from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
+from rdflib import Namespace, URIRef, Literal, Graph
+from rdflib.parser import StringInputSource
+from twisted.internet import reactor
+import cyclone.web
+import sys, logging, time, textwrap
+
 from luma.core.interface.serial import spi
-from luma.core.render import canvas
 from luma.oled.device import ssd1331
+from PIL import Image, ImageFont, ImageDraw
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
 
+logging.basicConfig()
+log = logging.getLogger()
 
 class Screen(object):
     def __init__(self, spiDevice=1, rotation=0):
-    self._dev = ssd1331(spi(device=spiDevice, port=0), rotation=rotation)
-    with canvas(self._dev) as draw:
-          draw.rectangle(d.bounding_box, outline="white", fill="red")
+        self._initOutput(spiDevice, rotation)
+        self.news = ""
+        self.animateTo(ROOM['boot'])
+
+    def _stateImage(self, state):
+        return Image.open('anim/%s.png' % state.rsplit('/')[-1])
+        
+    def _initOutput(self, spiDevice, rotation):
+        self._dev = ssd1331(spi(device=spiDevice, port=0), rotation=rotation)
+        
+    def setContrast(self, contrast):
+        """0..255"""
+        self._dev.contrast(contrast)
+
+    def hide(self):
+        """Switches the display mode OFF, putting the device in low-power sleep mode."""
+        self._dev.hide()
+
+    def show(self):
+        self._dev.show()
+
+    def display(self, img):
+        self._dev.display(img)
+
+    def animateTo(self, state):
+        """
+        boot
+        sleep
+        locked
+        lockedUnknownKey
+        unlockNews
+        """
+        self.goalState = state
+        self.display(self._stateImage(state))
+        if state == ROOM['unlockNews']:
+            self.renderNews()
+
+    def setNews(self, text):
+        self.news = text
+        if self.goalState == ROOM['unlockNews']:
+            # wrong during animation
+            self.renderNews()
+        
+    def renderNews(self):
+        bg = self._stateImage(ROOM['unlockNews'])
+        draw = ImageDraw.Draw(bg)
+
+        font = ImageFont.truetype("font/Oswald-SemiBold.ttf", 12)
+        #w, h = font.getsize('txt')
+        for i, line in enumerate(
+                textwrap.fill(self.news, width=12).splitlines()):
+            draw.text((24, 0 + 10 * i), line, font=font)
+        self.display(bg)
+        
+class ScreenSim(Screen):
+    def _initOutput(self, spiDevice, rotation):
+        self.windowScale = 2
+        import pygame
+        self.pygame = pygame
+        pygame.init()
+        self.surf = pygame.display.set_mode(
+            (96 * self.windowScale, 64 * self.windowScale))
+        time.sleep(.05) # something was causing the 1st update not to show
+        
+    def display(self, img):
+        pgi = self.pygame.image.fromstring(
+            img.tobytes(), img.size, img.mode)
+        self.pygame.transform.scale(pgi, self.surf.get_size(), self.surf)
+        self.pygame.display.flip()
 
+def rdfGraphBody(body, headers):
+    g = Graph()
+    g.parse(StringInputSource(body), format='nt')
+    return g
+
+class OutputPage(cyclone.web.RequestHandler):
+    def put(self):
+        arg = self.request.arguments
+        if arg.get('s') and arg.get('p'):
+            subj = URIRef(arg['s'][-1])
+            pred = URIRef(arg['p'][-1])
+            turtleLiteral = self.request.body
+            try:
+                obj = Literal(float(turtleLiteral))
+            except ValueError:
+                obj = Literal(turtleLiteral)
+            stmts = [(subj, pred, obj)]
+        else:
+            nt = self.request.body.replace("\\n", "\n") # wrong, but i can't quote right in curl
+            g = rdfGraphBody(nt, self.request.headers)
+            assert len(g)
+            stmts = list(g.triples((None, None, None)))
+        self._onStatement(stmts)
+            
+    def _onStatement(self, stmts):
+        """
+        (disp :brightness 1.0 . )
+        (disp :state :locked . )
+        (disp :state :sleep . )
+        (disp :state :readKeyUnlock . disp :news "some news text" . )
+        """
+        disp = ROOM['frontDoorOled']
+        for stmt in stmts:
+            if stmt[:2] == (disp, ROOM['news']):
+                self.settings.screen.setNews(stmt[2].toPython())
+            elif stmt[:2] == (disp, ROOM['state']):
+                self.settings.screen.animateTo(stmt[2])
+            else:
+                log.warn("ignoring %s", stmt)
+    
+if __name__ == '__main__':
+    arg = docopt("""
+    Usage: tiny_screen.py [options]
+
+    -v   Verbose
+    -x   Draw to X11 window, not output hardware
+    """)
+    log.setLevel(logging.WARN)
+    if arg['-v']:
+        from twisted.python import log as twlog
+        twlog.startLogging(sys.stdout)
+        log.setLevel(logging.DEBUG)
+
+    masterGraph = PatchableGraph()
+
+    if arg['-x']:
+        screen = ScreenSim()
+    else:
+        screen = Screen(spiDevice=1)
+
+    port = 10013
+    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),
+        ], screen=screen, masterGraph=masterGraph, debug=arg['-v']),
+                      interface='::')
+    log.warn('serving on %s', port)
+    
+    reactor.run()