two years downstream

A Microsoft Teams appliance, a signed-firehose wall, an amplifier driver that has worked for two years and was never sent to anyone, and the four times I was confidently wrong.

The Lenovo ThinkSmart View is an 8-inch touchscreen on a stand that exists to run one application. Snapdragon 625-class APQ8053, eight A53 cores, 2 GB of RAM, 8 GB of eMMC — welded to a Teams kiosk. Offices bought them by the pallet, offices moved on, and they now turn up used for about the price of a takeaway.

It is a perfectly good little computer wearing a straitjacket.

the way in, and the wall behind it

Qualcomm parts have an Emergency Download mode — EDL — that exists so a bricked phone can be revived over USB. It is also the way in. A deep-flash cable shorts the right pins and the device shows up as 05c6:9008.

Then you hit the actual gate, which is not the one everyone expects.

EDL by itself gives you a handshake and nothing else. To read or write a single byte you have to upload a firehose programmer, and on a secure-boot device it must be signed by the same OEM key. A programmer scavenged off another MSM8953 board is rejected, not mis-flashed. This is the wall people give up at, and it gets misdiagnosed as a dead device constantly.

qdl chipinfo prints the two numbers that name the right one:

HW ID        000660e160008000
OEM PK hash  b9eca30dfea46934...

bkerler’s edl project ships a collection of programmers whose filenames are literally <hwid>_<pkhash>_fhprg_peek.bin. The match was exact. An entire category of problem — “go source the stock firmware” — evaporated into a file that was already sitting on disk.

Before writing anything: 52 partitions, 2.8 GB, read back over firehose and MD5-verified onto two machines. That is not diligence. It is the only thing standing between you and a dead appliance.

the trap that eats these devices

The obvious move is to unlock the bootloader. fastboot oem unlock-go works.

And then nothing boots, ever again.

This device is AVB1 secure boot. Unlocked, it refuses to boot anything, and you are left in EDL or in a 900e state that some people never come back from. The correct move is the counter-intuitive one: leave the bootloader locked. Flash an AVB1-signed lk2nd second-stage bootloader to boot and let it chainload whatever you actually want.

One detail worth stealing. Check the signature is really there, and don’t check it by asking whether anything follows the payload — pmbootstrap’s own lk2nd carries 16 bytes of padding that reads as a signature and then fails to boot. Ours was 357,687 bytes: 350,460 of payload and about 7.2 KB of real signature.

postmarketOS with Phosh went on top. It booted, and it was quick.

the silence

It had no sound. Not broken sound — no sound card at all. ALSA listed nothing.

The single speaker is driven by a TI TAS5782M. Mainline’s tas5805m driver only matches ti,tas5805m, so the amplifier never bound, the ASoC card never finished probing, and the entire audio subsystem quietly declined to exist. Nothing errored. There was simply no card.

Searching turned up exactly one implementation of TAS5782M support anywhere on the internet: a branch called lenovo-cd-18781y in Felix Kaechele’s fork, last touched in July 2024, never submitted to alsa-devel. Two people had now independently written this driver downstream and neither version had ever reached upstream.

Forward-porting it to kernel 7.1.3 took exactly one change. snd_soc_kcontrol_component() is gone, so the volume callbacks use snd_kcontrol_chip() instead. Everything else applied clean.

1-0049 driver:  tas5805m
sound cards:    0 [cd18781y] - cd-18781y
capture:        MultiMedia2          <- the microphones

Then a 440 Hz sine wave, and a speaker that had spent its entire life saying you have been added to a meeting made a completely different noise.

giving it back

Two things went out.

A device-tree bug: the sound card node carries aux-devices = <&speaker_amp>, but the property the driver reads is aux-devs. The obvious fix is to correct the spelling, and the obvious fix is wrong. speaker_amp is already the codec of the MI2S link, so it is bound to the card that way, and soc_probe_component() returns early for a component already bound to the same card. Renaming the property would add a duplicate list entry and call it a bug fix. The patch deletes the line. Because nothing reads it, the change is provably a no-op at runtime, which makes it an easy review.

The driver did not go out under my name, and that matters. It is Kaechele’s work. Posting someone else’s code upstream with your name on it is not a contribution, it is a land grab. The patch went into a postmarketOS package with attribution, and an email went to the author offering the forward-port and asking whether they would rather submit it themselves.

four times I was wrong

The interesting part of a project like this is never the parts that worked.

“Software video decode would eat most of the CPU.” Said with confidence, and used to rule out an entire design direction. Measured: 1.85× realtime at the panel’s native resolution, about a third of the CPU, +5 °C after four minutes with no throttling at all. I had removed the only viable path to photo-realism on the strength of a guess.

“25 fps is exactly the refract benchmark number, so the tank glass is the bottleneck.” Beautiful theory. Quartering the pixel count moved it to 32 fps, not the ~100 that fill-rate limiting predicts. Coincidence.

“Then it’s CPU-bound on per-fish draw calls.” Five hundred fish cost exactly what one hundred cost.

“Then it’s the browser’s compositing path.” Our own native scene, outside any browser, managed 11 fps on the same panel. Worse than the browser.

The last one carries the real lesson. A synthetic benchmark reported 373 / 381 / 372 fps for geometry, bumpmapping and shading, and 25 for refraction. I read that as everything except refraction is fast. But its shading test is simple per-pixel lighting, while the shader I had written evaluated seven fish and five kelp blades per pixel, plus an atan and several pows. The benchmark had nothing to say about that, and I let it speak anyway.

making it earn its keep

It is a digital aquarium now. Google’s WebGL Aquarium, fullscreen at native resolution, served off local disk with no internet. Three-clause BSD, so it forks freely — GitHub reports NOASSERTION only because the licence file is prose.

Getting there was a comedy of platform papercuts, every one of which presents identically as a grey rectangle. The demo needs two directories from the repo root that nobody would think to clone. busybox httpd is missing from base busybox and silently fails to bind in the extras package. file:// can never work, because WebGL asset loading is CORS-blocked. And Firefox’s --kiosk flag is simply ignored under this compositor, so you drive F11 with wtype instead.

Chromium, tried as a possible speed-up, cannot render WebGL here at all. It wants Vulkan, and Vulkan on Adreno starts at a6xx. This is an a5xx. The GPU process dies on startup.

It runs at 26 frames per second. For fish, that is fine.

what it cost

A used appliance, a cable, and a weekend. What came out is a small, silent, fanless Linux computer with a touchscreen and working speakers, that plays an aquarium and could just as easily be a dashboard, a photo frame, or a doorbell display.

And a driver that has existed for two years, worked the whole time, and had never been offered to anyone is now on its way upstream.

That part was worth more than the hardware.