爆肝24小时,用 (Micro)Python 做了个墨水屏桌面摆件!(爆肝会死人吗)

作者:电子工程世界(EEWorld)ningh

储备了不少墨水屏,一直没派上用场,继【用三色墨水屏显示哪吒】之后,又摆弄了一下 7.5 寸 7 色墨水屏(悬空放了两个小时,弯了-。-),换了个 3.7 寸双色继续折腾。

由于之前大致摸清楚了图像抖动算法,突发奇想,只需要将网页转换为图片,不就可以在墨水屏上显示任何内容了?毕竟网页界面制作起来工作量可小多了,而且有丰富的库可以使用,说干就干!

首先,确定技术方案:服务端:Python + Playwright 负责提供接口渲染网页;客户端:MicroPython 连接 WiFi,定期请求网页更新屏幕。

服务端源码:

import asyncio

import logging

import time

import os

import uuid

from io import BytesIO

from urllib.parse import urljoin

from secrets import compare_digest

from aiohttp import web

from dotenv import load_dotenv

from playwright.async_api import async_playwright

from PIL import Image

logger = logging.getLogger(__name__)

load_dotenv()

HOST = os.environ.get('HTTP_HOST')

PORT = int(os.environ.get('HTTP_PORT'))

RENDER_TIMEOUT = int(os.environ.get('RENDER_TIMEOUT', 30))

jrhz.info

TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), os.environ.get('TEMPLATE_PATH', 'templates/hello.html'))

UPDATE_INTERVAL = int(os.environ.get('UPDATE_INTERVAL', 60 * 15))

IMAGE_URL = str(uuid.uuid4())

def _get_mac():

mac = uuid.getnode()

return ':'.join(("%012X" % mac)[i:i + 2] for i in range(0, 12, 2))

_device = {

'model': 'matters-370-opensource',

'firmware': '0.0.1',

'key': os.environ.get('API_KEY'),

# ignored!

'mac': _get_mac(),

'battery': 0.0,

'rssi': 0,

'ota': '',

# server only!

'width': 416,

'height': 240,

}

_image = None

_last_update = 0

_task = None

_task_lock = asyncio.Lock()

async def render(wait_for=UPDATE_INTERVAL):

global _image, _last_update

if wait_for:

logger.info(f'Waiting for {wait_for} seconds...')

await asyncio.sleep(wait_for)

logger.info('Rendering...')

async with async_playwright() as p:

try:

browser = await p.chromium.launch()

context = await browser.new_context()

page = await context.new_page()

await page.set_viewport_size({'width': _device['width'], 'height': _device['height']})

# TODO: Jinja2 template?

html = open(TEMPLATE_PATH).read()

await page.set_content(html, timeout=RENDER_TIMEOUT * 1000, wait_until='networkidle')

png_bytes = await page.screenshot(type='png')

img = Image.open(BytesIO(png_bytes))

img = img.convert('1', dither=Image.Dither.FLOYDSTEINBERG)

img_bytes = BytesIO()

img.save(img_bytes, format='BMP')

_image = img_bytes.getvalue()

_last_update = int(time.time())

except Exception as e:

logger.error('Render failed:', exc_info=e)

return

logger.info('Render √')

@web.middleware

async def auth_middleware(req, handler):

if req.method == 'POST':

if not compare_digest(req.headers.get('Access-Token', ''), _device['key']):

raise web.HTTPForbidden(reason='Invalid API key')

return await handler(req)

async def ping(req: web.Request) -> web.Response:

global _task

if _image:

image_url = urljoin(f'{req.scheme}://{req.host}{req.path}', IMAGE_URL)

else:

image_url = ''

res = {

'status': 200,

'message': 'OK',

'display': image_url,

'last_update': _last_update,

# unused!

'key': '',

'ota': '',

}

res['next_update'] = UPDATE_INTERVAL if _image else RENDER_TIMEOUT

async with _task_lock:

if not _task or _task.done():

_task = asyncio.create_task(render(max(res['next_update'] - RENDER_TIMEOUT, 0)))

return web.json_response(res)

def image(req: web.Request) -> web.Response:

if not _image:

raise web.HTTPNotFound()

return web.Response(body=_image, content_type='image/bmp')

def create_app() -> web.Application:

app = web.Application(middlewares=[auth_middleware])

app.add_routes([

web.post('/ping', ping),

web.get(f'/{IMAGE_URL}', image),

])

return app

if __name__ == '__main__':

logging.basicConfig(level=logging.INFO)

if not _device['key']:

_device['key'] = uuid.uuid4().hex

logger.info('API key generated: %s', _device['key'])

logger.info('Starting server...')

app = create_app()

web.run_app(app, host=HOST, port=PORT)

为了方便测试,还用 Python + tkinter 实现了一个简单的模拟器,效果如下:

今日霍州(www.jrhz.info)©️

模拟器源码:

import threading

import time

import tkinter as tk

import uuid

from datetime import datetime, timedelta

from io import BytesIO

from tkinter import ttk

import requests

from PIL import ImageTk, Image

API_HOST = 'http://127.0.0.1:1988'

API_KEY = ''

def _get_mac():

mac = uuid.getnode()

return ':'.join(('%012X' % mac)[i:i + 2] for i in range(0, 12, 2))

_device = {

'model': 'matters-370-opensource',

'firmware': '0.0.1',

'mac': _get_mac(),

'battery': 0.0,

'rssi': 0,

'ota': '',

'width': 416,

'height': 240,

'key': API_KEY,

'last_update': 0,

}

_req = {

'model': _device['model'],

'firmware': _device['firmware'],

'mac': _device['mac'],

'battery': 0.0,

'rssi': 0,

}

class Simulator:

def __init__(self, master):

self.master = master

master.title(f'Simulator: {_device["model"]}')

master.resizable(0, 0)

self.frame = ttk.Frame(master, width=_device['width'], height=_device['height'])

self.frame.pack_propagate(0)

self.frame.pack()

self.img_label = ttk.Label(self.frame)

self.img_label.pack(expand=1)

self.status = ttk.Label(master, text='Initializing...',

relief=tk.SUNKEN, anchor=tk.W)

self.status.pack(side=tk.BOTTOM, fill=tk.X)

threading.Thread(target=self.upate, daemon=True).start()

def upate(self):

global _device

while True:

self.update_status('Connecting...')

try:

resp = requests.post(url=f'{API_HOST}/ping', json=_req, timeout=10, headers={

'Access-Token': _device['key'],

})

resp.raise_for_status()

data = resp.json()

self.update_status(data['message'])

if data['key']:

_device['key'] = data['key']

if data['last_update'] != _device['last_update']:

resp = requests.get(data['display'], timeout=10)

resp.raise_for_status()

img_io = BytesIO(resp.content)

pil_img = Image.open(img_io).resize((_device['width'], _device['height']))

self.tk_img = ImageTk.PhotoImage(pil_img)

self.master.after(0, self.show_image)

_device['last_update'] = data['last_update']

next_update = datetime.now() + (timedelta(seconds=data['next_update']))

self.update_status(f'Next update: {next_update}')

time.sleep(data['next_update'])

except Exception as e:

self.master.after(0, lambda: self.show_error(str(e)))

time.sleep(30)

def show_image(self):

self.img_label.configure(image=self.tk_img)

def update_status(self, message):

self.status.config(text=message)

self.status.update_idletasks()

def show_error(self, message):

self.update_status(f'Error: {message}')

if __name__ == '__main__':

root = tk.Tk()

root.geometry(f'{_device["width"]}x{_device["height"]+20}')

app = Simulator(root)

root.mainloop()

最后,把模拟器逻辑,移植到 MicroPython,最终效果:

今日霍州(www.jrhz.info)©️

先到这,过几天整理发布到 GitHub 上。

特别声明:[爆肝24小时,用 (Micro)Python 做了个墨水屏桌面摆件!(爆肝会死人吗)] 该文观点仅代表作者本人,今日霍州系信息发布平台,霍州网仅提供信息存储空间服务。

猜你喜欢

王玉雯出席活动被骂惨!假卧蚕妆造太丑,本人崩溃发文回应(王玉雯参加综艺)

10号下午三点,王玉雯刚刚下车,站在她旁边的粉丝们顿时愣住了——她的『穿搭』明明是清新的粉短袖和灰色短裙,本应给人一种元气满满的感觉,但当她一笑,眼下那两条卧蚕竟然亮得有点不真实,像刚哭过眼角还残留着泪水,又被涂…

王玉雯出席活动被骂惨!假卧蚕妆造太丑,本人崩溃发文回应(王玉雯参加综艺)

浙江:女子穿特色衣服坐地铁,网友:这么好看的姑娘被一件衣服毁了。(浙江女子穿特色衣服坐地铁)

当下年轻人置身于信息爆炸、文化交融的浪潮中,着装风格愈发多元鲜活,他们敢于以服饰为媒,坦诚传递内心的真实诉求,让穿衣成为自我表达的重要载体。 谁都希望凭借得体『穿搭』,既获得自我认同,又赢得他人好感,这是对自身品…

浙江:女子穿特色衣服坐地铁,网友:这么好看的姑娘被一件衣服毁了。(浙江女子穿特色衣服坐地铁)

游戏广告模特跨界拍护肤广告还超惊艳?重庆这位“宝藏模特”报价多少(游戏广告模板)

游戏广告里又A又飒的酷girl,摇身一变成为护肤广告中温柔灵动的“水光肌女神”——这位来自重庆的广告模特最近火出圈了!我们根据模特经验、拍摄难度、品牌需求定制方案,拒绝“天价跨界费”,更用效果说话——毕竟,…

游戏广告模特跨界拍护肤广告还超惊艳?重庆这位“宝藏模特”报价多少(游戏广告模板)

女星『金晨』,应该是安全着陆了!举报人或许有3重人设,『娱乐圈』️的水还是太深了!(『金晨』 女主)

最魔幻的时刻发生在警方通报之后——『金晨』发出道歉声明后,不到二十四小时,粉丝不减反增,直播品牌也不全面终止,反而有人洗白“她是为了小狗,才撞车”。 他们说,『娱乐圈』️也有“道德骑士”,眼里不揉沙子,看见谁做了错事…

女星『金晨』,应该是安全着陆了!举报人或许有3重人设,『娱乐圈』️的水还是太深了!(『金晨』 女主)

『张雨绮』前夫前妻再开撕,直播爆料“鸠占鹊巢”细节!(『张雨绮』前夫前妻是谁)

这位金融系毕业的选美亚军最近悄悄干了一件大事——她正式跨界当上财经节目主持人了! 在最近一次电视台新春节目录制现场,施宇琪接受采访时难掩兴奋地宣布,自己将从下周二正式加入财经节目团队。她在『社交平台』分享的省钱技…

『张雨绮』前夫前妻再开撕,直播爆料“鸠占鹊巢”细节!(『张雨绮』前夫前妻是谁)