comparison web_to_mqtt.py @ 5:d97a5930db7e

closer
author drewp@bigasterisk.com
date Wed, 06 Mar 2024 16:38:58 -0800
parents e273cc60b389
children e36abecb48a1
comparison
equal deleted inserted replaced
4:e273cc60b389 5:d97a5930db7e
1 import asyncio 1 import asyncio
2 import base64 2 import itertools
3 import io 3 import random
4 import struct
4 import subprocess 5 import subprocess
5 import tempfile 6 import tempfile
6 7
7 import aiomqtt 8 import aiomqtt
8 from PIL import Image, ImageChops 9 from PIL import Image, ImageChops
9 10
10 11
11 class WebRenderer: 12 class WebRenderer:
13
12 def __init__(self): 14 def __init__(self):
13 self.chrome_proc = subprocess.Popen(["google-chrome"]) 15 self.chrome_proc = subprocess.Popen(["google-chrome", "--headless"])
14 print("Chrome subprocess started.") 16 print("Chrome subprocess started.")
15 17
16 def capture_screenshot(self, url, output_path): 18 async def capture_screenshot(self, url) -> Image.Image:
17 out = tempfile.NamedTemporaryFile(suffix=".png") 19 out = tempfile.NamedTemporaryFile(suffix=".png", prefix='webrenderer_')
18 screenshot_command = [ 20 screenshot_command = [
19 "google-chrome", 21 "google-chrome",
20 "--headless", 22 "--headless",
23 "--window-size=320,320",
21 f"--screenshot={out.name}", 24 f"--screenshot={out.name}",
22 url, 25 url,
23 ] 26 ]
24 subprocess.run(screenshot_command) 27 subprocess.run(screenshot_command,
25 return Image.open(out.name) 28 stdout=subprocess.DEVNULL,
29 stderr=subprocess.DEVNULL)
30 return Image.open(out.name).convert('RGB')
26 31
27 32
33 block = 320 // 10
28 34
29 async def render_webpage_to_png(): 35 dirtyQueue = {}
30 # Code to render the webpage to a PNG
31 # Replace this with your actual code to render the webpage to a PNG
32 # For example, you can use libraries like Selenium or requests-html to render the webpage and capture a screenshot
33 pass
34 36
35 37
36 async def check_for_changes(renderer, last_image): 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)
37 49
38 renderer.capture_screenshot("https://en.wikipedia.org", "/tmp/output.png")
39
40 current_image = await render_webpage_to_png()
41 if last_image is not None:
42 diff_image = ImageChops.difference(last_image, current_image)
43 # Iterate over 64x64 pixel squares and check for changes
44 for y in range(0, diff_image.height, 64):
45 for x in range(0, diff_image.width, 64):
46 box = (x, y, x + 64, y + 64)
47 region = diff_image.crop(box)
48 if (region.getbbox()
49 ): # Check if region is not empty (i.e., contains changes)
50 # Send changed square as MQTT message
51 queue these
52 await send_mqtt_message(region)
53 return current_image 50 return current_image
54 51
55 52
56 async def send_mqtt_message(region): 53 async def sendDirty(client):
57 # Convert changed region to base64 encoded string 54 while True:
58 buffer = io.BytesIO() 55 if dirtyQueue:
59 region.save(buffer, format="PNG") 56 # pos = random.choice(list(dirtyQueue.keys()))
60 base64_image = base64.b64encode(buffer.getvalue()).decode("utf-8") 57 pos = min(list(dirtyQueue.keys()))
61 mqtt_client = aiomqtt.Client("mqtt_client") 58 img = dirtyQueue.pop(pos)
62 await mqtt_client.connect("mqtt://broker.example.com") 59 await tell_lcd(client, pos[0], pos[1], img)
63 await mqtt_client.publish("changed_squares", base64_image, qos=1) 60 await asyncio.sleep(.15)
64 await mqtt_client.disconnect() 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)
65 71
66 72
67 async def main(): 73 async def main():
68 # also listen for dirty msgs from the web page; see ts 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.
69 renderer = WebRenderer() 76 renderer = WebRenderer()
70 async with aiomqtt.Client("mqtt_client") as client: 77 async with aiomqtt.Client("mqtt2") as client:
71 last_image = None 78 asyncio.create_task(sendDirty(client))
79 last_image = Image.new('RGB', (320, 320))
72 while True: 80 while True:
73 last_image = await check_for_changes(renderer,last_image) 81 last_image = await check_for_changes(renderer, client, last_image)
74 await asyncio.sleep(2) # Adjust the interval as needed 82 # we could get the web page to tell us when any dom changes
83 await asyncio.sleep(5)
75 84
76 85
77 if __name__ == "__main__": 86 if __name__ == "__main__":
78 asyncio.run(main()) 87 asyncio.run(main())