Files
diskos/docs/RE_CATALOGUE.md
T
b0hemia e0bc4785e9 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.
2026-08-26 15:26:14 +10:00

33 KiB
Raw Blame History

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 (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)

  • 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.
  • 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.
  • 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.