Skip to content

Commit f4e4cc8

Browse files
committed
Enhance scrobbling and storage management features
- Implemented a mechanism to check for the presence of an external scrobbler process and prevent duplicate logging in the .scrobbler.log file. - Updated the deferred_up function to utilize the new scrobble_open_ours function for better management of scrobbling based on the presence of other scrobbler processes. - Added functions to check the presence and mount status of microSD cards, ensuring that the player does not attempt to scan for music when the card is present but not mounted. - Improved storage reporting to include both internal storage and microSD card status, providing clearer feedback to users about their storage situation. - Enhanced database reading functions to handle legacy text and number formats gracefully, preventing library loading failures due to invalid UTF-8 or improperly formatted track numbers. - Added logging for storage state changes and library loading processes to aid in troubleshooting and user feedback. - Introduced a new API function to handle cases where a database scan is declined due to the SD card being unmounted, ensuring the UI reflects the correct state.
1 parent 5d4b0d9 commit f4e4cc8

10 files changed

Lines changed: 726 additions & 82 deletions

File tree

CHANGELOG.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,93 @@ level the commit history supports; from `v0.1.6` onward, entries are written as
1616

1717
## [Unreleased]
1818

19+
### Fixed
20+
21+
- **One unusual tag could empty the whole library.** *Reproduced and fixed on the host
22+
(`tracks_read_legacy_text_and_numbers_stored_as_text` failed with `Utf8Error` on the artist column
23+
before the change and passes after it); device-unverified on the reporter's library.* Reported
24+
2026-09-13 on r/walkman: a 1 TB card whose music shows on stock and not in Cinder. Their log settled
25+
where the fault was not — the card was mounted and its files read fine — and where it was:
26+
27+
```
28+
housekeeping: the library database changed -> reloading library and playlists
29+
[cinder-db] genres: 1196 entries
30+
cinder-ffi: library loaded — 0 tracks, 0 albums, 0 artists
31+
```
32+
33+
SQLite types values, not columns, and Sony's scanner stores what the tags said. A legacy-encoded tag
34+
(Latin-1 "Björk") arrives as text that is not valid UTF-8, and a track number written `03/12` stays
35+
text inside an integer column. The track query read both with strict types, so one such value made
36+
the *whole* query an error — and the library build turned that error into an empty list without a
37+
word. Every column the scanner fills from a tag is now read leniently (invalid UTF-8 replaced,
38+
`03/12` read as 3), a row that still will not read is skipped and logged instead of taking the
39+
library with it, and a failed query now logs `track query FAILED — <reason>` rather than showing
40+
"no music".
41+
42+
- **The player never powered itself off on a flat battery.** *Cause device-verified 2026-09-13;
43+
the fix is harness-verified, and the shutdown itself is device-unverified (it needs a real
44+
discharge).* Reported by the owner: "I don't think it actually powers off." Sony's critical-battery
45+
shutdown lives in the stock Qt app (`ForceShutdown.cpp`), which Cinder replaces, so Cinder's own
46+
low-battery guard is the only thing that can do it — and that guard decided "on battery" from
47+
`status == "Discharging"`. This board's battery driver never reports that. Pulling the cable with a
48+
2 s sysfs logger running:
49+
50+
```
51+
status=Full cap=100 usb=1 dc=0 on the cable
52+
status=Not charging cap=94 usb=0 dc=0 cable out: this is what "on battery" looks like
53+
status=Not charging cap=91 usb=1 dc=0 cable back in, before charging resumes
54+
status=Charging cap=99 usb=1 dc=0
55+
```
56+
57+
So every discharge read as charging, the guard returned on its first line, and the player ran to
58+
the hardware cutoff with no "charge soon" warning and no clean shutdown, `/contents` still mounted
59+
read-write. The same string also appears with the cable in, so no reading of it separates the two
60+
states. The guard now asks the charger-detect nodes (`usb/online`, `dc/online`, `ac/online`), and
61+
falls back to the status string only on a board without them. Manual power-off was already working
62+
(the same helper, device-verified again today). Four new harness scenarios pin both directions:
63+
`lowbatt-off`, `lowbatt-discharging`, `lowbatt-charger`, `lowbatt-warn`.
64+
65+
- **An exFAT microSD card stayed unmounted after USB file transfer.** *The mount command is
66+
device-verified against an exFAT image; the round trip with a real exFAT card is
67+
device-unverified.* Any card over 32 GB is exFAT, and the player formats a big card that way
68+
itself. Sony mounts one through FUSE (`mount.exfat``exfatfuse`), but `cinder-msc` only knew
69+
vfat: leaving mass-storage mode it tried a vfat mount twelve times, failed every time, and the card
70+
was gone for the rest of the boot. It now tells the two apart by the boot sector and remounts exFAT
71+
with Sony's own script and option string (`batch_sync,waitonfat,noatime,iocharset=UTF-8`,
72+
recovered from `libStorageMgrServiceFw.so`; the vfat string next to it is refused by `exfatfuse`,
73+
which is how the two were told apart). On the way *into* mass storage it also waits for the
74+
`exfatfuse` daemon to exit before handing the card to the PC, so the daemon's buffered writes can
75+
never overlap the PC's.
76+
77+
- **A library scan could erase the SD card's albums.** *Harness-verified that nothing changes for a
78+
card-less build; the card path is device-unverified.* The scanner rebuilds the store from what it
79+
can see, so a scan run while a card is in the slot but unmounted records every album on it as
80+
deleted. The automatic scan after a USB session now waits for the card, and Settings ▸ Database
81+
says "SD card not mounted — restart, then scan" instead of scanning.
82+
83+
- **Duplicate scrobbles with unknown321's scrobbler installed.** *Device-verified 2026-09-13 on the
84+
reference device, which had been running both: the boot logs `scrobble — unknown321/scrobbler is
85+
running (pid 308) … standing down`, and cinder-home holds no `.scrobbler.log` descriptor. Harness
86+
scenarios `scrobble-opens` and `scrobble-yields`.* Both scrobblers append to the same
87+
`.scrobbler.log`, so every play reached Last.fm twice. `/contents/cinder_no_scrobble` could silence
88+
Cinder's, but only for someone who had noticed the duplicates and found the flag — the reference
89+
device itself ran both for a week. Cinder now looks for the other scrobbler's process and stands
90+
its own down while it runs, including when `/contents` is reclaimed mid-boot.
91+
92+
### Added
93+
94+
- **Settings ▸ Storage shows the SD card.** *The label is built on device; the row itself has not
95+
yet been looked at on the panel.* A card in the slot reads `SD 612 / 954 GB`, or
96+
`SD not mounted` — before, the row showed internal storage only, so a card that had not mounted
97+
looked the same as one that had. It refreshes when the card's state changes, including a big exFAT
98+
card whose filesystem check finishes well after boot.
99+
- **The log says where the library is.** *Device-verified 2026-09-13: `library by storage — internal
100+
2281, SD 1130, unresolved 0` and `storage: SD card mounted at /contents_ext (vfat)`.* One line per
101+
library open and one per change in the card's mount state, with the filesystem type. "My SD music
102+
is missing" can now be answered from `cinderhome.log`.
103+
- **README: Walkman One is listed as untested.** Walkman One makes the player identify as a different
104+
model, and Cinder's package is packed for the stock NW-A50 model.
105+
19106
## [0.3.3] — 2026-09-12
20107

21108
### Fixed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ artefact that does not exist.
135135
from Linux for months; it has not yet been sent from Windows. If it fails it fails safely — the
136136
files are staged and verified first, and the installer tells you which step did not happen.
137137
- **The Windows installer is unsigned**, and now asks for administrator — see [Install](#install).
138+
- **Untested on top of Walkman One.** Walkman One makes the player identify as a different model,
139+
and Cinder's install package is packed for the stock NW-A50 model. Nobody has tried the
140+
combination yet; the likeliest failure is the player's updater refusing the package. Make a wbrt
141+
backup first either way, and please open an issue saying what happened.
138142

139143
## Install
140144

cinder-home/cinder-msc

5.15 KB
Binary file not shown.

cinder-home/harness/scenarios.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ static void healthy_device(void) {
6262
cinder_harness_fs_write("/sys/class/power_supply/battery/status", "Discharging\n");
6363
cinder_harness_fs_write("/sys/class/power_supply/usb/online", "0\n");
6464
cinder_harness_fs_write("/sys/class/power_supply/usb/present", "0\n");
65+
// The other charger-detect nodes, faked so a scenario never reads the build machine's: `dc` is
66+
// this board's wall-charger supply (type Mains), `ac` the name other boards use for it.
67+
cinder_harness_fs_write("/sys/class/power_supply/dc/online", "0\n");
68+
cinder_harness_fs_write("/sys/class/power_supply/ac/online", "0\n");
6569
cinder_harness_fs_write("/sys/class/android_usb/android0/state", "DISCONNECTED\n");
6670
cinder_harness_fs_write("/sys/class/switch/cxd3778gf_h2w/state", "1\n"); // headphones in
6771
cinder_harness_fs_mkdir("/data/cinder"); // so the bad-boot counter can be cleared
@@ -482,6 +486,69 @@ static void s_auto_off_charging(void) {
482486
"never powers off a device sitting on a charger");
483487
}
484488

489+
// ── low battery: warn, then power off before the cell browns out ─────────────────────────────
490+
// Sony's critical-battery shutdown lives in the stock Qt app (model/force_shutdown/ForceShutdown.cpp)
491+
// — the app Cinder replaces — so battery_guard is the ONLY thing on this device that turns it off on
492+
// a flat battery. It shipped deciding "on battery" from status == "Discharging", and this board's
493+
// battery driver reports "Not charging" with no charger attached: the guard returned on its first
494+
// line for every discharge, and the player ran to the hardware cutoff with no warning and no clean
495+
// shutdown. The rule is now the charger-detect nodes, and these pin both directions of it.
496+
static const char* kPowerOff = "system:/system/vendor/unknown321/bin/cinder-power off";
497+
498+
static void low_battery(const char* pct, const char* status, const char* usb_online) {
499+
healthy_device();
500+
cinder_harness_fs_write("/sys/class/power_supply/battery/capacity", pct);
501+
cinder_harness_fs_write("/sys/class/power_supply/battery/status", status);
502+
cinder_harness_fs_write("/sys/class/power_supply/usb/online", usb_online);
503+
cinder_harness_set_budget_ms(60000);
504+
cinder_harness_run();
505+
}
506+
507+
static void s_low_battery_not_charging(void) {
508+
low_battery("3\n", "Not charging\n", "0\n"); // what the device reports on battery alone
509+
long long at = cinder_harness_first_ms(kPowerOff);
510+
std::printf(" .... first power-off attempt at %lldms\n", at);
511+
check_range(at, 0, 25000, "a critical battery with no charger attached powers off");
512+
check_eq(cinder_harness_count(kPowerOff), 1, "once — the retry back-off holds for a minute's run");
513+
}
514+
515+
static void s_low_battery_discharging(void) {
516+
low_battery("3\n", "Discharging\n", "0\n"); // the kernel's other name for the same state
517+
check_range(cinder_harness_first_ms(kPowerOff), 0, 25000,
518+
"\"Discharging\" with no charger attached powers off too");
519+
}
520+
521+
static void s_low_battery_on_charger(void) {
522+
low_battery("3\n", "Not charging\n", "1\n"); // cable in, charge held: plugged in, not dying
523+
check_eq(cinder_harness_count(kPowerOff), 0,
524+
"never powers off at 3% while a charger is attached, whatever the status string says");
525+
}
526+
527+
// ── one scrobbler per play ───────────────────────────────────────────────────────────────────
528+
// unknown321/scrobbler appends to the same .scrobbler.log, so with both alive every play reached
529+
// Last.fm twice. Cinder stands its own down when that process is running — and only then.
530+
static void s_scrobble_opens(void) {
531+
healthy_device();
532+
cinder_harness_set_budget_ms(20000);
533+
cinder_harness_run();
534+
check_eq(cinder_harness_count("cinder_scrobble_open"), 1, "scrobbles when it is the only scrobbler");
535+
}
536+
537+
static void s_scrobble_yields(void) {
538+
healthy_device();
539+
cinder_harness_fs_write("/proc/321/cmdline", "/system/vendor/unknown321/bin/scrobbler");
540+
cinder_harness_set_budget_ms(20000);
541+
cinder_harness_run();
542+
check_eq(cinder_harness_count("cinder_scrobble_open"), 0,
543+
"stands down while unknown321/scrobbler is running");
544+
}
545+
546+
static void s_low_battery_warns(void) {
547+
low_battery("8\n", "Not charging\n", "0\n");
548+
check(cinder_harness_count("cinder_toast") >= 1, "warns below 10% on battery");
549+
check_eq(cinder_harness_count(kPowerOff), 0, "…and does not power off above the critical level");
550+
}
551+
485552
// ── the DSP reconcile must not depend on having found a settings file ────────────────────────
486553
// The DSP is not ours and does not boot empty: it holds whatever the stock player last left in it.
487554
// This used to run only `if (g_settings_loaded)`, so on a boot with no readable
@@ -968,6 +1035,12 @@ static const Scenario kScenarios[] = {
9681035
{"autooff-idle", s_auto_off_idle, "idle and silent: power off, and back off if it fails"},
9691036
{"autooff-playing", s_auto_off_playing, "never power off while audio is playing"},
9701037
{"autooff-charging", s_auto_off_charging, "never power off a device on a charger"},
1038+
{"lowbatt-off", s_low_battery_not_charging, "critical battery, no charger (\"Not charging\"): power off"},
1039+
{"lowbatt-discharging", s_low_battery_discharging, "critical battery reported as \"Discharging\": power off"},
1040+
{"lowbatt-charger", s_low_battery_on_charger, "critical battery on a charger: stay up"},
1041+
{"lowbatt-warn", s_low_battery_warns, "low battery on battery: warn, do not power off"},
1042+
{"scrobble-opens", s_scrobble_opens, "the built-in scrobbler opens when it is alone"},
1043+
{"scrobble-yields", s_scrobble_yields, "…and stands down while unknown321/scrobbler runs"},
9711044
{"dsp-reconcile", s_dsp_reconcile_no_settings, "the DSP is reconciled even with no settings file"},
9721045
{"wake-on-touch", s_wake_on_touch, "a dark panel wakes on touch, without pressing anything"},
9731046
{"touch-gestures", s_touch_gestures, "a tap is a tap and a drag is a drag"},

0 commit comments

Comments
 (0)