4
|
1 import asyncio
|
5
|
2 import itertools
|
|
3 import random
|
|
4 import struct
|
4
|
5 import subprocess
|
|
6 import tempfile
|
|
7
|
|
8 import aiomqtt
|
|
9 from PIL import Image, ImageChops
|
|
10
|
|
11
|
|
12 class WebRenderer:
|
5
|
13
|
4
|
14 def __init__(self):
|
5
|
15 self.chrome_proc = subprocess.Popen(["google-chrome", "--headless"])
|
4
|
16 print("Chrome subprocess started.")
|
|
17
|
5
|
18 async def capture_screenshot(self, url) -> Image.Image:
|
|
19 out = tempfile.NamedTemporaryFile(suffix=".png", prefix='webrenderer_')
|
4
|
20 screenshot_command = [
|
|
21 "google-chrome",
|
|
22 "--headless",
|
5
|
23 "--window-size=320,320",
|
4
|
24 f"--screenshot={out.name}",
|
|
25 url,
|
|
26 ]
|
5
|
27 subprocess.run(screenshot_command,
|
|
28 stdout=subprocess.DEVNULL,
|
|
29 stderr=subprocess.DEVNULL)
|
|
30 return Image.open(out.name).convert('RGB')
|
4
|
31
|
|
32
|
6
|
33 blockX = 16
|
|
34 blockY = 64
|
|
35 msgDelay = .12
|
5
|
36 dirtyQueue = {}
|
4
|
37
|
|
38
|
5
|
39 async def check_for_changes(renderer, client, last_image):
|
|
40 current_image = await renderer.capture_screenshot(
|
|
41 "http://localhost:8002/front-door-display/scheduleLcd.html")
|
|
42 diff_image = ImageChops.difference(last_image, current_image)
|
6
|
43 for y in range(0, current_image.height, blockY):
|
|
44 for x in range(0, current_image.width, blockX):
|
|
45 box = (x, y, x + blockX, y + blockY)
|
5
|
46 region = diff_image.crop(box)
|
|
47 if region.getbbox():
|
|
48 dirtyQueue[(x, y)] = current_image.crop(box)
|
|
49 await asyncio.sleep(0)
|
4
|
50
|
|
51 return current_image
|
|
52
|
|
53
|
5
|
54 async def sendDirty(client):
|
|
55 while True:
|
|
56 if dirtyQueue:
|
|
57 # pos = random.choice(list(dirtyQueue.keys()))
|
|
58 pos = min(list(dirtyQueue.keys()))
|
|
59 img = dirtyQueue.pop(pos)
|
|
60 await tell_lcd(client, pos[0], pos[1], img)
|
6
|
61 await asyncio.sleep(msgDelay) # too fast and esp restarts
|
5
|
62
|
|
63 framesSent = itertools.count()
|
|
64
|
|
65
|
|
66 async def tell_lcd(client: aiomqtt.Client, x: int, y: int,
|
|
67 region: Image.Image):
|
6
|
68 seq = next(framesSent) % 65535
|
5
|
69 msg = struct.pack('HHHHH', seq, x, y, region.width, region.height) + region.tobytes()
|
|
70 print(f'send {seq=} {x=} {y=} {region.width=} {region.height=} ', end='\r', flush=True)
|
|
71 await client.publish('display/squib/updates', msg, qos=0)
|
4
|
72
|
|
73
|
|
74 async def main():
|
5
|
75 # also listen for dirty msgs from the web page; see ts.
|
|
76 # also get notified of new mqtt listeners who need a full image refresh.
|
4
|
77 renderer = WebRenderer()
|
5
|
78 async with aiomqtt.Client("mqtt2") as client:
|
|
79 asyncio.create_task(sendDirty(client))
|
|
80 last_image = Image.new('RGB', (320, 320))
|
4
|
81 while True:
|
5
|
82 last_image = await check_for_changes(renderer, client, last_image)
|
|
83 # we could get the web page to tell us when any dom changes
|
|
84 await asyncio.sleep(5)
|
4
|
85
|
|
86
|
|
87 if __name__ == "__main__":
|
|
88 asyncio.run(main())
|