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
|
5
|
33 block = 320 // 10
|
|
34
|
|
35 dirtyQueue = {}
|
4
|
36
|
|
37
|
5
|
38 async def check_for_changes(renderer, client, last_image):
|
|
39 current_image = await renderer.capture_screenshot(
|
|
40 "http://localhost:8002/front-door-display/scheduleLcd.html")
|
|
41 diff_image = ImageChops.difference(last_image, current_image)
|
|
42 for y in range(0, current_image.height, block):
|
|
43 for x in range(0, current_image.width, block):
|
|
44 box = (x, y, x + block, y + block)
|
|
45 region = diff_image.crop(box)
|
|
46 if region.getbbox():
|
|
47 dirtyQueue[(x, y)] = current_image.crop(box)
|
|
48 await asyncio.sleep(0)
|
4
|
49
|
|
50 return current_image
|
|
51
|
|
52
|
5
|
53 async def sendDirty(client):
|
|
54 while True:
|
|
55 if dirtyQueue:
|
|
56 # pos = random.choice(list(dirtyQueue.keys()))
|
|
57 pos = min(list(dirtyQueue.keys()))
|
|
58 img = dirtyQueue.pop(pos)
|
|
59 await tell_lcd(client, pos[0], pos[1], img)
|
|
60 await asyncio.sleep(.15)
|
|
61
|
|
62 framesSent = itertools.count()
|
|
63
|
|
64
|
|
65 async def tell_lcd(client: aiomqtt.Client, x: int, y: int,
|
|
66 region: Image.Image):
|
|
67 seq = next(framesSent)
|
|
68 msg = struct.pack('HHHHH', seq, x, y, region.width, region.height) + region.tobytes()
|
|
69 print(f'send {seq=} {x=} {y=} {region.width=} {region.height=} ', end='\r', flush=True)
|
|
70 await client.publish('display/squib/updates', msg, qos=0)
|
4
|
71
|
|
72
|
|
73 async def main():
|
5
|
74 # also listen for dirty msgs from the web page; see ts.
|
|
75 # also get notified of new mqtt listeners who need a full image refresh.
|
4
|
76 renderer = WebRenderer()
|
5
|
77 async with aiomqtt.Client("mqtt2") as client:
|
|
78 asyncio.create_task(sendDirty(client))
|
|
79 last_image = Image.new('RGB', (320, 320))
|
4
|
80 while True:
|
5
|
81 last_image = await check_for_changes(renderer, client, last_image)
|
|
82 # we could get the web page to tell us when any dom changes
|
|
83 await asyncio.sleep(5)
|
4
|
84
|
|
85
|
|
86 if __name__ == "__main__":
|
|
87 asyncio.run(main())
|