feat(ppclock): 倒计时前缀文字位图(clock 拆包/calendar 990 序列)+ CLI --prefix/--show-calendar,102 测试全绿

This commit is contained in:
agent committed 2026-07-24 11:01:44 +00:00
1 parent 2b01e5f589
commit f291c61676
7 files changed
+367 -3

No files matched your search

+12 -2
View File
@@ -126,6 +126,9 @@ def build_parser() -> argparse.ArgumentParser:
sp = sub.add_parser("countdown", help="倒计时")
sp.add_argument("date", nargs="?", help="YYYY-MM-DD")
sp.add_argument("--mode", default="clock", choices=["clock", "calendar"])
sp.add_argument("--prefix", help="前缀文字(时钟≤8单位/日历≤4字)")
sp.add_argument("--show-calendar", action="store_true",
help="设置日历中文字并切日历模式一(upload_rili 序列)")
sp.add_argument("--off", action="store_true")
sp = sub.add_parser("sleep", help="休眠时段")
@@ -190,11 +193,18 @@ async def _dispatch(args) -> dict | None:
if args.off:
await C.countdown_off(t)
return {"countdown": "off"}
if args.show_calendar:
if not args.prefix:
raise ValueError("--show-calendar 需要 --prefix 提供日历文字")
await C.countdown_set(t, None, args.mode, prefix=args.prefix,
show_calendar=True)
return {"calendar_text": args.prefix}
if not args.date:
raise ValueError("缺少日期(YYYY-MM-DD)或 --off")
d = datetime.date.fromisoformat(args.date)
await C.countdown_set(t, d, args.mode)
return {"date": d.isoformat(), "mode": args.mode}
await C.countdown_set(t, d, args.mode, prefix=args.prefix)
return {"date": d.isoformat(), "mode": args.mode,
"prefix": args.prefix}
if c == "sleep":
on = args.on or not args.off
await C.sleep(t, on, args.start, args.end)
+16 -1
View File
@@ -51,7 +51,22 @@ async def upload_image(t, bw: bytes, red: bytes | None, *, small: bool,
await t.write_epd(P.end_frame(small=small))
async def countdown_set(t, d: datetime.date, mode: str):
async def countdown_set(t, d: datetime.date, mode: str, prefix: str | None = None,
show_calendar: bool = False):
"""倒计时:前缀文字位图(可选)→ 日期帧 → E2。
show_calendar=True 走 upload_rili 序列:990* → FA → e10103 → E2(timedjs2.js:308-312)。"""
from . import textbitmap as TB
if prefix:
for frame in TB.prefix_frames(prefix, "calendar" if show_calendar else mode):
await t.write_rxtx(frame)
await t.delay(1.0)
if show_calendar:
await t.write_rxtx(bytes([0xFA]))
await t.delay(1.0)
await t.write_rxtx(P.mode_frame("calendar1"))
await t.delay(1.0)
await t.write_rxtx(P.simple_command("commit"))
return
await t.write_rxtx(P.countdown_date_frame(d, mode))
await t.delay(1.0)
await t.write_rxtx(P.simple_command("commit"))
+65
View File
@@ -0,0 +1,65 @@
"""倒计时前缀文字位图(证据 analysis/web/timedjs2.js:180-236)。
与 JS 差异:JS 帧有 off-by-one(丢末字节),本实现发完整位图(设备缓冲兼容),见 DECISIONS.md。
"""
from __future__ import annotations
from PIL import Image, ImageDraw
from .templates import _font
GRID_CLOCK = 16 # 时钟界面字格 16px,≤8 单位(中文≤4+英文),>4 拆两包
GRID_CALENDAR = 24 # 日历界面字格 24px,≤4 单位,每字一包
def char_units(text: str) -> float:
"""中文=1 单位,其他=0.5(timedjs2.js charCount 语义)。"""
return sum(1.0 if ord(ch) > 0x2E7F else 0.5 for ch in text)
def render_text_bitmap(text: str, grid: int) -> bytes:
"""单行文本 → 1bpp MSB-first 行主序位图。宽度=units*grid,高度=grid。"""
if grid not in (GRID_CLOCK, GRID_CALENDAR):
raise ValueError("grid 必须是 16 或 24")
units = char_units(text)
if units != int(units):
raise ValueError("中英混排产生 0.5 单位,JS 端会生成非法帧,CLI 拒绝")
w, h = int(units) * grid, grid
if w == 0:
raise ValueError("空文本")
img = Image.new("L", (w, h), 0)
d = ImageDraw.Draw(img)
font = _font(int(grid * 0.875)) # 16px格→14px 字
d.text((w // 2, h // 2), text, font=font, fill=255, anchor="mm")
px = img.load()
out = bytearray()
for y in range(h):
for xb in range(w // 8):
byte = 0
for bit in range(8):
if px[xb * 8 + bit, y] > 128:
byte |= 1 << (7 - bit)
out.append(byte)
return bytes(out)
def prefix_frames(text: str, mode: str) -> list[bytes]:
"""前缀文字 → 帧列表。
clock: ≤4 单位单包 0{n}+位图;>4 两包 04+前128B / 0{n}+余量。
calendar: 每字一包 990{j}+72B。"""
units = char_units(text)
if units != int(units):
raise ValueError("中英混排产生 0.5 单位,不支持")
n = int(units)
if mode == "clock":
if n > 8:
raise ValueError("时钟界面最多 8 单位(纯中文 4 字)")
bmp = render_text_bitmap(text, GRID_CLOCK)
if n <= 4:
return [bytes([n]) + bmp]
return [bytes([0x04]) + bmp[:128], bytes([n]) + bmp[128:]]
if mode == "calendar":
if n > 4:
raise ValueError("日历界面最多 4 字")
bmp = render_text_bitmap(text, GRID_CALENDAR)
return [bytes([0x99, j]) + bmp[j * 72:(j + 1) * 72] for j in range(n)]
raise ValueError("mode 必须是 clock 或 calendar")