A few protocol findings: checksum byte, a clear command, and a correction
Thanks for this repo. It's the only real documentation of this panel and it
got me from "the thing won't even show up on USB" to a working implementation
in an afternoon.
Building on it I ended up verifying a few things that aren't in protocol.txt,
plus one correction to a note in store_image_commands.txt. Evidence for each
is below. Hardware is a MI-LNL62-999W driven from macOS with bleak.
Commands carry a checksum
The byte before the 0x55 terminator is the sum of the payload bytes after
the 0xBC, mod 256. It works out on everything in protocol.txt:
bc ff 01 00 55 power on 0xff + 0x01 = 0x100 -> 00
bc ff 00 ff 55 power off 0xff + 0x00 -> ff
bc 00 01 01 55 graffiti a 0x00 + 0x01 -> 01
bc 00 0d 0d 55 graffiti b 0x00 + 0x0d -> 0d
bc 00 12 12 55 slideshow 0x00 + 0x12 -> 12
bc 0f f1 08 08 55 frame begin 0x0f + 0xf1 + 0x08 -> 08
bc 0f f2 08 09 55 frame commit 0x0f + 0xf2 + 0x08 -> 09
It holds for the pixel blocks too, which means blocks are 101 bytes rather
than 100:
bc 0f <index> <96 bytes RGB> <checksum> 0x55
Checked against segment 1 in store_image_commands.txt: 32 copies of
d8 ff e9 sum to 22528, which is 0 mod 256, leaving 0x0f + 0x01 = 0x10.
That's exactly the byte in the capture.
draw_picture.py sends 100-byte blocks with no checksum. That works — mine
ran for hours that way before I noticed — so the panel clearly tolerates it.
But the app sends the checksum, so it seems worth having in the docs.
bc 00 15 15 55 clears stored images
Sending this wipes the stored image list and the panel goes back to its
built-in animation. Store something afterwards and you get a list containing
only the new frames.
It only appears in snoops/commited_images.txt twice, at the very start and
end of the session, so it reads like an open/close marker — that's why I
nearly skipped it. But it does clear storage.
This matters because stores append, so without it there's no way back to a
clean slate short of the phone app.
f1/f2 aren't slot indices
store_image_commands.txt has this note:
f1 might be which slot to store the image in, f1, f2, .. The second last
byte seem to change with the number
I don't think they are. There are five store operations in
snoops/saves_to_device.txt and two in snoops/commited_images.txt, and
every single one is byte-identical:
bc 00 11 f1 03 55 ...store... bc 00 11 f2 04 55
Nothing increments across them. They look like fixed begin/end markers, and
each store appends to a list that bc 00 12 12 55 cycles.
Worth noting these two are also the only commands that break the checksum
rule above — both are exactly one higher than the sum predicts, consistently,
in every capture. I have no explanation, so I just send the captured bytes
rather than computing them.
ffd2 never sends anything
I subscribed to the notify characteristic and sent every documented command
plus deliberate garbage (bc aa 00 aa 55), waiting a few seconds after each.
Zero notifications, including nothing on connect.
Combined with ffd1 being write-without-response and nothing readable, there
is no feedback path at all. A write into a panel that some other app has taken
over just succeeds silently, which cost me a while to work out.
Slideshow holds each frame about 1.8s
Measured off video of a two-frame stored loop: transitions at a steady 1.8s.
I couldn't find anything in the captures that looks like a rate control.
How many images it stores (not sure)
Six distinct stored frames cycle cleanly. I checked by autocorrelating video
of the panel — it peaks at 10.8s, which is 6 x 1.8s, with clean harmonics.
Twelve doesn't give a clean twelve-frame cycle. Clustering the stable video
frames found six distinct images in an irregular order. Storing lots of
repeats of the same image doesn't lengthen the loop either.
Could be a slot limit, deduplication, or writes failing when driven too fast —
I couldn't tell which. Flagging it as something I saw rather than a documented
limit.
One measurement note in case it saves someone time: the interval between
slideshow switches is 1.8s whether it's cycling 4 images or 64, so counting
switches tells you nothing. Only the loop period does. I got this wrong twice
before working that out.
The brightness response is very non-linear
Relevant if you're dimming pixels for anti-aliasing.
I photographed a brightness ramp with a black band in frame, converted sRGB to
linear and subtracted the black. The floor turns out to matter a lot — bloom
plus room light on the diffuser was enough to make my first reading useless (a
12% pixel measured as 49%).
driven emitted
1.00 1.00
0.55 0.66
0.25 0.52
0.12 0.41
Pre-compensating with an exponent around 2.0 got the top two levels within
0.03 of target when I re-measured. Below roughly 0.15 it stops working, since
the corrected value lands under 8/255 and quantisation takes over.
A few protocol findings: checksum byte, a clear command, and a correction
Thanks for this repo. It's the only real documentation of this panel and it
got me from "the thing won't even show up on USB" to a working implementation
in an afternoon.
Building on it I ended up verifying a few things that aren't in
protocol.txt,plus one correction to a note in
store_image_commands.txt. Evidence for eachis below. Hardware is a MI-LNL62-999W driven from macOS with bleak.
Commands carry a checksum
The byte before the
0x55terminator is the sum of the payload bytes afterthe
0xBC, mod 256. It works out on everything inprotocol.txt:It holds for the pixel blocks too, which means blocks are 101 bytes rather
than 100:
Checked against segment 1 in
store_image_commands.txt: 32 copies ofd8 ff e9sum to 22528, which is 0 mod 256, leaving0x0f + 0x01=0x10.That's exactly the byte in the capture.
draw_picture.pysends 100-byte blocks with no checksum. That works — mineran for hours that way before I noticed — so the panel clearly tolerates it.
But the app sends the checksum, so it seems worth having in the docs.
bc 00 15 15 55clears stored imagesSending this wipes the stored image list and the panel goes back to its
built-in animation. Store something afterwards and you get a list containing
only the new frames.
It only appears in
snoops/commited_images.txttwice, at the very start andend of the session, so it reads like an open/close marker — that's why I
nearly skipped it. But it does clear storage.
This matters because stores append, so without it there's no way back to a
clean slate short of the phone app.
f1/f2aren't slot indicesstore_image_commands.txthas this note:I don't think they are. There are five store operations in
snoops/saves_to_device.txtand two insnoops/commited_images.txt, andevery single one is byte-identical:
Nothing increments across them. They look like fixed begin/end markers, and
each store appends to a list that
bc 00 12 12 55cycles.Worth noting these two are also the only commands that break the checksum
rule above — both are exactly one higher than the sum predicts, consistently,
in every capture. I have no explanation, so I just send the captured bytes
rather than computing them.
ffd2never sends anythingI subscribed to the notify characteristic and sent every documented command
plus deliberate garbage (
bc aa 00 aa 55), waiting a few seconds after each.Zero notifications, including nothing on connect.
Combined with
ffd1being write-without-response and nothing readable, thereis no feedback path at all. A write into a panel that some other app has taken
over just succeeds silently, which cost me a while to work out.
Slideshow holds each frame about 1.8s
Measured off video of a two-frame stored loop: transitions at a steady 1.8s.
I couldn't find anything in the captures that looks like a rate control.
How many images it stores (not sure)
Six distinct stored frames cycle cleanly. I checked by autocorrelating video
of the panel — it peaks at 10.8s, which is 6 x 1.8s, with clean harmonics.
Twelve doesn't give a clean twelve-frame cycle. Clustering the stable video
frames found six distinct images in an irregular order. Storing lots of
repeats of the same image doesn't lengthen the loop either.
Could be a slot limit, deduplication, or writes failing when driven too fast —
I couldn't tell which. Flagging it as something I saw rather than a documented
limit.
One measurement note in case it saves someone time: the interval between
slideshow switches is 1.8s whether it's cycling 4 images or 64, so counting
switches tells you nothing. Only the loop period does. I got this wrong twice
before working that out.
The brightness response is very non-linear
Relevant if you're dimming pixels for anti-aliasing.
I photographed a brightness ramp with a black band in frame, converted sRGB to
linear and subtracted the black. The floor turns out to matter a lot — bloom
plus room light on the diffuser was enough to make my first reading useless (a
12% pixel measured as 49%).
Pre-compensating with an exponent around 2.0 got the top two levels within
0.03 of target when I re-measured. Below roughly 0.15 it stops working, since
the corrected value lands under 8/255 and quantisation takes over.