feat(ppclock): 倒计时前缀文字位图(clock 拆包/calendar 990 序列)+ CLI --prefix/--show-calendar,102 测试全绿
This commit is contained in:
1 parent
2b01e5f589
commit
f291c61676
7 files changed
+367
-3
No files matched your search
@@ -0,0 +1,96 @@
|
||||
package z7;
|
||||
public final class g0 {
|
||||
public final java.util.List a;
|
||||
public final z7.n0 b;
|
||||
|
||||
public g0(java.util.List p1, z7.n0 p2)
|
||||
{
|
||||
this.a = p1;
|
||||
this.b = p2;
|
||||
return;
|
||||
}
|
||||
|
||||
public final java.util.ArrayList a()
|
||||
{
|
||||
java.util.ArrayList v0_1 = new java.util.ArrayList();
|
||||
java.util.Iterator v1_1 = this.a.iterator();
|
||||
while (v1_1.hasNext()) {
|
||||
Object v2_0 = v1_1.next();
|
||||
if (((z7.l0) v2_0).c == z7.m0.e) {
|
||||
v0_1.add(v2_0);
|
||||
}
|
||||
}
|
||||
return v0_1;
|
||||
}
|
||||
|
||||
public final java.util.ArrayList b()
|
||||
{
|
||||
java.util.ArrayList v0_1 = new java.util.ArrayList();
|
||||
java.util.Iterator v1_1 = this.a.iterator();
|
||||
while (v1_1.hasNext()) {
|
||||
Object v2_0 = v1_1.next();
|
||||
if (((z7.l0) v2_0).c == z7.m0.c) {
|
||||
v0_1.add(v2_0);
|
||||
}
|
||||
}
|
||||
return v0_1;
|
||||
}
|
||||
|
||||
public final java.util.ArrayList c()
|
||||
{
|
||||
java.util.ArrayList v0_1 = new java.util.ArrayList();
|
||||
java.util.Iterator v1_1 = this.a.iterator();
|
||||
while (v1_1.hasNext()) {
|
||||
Object v2_0 = v1_1.next();
|
||||
if (((z7.l0) v2_0).c == z7.m0.d) {
|
||||
v0_1.add(v2_0);
|
||||
}
|
||||
}
|
||||
return v0_1;
|
||||
}
|
||||
|
||||
public final boolean d()
|
||||
{
|
||||
if (this.b().size() != 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public final boolean equals(Object p5)
|
||||
{
|
||||
if (this != p5) {
|
||||
if ((p5 instanceof z7.g0)) {
|
||||
if (kotlin.jvm.internal.s.b(this.a, ((z7.g0) p5).a)) {
|
||||
if (this.b == ((z7.g0) p5).b) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public final int hashCode()
|
||||
{
|
||||
return (this.b.hashCode() + (this.a.hashCode() * 31));
|
||||
}
|
||||
|
||||
public final String toString()
|
||||
{
|
||||
String v0_2 = new StringBuilder("ExecutionState(tasks=");
|
||||
v0_2.append(this.a);
|
||||
v0_2.append(", taskType=");
|
||||
v0_2.append(this.b);
|
||||
v0_2.append(")");
|
||||
return v0_2.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package z7;
|
||||
public final class h0 {
|
||||
public final byte[] a;
|
||||
public final int b;
|
||||
public final int c;
|
||||
|
||||
public h0(byte[] p4, int p5, int p6)
|
||||
{
|
||||
if ((p6 & 4) != 0) {
|
||||
p5 = 20;
|
||||
}
|
||||
kotlin.jvm.internal.s.f(p4, "rawBytes");
|
||||
int v0_2 = 0;
|
||||
int v1_1 = 0;
|
||||
while (v0_2 < p4.length) {
|
||||
v1_1 ^= (p4[v0_2] & 255);
|
||||
v0_2++;
|
||||
}
|
||||
int v6_2 = ((byte) v1_1);
|
||||
int v0_0 = p4.length;
|
||||
byte[] v4_1 = java.util.Arrays.copyOf(p4, (v0_0 + 1));
|
||||
v4_1[v0_0] = v6_2;
|
||||
this.a = v4_1;
|
||||
int v6_4 = Math.max(240, p5);
|
||||
if (v6_4 > v4_1.length) {
|
||||
v6_4 = v4_1.length;
|
||||
}
|
||||
if (p5 > v6_4) {
|
||||
p5 = v6_4;
|
||||
}
|
||||
this.c = p5;
|
||||
this.b = v6_4;
|
||||
return;
|
||||
}
|
||||
|
||||
public final java.util.ArrayList a(int p5)
|
||||
{
|
||||
java.util.ArrayList v0_0 = this.b;
|
||||
byte[] v5_1 = (p5 * v0_0);
|
||||
int v1_2 = this.a;
|
||||
byte[] v5_2 = q6.n.k0(v1_2, v5_1, Math.min((v0_0 + v5_1), v1_2.length));
|
||||
java.util.ArrayList v0_1 = new java.util.ArrayList();
|
||||
int v1_0 = 0;
|
||||
while (v1_0 < v5_2.length) {
|
||||
int v2_2 = (this.c + v1_0);
|
||||
v0_1.add(q6.n.k0(v5_2, v1_0, Math.min(v2_2, v5_2.length)));
|
||||
v1_0 = v2_2;
|
||||
}
|
||||
return v0_1;
|
||||
}
|
||||
|
||||
public final String b()
|
||||
{
|
||||
String v1_0 = this.a;
|
||||
kotlin.jvm.internal.s.f(v1_0, "<this>");
|
||||
if (v1_0.length == 0) {
|
||||
throw new java.util.NoSuchElementException("Array is empty.");
|
||||
} else {
|
||||
java.util.NoSuchElementException v0_3 = (v1_0[(v1_0.length - 1)] & 255);
|
||||
c7.a.l(16);
|
||||
java.util.NoSuchElementException v0_4 = Integer.toString(v0_3, 16);
|
||||
kotlin.jvm.internal.s.e(v0_4, "toString(...)");
|
||||
java.util.NoSuchElementException v0_5 = v0_4.toUpperCase(java.util.Locale.ROOT);
|
||||
kotlin.jvm.internal.s.e(v0_5, "toUpperCase(...)");
|
||||
return p.p1.j("0x", i7.i.B0(2, v0_5));
|
||||
}
|
||||
}
|
||||
|
||||
public final int c()
|
||||
{
|
||||
int v1 = this.b;
|
||||
return (((this.a.length + v1) - 1) / v1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package z7;
|
||||
public final enum class n0 extends java.lang.Enum {
|
||||
public static final enum z7.n0 c;
|
||||
public static final enum z7.n0 d;
|
||||
public static final enum z7.n0 e;
|
||||
public static final synthetic z7.n0[] f;
|
||||
|
||||
static n0()
|
||||
{
|
||||
z7.n0[] v0_2 = new z7.n0("OTA", 0);
|
||||
z7.n0.c = v0_2;
|
||||
z7.n0 v1_2 = new z7.n0("CMD", 1);
|
||||
z7.n0.d = v1_2;
|
||||
z7.n0 v2_1 = new z7.n0("ACTIVATE", 2);
|
||||
z7.n0.e = v2_1;
|
||||
z7.n0[] v0_1 = new z7.n0[] {v0_2, v1_2, v2_1});
|
||||
z7.n0.f = v0_1;
|
||||
u3.a.a(v0_1);
|
||||
return;
|
||||
}
|
||||
|
||||
public static z7.n0 valueOf(String p1)
|
||||
{
|
||||
return ((z7.n0) Enum.valueOf(z7.n0, p1));
|
||||
}
|
||||
|
||||
public static z7.n0[] values()
|
||||
{
|
||||
return ((z7.n0[]) z7.n0.f.clone());
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1,73 @@
|
||||
"""倒计时前缀文字位图测试(证据 analysis/web/timedjs2.js:180-236)。"""
|
||||
import pytest
|
||||
|
||||
from ppclock import textbitmap as TB
|
||||
|
||||
|
||||
class TestCharUnits:
|
||||
def test_chinese_counts_one(self):
|
||||
assert TB.char_units("距离") == 2.0
|
||||
|
||||
def test_english_counts_half(self):
|
||||
assert TB.char_units("ab") == 1.0
|
||||
assert TB.char_units("ab中") == 2.0
|
||||
assert TB.char_units("a中") == 1.5
|
||||
|
||||
|
||||
class TestBitmap:
|
||||
def test_clock_bitmap_size(self):
|
||||
"""16px 格:n 个单位 → n*32 字节"""
|
||||
bmp = TB.render_text_bitmap("中考", grid=16)
|
||||
assert len(bmp) == 2 * 32
|
||||
|
||||
def test_calendar_bitmap_size(self):
|
||||
"""24px 格:n 个单位 → n*72 字节"""
|
||||
bmp = TB.render_text_bitmap("中考", grid=24)
|
||||
assert len(bmp) == 2 * 72
|
||||
|
||||
def test_bitmap_not_blank(self):
|
||||
bmp = TB.render_text_bitmap("试", grid=16)
|
||||
assert any(bmp)
|
||||
|
||||
def test_grid_must_be_16_or_24(self):
|
||||
with pytest.raises(ValueError):
|
||||
TB.render_text_bitmap("x", grid=12)
|
||||
|
||||
|
||||
class TestFrames:
|
||||
def test_clock_single_pack(self):
|
||||
"""≤4 单位:单包 0{n}+位图"""
|
||||
frames = TB.prefix_frames("目标", "clock")
|
||||
assert len(frames) == 1
|
||||
assert frames[0][0:1] == bytes([0x02]) # '0'+2
|
||||
assert len(frames[0]) == 1 + 2 * 32
|
||||
|
||||
def test_clock_two_packs(self):
|
||||
""">4 单位:首包 04+128B,次包 0{n}+余量"""
|
||||
frames = TB.prefix_frames("距离中考还有", "clock") # 6 单位
|
||||
assert len(frames) == 2
|
||||
assert frames[0][0] == 0x04
|
||||
assert len(frames[0]) == 1 + 128
|
||||
assert frames[1][0] == 0x06
|
||||
assert len(frames[1]) == 1 + 2 * 32
|
||||
|
||||
def test_clock_over_8_units_rejected(self):
|
||||
with pytest.raises(ValueError):
|
||||
TB.prefix_frames("一二三四五六七八九", "clock")
|
||||
|
||||
def test_calendar_per_char_pack(self):
|
||||
"""日历模式:每字一包 990{j}+72B"""
|
||||
frames = TB.prefix_frames("中考", "calendar")
|
||||
assert len(frames) == 2
|
||||
assert frames[0][:2] == bytes([0x99, 0x00])
|
||||
assert len(frames[0]) == 2 + 72
|
||||
assert frames[1][:2] == bytes([0x99, 0x01])
|
||||
|
||||
def test_calendar_over_4_units_rejected(self):
|
||||
with pytest.raises(ValueError):
|
||||
TB.prefix_frames("一二三四五", "calendar")
|
||||
|
||||
def test_fractional_units_rejected(self):
|
||||
"""0.5 单位(英文混排)JS 会产生非法 hex——CLI 拒绝"""
|
||||
with pytest.raises(ValueError):
|
||||
TB.prefix_frames("a中", "clock")
|
||||
Reference in new issue
Block a user