Skip to content

Commit 0438a50

Browse files
committed
fix(server): "http-pull" panels are locked to PNG, whatever the photo knobs say
An ESPHome `online_image` picks its decoder at COMPILE time (`format: png`) and sniffs the magic bytes, so a JPEG/WebP frame isn't merely lower quality to it -- it's undecodable ("Incorrect PNG signature") and the panel silently holds its last frame. That bit the M5Paper for real: its `photo_format` sat on "Auto", which inherits the global JPEG default (deliberately JPEG for the ARMv6 photo Pi, which SIGILLs on WebP), so every photo-frame push was undecodable and the panel looked frozen. A retained per-device override fixes it today but is one HA select away from silently breaking again, so gate it on the thing that actually implies the constraint: `imageDelivery: "http-pull"`. The guard sits in pushController -- the chokepoint every render path funnels through -- and is scoped to the delivery mechanism, so the mqtt-image Pi fleet still gets its ~10x smaller JPEG. Tests cover both directions; the http-pull one fails without the guard.
1 parent 71e40b3 commit 0438a50

2 files changed

Lines changed: 74 additions & 7 deletions

File tree

packages/server/src/pushController.test.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,28 @@ const PNG = Buffer.from([
1111
* A push controller wired to the REAL device config store (the thing under
1212
* test) and thin fakes for everything else, recording what reached MQTT.
1313
*/
14-
const makeController = () => {
14+
const makeController = ({
15+
activeView = "Clock",
16+
imageDelivery,
17+
photoEncoding = { format: "png" },
18+
}: {
19+
activeView?: string
20+
imageDelivery?: "mqtt-image" | "http-pull"
21+
photoEncoding?: { format: string; quality?: number }
22+
} = {}) => {
1523
const deviceConfigStore = createDeviceConfigStore()
1624
const publishedTopics: string[] = []
25+
const renderedEncodings: unknown[] = []
26+
27+
const device = {
28+
...IMPRESSION_DEVICE,
29+
...(imageDelivery ? { imageDelivery } : {}),
30+
}
1731

1832
const pushController = createPushController({
19-
devices: [IMPRESSION_DEVICE] as never,
33+
devices: [device] as never,
2034
deviceStore: {
21-
getActiveView: () => "Clock",
35+
getActiveView: () => activeView,
2236
setActiveView: () => {},
2337
} as never,
2438
deviceConfigStore,
@@ -29,16 +43,22 @@ const makeController = () => {
2943
getAgenda: () => undefined,
3044
} as never,
3145
renderService: {
32-
renderDevice: async () => PNG,
46+
renderDevice: async ({
47+
fullColourEncoding,
48+
}: {
49+
fullColourEncoding: unknown
50+
}) => {
51+
renderedEncodings.push(fullColourEncoding)
52+
return PNG
53+
},
3354
} as never,
3455
publisher: {
3556
publish: async ({ topic }: { topic: string }) => {
3657
publishedTopics.push(topic)
3758
},
3859
} as never,
3960
baseTopic: "castkit",
40-
resolvePhotoEncoding: () =>
41-
({ format: "png" }) as never,
61+
resolvePhotoEncoding: () => photoEncoding as never,
4262
resolveClockConfig: () =>
4363
({
4464
timeZone: "America/Chicago",
@@ -55,9 +75,47 @@ const makeController = () => {
5575
pushController,
5676
deviceConfigStore,
5777
publishedTopics,
78+
renderedEncodings,
5879
}
5980
}
6081

82+
describe("pushDevice — 'http-pull' panels are locked to PNG", () => {
83+
// An ESPHome `online_image` picks its decoder at COMPILE time, so a JPEG or
84+
// WebP frame is not "lower quality" to it — it is undecodable ("Incorrect PNG
85+
// signature") and the panel silently keeps its last frame. This bit the
86+
// M5Paper for real: photo_format sat on "Auto", inherited the global JPEG
87+
// default meant for the ARMv6 Pi, and the panel went blank.
88+
test("a photo view still renders PNG despite a lossy photo encoding", async () => {
89+
const { pushController, renderedEncodings } =
90+
makeController({
91+
activeView: "Photo Frame",
92+
imageDelivery: "http-pull",
93+
photoEncoding: { format: "jpeg", quality: 80 },
94+
})
95+
96+
await pushController.pushDevice(IMPRESSION_DEVICE.id)
97+
98+
expect(renderedEncodings).toEqual([{ format: "png" }])
99+
})
100+
101+
test("an mqtt-image panel on the same view still gets the lossy encoding", async () => {
102+
// The guard must be scoped to the delivery mechanism — the Pi fleet decodes
103+
// with PIL and genuinely wants the ~10x smaller JPEG.
104+
const { pushController, renderedEncodings } =
105+
makeController({
106+
activeView: "Photo Frame",
107+
imageDelivery: "mqtt-image",
108+
photoEncoding: { format: "jpeg", quality: 80 },
109+
})
110+
111+
await pushController.pushDevice(IMPRESSION_DEVICE.id)
112+
113+
expect(renderedEncodings).toEqual([
114+
{ format: "jpeg", quality: 80 },
115+
])
116+
})
117+
})
118+
61119
describe("pushDevice — the Updates pause switch", () => {
62120
test("publishes normally when updates were never configured", async () => {
63121
const { pushController, publishedTopics } =

packages/server/src/pushController.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,19 @@ export const createPushController = ({
136136
// Text views honour the mat's safe-area crop; photos bleed to the edge.
137137
const activeView = deviceStore.getActiveView(deviceId)
138138
const isBleedView = getIsBleedView(activeView)
139+
// A "http-pull" panel is an ESPHome `online_image`, whose decoder is chosen
140+
// at COMPILE time (`format: png`) — it sniffs the magic bytes and rejects
141+
// anything else with "Incorrect PNG signature". So the photo-format knobs
142+
// must not apply to it: leaving it on "Auto" made it inherit the global
143+
// JPEG default (which exists for the ARMv6 photo Pi) and the panel went
144+
// blank until someone noticed. Force PNG here rather than relying on a
145+
// retained per-device override a future session could set back to "Auto".
146+
const isFormatLockedToPng =
147+
device.imageDelivery === "http-pull"
139148
// Only the bleed photo view may ship a lossy full-colour frame; every
140149
// other view stays lossless PNG (exact text + palette colours).
141150
const fullColourEncoding: FullColourEncoding =
142-
isBleedView
151+
isBleedView && !isFormatLockedToPng
143152
? resolvePhotoEncoding(deviceId)
144153
: { format: "png" }
145154
const safeAreaInset = isBleedView

0 commit comments

Comments
 (0)