diskOS installer: initial public beta

Flashes the diskOS custom UI onto the FiiO Snowsky Disc over Ingenic mask-ROM USB,
building the image from your own stock firmware. Runs from source via install.sh.
This commit is contained in:
b0hemia
2026-08-26 15:26:14 +10:00
commit e0bc4785e9
109 changed files with 12625 additions and 0 deletions
+257
View File
@@ -0,0 +1,257 @@
# Snowsky Disc V2.09 - mq_player full command map
Dispatch entry = `{tag_str_ptr, handler_thunk_ptr}` (8B); thunks tail-jump via GOT to the real handler.
Confidence: **V**=string-verified · **I**=inferred from family · **U**=unknown (runtime-registered, GOT slot 0).
⚠ = static reading conflicts with our 1.95-era LIVE tests (trust live; V2.09 meanings need live re-verify).
## SOURCE / WORK-MODE SWITCH - 0657 (corrected; supersedes "close player" reading)
`0657` = switch audio SOURCE, payload = integer mode index (jump table @0x6736b0). Frame = `0657000C000<hex>`:
- `0657000C0001` = **LOCALPLAYER** (safe recover) · `0657000C0009` = **USB-DAC (UAC)** · `0657000C000C` = **BTSINK**
- **Network receivers (AirPlay/DLNA/Roon):** `0657000C0008` PLUS companion `0642000C000X` (NETWORK_MODE selector, X in {0,1,2,5}; exact AirPlay value UNMAPPED). Prereq: WLAN up.
- ✅ **PRE-STOP PINNED + CONFIRMED (2026-08-03, live strace of stock mq_ui + fixed on device):** the pre-stop is **`0666000C0006`** (out_dev=6, local). Stock always sends it BEFORE `0666000C0002` (route to BT). Skipping it = the **g_fiio_local trap** → mq_player SIGSEGV → SD freed → MCU reboot. Mechanism: direct→2 can leave shadow-out=2 with DAC-flag=1 (split state); 6→2 normalizes DAC-flag=0. **This was THE cause of every BT-route reboot.** See "BT AUDIO OUTPUT" section below.
- ⚠ AirPlay discovery CANNOT be tested on an iPhone Personal Hotspot (client/mDNS isolation) - needs a normal router. PARKED pending router + 0666 pre-stop.
NB: this 0657 is the V2.09 SOURCE switch; the 1.95 "0657=play-mode" was a different binary/table. Our earlier play-mode toggle using 0657 on V2.09 was therefore WRONG (it sent source-switch values) - FIXED 2026-06-25 (now uses 0102, below).
## PLAY-MODE - 0102 (GROUND TRUTH, captured from stock UI 2026-06-25)
`0102` = LOCAL play-mode setter. Frame = `0102000C000<v>`. Captured by strace'ing the stock
`/usr/bin/mq_ui`'s `mq_timedsend` while tapping its play-mode control (the loop icon, NP
transport page) - the stock UI cycles these 5 values 0→1→2→3→4→0:
| value | frame | stock icon | mode |
|---|---|---|---|
| 0 | `0102000C0000` | →→ (two arrows) | Sequential (play in order) |
| 1 | `0102000C0001` | ⇄ (crossed) | Shuffle (random) |
| 2 | `0102000C0002` | ↻ with "1" | Repeat One (single loop) |
| 3 | `0102000C0003` | ↻ (loop) | Repeat All (list loop) |
| 4 | `0102000C0004` | "1" + arrow | Single (play one track, stop) |
NB: this SUPERSEDES the old "0102 = Roon-only no-op" reading - stock uses 0102 for LOCAL
play-mode live. diskOS sends this via ui_set_workmode (main.c). The cycle order above is
the stock order; diskOS's prior 4-mode icons (seq/shuffle/repeat-one/repeat-all) already
match values 0-3.
## LIVE-tested (ground truth)
| tag | meaning |
|---|---|
| 0100 | Play by list (list_type + start index) ✅ |
| 0102 | **Play-mode** 0102000C000<0..4> (seq/shuffle/rep-one/rep-all/single) ✅ stock-captured 2026-06-25 |
| 0103 | Seek (ms) ✅ verified live 2026-06-25 (pos jumped to target) |
| 0104 | Favorite toggle (current song) |
| 0201 | Transport play/pause toggle (generic, VALUE1-driven) - verified NOT Roon-specific |
| 0622 | Rescan SD / rebuild DB |
| 0657 | **SOURCE switch** (NOT play-mode) - see section above |
| 0666 | Output route (→set_out_device 0x461e74): 2=BTSRC 4=SPDIF 6=local-DAC. V2.09-confirmed; the "close_player" sighting was a shared teardown preamble, not this cmd's meaning. |
| 0715 | Volume absolute 0-120 ✅ verified 2026-06-25 (set 20 via 0715000C0014, persisted+displayed) |
## BT AUDIO OUTPUT (transmit to a BT speaker, a2dp-source) - ✅ WORKING (2026-08-03)
Captured from a live strace of stock mq_ui doing a working transmit, then replicated + fixed in diskOS `ui_route_bt()`. **Plays stereo, no stutter, no reboot.** The device IS designed to transmit (not only the "Bluetooth Receiving Mode"/a2dp-sink); stock transmit works but STUTTERS because its default-quality stereo SBC exceeds the X2000 CPU.
Working route-to-BT sequence (diskOS, MAC = the connected speaker, uppercased):
```
0666000C0006 PRE-STOP: switch output to LOCAL first (MANDATORY - skip = g_fiio_local crash → MCU reboot)
0642000C0000 reset network/output mode
0657000C0008 work-mode 8
06c1000C0000 start player BT-init thread (06b3 no-ops until this completes; async on cold start)
0666000C0002 route: out_dev = BT source
0657000C0008 work-mode 8 (again, as stock does)
06b3001D0000<MAC> codec select: 0=SBC (our bluealsa is sbc-only) + MAC payload (stock frame-shape; worker ignores it)
0715000C<vol> volume
```
Then play normally (`0100…`). Reverse (BT→local) = `ui_route_analog()`: `0666000C0006` then `0657000C0008`.
**No-stutter requires bluealsa `--sbc-quality=medium`** (bit-pool ~33): stock default-quality stutters; medium plays clean stereo (~86% CPU idle on device). Set in `bt.c` bt_enable/bt_ensure_services. `--a2dp-force-mono` also works but is NOT needed (stereo is fine at medium quality).
## Table A @0x7c9d30 - 131 entries (terminator 0x7ca148)
| tag | thunk | meaning | conf |
|---|---|---|---|
| 0802 | 0x411790 | get total song count (SELECT count(*) FROM SONG) | V |
| 0620 | 0x4117b0 | get WIFI MAC (/usr/data/fiio/nb.txt) | V |
| 0710 | 0x4117d0 | network/wifi getter | I |
| 0711 | 0x4117f0 | network/wifi getter | I |
| 0712 | 0x411810 | network/wifi op | I |
| 0713 | 0x411830 | network/wifi op | I |
| 0714 | 0x411850 | network/wifi op | I |
| 0715 | 0x411870 | network/wifi op (NB: vs live 0715=volume - table-A 0715 differs) | I |
| 0716 | 0x411890 | network/wifi op | U |
| 0601 | 0x4118b0 | media getter | U |
| 0651 | 0x4118d0 | media getter/setter | U |
| 0602 | 0x4118f0 | media getter | U |
| 0652 | 0x411910 | media getter/setter | U |
| 0606 | 0x411930 | media DB getter | I |
| 0656 | 0x411950 | media DB getter/setter | I |
| 0603 | 0x411970 | media DB getter | I |
| 0653 | 0x411990 | media DB getter/setter | I |
| 0604 | 0x4119b0 | media DB getter | I |
| 0654 | 0x4119d0 | media DB getter/setter | I |
| 0605 | 0x4119f0 | media query -> reply ([PLAY] send) | V |
| 0655 | 0x411a10 | media DB getter/setter | I |
| 0608 | 0x411a30 | media DB getter | I |
| 0658 | 0x411a50 | media DB getter/setter | I |
| 0611 | 0x411a70 | media DB getter | I |
| 0661 | 0x411a90 | media DB getter/setter | I |
| 0612 | 0x411ab0 | media DB getter | I |
| 0662 | 0x411ad0 | media DB getter/setter | I |
| 0613 | 0x411af0 | media DB getter | I |
| 0663 | 0x411b10 | media DB getter/setter | I |
| 0609 | 0x411b30 | media DB getter | I |
| 0659 | 0x411b50 | media DB getter/setter | I |
| 0615 | 0x411b70 | media DB getter | I |
| 0665 | 0x411b90 | media DB getter/setter | I |
| 0614 | 0x411bb0 | media DB getter | I |
| 0664 | 0x411bd0 | media DB getter/setter | I |
| 0607 | 0x411bf0 | media query -> reply ([PLAY] send) | V |
| 0657 | 0x411c10 | reads "close_player"/killall avahi-publish (⚠ 1.95=play-mode) | V⚠ |
| 0801 | 0x411c30 | playback/system getter | I |
| 0702 | 0x411c50 | wifi/network getter | I |
| 0703 | 0x411c70 | wifi/network getter | I |
| 0704 | 0x411c90 | get wifi scan list | V |
| 0705 | 0x411cb0 | connect wifi (psid/passwd) / SET_POWER_DOWN_TO_MCU | V |
| 0706 | 0x411cd0 | wifi/network op | I |
| 0707 | 0x411cf0 | wifi/network op | U |
| 0708 | 0x411cf0 | wifi/network op | U |
| 0639 | 0x411d30 | media-info query -> reply a639 | V |
| 0689 | 0x411d50 | **EQ PRESET SELECT** - invokes 21-way preset engine 0x449544 (via 0x4961bc), updates rate caps, replies a639. NOT a media-info query. | V |
| 0690 | 0x411d70 | media-info query -> play-send | V |
| 0675 | 0x411d90 | get EQ/PEQ (gain/filterType/frequency/loading) | V |
| 0626 | 0x411db0 | EQ getter (family) | I |
| 0627 | 0x411dd0 | EQ getter | I |
| 0677 | 0x411df0 | EQ getter/setter | I |
| 0628 | 0x411e10 | EQ getter/setter (PEQ) | I |
| 0678 | 0x411e30 | SET PEQ band (filterType/frequency/gain/qValue) | V |
| 0629 | 0x411e50 | EQ getter/setter | I |
| 0630 | 0x411e70 | get EQ (gain/filterType/frequency/loading) | V |
| 0803 | 0x411e90 | playback/system getter | I |
| 0724 | 0x411eb0 | system/OTA op | I |
| 0720 | 0x411ed0 | OTA/network (killall wget, ip route, wlan0) | V |
| 0624 | 0x411ef0 | mount/storage path (mnt/) | V |
| 0622 | 0x411f10 | system getter (NB: live 0622=rescan; table-A differs) | I |
| 0800 | 0x411f30 | write wpa_supplicant.conf (country=%s) | V |
| 0623 | 0x411f50 | system getter | I |
| 0616 | 0x411f70 | media/system getter | I |
| 0a51 | 0x411f90 | extended command (only 0aXX tag) | I |
| 0634 | 0x411fb0 | media-info query -> play-send | V |
| 0684 | 0x411fd0 | media getter | I |
| 0725 | 0x411ff0 | system/OTA op | I |
| 0617 | 0x412010 | media/system getter | I |
| 0667 | 0x412030 | media getter/setter | I |
| 0621 | 0x412050 | DROP DB tables (SONG/MY_LOVE/PLAY_LIST) | V |
| 06a0 | 0x412070 | BT/media getter | I |
| 06a1 | 0x412090 | BT/media getter | I |
| 06a3 | 0x4120b0 | BT/media getter | I |
| 0640 | 0x4120d0 | media getter/setter | I |
| 0644 | 0x4120f0 | media getter/setter | I |
| 0643 | 0x412110 | media op | U |
| 06a2 | 0x412130 | BT/media getter | I |
| 06b1 | 0x412150 | BT sample-rate param (→0x496ac4→change_rate_set_params 0x40d4d8; VALUE1 selects 44100/48000/82000?/96000) | V |
| 06b2 | 0x412170 | BT bit-depth param (→0x496b58→change_format_set_param 0x40d66c; 16/24/32-bit) | V |
| 06b3 | 0x412190 | **BT CODEC SELECT** (→0x496bb8→worker 0x40eaf4): VALUE1 0=SBC 1=AAC 2=LDAC-mob 3=LDAC-std 4=LDAC-high. Stock's connect callback sends `06b3<len>000X<MAC>` (X=persisted BT_CODEC, MAC as ignored-by-worker payload for frame-shape). **diskOS sends `06b3001D0000<MAC>` (X=0 SBC, our bluealsa is `--codec=sbc` only) - value 3 only "works" on an SBC sink via a fragile `/usr/data/bt_codec` fallback, so pick the codec our bluealsa actually enables.** Worker requires `06c1` BT-init done first (else no-ops). | V (live) |
| 06b4 | 0x4121b0 | LDAC **quality** only (→0x496c94→0x40dbe8 via /usr/data/bt_pipe_recv): VALUE1 0=mobile 1=standard 2=high. Does NOT select SBC or set rate. | V |
| 06b6 | 0x4121d0 | BT op | I |
| 06b7 | 0x4121f0 | BT get paired addr+name | V |
| 06b8 | 0x412210 | BT send connected device list | V |
| 06c3 | 0x412230 | BT op | I |
| 06c2 | 0x412250 | BT op | I |
| 06c0 | 0x412270 | BT trust/power (trust/power off/hci down) | V |
| 06c4 | 0x412290 | BT disconnect/remove (bluetooth.db) | V |
| 06c5 | 0x4122b0 | BT op | I |
| 06c1 | 0x4122d0 | BT set device alias | V |
| 0679 | 0x4122f0 | media getter/setter | I |
| 0666 | 0x412310 | reads close_player (⚠ 1.95=output route) | V⚠ |
| 06b5 | 0x412330 | BT op | I |
| 0804 | 0x412350 | playback/system getter | I |
| 0805 | 0x412370 | playback/system op | U |
| 0701 | 0x412390 | wifi init/restart (init_wifi; killall udhcpc/wpa) | V |
| 0700 | 0x4123b0 | close network card (network.c) | V |
| 0808 | 0x4123d0 | playback/system getter | I |
| 0813 | 0x4123f0 | playback/system getter | I |
| 0816 | 0x412410 | playback/system getter | I |
| 0818 | 0x412430 | playback/system getter | I |
| 0817 | 0x412450 | playback/system getter | I |
| 0811 | 0x412470 | playback/system getter | I |
| 0809 | 0x412490 | playback/system getter | I |
| 0815 | 0x4124b0 | set MEMORY_PLAY position (UPDATE ... POSITION) | V |
| 0810 | 0x4124d0 | playback/system getter/setter | I |
| 0812 | 0x4124f0 | playback/system getter/setter | I |
| 0806 | 0x412510 | playback/system getter | I |
| 0645 | 0x412530 | media getter/setter | I |
| 0646 | 0x412550 | media getter/setter | I |
| 0807 | 0x412570 | playback/system getter | I |
| 0660 | 0x412590 | media op | U |
| 0610 | 0x4125b0 | media op | U |
| 0687 | 0x4125d0 | media getter | I |
| 0637 | 0x4125f0 | media op | U |
| 0814 | 0x412610 | playback/system getter/setter | I |
| 0642 | 0x412630 | media getter/setter | I |
| 0641 | 0x412650 | media-info query -> play-send | V |
| 0618 | 0x412670 | media op | U |
| 0668 | 0x412690 | media op | U |
| 0619 | 0x4126b0 | media op | U |
| 0669 | 0x4126d0 | media op | U |
| 0722 | 0x412710 | OTA update (wget ota_patch_user.json) | V |
| 0820 | 0x412730 | set key SINGLE-click action | V |
| 0821 | 0x412750 | set key DOUBLE-click action | V |
| 0822 | 0x412770 | set key LONG-press action | V |
| 06b9 | 0x4126f0 | BT op | U |
| 0647 | 0x412790 | set gapless | V |
| 0648 | 0x4127b0 | set artist_class_type | V |
| 0649 | 0x4127d0 | system-config op | U |
## Table B @0x7ca150 - 75 entries (terminator 0x7ca3a8); many NULL=unimplemented
| tag | handler | meaning | conf |
|---|---|---|---|
| 0425 0435 0445 | NULL | unimplemented | V |
| 0501 | 0x4130f0 | media/thumb type (png/gif/mp4) -> a501/a60a | I |
| 0105 | 0x413110 | emits a102 (status/ack) | I |
| 0202 | 0x413130 | NAS getter/setter | I |
| 0599 | 0x413150 | system/version (-> a599) | I |
| 0401 | 0x413170 | boolean setter -> a401 | I |
| 0411 0450 0451 0452 0453 | NULL | unimplemented (04xx settings) | V |
| 0402 | 0x413190 | boolean setter -> a402 | I |
| 0416 0460 0461 0462 | NULL | unimplemented | V |
| 0403 | 0x4131b0 | boolean setter -> a403 | I |
| 0410 0470 0471 0473 0474 | NULL | unimplemented | V |
| 0404 | 0x4131d0 | setter -> a404 | I |
| 0417 0480 0481 0482 | NULL | unimplemented | V |
| 0405 0418 0419 0420 0421 0422 | NULL | unimplemented (replies declared) | V |
| 0406 | 0x4131f0 | setter -> a406 | I |
| 0426 0407 | NULL | unimplemented | V |
| 0502 | 0x413210 | 05xx misc -> a502 | I |
| 0201 | 0x413230 | Transport play/pause toggle (generic, VALUE1-driven → 0x419ff4) - verified NOT Roon-specific | V |
| 0102 | 0x413250 | playback control (transport) | I |
| 0104 | 0x413270 | playback control (72-byte frame; live: favorite) | I |
| 0111 | 0x413290 (GOT0) | unimplemented stub | V |
| 0112 | 0x4132b0 | playlist: add song (playlist idx, song_path) | V |
| 0115 | 0x4132d0 | add to custom list | V |
| 0113 | 0x4132f0 | love-list delete | V |
| 0114 | 0x413310 | custom-list delete | V |
| 0116 | 0x413330 | playlist op (list mutation) | I |
| 0203 | 0x413350 | NAS op (shares worker w/0412) | I |
| 0412 | 0x413370 | -> reply a412 | I |
| 0413 | 0x413390 | album query (unknown_album) -> a413 | V |
| 0414 | 0x4133b0 | style/genre query (unknown_style) -> a414 | V |
| 0415 | 0x4133d0 | query w/ strcmp -> a415 | I |
| 0465 | 0x4133f0 | -> a465 | I |
| 0495 0496 0497 0498 | NULL | unimplemented | V |
| 0408 | 0x413410 | file/folder browse (mnt/, path build) -> a408 | V |
| 0490 0491 0409 | NULL | unimplemented | V |
| 0117 | 0x413490 | playlist reorder/move (src_list/dst_list) | V |
| 0463 0464 0483 0484 | GOT0 | unimplemented stubs | V |
| 0210 | 0x4134b0 (GOT0) | NAS stub | V |
| 0211 | 0x4134d0 (GOT0) | NAS (json_data, /tmp/nas/) stub | V |
| 0212 0213 0214 | GOT0 | NAS folder op stubs | V |
| 0103 | 0x413430 | song-info/get op | I |
| 0100 | 0x413450 | MAIN PLAY (jumptable @0x650e2c by list_type; unknown_artist) | V |
| 0101 | 0x413470 (GOT0) | unimplemented stub (a101 declared) | V |
## MCU / SPI name-commands (hw_ctrl.c @0x48d0b8, over internal SPI to MCU)
GET_FIRMWARE_VERSION · GET/SET_DEVICE_MAX_VOL · GET/SET_BALANCED_VOL · REPORT/READ_DEVICE_VOL · SET_DEVICE_VOL ·
REPORT_LCD_ACTION · GET/SET_INPUT_MODE · SET_OUTPUT_MODE · SET_GAIN · GET/SET_DAC_FILTER · SET_USB_MODE ·
GET/SET_EQ_PRE · GET/SET_EQ_PARAMETER · SET_EQ_RESET · SAVE_EQ/RESAVE_EQ · SET/REPORT_AUDIO_FORMAT ·
MCU/ARM_REPORT_STATUS · SET_FACTORY · ENTER_MCU_UPDATE_MODE/REPORT_UPDATE_STATUS ·
GET/SET_ZERO_DATA_DETECT_TIME + ZERO_DATA_STATUS · SET_MCU_POWER · SET_MUTE ·
SET_STATUS_TO_MCU/SET_POWER_DOWN_TO_MCU (shutdown, 63 call sites) · BT_REPORT_RATE/STATE/CODEC_TO_MCU
## sysconfig-key setters (system_c... @0x488000)
RGB_COLOUR · TRIGGER_IN · SYS_THEME · LANGUAGE · USB_MODE · NETWORK_MODE · EQ_TYPE · MAX_VOL · BALANCE_VOL ·
POWER_SAVE · FILTER_TYPE · PLAY_MODE · FOLDER_JUMP · PLAY_GAP · INPUT_MODE · VOL_KNOB_MODE · OTA_CFG ·
PO_PRE_VOL · PO_VOL · PRE_VOL · TREBLE · BASS · LO_DISABLE · OS_MODE · DSD_DECODE
## Reply frames
~102 `axxx` player->UI frames. Known: a639=media-info, a706=net status, a714=volume, aa1b=UAC srate, a644=now-playing JSON, a704/a705=wifi status, a6c*=BT. Most undocumented.
+298
View File
@@ -0,0 +1,298 @@
# Snowsky Disc - Hardware Capability Map
Live-probed from the device root shell (serial `/dev/ttyACM0`) on 2026-06-25, while
running **stock firmware** (V2.09 family, kernel built 2026-06-03). This documents what
the *hardware* can do, independent of what stock software chooses to use - to scope
diskOS enhancements and the "write our own mq_player/mq_ui" question.
Probing was read-only (`/proc`, `/sys`, `aplay --dump-hw-params`, `i2cdetect`-equivalent
via sysfs names, `dmesg`). Nothing was written to the device during this survey.
---
## 1. SoC & Compute
| Item | Value | Source |
|---|---|---|
| SoC | Ingenic **X2000** (xburst2), board `ingenic,x2000_halley5_module_base` | `/proc/cpuinfo` |
| Cores | **2× XBurst II V2**, SMP | `/proc/cpuinfo` (processor 0,1) |
| Clock | ~1.2 GHz (BogoMIPS ≈ 2390) | `/proc/cpuinfo` |
| FPU | Yes (per-core) | `/proc/cpuinfo` |
| SIMD | **MSA** (MIPS SIMD Architecture, 128-bit) - `ASEs implemented: msa` | `/proc/cpuinfo` |
| ISA | mips1 / mips2 / mips32r2 + MSA | `/proc/cpuinfo` |
| TLB | 288 entries; 1 HW watchpoint; 6 kscratch regs | `/proc/cpuinfo` |
| DVFS | **None exposed** - no `cpufreq/scaling_available_frequencies` | `/sys/.../cpufreq` |
| Thermal | **No thermal zones** - `/sys/class/thermal` empty | sysfs |
| GPU | **None** (X2000 has no GPU) | `/dev` has no gpu node |
| VPU / video decode | **None** - no `vpu`/`video`/`mem2mem` dev nodes | `/dev` |
| Hardware JPEG | **None** as a dev node (stock `jpg_to_png.c` is software) | `/dev`, RE |
**Implication:** all audio DSP and all UI rendering are CPU-bound. The single biggest
untapped compute lever is **MSA SIMD** - diskOS's LVGL is almost certainly built without
`-mmsa`, so blends/scales/rotations run scalar. Rebuilding LVGL (and any DSP/resampler)
with MSA is the highest-leverage perf win available.
---
## 2. Memory
| Item | Value |
|---|---|
| RAM total | **120 MB** (`MemTotal: 120308 kB`) |
| Free / available (stock running) | ~19 MB free / ~64 MB available |
Tight but workable. diskOS already runs in this budget. Big in-RAM buffers (e.g. a
full-res rotating album-art layer) must be sized carefully.
---
## 3. Storage
NAND (MTD) with **A/B dual-boot** for OTA, plus the user microSD:
| mtd | size | name | notes |
|---|---|---|---|
| mtd0 | 2 MB | uboot | bootloader |
| mtd1 | 8 MB | kernel | slot A |
| mtd2 | 128 MB | rootfs | slot A - **squashfs, read-only** (why `fiio_init.sh` can't be edited in place) |
| mtd3 | 8 MB | kernel2 | slot B |
| mtd4 | 25 MB | rootfs2 | slot B (smaller - recovery/fallback) |
| mtd5 | 1 MB | ota | OTA state |
| mtd6 | 1 MB | mac | MAC/calibration |
| mtd7 | 83 MB | **userdata** | writable - mounted as `/usr/data`, where **diskOS lives** |
| mmcblk0 | ~238 GB | microSD | single partition `mmcblk0p1` - the music card |
**Implication:** the **boot hook** is a small edit patched into the read-only rootfs's
`usr/project/fiio_init.sh` (which is why enabling it requires rewriting the rootfs partition - the
flash). The **payload it launches** - the `mq_ui` binary and diskOS's runtime state - lives in the
writable `/usr/data` (mtd7), the safe persistent target. So: the hook is baked into the RO rootfs;
only the UI/state are on `/usr/data`.
---
## 4. Audio - the headline subsystem
### 4.1 DACs - quad CS43131, fully balanced
Four Cirrus **CS43131** chips on I²C bus 3:
| I²C addr | sysfs name | /dev node (major) |
|---|---|---|
| 3-0030 | cs43131 | `/dev/cs43131` (248) |
| 3-0031 | cs43131b | `/dev/cs43131b` (247) |
| 3-0032 | cs43131c | `/dev/cs43131c` (246) |
| 3-0033 | cs43131d | `/dev/cs43131d` (245) |
dmesg tags include `cs43131_left_negetive` and `cs43131b_open` → the four DACs are wired
as **L+/L−/R+/R− (fully differential / balanced)**, two CS43131 per channel. This is an
unusually serious analog design for the form factor. *(Whether the physical jack exposes
balanced (4.4 mm) or sums to single-ended (3.5 mm) is a board question - confirm against
the unit's connectors.)*
Each CS43131 is a stereo DAC + integrated headphone amp; the family supports PCM to
384 kHz and **DSD64/128/256**. Control is via a **custom FiiO `cs43131` kernel driver**
exposing the four char devices above; stock `mq_player` drives them with **ioctl** (not
ALSA controls). Raw `/dev/i2c-3` is also present as a fallback.
### 4.2 SoC I²S/DMA path - the streaming ceiling
The Ingenic audio controller (ALSA card 0 `x2000`) exposes **5 playback + 5 capture DMA
channels** (`hw:0,0`-`hw:0,4` playback). `aplay --dump-hw-params` on an idle channel:
```
FORMAT: ALL
SAMPLE_BITS: [3 64]
CHANNELS: [1 8]
RATE: [8000 768000]
```
So the **kernel/I²S link can stream up to 768 kHz / 64-bit / 8-channel** - far beyond the
current track (S32_LE / 48 kHz / 2ch on DMA3). The real output ceiling is set by the
CS43131 (~384 kHz PCM, DSD256), not the SoC.
### 4.3 ALSA mixer surface
`amixer controls` shows only the **SoC internal codec** (`ICODEC HPOUTL/MIC GAIN`,
`MICBIAS`), digital mic (`DMIC ...`), line-out muxes (`LO0_MUX`…`LO11_MUX`), and the five
audio-interface formatters (`baic0_fmt`…`baic4_fmt`). There is **no CS43131 control, no
DSD switch, and no digital-filter control in ALSA** - all of that is the kernel driver +
mq_player ioctl path. `BAIC: baic start/stop` in dmesg marks I²S on/off per track.
### 4.4 Decode & format support (from mq_player RE)
- Decoder backend: **libavcodec.so.58 (ffmpeg)** - string `decoder_ffmpeg`.
- DSD: `DSD_MODE_NONE` / `DSD_MODE_NATIVE` / `DSD_MODE_DOP` - native DSD **and** DoP.
- MQA: `is_mqa` flag (detection/passthrough).
- Containers: FLAC, APE, DSF/DFF, **SACD ISO**, CUE sheets; ffmpeg covers ALAC/OPUS/
WavPack/etc.
- Param validation: `check_sample_param`, `reset hw params`, `AudioCodecOpen dsd`,
`no support sample! dsd_mode/out_dev/...` - the player gates rate/format per DAC mode.
**Tools present:** `aplay`, `amixer`, `tinyplay`, `tinymix` - raw PCM playback is possible
today (the DAC just has to already be configured for the rate).
---
## 5. Display
| Item | Value |
|---|---|
| Driver | `ingenicfb` |
| Visible | 360×360, 32 bpp (XRGB8888/BGRA, panel mounted 180°-rotated) |
| Framebuffer virtual | 360×**1080** = triple-buffered 360×360 |
| **Overlay layers** | `fb0`-`fb3` = **4 hardware LCDC planes** (`/dev/fb0..fb3`) |
| Backlight | standard `backlight` class, **41 levels (0-40)** |
**Layer control interface (confirmed via sysfs):** `ingenicfb` exposes `layer0`-`layer3`
under `/sys/class/graphics/fb0/device/`, each with `enable`, `src_fmt`, `src_size`,
`target_pos`, and **`target_size`**. `target_size` ≠ `src_size` ⇒ the LCDC has a
**per-layer hardware scaler** (notable, since there's otherwise no GPU/VPU). layer0 is the
active UI plane (`enable: 1`, src 360×360). Layers position + scale in hardware but **do
not rotate**.
**Implication:** static scaled art/backdrops *could* be HW-composited on fb1-3 (e.g. a
scaled cover or a dim backdrop under the LVGL UI) with no CPU blend. BUT:
- A **spinning** cover still needs software rotation (layers don't rotate) - LVGL already
does this fine, so the overlay is an optimization, not a requirement.
- **Alpha/blend mode is NOT in sysfs** (no alpha/zorder/colorkey node) - likely an FBIO
ioctl in the ingenicfb driver; blending behavior is **unverified**.
- **Visual confirmation needs a camera:** `fbshot` reads fb0's memory, but overlays
composite at *scanout*, so an fb1 test pattern won't appear in an fb0 capture. Defer the
live overlay test until we're ready to pursue HW-layer art and can eyeball the panel.
---
## 6. Input
| Device | node | notes |
|---|---|---|
| Touch | `/dev/input/event1` - **cst816t** | single-finger panel, but speaks **MT type-B** protocol (slot/tracking-id/ABS_MT_POSITION_X/Y/TOUCH_MAJOR/PRESSURE; no plain ABS_X/Y). Caps `EV=0xb`, `ABS=0x6618000` (high word). |
| Keys | `/dev/input/event0` - **x2000_key** | **physical buttons** (GPIO keys) |
We can synthesize input by writing the 32-bit-ABI `input_event` (16-byte) MT-B sequence to
`/dev/input/event1` - verified working (used to drive stock's UI pages over serial during RE). diskOS can also read the hardware keys via event0.
---
## 7. Power
| IC | I²C | role |
|---|---|---|
| **SGM41513** | 2-001a | battery charger (Li-ion, ~3A class) |
| **CW221X** (Cellwise) | 2-0064 | battery **fuel gauge** → `/sys/class/power_supply/cw221X-bat` |
| **AW35615** | 2-0022 | **USB-C PD / CC** controller (Type-C orientation + power delivery) |
Live read: capacity 100%, 4.32 V, source "Mains". The fuel gauge gives real %/voltage;
`/dev/usbcc_ioctl` (from RE) is the PD/CC control path. **PD negotiation hardware exists**,
so faster charging / power-role awareness is at least theoretically addressable.
---
## 8. Connectivity (wireless)
| Item | Value | Source |
|---|---|---|
| Module | **AP6212** = Broadcom **BCM43438 / 4343A1** | dmesg: `chip:0xa9a6`, `fw_bcm43438a1.bin`, `nvram_ap6212a.txt` |
| WiFi | **2.4 GHz only**, 802.11 b/g/n (single-band) | BCM43438 spec |
| WiFi attach | SDIO (`[dhd]` driver v101.10.591.91.40, 512 KB dongle RAM) | dmesg |
| Bluetooth | Broadcom BT over **UART `/dev/ttyS0`** @3Mbaud (`hci0`, BD address (device-specific, redacted)) | `hciconfig` |
| BT firmware | `BCM4343A1_001.002.009.1026.1055.hcd` (the `BCM4345C5/C0` `.hcd` files are leftovers for other FiiO models) | `/lib/firmware/bt_bcm` |
| BT bring-up | **hci0 is NOT attached at boot** - `bcmdhd.ko` only loads the driver/GPIOs. The HCI iface is created on-demand by `brcm_patchram_plus --enable_lpm --enable_hci --baudrate 3000000 --patchram <.hcd> /dev/ttyS0`, then `/usr/project/bluetoothd` (a2dp,avrcp,source) + `bluealsa --device=hci0` (SBC/LDAC). **Stock = bluez; `bsa_server`/`bt_enable_bsa*.sh` = dead code for the wrong chip.** Decoded from `mq_player` strings. | mq_player RE |
> **Corrects prior note:** earlier memory said "BCM4345C5". The actual silicon is
> **BCM43438 (AP6212)** - single-band 2.4 GHz + BT 4.x.
**Implication:** no 5 GHz → WiFi music transfer / streaming is capped at 2.4 GHz real
throughput (tens of Mbps, congestion-sensitive). BT codec quality (LDAC/aptX) is a
userspace-stack question, not a chip blocker for A2DP.
---
## 9. USB
| Item | Value |
|---|---|
| Controller | **DWC2 OTG** (`13500000.otg_new`) - **dual-role** (host *or* device) |
| Current mode | device; gadget `serial_demo` exposing **ACM** only (VID 0x0525 / PID 0xa4a7) → this *is* our serial shell |
| USB-DAC mode | stock "UAC" work-mode reconfigures the gadget to **USB Audio Class** (device-as-DAC for a host PC) |
**Untapped:** DWC2 is OTG, so **USB host mode is physically possible** - mounting USB
storage, or driving an *external* USB DAC (device as a pure transport). Stock only ships
device-mode (serial + UAC). Host-mode would need role switch + the right gadget/host
config and likely a USB-C OTG adapter.
---
## 10. Misc
- **RTC:** `rtc0` present and correct - real hardware clock.
- **ADC:** SoC SAR ADC, 6 aux channels (`/dev/jz_adc_aux_0..5`) - analog reads (e.g.
jack/line detect, if wired).
- **Watchdog:** hardware `/dev/jz_watchdog`.
- **LED:** **none.** `/sys/class/leds` is empty and there is **no physical LED** on the
unit (confirmed visually). The `RGB_LEVEL`/`RGB_STATUS` fields in mq_player's config blob
are vestigial, inherited from the halley5 reference design / other FiiO products.
→ diskOS should plan **no LED features**.
- **Motion sensors:** none on I²C (no accel/gyro) - no tilt/gesture input despite the
round watch-like shape.
---
## 11. Stock-uses vs hardware-can-do (gap list)
| Capability | HW supports | Stock uses | diskOS opportunity |
|---|---|---|---|
| MSA SIMD | Yes (128-bit) | UI not built for it (unknown) | Rebuild LVGL/DSP `-mmsa` → faster render/effects |
| LCDC overlay planes | 4 (fb0-3) | fb0 only | HW-composited art/video layer (spinning cover, backdrops) |
| I²S rate | 768k/64b/8ch | ≤384k/DSD256 (DAC-limited) | none beyond DAC; already maxes the DAC |
| DSD native + DoP | Yes | Yes | parity - reuse driver ioctls |
| Quad balanced DACs | Yes | Yes | direct ioctl control for bit-perfect / HW volume |
| USB host (OTG) | Yes | No (device-only) | USB storage / external USB-DAC transport |
| UAC2 gadget | Yes | Yes (UAC mode) | expose/control USB-DAC from diskOS |
| USB-C PD | Yes (AW35615) | basic charge | PD-aware fast charge / power-role UI |
| Physical keys | Yes (x2000_key) | Yes | map HW buttons in diskOS |
| WiFi 5 GHz | **No** | - | hard ceiling: 2.4 GHz only |
| GPU / VPU / HW JPEG | **No** | - | hard ceiling: video is CPU-only (MSA-assisted at best) |
| Thermal / DVFS | **Not exposed** | - | no power/thermal tuning via standard sysfs |
| LED | **None** | vestigial config | none - drop from plans |
---
## 12. "Write our own mq_player / mq_ui?" - assessment
**mq_ui: already done.** diskOS *is* our own UI; it reuses stock `mq_player` purely as an
audio engine over mqueue IPC. No reason to change that split for UI work.
**mq_player: a full rewrite is high-effort but the local-playback core is feasible.**
What a from-scratch local player needs, and how hard each piece is:
| Piece | Feasibility | Notes |
|---|---|---|
| Decode | **Easy** | reuse on-device `libavcodec.so.58` (same as stock) |
| PCM out | **Easy** | tinyalsa/libasound to card 0; up to 768k/64b |
| DAC control (rate/DSD/filter/volume/mute) | **Medium** | open `/dev/cs43131*` and replay the kernel-driver **ioctls** - must RE the ioctl numbers + sequences from `mq_player.asm` (bounded but real work). This is the **critical enabler**. |
| Native DSD / DoP | **Medium** | once DAC ioctls are known, feed the right format |
| MCU glue (keys/charge/USB-detect/power) | **Medium** | stock player talks to the FiiO MCU; our player must too (MCU command surface mapped during RE) |
| Streaming receivers (AirPlay/DLNA/Roon/QPlay/BT-sink) | **Hard** | large independent stacks - **do not rewrite**; keep stock or graft open-source (e.g. shairport-sync) |
**Recommendation (hybrid, incremental):**
1. **Keep stock mq_player** as the engine for now - it already handles DAC/DSD/streaming/
MCU and diskOS drives it fine over IPC.
2. **RE the `cs43131` ioctl interface regardless** - it's the key that lets diskOS (or a
future thin player) control bit-perfect output, hardware volume, DSD, and filters
directly. Highest-value RE next step.
3. **Only build our own *local* player** if we need something stock won't expose - e.g.
software parametric EQ, ReplayGain, custom gapless/crossfade, or a DSP chain - using
ffmpeg + tinyalsa + the RE'd DAC ioctls. Leave streaming to stock.
The streaming receivers and MCU dependency are what make a *complete* replacement
expensive; the audiophile local-playback path is the tractable, high-value slice.
---
## Appendix: probe commands (reproducible)
- SoC/mem: `cat /proc/cpuinfo /proc/meminfo`, `uname -a`
- Audio: `cat /proc/asound/{cards,pcm}`, `aplay -l`, `aplay -D hw:0,0 --dump-hw-params /dev/zero`, `amixer -c0 controls`
- I²C map: `for d in /sys/bus/i2c/devices/*/name; do echo "$d: $(cat $d)"; done`
- Power: `cat /sys/class/power_supply/*/{type,capacity,voltage_now,status}`
- Storage: `cat /proc/partitions /proc/mtd`
- USB: `ls /sys/class/udc`, `cat /sys/class/udc/*/state`, `ls /sys/kernel/config/usb_gadget/*`
- Wireless: `dmesg | grep -iE 'dhd|bcm|chip'`, `hciconfig -a`, `ls /lib/firmware/{wifi_bcm,bt_bcm}`
- Display/input: `cat /sys/class/graphics/fb0/{name,virtual_size,bits_per_pixel}`, `ls /dev/fb*`, `cat /proc/bus/input/devices`
- Engines: `ls /dev | grep -iE 'ipu|vpu|jpeg|adc|watchdog'`
+61
View File
@@ -0,0 +1,61 @@
# diskOS privacy and network disclosure
This lists everything the installer and the diskOS UI (`mq_ui`) send over the network. The UI ships
as a binary-only component (source not yet published), so this is a good-faith disclosure based on the
code and observed behaviour, not a guarantee; it will be tightened when the UI source is published.
## The installer (this repo)
- **`./install.sh` uses `pip`** to upgrade `pip` itself and download two packages - `pyusb` and
`pycryptodome` - from **PyPI** into a local `.venv`. That `pip`/PyPI traffic is the only network
activity in setup. Point `pip` at your own mirror if you prefer, or pre-install the packages offline.
- **The installer app itself makes no network requests.** Building the image, decrypting/extracting
your firmware, saving the recovery image, and flashing over USB are all fully local. It does not
phone home and sends no telemetry.
## The diskOS UI (`mq_ui`) on the device
diskOS is a local music player. The online features below reach out **only when you use them**; each
request unavoidably reveals your device's public IP to the service it contacts.
| Feature | Endpoint | Protocol | What is sent |
|---|---|---|---|
| **Weather** | `wttr.in` | **plain HTTP** | A location string if you set one, else nothing - wttr.in then **auto-geolocates you by IP** (approximate). Over HTTP, so treat it as visible on your network. |
| **Lyrics** | `lrclib.net` | HTTPS | The current track's **title and artist**, to find matching lyrics. |
| **Scrobbling (Last.fm)** | `ws.audioscrobbler.com` | HTTPS | If enabled and connected: the tracks you play (artist/title/album/timestamp) are reported to **your** Last.fm account. |
| **Last.fm sign-in** | `www.last.fm/api/auth/` | HTTPS | Only a QR code containing the approval URL is shown; **your phone**, not the device, opens it. `www.last.fm/api/account/create` is shown as text so you know where to get an API key. |
### Last.fm credential setup happens over your LOCAL network in plaintext
To connect Last.fm, diskOS runs a **temporary web server on the device** at
`http://<device-wifi-ip>:8080/<random-token>/`, shows a QR for it, and your phone posts your Last.fm
**API key and shared secret** to it. Important properties:
- It is **plain HTTP on your Wi-Fi LAN** - your API key/secret cross your local network unencrypted.
Do this on a network you trust.
- The server is **transient** (runs only during setup), bound to the Wi-Fi interface, and gated by a
random URL token.
- Your Last.fm **API key, secret, and session key are then stored on the device** under `/usr/data`
(device config); **offline scrobbles are queued** in `/usr/data/lastfm.queue` until they can be sent.
Nothing Last.fm-related is sent anywhere except your device, your phone (during setup), and Last.fm.
> **Last.fm is BETA and unverified end-to-end** - see the README's Known Issues. The transport and
> signing are tested, but a full connect-and-scrobble round-trip to a live account has not been.
### The stock FiiO player still runs alongside diskOS
diskOS replaces only the UI; FiiO's original player process still runs underneath it. That stock
component - not diskOS - is responsible for any Bluetooth, firmware-update checks, and LAN media
endpoints (DLNA/AirPlay/Roon/QPlay-style discovery) the device may present on your local network. Those
behaviours are inherited from the stock firmware and are outside diskOS's code.
## What diskOS does NOT do
- No analytics, telemetry, or usage tracking.
- It does not upload your music library, your listening history (beyond Last.fm scrobbles if you turn
them on), or any personal files.
- If you never enable weather, lyrics, or Last.fm, diskOS itself makes no outbound requests.
## Reporting
Report anything that looks like unexpected data leaving the device via the process in
[`SECURITY.md`](../SECURITY.md).
+427
View File
@@ -0,0 +1,427 @@
# Snowsky Disc - mq_player / mq_ui Reverse-Engineering Catalogue
Teardown of the stock V2.09 firmware binaries to document everything the player
and UI are capable of, so diskOS (our LVGL UI) can drive every feature.
- Targets: stock `usr/bin/mq_player` (4126620 B) and `usr/bin/mq_ui` (4418300 B).
MIPS32 little-endian, **glibc dynamically linked**
(interp `/lib/ld-linux-mipsn8.so.1`; 28 DT_NEEDED incl libc.so.6/libsqlite3/FFmpeg - NOT
static musl; only *our* diskOS binary is static-musl).
- Disasm cache (regenerate with `mipsel-linux-gnu-objdump`):
disassembly, data, and string dumps of the two binaries.
- Verification: addresses below were spot-checked against the cache on 2026-06-25.
Items marked **(unverified)** have not yet been individually confirmed at the
instruction level.
---
## ⚑ CORRECTIONS - full verification pass (2026-07)
Handler-level RE of both binaries; corrections spot-checked against the binary and applied inline below.
- **Binaries are glibc dynamic, not static musl** (interp `/lib/ld-linux-mipsn8.so.1`, 28 DT_NEEDED). Affects any replacement-binary / preload work.
- **IPC frame = `TAG(4) + TOTAL_LEN(4hex) + VALUE1(4hex) + optional payload`** - LEN is *total*, not payload length; class-2 cmds add VALUE2. Builder `mq_ui 0x43f82c` (`%s%04X%04X%s`). Replies `%s%04X%04X%s` to `/ui`. Queue `/player` maxmsg=20 msgsize=8192.
- **`06b3` = BT CODEC SELECT** (0=SBC 1=AAC 2/3/4=LDAC; worker 0x40eaf4 → `set_out_dev_sample_array(2,44100,44100)` for SBC/AAC, 96000 for LDAC). **`06b4` = LDAC quality only** (mob/std/high via /usr/data/bt_pipe_recv). ← the two were swapped in our old notes.
- **✅ BT AUDIO OUTPUT NOW WORKS (2026-08-03, live-fixed).** The DAP CAN transmit to a BT speaker (a2dp-source) - it just wasn't exposed and the code is fragile. The crash we hit was NOT the 06b3 codec value and NOT unfinished code: it was **skipping the mandatory `0666000C0006` pre-stop** before `0666000C0002`. Full working route sequence + the `--sbc-quality=medium` (stereo, no stutter) fix → **COMMAND_MAP.md "BT AUDIO OUTPUT" section**. The earlier "SBC fix frame = 06b3000C0000" was wrong/incomplete: diskOS uses `06b3001D0000<MAC>` after the pre-stop + `06c1` init.
- **`06b1`/`06b2`** = BT sample-rate / bit-depth params (not list/query).
- **`0689`** = EQ preset *apply* (21-way engine 0x449544), not a DB-only/media-info op.
- **`0201`** = generic transport toggle (not Roon-specific). **`0657`/`0666`/`06b3` "close_player" = false positive** (shared teardown preamble). `0666`=route (2=BTSRC 4=SPDIF 6=DAC).
- **out_dev** enum has no BTSINK (that's an input/work-mode). 48k→44.1k resample is `libfiio_decoder` over FFmpeg `libswresample` - a stock path, so codec-feasible (CPU headroom still needs an on-device xrun benchmark).
- **IPC-hardening RISK:** stock parser trusts declared LEN and uses `strlen` over the real `mq_receive` byte count → diskOS must emit strictly-correct total lengths and never forward untrusted frames.
- Still UNVERIFIED / deep-RE TODO: exact `0642` receiver-mode values + AirPlay/DLNA/Roon/QPlay trigger sequences; `0715` runtime volume callback @0x822b14; full `SET_USB_MODE` wire encoding; `0722` OTA worker chain; CS43131 ioctl ABI; the remaining ~190 family-inferred tag meanings.
---
## 1. How mq_player is driven - the command dispatch
mq_player is a background command server. The UI never touches audio; it mails
text commands to a POSIX message queue **`/player`**, and the player mails status
frames back on **`/ui`**.
### Wire format
- Command frame: `TAG(4 ASCII) + LEN(4 ASCII hex) + DATA`.
- Response builder @ **0x488d64** uses format `%s%04X%04X%s`
(tag, then two 16-bit hex fields, then payload). **(verified: prologue @0x488d64)**
### Dispatch tables (verified structure)
Two arrays of 8-byte `{descriptor_ptr, handler_ptr}` entries:
- **Table A @ 0x7c9d30** - primary handlers. Entry 0 = `{0x64f6e0, 0x411790}`.
~131 entries. **(count unverified; structure verified)**
- **Table B @ 0x7ca150** - secondary/extended. Many entries have
`handler = 0x00000000` = **declared but unimplemented** (e.g. `{0x64fca8, NULL}`).
~76 entries. **(count unverified; NULL-handler structure verified)**
Note: 0x7b7870 / 0x7b7c90 (noted in older sessions) are **PLT stubs**, not the
real tables. ~194 tag-like ASCII strings exist in `.str` total; ~30 of the table
slots are NULL / reloc-only / unimplemented (NAS / qplay family). **(unverified)**
A command arrives → tag matched against table → handler thunk → real handler.
Most handlers are tiny getters/setters over an in-memory settings struct.
### Confirmed action commands (tag strings verified present in `.str`)
| Tag | Action |
|------|------------------------------------------|
| 0100 | play by list (list_type + index) |
| 0103 | seek |
| 0104 | favorite / unfavorite |
| 0201 | transport (play/pause/next/prev) |
| 0202 | request current state |
| 0622 | rescan SD / rebuild DB |
| 0657 | source/work-mode transition (NOT play-mode) |
| 0666 | set output route (2=BTSRC 4=SPDIF 6=local-DAC) |
| 0689 | select+APPLY EQ preset (21-way engine 0x449544; NOT DB-only) |
| 0715 | set volume |
(0100/0103/0104/0201/0202/0622/0657/0666/0689/0715 all confirmed as ASCII tag
strings in `mq_player.str`. Full 207-tag enumeration is in the raw table dump and
should be transcribed here in a follow-up pass.)
---
## 2. Audio pipeline
Decode → process → output.
### Decode
- **FFmpeg** stack for mp3/flac/wav/aac/aif/m4a/ape/ogg/wma. **(unverified)**
- **libfiio_decoder** for DSD / DFF / DSF / SACD-ISO; DTS. **(unverified)**
- libsndfile present. **(unverified)**
### DSD / DoP
- Modes: NATIVE / DOP / D2P. DoP carrier up to 768k. Roon `configure_*` files in
`usr/project/config/roon/` corroborate (configure_768_dop, _384_d2p, etc.).
**(config files verified on disk; mode enum unverified)**
### Output routing (`out_dev`, tag 0666)
LOCAL_ANALOG (CS43131 headphone DAC) / BTSRC / USB_HOST / SPDIF / I2S3.
(BTSINK is NOT an out_dev route - it is an input/work-mode; see §1 and the work-mode enum in §7.)
**(unverified)**
### Volume - set_volume @ 0x489558 **(verified: prologue @0x489558)**
- Applied by shelling out to an amixer control named **`ICODEC HPOUTL GAIN`**
(string verified @ 0x264dd4 in `.str`).
- Uses inotify to react to external volume changes. **(unverified)**
- On-board MCU over SPI also carries gain/filter/mute opcodes
(SET_EQ_PARAMETER 0x100B etc.). **(unverified)**
### Bluetooth
- bluealsa-based; source + sink; codecs sbc / aac / ldac. **(unverified)**
- **Stock BT stack = BLUEZ, decoded from `mq_player` strings** (NOT BSA - `bsa_server`/`bt_enable_bsa*.sh`
are dead code, wrong chip BCM4345C5, no caller; mq_player references bsa_server 0×). Full bring-up embedded
in mq_player: `brcm_patchram_plus --enable_lpm --enable_hci --no2bytes --tosleep 200000 --baudrate 3000000
--patchram /lib/firmware/bt_bcm/BCM4343A1_001.002.009.1026.1055.hcd /dev/ttyS0` (downloads the chip fw patch
over UART → creates working hci0; nothing does this at boot) → `/usr/project/bluetoothd --noplugin=sap
--plugin=a2dp,avrcp --mode=source` → `bluealsa -S --device=hci0 --profile=a2dp-source --ldac-abr
--ldac-quality=standard --codec=sbc --initial-volume=48` → `hciconfig hci0 up/piscan/class 0x200414` →
`bluetoothctl agent on/default-agent/pairable on`. Sink mode = no-LPM patchram + `--mode=sink -p hfp-ag
--codec=sbc/aac/ldac`. The missing patchram step is why a naive BT enable produces a
poor scan.
### Gapless
- `audio_track_update_play_gapless`. **(unverified)**
### ReplayGain
- Stored / read from DB. **(unverified)**
---
## 3. EQ - the apply path (the key unlock)
The EQ is a **software parametric biquad cascade inside mq_player**, NOT a
hardware DAC feature. **CORRECTION:** `0689` does NOT merely write
to SQLite - its handler (`0x4961bc`) invokes the **21-way preset engine `0x449544`**,
updates the rate caps and applies the DSP, then replies `a639`. Custom bands are set
separately via `0678` (`0x44a658`). Both are live apply paths, not DB-only.
### DSP engine (verified prologues exist)
- Coefficient calc @ **0x448144** - uses pow/sqrt/sincos to turn
{filterType, frequency, gain, qValue} into biquad coefficients.
- IIR cascade @ **0x447ed0** and @ **0x4483a4** - per-sample filter, 32-bit
saturation. **(verified: both are function prologues)**
### Storage - PEQ table (SQL strings verified in `.str`)
Columns: `STYLE_NAME, MASTER_GAIN (REAL), PARAMS_JSON (text), STYLE_PRESET (int)`.
- PARAMS_JSON = array of bands `{filterType, frequency, gain, qValue}` (up to 10)
+ a master gain.
- Confirmed SQL @ ~0x257cb8 region:
- `INSERT INTO PEQ (STYLE_NAME, MASTER_GAIN, PARAMS_JSON, STYLE_PRESET) VALUES ('%s', %.1f, '%s', %d)`
- `UPDATE PEQ SET STYLE_NAME = ?, MASTER_GAIN = %.1f, PARAMS_JSON = ? WHERE STYLE_PRESET = ?`
- `SELECT 1 FROM PEQ WHERE STYLE_PRESET = ? LIMIT 1`
### To apply a custom EQ from diskOS
1. Write/UPDATE the PEQ row (bands JSON + master gain) for a STYLE_PRESET.
2. Send **0689** to select that preset.
The player loads bands → computes coeffs @0x448144 → runs the cascade
@0x447ed0/0x4483a4 on every sample before output.
### ✅ WIRED + CONFIRMED in diskOS (2026-06-30)
Custom EQ is live: `eqcustom.c` (10-band UI, horizontal scroll) → `mdb_set_peq()` writes PEQ
slot 11 in the format below → `ui_apply_eq(11)` sends 0689 → **player applies, sound changes
on-device (user-verified).** `ui_apply_eq` clamp raised 0x0A→20 to reach user slots 11-20.
### CAPTURED PARAMS_JSON format (live, V2.09, 2026-06-30)
Saved a stock "User 1" custom EQ (+12 @ 32 Hz, −12 @ 16 kHz) and read it back. Ground truth:
- **10 fixed bands**: 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 Hz, each ±12.0 dB; plus a **master ±12 dB** = the `MASTER_GAIN` REAL column.
- PARAMS_JSON = JSON **array of 10 objects**, e.g. one band:
`{"filterType":0,"frequency":32,"position":0,"gain":"12.0","qValue":"0.7"}`
- `filterType` = 0 (peaking) - **int**; `frequency` Hz - **int**; `position` 0-9 band index - **int**.
- **`gain` and `qValue` are JSON STRINGS** (quoted: `"12.0"`, `"-12.0"`, `"0.7"`), NOT numbers. gain is one-decimal dB; default qValue `"0.7"`.
- **User slot → STYLE_PRESET**: User 1-10 = **11-20** (User 1 = 11). Built-in presets = 0-10 (off,jazz,rock,r&b,hip-hop,pop,dance,classical,retro,sibilance-atten-1,sibilance-atten-2). Rows 160-169 exist but are unused/placeholder (`PARAMS_JSON` = literal string `"(null)"`).
- Stock writes via `UPDATE PEQ SET ... PARAMS_JSON = ? WHERE STYLE_PRESET = ?` then selects with **0689000C<preset hex>** (User 1 = `0689000C000B`).
- Other stock audio settings seen in the same menu (candidates for diskOS, map to documented name-cmds): Gain H/L, BT codec, SPDIF on/off, DAC digital filter (Fast/Slow LL, Fast/Slow PC, NOS, Wideband FF), DRE on/off.
---
## 4. The `/ui` frames - player → UI
mq_ui opens the **`/ui`** mqueue (name built @0x41b388, recv thread @0x415584,
`mq_receive` wrapper @0x41b5c8). Each frame = `TAG(4) + %x extension + JSON "other"`
(parser @0x415674: `strncpy …,4` then `sscanf %x`). Dispatch is a
`strncmp(cmd,tag,4)` chain in `ui_ctrl_response` (@0x41bfd0-0x41c098). Recv log
`[UI RECV]: %s` @0x272960. **(IPC path + format instruction-verified)**
Player→UI frames use **`a`-prefixed** tags (replies/reports) + `06d*` (OTA). Tag
meaning is taken from the adjacent log string (string-inferred, high confidence).
| TAG | meaning | payload |
|------|---------|---------|
| a620 | version reply | version `%s` |
| a710 | max-volume reply | `maxVolume` |
| a715 | volume / UAC srate change | `currentVolume` |
| aa1b | UAC sample-rate change | rate; `usbAudio` |
| a704/a705 | wifi connect status | state; `wifi_ssid` |
| a706 | net status | `%d` |
| a615 | language config | `%d` |
| a609 | theme config | `%d` |
| aa27 | charge-protect | `charge_protect` |
| aa24 | sys config | `%d`; `sys_count` |
| a634 | memory-play | `memoryPlay`/`memoryType` |
| a639 | EQ type | preset + `filterType/frequency/gain/qValue` |
| a6b6 | BT paired devices | count + list |
| a6c5/a6c3/a6c2/a6c1/a6c4/a6c0 | BT disc/connect/open replies | state + address |
| aa1c | power-key event | `%d` |
| aa22 | TF-card (SD) insert/remove | event `%d` |
| aa0c/aa0f | screen status | `%X` |
| a60a | tip/toast popup | tip code |
| a644 | now-playing / UI state update | song JSON (below) |
| a622 | SD rescan / DB-rebuild status | scan state |
| 06d0/06d1/06d2 | OTA progress / success / file-count | `%d` |
Verified-present log strings: GET_VERSION_REPLY @0x27255c, GET_WIFI_CONNECT_STATUS
@0x272658, UAC_SRATE_CHANGE @0x27252c.
**Now-playing JSON payload** (fields verified in `.str`): `song_name`, `song_artist_name`
(@0x271b90), `song_album_name`, `song_style_name`, `song_track`, `song_channel`,
`song_duration_time` (@0x271b34), `duration`, `songposition` (@0x271c2c),
`song_sample_rate`, `song_encoding_rate`, `song_bit_rate`, `song_mimetype`,
`song_file_path`, `is_sacd/is_cue/is_dsd`, `love` (favorite), `state` (play state),
`playerflag` (@0x271c08), `curlistlength`, `pos_id`, `playing_num`,
`work_mode` (@0x271b… 0x272b34), `battery` (@0x272c58). Album art → width/height/
quality/rgb → `/usr/data/fiio/cover.png`.
→ For diskOS status indicators (battery/BT/wifi) we read tags a704/a706 (wifi/net),
the BT a6c* family, and `battery` inside a644. **This is the data we were missing.**
### mq_ui feature surface (subsystems)
IPC core (`/ui` in; `/player`,`/bt_control` out) · frame dispatch (cJSON) · local
browse/play (all-songs/album/artist/style/favorite/custom, `db_song_ctrl.c`) ·
now-playing (`class_play_screen.c`, album art `jpg_to_png.c`) · Roon endpoint ·
home/menus (app/system/audio/others/popup `.json`) · EQ (`equalizer.json`) ·
Bluetooth (`bt.json`) · WiFi/NAS (`wpa_supplicant`, `hostapd`, `wl rssi`) ·
system/version/OTA (`ota_update.c`, `set_local_time.c`) · FiiO Link (UDP
224.0.0.255 discovery + TCP control, device id "SNOWSKY DISC") · bundled zlog.
---
## 5. Config + database schema
SQLite 3.23.1, opened via `sqlite3_open_v2`. DB files (paths verified in `.str`):
| File | Holds |
|------|-------|
| `/usr/data/fiio/db/sysconfig.db` | SYSCONFIG (settings, single row ID=1), CUSTOM_THEME, NAS_CONFIG |
| `/usr/data/fiio/db/song.db` | SONG, MY_LOVE, MEMORY_PLAY, PLAY_LIST, LIST_SONG_0/1/2, CUSTOM_PLAYLIST, PLAYLIST_INFO, PEQ |
| `/usr/data/fiio/db/dic.db` | HAN_PINYIN (CODE INTEGER, PINYIN char(1)) - CJK pinyin sort (on-disk verified) |
| `/usr/data/fiio/db/theme.db` | theme assets |
| `/usr/data/fiio/db/ebook.db` | e-book |
| `/usr/data/bluetooth.db` | bt_devices |
Only `dic.db` ships in the rootfs; the rest are created at first boot under
`/usr/data/fiio/db/`. db→file binding is from co-located source names + init block
(high confidence for SONG-family→song.db, SYSCONFIG/theme/NAS→sysconfig.db).
### SONG (song.db) - literal CREATE for MY_LOVE verified verbatim; SONG = same + IS_LOVE
ID(PK) · PATH · NAME · TITLE · ALBUM · ARTIST · GENRE · DISC · TRACK · IS_CUE ·
IS_ISO · IS_DSD · OFFSET(BIGINT) · DURATION(BIGINT) · NAME_CODE · TITLE_CODE ·
ALBUM_CODE · ARTIST_CODE · GENRE_CODE(pinyin sort keys) · ADD_TIME(INT8) ·
SAMPLE_RATE · BIT_PER_SAMPLE · CHANNELS · BIT_RATE · SONG_MIMETYPE ·
SONG_PRODUCTION_YEAR · IS_SELECT · ALBUM_ARTIST · ALBUM_ARTIST_CODE ·
**IS_LOVE** (SONG only, added by upgrade ALTER - favorites flag).
MY_LOVE = same minus IS_LOVE, `UNIQUE(PATH,TRACK)`. **(CREATE verified @mq_ui.str:9793)**
### CUSTOM_PLAYLIST (song.db) - literal CREATE verified @mq_ui.str:9790
MY_LOVE columns + `PLAYLIST_ID` (after ID), `FOREIGN KEY(PLAYLIST_ID) REFERENCES
PLAYLIST_INFO(ID) ON DELETE CASCADE`, `UNIQUE(PLAYLIST_ID,PATH,TRACK)`.
### PLAYLIST_INFO (song.db) - parent of CUSTOM_PLAYLIST. ID(PK) firm; name/count inferred (LOW confidence).
### PLAY_LIST (song.db) - queue registry: ID(PK) · LIST_ID · LIST_NAME.
### LIST_SONG_0/1/2 (song.db) - active play queues, built by `INSERT INTO LIST_SONG_%d … SELECT … FROM SONG` (verified). Cols: ID · LIST_ID · POS_ID(=MUSIC_ID join key) · PATH · NAME · TITLE · ALBUM · ARTIST · GENRE · DISC · TRACK · IS_CUE · IS_ISO · OFFSET · DURATION · ADD_TIME · IS_SELECT · SONG_TYPE · ALBUM_ARTIST.
### MEMORY_PLAY (song.db) - resume state, single row. ID(=1) · MUSIC_ID · IS_PLAYING · POSITION · IS_CUE · IS_ISO · TRACK. **(UPDATE verified @mq_player.str:13412)**
### PEQ (song.db) - see §3. ID · STYLE_NAME · MASTER_GAIN(REAL) · PARAMS_JSON · STYLE_PRESET.
### CUSTOM_THEME (sysconfig.db) - ID · POS_ID · NAME · PATH · ALIAS · TYPE · IS_SYSTEM · ATTR · ALPHA · LOCK_TIME/DATE/BATTERY/ID3/MEM_TYPE · FRONT_COLOR.
### NAS_CONFIG (sysconfig.db) - ID · NAS_NAME · NAS_IP · USER · (password) · TYPE · AUTO_LOGIN.
### bt_devices (bluetooth.db) - ID · NAME · ADDR(MAC key) · CODEC · SAMPLING · VOLUME.
### SYSCONFIG (sysconfig.db) - single row, all INT, `UPDATE SYSCONFIG set %s = %d where ID = 1` (verified @0x25a910)
Column names verified from descriptor @mq_player.str:13091+. Runtime pak dump
(verified @0x25e7a4): `DSD_MODE,OUT_DEV,VOLUME,WORK_MODE,LIGHT_LEVEL,LIGTH_ON_TIME
(sic),RGB_LEVEL,MUTE,VOL_MODE,RGB_STATUS,MEM_PLAY`. Full key set (names verified;
**enum value mappings NOT proven** - inferred from name + cross-ref to command tags):
DSD_MODE · OUT_DEV/DEVICE_OUTPUT(→0666) · VOLUME · WORK_MODE · LIGHT_LEVEL ·
LIGTH_ON_TIME(backlight timeout, sic) · RGB_LEVEL · MUTE · VOL_MODE · RGB_STATUS ·
RGB_COLOUR · TRIGGER_IN · SYS_THEME · LANGUAGE · USB_MODE · NETWORK_MODE ·
EQ_TYPE(→0689, =PEQ.STYLE_PRESET) · MAX_VOL · BALANCE_VOL · POWER_SAVE ·
FILTER_TYPE(DAC digital filter) · PLAY_MODE(→0102) · MEMORY_PLAY(resume) ·
FOLDER_JUMP · PLAY_GAP(gapless) · INPUT_MODE · VOL_KNOB_MODE · OTA_CFG · BATTERY ·
BT_CODEC · SCREEN_ROT · PO_PRE_VOL/PO_VOL/PRE_VOL · THEME_MODE · CHARGE_PROTECT ·
LOCK_THEME · TREBLE · BASS · WIFI_STATUS · BT_STATUS · LO_DISABLE · OS_MODE ·
DSD_DECODE · SPDIF · AUTO_TIME · DRE_STATUS · LOCAL_IMG_ANIM · IMG_ANIM_BRIGHTNESS ·
ARM_VERSION · MCU_OTA_FAILED_TIME · KEY_SINGLE/DOUBLE/LONG_CLICK_SLE(gesture→action) ·
ARTIST_CLASS_TYPE · AUDIO_VOLUME_SET.
Config is committed to flash on write (`now try to save config to flash` @0x25a…).
---
## 7. Network streaming + USB modes + COMPLETENESS
**Major gap found:** the earlier sections missed the network-streaming receivers and several subsystems. The device is far more capable than §1-6 implied.
### Work-mode / OUT_DEV enum (the master "audio source" list) - names VERIFIED
mq_player has ONE work-mode enum (pointer-array @ ~0x7bb0a0 data; names in `.str` @0x25f9e0+). Switching mode is **name-driven**: a supervisor `@0x444604` does `strcmp(name,"AIRPLAY"/"DLNA"/"ROON"/...)` and starts/stops that receiver. **Names verified present; integer indices unverified:**
`0 NO_DEFINED · 1 I2S3_OUT · 2 USB_HOST_NULL · 3 NO_WORK_MODE · 4 LOCALPLAYER · 5 BTSINK · 6 ANALOG · 7 UAC(USB-DAC) · 8 DLNA · 9 AIRPLAY · 10 ROON · 11 SPDIF_OPT · 12 SPDIF_RX · 13 DMR · 14 DMC · 15 DMS · 16 MIX · 17 I2S_IN · 18 STREAM_AUDIO` (+ QPLAY). sysconfig keys NETWORK_MODE (cfg+0x5c) and OUT_DEV both feed this.
### AirPlay receiver - BUILT IN (was completely missed)
mq_player embeds a **full shairport-sync fork**: `fiio_airplay.c` + `src/shairport.c, rtsp.c, rtp.c, dacp.c, metadata.c, mdns_avahi.c, player.c` + `libshairplay.so`. Advertises **`_raop._tcp`/`_airplay._tcp`** as **"SNOWSKY DISC"**. Config: `/usr/project/config/airplay2/x2000_airplay2.conf` (Shairport-Sync style, AirPlay 2). Flow: supervisor sees mode "AIRPLAY" → sets `airplay_play=1` (@0x7efbe4) → `start_airplay@0x436008` → pthread `airplay_func@0x435fc0` → shairport loop. Needs **wlan0 up** (`start_switch_network_mode_thread@0x46e634` monitors `/sys/class/net/wlan0/operstate`=="up", then emits `/ui` tag **a706** = GET_NET_STATUS_REPLY - a706 is STATUS, NOT the trigger). Has `airplay_artwork_thread`, DACP remote. Audio → ALSA `snd_pcm_writei` to the DAC after out_dev switch. **TRIGGER COMMAND TAG: NOT yet pinned** (a `06xx` work-mode-switch carrying AIRPLAY=9, sent via `ui_send_cmd@0x43f82c` → mqueue `/player` idx 3, frame `TAG(4)+ext1(4hex)+len(4hex)+data`). Player also has a name-string command dispatch @0x482328 (e.g. `SET_USB_MODE`). **Next step to enable AirPlay from diskOS = read the mq_ui output/work-mode menu handler (callers of 0x43f82c) to capture the literal tag+payload for AIRPLAY/UAC/DLNA/ROON.**
### Also built-in network receivers (missed):
- **DLNA/UPnP renderer** - `dlna_player.c`, UPnP RenderingControl/AVTransport SCPD. (mode DLNA=8)
- **Roon Ready endpoint** - `roon_player.c` + `libroon.so` RAAT (`roon_transport_*`, volume/seek/shuffle). (mode ROON=10) UI screen `ui_roon_play.c`.
- **QPlay** (Tencent/QQ Music) receiver - `mq_player_server_qplay.c`, MD5(Seed+PSK) auth, SetNetwork/SetLyric.
- **BT SINK** (phone→player A2DP in) - `bt_sink_server.c/bt_sink_control.c` + AVRCP. (mode BTSINK=5)
### USB modes (partially missed)
USB_MODE sysconfig + `usb_mode_change_handler@0x48eb80` (named cmd `SET_USB_MODE`@dispatch 0x482328). Gadgets via configfs: **`uac_demo`** (USB-DAC, functions uac1.a/uac2.a, /dev/usb_dac, UAC_SRATE→/ui tag aa1b) = work-mode UAC(7); **`storage_demo`** (card-reader, mass_storage.0, lun.0/file=/dev/mmcblk0). `set_usb_host`@0x24da24. **USB_MODE integer values (charge/DAC/reader/host) NOT yet mapped.**
### CORRECTIONS to earlier sections
- **NAS is FULLY IMPLEMENTED** (`nas_control.c`: `mount -t cifs vers=3.0`, NFS, `smbclient -L`; NAS_CONFIG live) - §1's "NAS/qplay = NULL/unimplemented" was WRONG.
- **Tag counts:** ~**221** distinct `/player` `0xxx` command tags + ~**102** `axxx` reply tags (mq_ui emits ~161 cmds, handles ~102 replies). §1/§4 documented ~10 + ~30 → **~95% of the command surface is still undocumented** (entire a4xx reply family untouched).
- Other missed subsystems: **lyrics** (`lrc_paser.c`, `/usr/data/fiio/encoder.lrc`!), generic stream/I2S_IN/capture (`stream_audio.c`,`player_capture.c`), animated gif screensaver (LOCAL_IMG_ANIM), serial-number (`sn_nb.c`), image loaders (bmp/jpeg/png/yabmp), SACD/DST internals, MCU OTA (`ENTER_MCU_UPDATE_MODE`), full SPI/MCU handler family.
## 8. FULL command map - inventory complete, meanings mixed-confidence
**V2.09 dispatch tables** (entry = `{tag_str_ptr, handler_thunk_ptr}` 8B; thunks tail-jump via GOT to real handler):
- **Table A @0x7c9d30 = 131 entries** (terminator @0x7ca148). Tags 06xx/07xx/08xx/0a51.
- **Table B @0x7ca150 = 75 entries** (terminator @0x7ca3a8). Tags 00xx/01xx/02xx/04xx/05xx. ~36 in-table NULL (declared-unimplemented), ~11 thunk-but-GOT-null, ~28 live.
- **MCU/SPI name-commands** via hw_ctrl.c @0x48d0b8 + sysconfig-key dispatch @0x488000 (NOT 0x482328 = that's the tag↔enum resolver).
**⚠️ RELIABILITY:** tag list = reliable (mechanical table walk). Per-tag MEANINGS: ~36 string-VERIFIED, ~80 INFERRED-from-family, ~15 UNKNOWN(runtime-registered GOT=0). **Some conflict with the 1.95-era LIVE-tested set** (1.95 tables were @0x7b7870/0x7b7c90; V2.09 layout differs). **RESOLVED:** the `0657`/`0666`/`06b3` "close_player" reading was a false positive - all three call a shared teardown preamble at the start of a disruptive mode change; that teardown is NOT their meaning. `0657`=source/work-mode transition, `0666`=output route (2/4/6), `06b3`=BT codec select. **Treat remaining INFERRED meanings as needing verification; trust live tests + resolved-handler traces over family inference.**
**String-VERIFIED V2.09 commands (Table A):** 0802=song count, 0620=get WIFI_MAC(/usr/data/fiio/nb.txt), 0704=wifi scan list, 0705=wifi connect(psid/passwd), 0800=write wpa_supplicant.conf, 0701=wifi init, 0700=close network card, 0720/0722=OTA(wget ota_patch), 0639/0689/0690/0634/0641=media-info query→a639 reply, 0675/0630=EQ/PEQ get(gain/filterType/frequency), **0678=set PEQ band(filterType/frequency/gain/qValue)**, 0621=DROP DB tables, 0624=mount/storage, 0815=set MEMORY_PLAY position, 0820/0821/0822=key single/double/long action, 0647=gapless, 0648=artist_class_type, **06b1=BT sample-rate param(0x40d4d8), 06b2=BT bit-depth param(0x40d66c), 06b3=BT CODEC SELECT(0=SBC 1=AAC 2/3/4=LDAC; 0x40eaf4; SBC frame `06b3000C0000` pins 44100), 06b4=LDAC QUALITY only(0x40dbe8; mob/std/high)**, 06b7=BT paired, 06c0=BT trust/power, 06c1=BT alias, 06c4=BT disconnect/remove. (0657/0666/06b3 close_player = RESOLVED false positive, see above.)
**Table B verified:** **0100=main PLAY** (jumptable @0x650e2c by list_type), 0201=transport play/pause toggle (generic, →0x419ff4; NOT Roon-specific), 0112=playlist add song, 0115=add to custom list, 0113=love-list delete, 0114=custom-list delete, 0117=playlist reorder(src/dst), 0408=file/folder browse, 0413=album query, 0414=style/genre query, 02xx=NAS scan/browse(mq_player_server_nas.c, many GOT-null/unimplemented).
**MCU/SPI name-commands (hw_ctrl.c, SPI to MCU):** GET/SET_DEVICE_MAX_VOL, SET_DEVICE_VOL, GET/SET_INPUT_MODE, SET_OUTPUT_MODE, SET_GAIN, GET/SET_DAC_FILTER, **SET_USB_MODE**, GET/SET_EQ_PRE, GET/SET_EQ_PARAMETER, SET_EQ_RESET, SAVE_EQ, SET_AUDIO_FORMAT, SET_FACTORY, **ENTER_MCU_UPDATE_MODE**, SET_MCU_POWER, SET_MUTE, SET_POWER_DOWN_TO_MCU (shutdown path, 63 call sites), GET/SET_ZERO_DATA_DETECT_TIME, BT_REPORT_RATE/STATE/CODEC_TO_MCU. sysconfig-key setters: RGB_COLOUR/TRIGGER_IN/SYS_THEME/LANGUAGE/USB_MODE/NETWORK_MODE/EQ_TYPE/MAX_VOL/BALANCE_VOL/POWER_SAVE/FILTER_TYPE/PLAY_MODE/FOLDER_JUMP/PLAY_GAP/INPUT_MODE/VOL_KNOB_MODE/OTA_CFG/PO_PRE_VOL/PO_VOL/PRE_VOL/TREBLE/BASS/LO_DISABLE/OS_MODE/DSD_DECODE.
## 6. TODO (next passes)
- [x] ~~Live-verify 0657/0666~~ RESOLVED (handler-trace): 0657=source/work-mode transition, 0666=output route(2/4/6). "close_player" was a shared teardown preamble.
- [ ] **Pin the AirPlay trigger tag** (mq_ui work-mode menu → callers of ui_send_cmd@0x43f82c) - the one fact needed to enable AirPlay from diskOS.
- [x] **0622 is NOT a working rescan on V2.09:** diskOS "Rescan Library" sends 0622 → runs a long scan
(~13min, scan_songs_thread) but writes 0 rows to song.db (mtime unchanged). Clearing SONG + reboot also scanned but
wrote nothing. So song.db must be hand-built (laptop `tools/build_db.py` from sync_manifest.json → SONG +
PLAYLIST_INFO + CUSTOM_PLAYLIST; diskOS lib sorts by TEXT so *_CODE only affects play-queue order). The real scan
trigger (comm_scan_songs_thread) is still un-pinned. song.db schema is in this catalogue's table notes / dump from
song.db.bak. Stock leaves DURATION/SAMPLE_RATE/BIT_RATE/CHANNELS=0 and ALBUM=TITLE for tagless rips - match that.
- [x] **USB_MODE card-reader mode = VALUE 2 - LIVE-VERIFIED WORKING (transferred 32GB this way).** This is the clean
stock MSC path for file transfer, and it SIDESTEPS the dangerous 0666 entirely. Details:
- `usb_mode_change_handler` @ **0x48eb??** (log str "usb_mode_change_handler = %d" @vaddr 0x670358, ref'd
by `addiu v1,v1,856` @0x48eb84). Reads/writes current usb_mode global @ **0x822d20**. Branches on the
target mode value (s0): observed cases 2, 5, 6.
- On **usb_mode==2** (`bne s0,2` fall-through @0x48ebc4) it accesses
**`/sys/kernel/config/usb_gadget/storage_demo`** (lui 0x66 + 13620 = vaddr 0x663534) → builds a
`mass_storage.0` function, **lun.0/file = `/dev/mmcblk0`** (whole SD), toggles cdrom/nofua/removable/ro,
binds the UDC. (All configfs paths string-verified @ file 0x263534-0x263d8f.)
- **CLEAN handoff CONFIRMED:** before/while exporting it UNMOUNTS the SD - `unmount_udisk` @0x668ed4 (called
~7× around 0x470134-0x470388) runs `umount %s` with 5 retries + `umount -lf %s 2>/dev/null` lazy-force
fallback on `/tmp/sdcard` (strs @0x268be8/0x266e40/0x268e40). So it properly releases mq_player's SD hold
(the EBUSY blocker) - no corruption, unlike a raw composite-gadget export.
- Trigger: **`SET_USB_MODE`** name-command (str @vaddr 0x66fe40; dispatch refs @0x48232c/0x4863ec/0x48d154;
builders @0x408044/0x40fa94) and/or the `USB_MODE` sysconfig field. EXACT payload/value-encoding for
SET_USB_MODE still to pin (one more trace), but the value is **2**.
- ⚠ CAVEAT (untested): a UDC binds ONE gadget at a time, so binding `storage_demo` almost certainly UNBINDS
our `serial_demo` ACM → **the serial shell will drop while in reader mode**; exiting reader mode (mode
switch / replug) should restore it. TEST LIVE WITH USER PRESENT (power-cycle fallback). This is still far
safer than 0666 (no player crash / MCU-reboot path).
- ⇒ This unblocks **music-transfer-over-USB** via the stock mechanism: set USB_MODE=2 → laptop sees the SD as
a USB drive → copy 32GB at USB speed + fsck → exit reader mode → in-device rescan. Replaces the unsafe
wifi-push (watchdog reboots) and the blocked raw-MSC attempt.
- [ ] Transcribe the full 221-tag command table + 102 a-frame replies.
- [ ] Transcribe the full 207-tag command table (A+B) with handler addresses.
- [ ] Verify audio/decoder/BT/DSD claims (§2) at instruction level (currently unverified).
- [ ] Resolve SYSCONFIG enum value→option integer mappings.
- [ ] Pin PLAYLIST_INFO's full column list (currently only ID firm).
- [ ] diskOS wiring: status indicators from a644/a704/a706/a6c* + custom-EQ via PEQ write + 0689.
## 5b. SYSCONFIG - the master settings table (decoded 2026-06-30)
`/usr/data/fiio/db/sysconfig.db` → table **SYSCONFIG** (single row), one INT column per setting.
This is where every audio/system setting persists (the config-named commands write here).
Columns incl: DSD_MODE, OUT_DEV, VOLUME, WORK_MODE, LIGHT_LEVEL(brightness), LIGTH_ON_TIME,
MUTE, VOL_MODE, USB_MODE, NETWORK_MODE, EQ_TYPE(=0689 preset), MAX_VOL, BALANCE_VOL(channel
balance), POWER_SAVE, FILTER_TYPE(DAC filter), PLAY_MODE(=0102), MEMORY_PLAY, FOLDER_JUMP,
PLAY_GAP, INPUT_MODE, VOL_KNOB_MODE, BT_CODEC, SCREEN_ROT, PO_PRE_VOL/PO_VOL/PRE_VOL,
THEME_MODE, CHARGE_PROTECT, LOCK_THEME, TREBLE, BASS, WIFI_STATUS, BT_STATUS, LO_DISABLE,
OS_MODE, DSD_DECODE, SPDIF, AUTO_TIME, DRE_STATUS, DEVICE_OUTPUT, KEY_SINGLE/DOUBLE/LONG_CLICK_SLE,
ARTIST_CLASS_TYPE, AUDIO_VOLUME_SET.
Live sample (V2.09): FILTER_TYPE=1, DRE_STATUS=1, BALANCE_VOL=0, SPDIF=0, MAX_VOL=120,
BT_CODEC=3, MEMORY_PLAY=0, FOLDER_JUMP=0, PLAY_GAP=0, SCREEN_ROT=0, CHARGE_PROTECT=0,
ARTIST_CLASS_TYPE=0, TREBLE=0, BASS=0, OUT_DEV=6, OS_MODE=0, LO_DISABLE=0, DSD_MODE=1,
INPUT_MODE=1, VOL_MODE=1. (No explicit GAIN column - likely VOL_MODE or encoded in OUT_DEV;
confirm by toggling.) Filter enum (others.json 70-75): 0=FAST_LL,1=SLOW_LL,2=SLOW_PC,3=FAST_PC,
4=NON_OS,5=Wideband_FF. To wire a setting in diskOS: write its SYSCONFIG column + send its apply
command (verified: gapless 0647, BT codec 06b3 [0=SBC…4=LDAC-high], artist 0648, memory 0815, keys 0820-22; Gain/
Filter/DRE/Balance apply-cmds TBD via strace-correlate of stock mq_ui - strace on mq_ui is SAFE,
only strace-on-mq_player triggers the MCU reboot).
## 5c. Audio/DAC apply-commands - DECODED LIVE (2026-06-30, strace of stock mq_ui)
Captured by strace `mq_timedsend` on stock mq_ui while toggling each setting (SAFE - strace
on mq_ui, never mq_player). Frame format `<tag>000C<value 4hex>`. Note: SYSCONFIG column
writes are DEFERRED (didn't update live), so these were correlated by the FRAME, not the DB.
Background-noise tags to ignore: 0807 (recurring 0/1 status), 0704 (wifi scan).
- **DRE** = `0812` - `0812000C0000` = ON, `0812000C0001` = OFF (startup sent 0 when DRE on).
- **Gain** = `0645` - `0645000C0000`/`0001` (Low/High; startup sent 0).
- **Channel balance** = `0713` - `0713000C01<level>`; center = `0101`, nudging streamed 0105..010C
(one frame per step; 0x01 hi-byte = channel/side, lo-byte = level).
- **DAC Filter** = `0653` - enum = menu order (clean sweep): 0=FAST_LL, 1=SLOW_LL, 2=SLOW_PC, 3=FAST_PC, 4=NON_OS, 5=Wideband_FF.
- **SPDIF** = via output-route `0666` - SPDIF on = `0666000C0004`, analog = `0666000C0006`
(0666 = OUT_DEV/output route: LOCAL_ANALOG=6, SPDIF=4; mutually exclusive with headphone).
- **BT codec** = `06b3` (**CORRECTED**: mq_ui codec menu 0x464e90 sends 06b3, not 06b4):
codec-settings menu sends payload-less `06b3000C0000`=SBC `…0001`=AAC `…0002/3/4`=LDAC mob/std/high.
**But the ROUTE-to-speaker frame (live-captured 2026-08-03) is `06b3<len>000X<MAC>`** (X=codec, MAC payload
kept for stock frame-shape; worker ignores it). diskOS route uses `06b3001D0000<MAC>` (SBC). `06b4` is
LDAC-quality only. **Full working BT-transmit sequence → COMMAND_MAP.md "BT AUDIO OUTPUT" section.**
- Also re-confirmed: `0666`=output route, `0642`=network mode (from startup sync).
## 5d. More settings - DECODED LIVE 2026-06-30 (strace stock mq_ui, clean batch)
- **Channel balance** = `0713` - `0713000C<HHLL>`: center=`0000`; one side `00NN` (NN=step), other side `01NN`. (Same tag the audio-cluster capture saw.) Mixer-style, likely safe.
- **Gapless** = `0647` - `0647000C0001` on / `0000` off. (matches COMMAND_MAP 0647=set gapless)
- **Artist list grouping** = `0648` - `0648000C0000` Artist / `0001` Album-Artist. (ARTIST_CLASS_TYPE)
- **Memory playback (resume)** = `0684` - `0684000C00<0|1|2>` = Off / Position / Song. (0684 was "media getter" in COMMAND_MAP - now confirmed the MEMORY_PLAY setter)
- **Max volume** = `0711` - `0711000C00<NN>` where NN(hex)=cap, 0..0x78(120).
NOTE: only SPDIF (0666 route) wedges on a raw send; these are config/mixer commands, expected safe - but per the wedge lesson, apply on live user change only, don't blind-send at boot.
## 5e. Sibilance EQ presets wired (2026-07-01, code-only, staged not-yet-deployed)
Stock has 11 EQ presets (equalizer.json 0-10); diskOS had 9. Added preset 9="Sibilance 1",
10="Sibilance 2" to OPT_EQ (settings.c) + T_EQ (npmenus.c), nopts 9->11, clamps q>8->q>10.
Uses the existing 0689 path (ui_apply_eq, clamp already 0..20) so 0689000C0009/000A select them.