radiocli Home Blog GitHub

All posts

Return Prevous Mode

Every reverse-engineering project grows a junk drawer. Mine is oddities.md, a running log with one rule for entry: it cost somebody time, or it will. I’ve already written up this scanner’s colors in “Ask the Screen”, its font in “Not ASCII”, its layout detection in “Ask Again” and its menus in “Off the Map”, and the whole time that file was filling up underneath with the stuff that fit nowhere: the mislabels, the typos, the lies, the walls. It’s the best file in the repo. It boils down to this: everything weird about this radio is inherited, and everything dangerous about it is polite.

A radio wearing another radio’s name

Open the SDS150’s memory card and there is one directory on it: BCDx36HP, the older scanner line this one descends from. Every configuration file inside opens by declaring a TargetModel of BCDx36HP. The identity file does name the radio as an SDS150, a few fields in, but the format announces itself as a different machine throughout, so anything searching that card for “SDS” finds nothing. The serial protocol tells the same story. The current spec marks nine of its status fields reserved and says nothing more; decoded against the legacy BCD documentation in protocol.md, one of them turns out to be BK_COLOR, the backlight color from the BCD436HP days, when the display was monochrome and the color lived in the backlight. On this radio it reads OFF and always will. The spec also documents a WiFi property with values Off, 0 through 3 and AP; on this radio the attribute isn’t Off, it’s absent, and the card holds no network configuration of any kind, which pairs nicely with the four WiFi icons sitting unused in the font, souvenirs of a sibling model. And one of the card’s configuration files is named discvery.cfg. The word is discovery.

None of that is decay. It’s compatibility, the strongest force in engineering: formats outlive the machines they were designed for, and this is a 2020s radio speaking an older machine’s file formats under an older machine’s name. Nothing here is broken. It’s all just older than the box it came in.

Copper is magenta

The color picker offers 147 named colors, and one of them, Copper, is #BD00DE. That is bright magenta. Copper is not magenta. I checked it against the raw screen, in case my own reader was garbling it:

'  Copper'
'  RGB = BD00DEh'

The scanner really does pair those, and the same palette contains Coolcopper at #D68418, a perfectly sensible copper, so the table has a reasonable copper and a wrong one. Another of the 147 is named Feldsper. The mineral is feldspar, the German is Feldspat, and neither spelling is Feldsper: it looks like a typo that shipped and stayed. Seven of the 147 are not CSS names at all (Brass, Coolcopper, Copper, Cornflower, Darkbrown, Feldsper, Richblue), the values drift from CSS by up to seven per channel, and eight CSS names are missing, including every British “grey”, so anyone typing a color from web memory gets refused.

Those seven names are the tell, because they belong to the X11 and raytracer era of named color tables, not to the web. My guess, and it’s only a guess: this palette descends from the color lists that shipped with early-90s renderers like POV-Ray, whose standard includes carried names like Brass, CoolCopper, RichBlue and Feldspar, and somewhere in thirty years of hand-me-downs feldspar lost its “a” and copper picked up somebody else’s value. A transposed table entry is the obvious explanation for the magenta; nothing confirms it. If the guess is right, this palette is older than the web, and it was carried forward with its mistakes on board.

The typo is load-bearing

The command for leaving the scanner’s menus takes an argument the specification spells RETURN_PREVOUS_MODE, missing “I” and all. The scanner accepts the misspelling and does not accept the correction. So my tool misspells it too, on purpose, with a comment, because fixing the spelling breaks the command.

If that offends you, HTTP would like a word. The web has shipped its Referer header minus an R since the spec drafts of the mid-90s; the misspelling standardized before anyone caught it, and every browser and server since has been contractually obligated to spell referrer wrong. Thirty years of the entire internet agreeing to honor a typo. Uniden is in good company, and so is the comment in my code apologizing to whoever reads it next.

OK is not evidence

Press a key this model doesn’t have, KEY,R for Range, say, which exists on other Unidens but not here, and the scanner answers KEY,OK and does nothing. Wicked polite, completely useless: a successful reply is not evidence that anything happened.

Ask GLT, the list-fetching command, for a keyword it doesn’t recognize, and it doesn’t refuse either. It answers with the favorites list document, for any unknown keyword, any index, no error, and that answer is indistinguishable from a real request failing strangely. It cost this project two firmware bugs that never existed. I invented a keyword, CHN, for “a department’s channels”, read the favorites-list reply as broken firmware, and built the workaround: a menu walk taking seconds per department, the scan stopped while it ran, and no way to report a talkgroup channel at all. The real keywords are CFREQ for conventional frequencies and TGID for talkgroups, a department answers on exactly one of them depending on its system, and both worked on this firmware all along. The second phantom bug was the same disease, SITE_FRQ for the real SFREQ. protocol.md had the right keywords written down the whole time; I just didn’t look. And the firmware can be honest when it’s asked properly: a conventional department asked for talkgroups, a well-formed question with no answer, returns a correctly empty document, fully distinguishable from the wrong-keyword reply. The machinery for honesty is in there. The unknown-keyword path just doesn’t use it.

The politest failure of all: turn the knob and the scanner parks on one channel, on hold. It’s out of the menus, it answers every command correctly, and it shows a channel name, the same thing a scanning radio shows every time it stops on a live transmission. Nothing on the screen distinguishes the two states; only the mode field does, Scan Hold against Scan Mode. My scan command, whose entire job is returning the radio to scanning, knew about menus and quick search, saw nothing it recognized as wrong, and walked away from a held scanner reporting success. I found it because the radio sat on one channel for ten minutes while the tool insisted everything was fine.

My own stack joined in

I would love to tell you only the radio lies. The screen is bytes, not text, its font draws pictures above 0x7E, and a Go string holding those raw bytes is not valid UTF-8. Go’s encoding/json replaces every invalid byte with U+FFFD and returns no error, so the same screen came out two different ways:

$ radiocli screen | cat -v
                Aug.6 21:42 M-,M--        <- 0xAC 0xAD
$ radiocli screen -o json
"text": "              Aug.6 21:43 \ufffd\ufffd"

The two halves of the signal meter, 0xAC and 0xAD, different pictures, became the same replacement character. Not corrupted, collapsed: nothing downstream could recover them or even detect that anything was lost. Bytes below 0x80 pass through untouched, which is why the modulation marks survived while the meter vanished, and why this masqueraded for a while as the scanner only sometimes sending icons. A marshaller that returns nil has not promised it kept your data. Test anything that carries bytes through JSON with a byte above 0x7F.

My parser pulled the same trick on me. A screen row arrives with an attribute string marking its reverse video, and I decided which field was which by measuring lengths. Then a layout drew two lines that are nothing but a rule across the panel, empty text against thirty underscores of attribute, and the measurement glued the rule onto the text as if it were more text. In the other direction, an unescaped comma in a favorites list called GREENDALE, ST 00000 split a field so that the back half of the name was read as a line of attributes. The parse was wrong in the middle and surfaced at the edges: screen kept drawing something close enough to look right, display and status refused to run, and the test harness reported the scanner as not answering. A parse that is wrong in the middle shows up as an unrelated command refusing to run. The fix was to stop measuring and start reading: attributes are built from space, star and underscore and nothing else, so anything holding another character is text. That day went to a comma.

Dead ends, measured to the byte

The firmware image would have answered half this blog series on its own, the font, the layout tables, the strings, and it answers nothing, because SDS150_V1_00_43.bin is exactly 2 MiB of ciphertext. Measured: entropy of 8.000 bits per byte in every one of its 32 blocks of 64 KiB; chi-square against uniform of 248.2, where 256 is the expectation for true randomness; zero repeated aligned blocks at 8, 16 and 32 bytes, which rules out ECB and with it the classic block-repeat way in; a longest single-byte run of 3, where real firmware is full of long runs of 00 and FF; no magic, no header, no readable index. A stream cipher or a chained mode, key in the bootloader, nothing liftable. OpenScanner, the one group modifying Uniden images, publishes built binaries only, documents no algorithm and no key, and does not list the SDS150. The font came out of an unpublished document instead, and the wall stands for everything else. But now it’s measured, so nobody has to re-derive the disappointment.

The other wall has a doorbell. Boot the radio with a cable attached and it offers a choice for about fifteen seconds:

Cable Detected
Select USB mode

Mass Storage="E"
Serial Port ="."

Choose mass storage and the serial port is not shared, it is replaced. The radio enumerates as a disk, /dev/cu.* is gone, and no command can bring the port back, because there is no channel left to carry one. Ejecting the volume doesn’t help; the radio stays a disk with nothing mounted on it. Every other mode this scanner can enter is escapable over the wire. This one takes the power button.

A typo, once shipped, stops being a typo. It becomes a protocol.