changeset 5:d97a5930db7e

closer
author drewp@bigasterisk.com
date Wed, 06 Mar 2024 16:38:58 -0800
parents e273cc60b389
children e36abecb48a1
files .vscode/settings.json lcd_simulator.py pdm.lock pyproject.toml src/scheduleLcd.css src/scheduleLcd.html src/scheduleLcd.ts web_to_mqtt.py
diffstat 8 files changed, 131 insertions(+), 74 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.vscode/settings.json	Wed Mar 06 16:38:58 2024 -0800
@@ -0,0 +1,5 @@
+{
+    "python.analysis.extraPaths": [
+        "/home/drewp/.local/share/pdm/venvs/front-door-display-qUcTJqMw-3.11/lib/python3.11/site-packages"
+    ]
+}
\ No newline at end of file
--- a/lcd_simulator.py	Tue Mar 05 18:12:15 2024 -0800
+++ b/lcd_simulator.py	Wed Mar 06 16:38:58 2024 -0800
@@ -1,40 +1,38 @@
 import asyncio
-import pygame
-import aiomqtt
 import struct
 
-WIDTH = 320
-HEIGHT = 320
+import aiomqtt
+import pygame
 
-pygame.init()
-screen = pygame.display.set_mode((WIDTH, HEIGHT))
+screen = pygame.display.set_mode((320, 320))
 clock = pygame.time.Clock()
 
 
-async def on_message(client, userdata, message):
-    payload = bytes(message.payload)
-    x, y, w, h = struct.unpack("HHHH", payload[:8])
-    buf = payload[8:]
-    for dy in range(h):
-        for dx in range(w):
-            off = w * dy + dx
-            r, g, b = buf[off * 3 : off * 3 + 3]
-            screen.set_at((x + dx, y + dy), (r, g, b))
+async def on_message(client):
+    await client.subscribe('display/squib/updates')
+    async for message in client.messages:
+        payload = bytes(message.payload)
+        head, x, y, w, h = struct.unpack("HHHHH", payload[:10])
+        buf = payload[10:]
+        pygame.draw.rect(screen, (255, 255, 0), (x, y, w, h))
+        pygame.display.flip()
+        await asyncio.sleep(.02)
+        for dy in range(h):
+            for dx in range(w):
+                off = w * dy + dx
+                r, g, b = buf[off * 3:off * 3 + 3]
+                screen.set_at((x + dx, y + dy), (r, g, b))
+        pygame.display.flip()
 
 
 async def main():
     async with aiomqtt.Client("mqtt2.bigasterisk.com") as client:
-        client.on_message = on_message
-        await client.subscribe('display/squib/updates')
+        asyncio.create_task(on_message(client))
 
         while True:
             for event in pygame.event.get():
-                if event.type == pygame.QUIT:
-                    raise SystemExit
-
-            await client.loop_read()
-            pygame.display.flip()
-            clock.tick(60)
+                pass
+            await asyncio.sleep(1 / 30)
 
 
 asyncio.run(main())
--- a/pdm.lock	Tue Mar 05 18:12:15 2024 -0800
+++ b/pdm.lock	Wed Mar 06 16:38:58 2024 -0800
@@ -5,7 +5,7 @@
 groups = ["default"]
 strategy = ["cross_platform", "inherit_metadata"]
 lock_version = "4.4.1"
-content_hash = "sha256:6afa60fb1d3171c06f7d569932d978d79af5c9e326af7ce4c7e5a60a971c6a4b"
+content_hash = "sha256:e29e4766450ee400518622d856b8b4996dba783c1330bcad4b09485749f69f2d"
 
 [[package]]
 name = "aiomqtt"
@@ -31,6 +31,40 @@
 ]
 
 [[package]]
+name = "pillow"
+version = "10.2.0"
+requires_python = ">=3.8"
+summary = "Python Imaging Library (Fork)"
+groups = ["default"]
+files = [
+    {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
+    {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
+    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
+    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
+    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
+    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
+    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
+    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
+    {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
+    {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
+    {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
+    {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
+    {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
+    {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
+    {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
+]
+
+[[package]]
 name = "pygame"
 version = "2.5.2"
 requires_python = ">=3.6"
--- a/pyproject.toml	Tue Mar 05 18:12:15 2024 -0800
+++ b/pyproject.toml	Wed Mar 06 16:38:58 2024 -0800
@@ -8,6 +8,7 @@
 dependencies = [
     "pygame>=2.5.2",
     "aiomqtt>=2.0.0",
+    "pillow>=10.2.0",
 ]
 requires-python = "==3.11.*"
 readme = "README.md"
--- a/src/scheduleLcd.css	Tue Mar 05 18:12:15 2024 -0800
+++ b/src/scheduleLcd.css	Wed Mar 06 16:38:58 2024 -0800
@@ -2,10 +2,11 @@
 body {
   width: 320px;
   height: 320px;
-  font-size:22px;
+  font-size: 22px;
+  position: relative;
 }
 .area {
-    border:1px solid gray;
-    background: rgb(50, 48, 48);
-    position:absolute;
-}
\ No newline at end of file
+  border: 1px solid gray;
+  background: rgb(39, 45, 103);
+  position: absolute;
+}
--- a/src/scheduleLcd.html	Tue Mar 05 18:12:15 2024 -0800
+++ b/src/scheduleLcd.html	Wed Mar 06 16:38:58 2024 -0800
@@ -30,8 +30,12 @@
       class="closeup-cal area"
       style="left: 2px; top: 50px; width: 300px; height: 80px"
     >
-      <div>12:34 - - - - - - ⤵ 6 minutes left</div>
-      <div>youtube | minecraft</div>
+      <div><span id="time">.....</span> 😼 🐶  ⤵ 6 minutes left</div>
+      <div>youtube | minecraft 
+        <span style="display: inline-block; background: red;">R</span>
+        <span style="display: inline-block; background: green;">G</span>
+        <span style="display: inline-block; background: blue;">B</span>
+      </div>
     </div>
     <div class="area" style="left: 2px; top: 180px; width: 60px; height: 100px">
       map
--- a/src/scheduleLcd.ts	Tue Mar 05 18:12:15 2024 -0800
+++ b/src/scheduleLcd.ts	Wed Mar 06 16:38:58 2024 -0800
@@ -10,6 +10,11 @@
 const EV = "http://bigasterisk.com/event#";
 const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
 
+function updateTime() {
+  document.querySelector("#time").innerText = new Date().toTimeString().slice(0, 8);
+}
+setInterval(updateTime, 1000)
+updateTime()
 
 // Function to send a POST request
 function sendPostRequest(data) {
--- a/web_to_mqtt.py	Tue Mar 05 18:12:15 2024 -0800
+++ b/web_to_mqtt.py	Wed Mar 06 16:38:58 2024 -0800
@@ -1,6 +1,7 @@
 import asyncio
-import base64
-import io
+import itertools
+import random
+import struct
 import subprocess
 import tempfile
 
@@ -9,69 +10,77 @@
 
 
 class WebRenderer:
+
     def __init__(self):
-        self.chrome_proc = subprocess.Popen(["google-chrome"])
+        self.chrome_proc = subprocess.Popen(["google-chrome", "--headless"])
         print("Chrome subprocess started.")
 
-    def capture_screenshot(self, url, output_path):
-        out = tempfile.NamedTemporaryFile(suffix=".png")
+    async def capture_screenshot(self, url) -> Image.Image:
+        out = tempfile.NamedTemporaryFile(suffix=".png", prefix='webrenderer_')
         screenshot_command = [
             "google-chrome",
             "--headless",
+            "--window-size=320,320",
             f"--screenshot={out.name}",
             url,
         ]
-        subprocess.run(screenshot_command)
-        return Image.open(out.name)
-
+        subprocess.run(screenshot_command,
+                       stdout=subprocess.DEVNULL,
+                       stderr=subprocess.DEVNULL)
+        return Image.open(out.name).convert('RGB')
 
 
-async def render_webpage_to_png():
-    # Code to render the webpage to a PNG
-    # Replace this with your actual code to render the webpage to a PNG
-    # For example, you can use libraries like Selenium or requests-html to render the webpage and capture a screenshot
-    pass
+block = 320 // 10
+
+dirtyQueue = {}
 
 
-async def check_for_changes(renderer, last_image):
-
-    renderer.capture_screenshot("https://en.wikipedia.org", "/tmp/output.png")
+async def check_for_changes(renderer, client, last_image):
+    current_image = await renderer.capture_screenshot(
+        "http://localhost:8002/front-door-display/scheduleLcd.html")
+    diff_image = ImageChops.difference(last_image, current_image)
+    for y in range(0, current_image.height, block):
+        for x in range(0, current_image.width, block):
+            box = (x, y, x + block, y + block)
+            region = diff_image.crop(box)
+            if region.getbbox():
+                dirtyQueue[(x, y)] = current_image.crop(box)
+                await asyncio.sleep(0)
 
-    current_image = await render_webpage_to_png()
-    if last_image is not None:
-        diff_image = ImageChops.difference(last_image, current_image)
-        # Iterate over 64x64 pixel squares and check for changes
-        for y in range(0, diff_image.height, 64):
-            for x in range(0, diff_image.width, 64):
-                box = (x, y, x + 64, y + 64)
-                region = diff_image.crop(box)
-                if (region.getbbox()
-                    ):  # Check if region is not empty (i.e., contains changes)
-                    # Send changed square as MQTT message
-queue these
-                    await send_mqtt_message(region)
     return current_image
 
 
-async def send_mqtt_message(region):
-    # Convert changed region to base64 encoded string
-    buffer = io.BytesIO()
-    region.save(buffer, format="PNG")
-    base64_image = base64.b64encode(buffer.getvalue()).decode("utf-8")
-    mqtt_client = aiomqtt.Client("mqtt_client")
-    await mqtt_client.connect("mqtt://broker.example.com")
-    await mqtt_client.publish("changed_squares", base64_image, qos=1)
-    await mqtt_client.disconnect()
+async def sendDirty(client):
+    while True:
+        if dirtyQueue:
+            # pos = random.choice(list(dirtyQueue.keys()))
+            pos = min(list(dirtyQueue.keys()))
+            img = dirtyQueue.pop(pos)
+            await tell_lcd(client, pos[0], pos[1], img)
+        await asyncio.sleep(.15)
+
+framesSent = itertools.count()
+
+
+async def tell_lcd(client: aiomqtt.Client, x: int, y: int,
+                   region: Image.Image):
+    seq = next(framesSent)
+    msg = struct.pack('HHHHH', seq, x, y, region.width, region.height) + region.tobytes()
+    print(f'send {seq=} {x=} {y=} {region.width=} {region.height=}  ', end='\r', flush=True)
+    await client.publish('display/squib/updates', msg, qos=0)
 
 
 async def main():
-    # also listen for dirty msgs from the web page; see ts
+    # also listen for dirty msgs from the web page; see ts.
+    # also get notified of new mqtt listeners who need a full image refresh.
     renderer = WebRenderer()
-    async with aiomqtt.Client("mqtt_client") as client:
-        last_image = None
+    async with aiomqtt.Client("mqtt2") as client:
+        asyncio.create_task(sendDirty(client))
+        last_image = Image.new('RGB', (320, 320))
         while True:
-            last_image = await check_for_changes(renderer,last_image)
-            await asyncio.sleep(2)  # Adjust the interval as needed
+            last_image = await check_for_changes(renderer, client, last_image)
+            # we could get the web page to tell us when any dom changes
+            await asyncio.sleep(5)
 
 
 if __name__ == "__main__":