diff service/tinyScreen/tiny_screen.py @ 388:c146fa2bc7d4

tinyscreen can flip images and render news Ignore-this: 72c89530e71d4c6f2550bb4deeccd807
author drewp@bigasterisk.com
date Wed, 02 Jan 2019 18:40:16 -0800
parents 5cdc6e85265f
children 5fc79536885a
line wrap: on
line diff
--- 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()