Mercurial > code > home > repos > front-door-display
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__":