@@ -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