From eb13c18ea8576f355fdaf7a6d38f5d7fd9ee4dee Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 24 Aug 2026 04:03:06 +0200 Subject: [PATCH] aports: add fp6-charging-mode (offline charging, charger-boot detection) Charger-insertion boots (androidboot.mode=charger, ABL-appended) divert to a minimal charging.target instead of the full UI, ending the dead-battery bootloop: ADSP charging/USB-PD runs, splash+panel killed ~6-10s in via an initramfs hook, modem+cdsp stopped, radios/sensors suppressed, CPU capped. Power key reboots to a normal boot, volume-up shows battery status, volume-down enables USB ssh, unplug powers off. Measured >= breakeven on a 100mA-classified SDP port, strongly net-positive on real chargers. Suspend duty-cycling (~13mA floor) exists behind an off-by-default flag: resume from deep suspend intermittently kills the UFS link (hibern8 exit failed ret=5) - do not enable until that kernel bug is fixed. Byte-identical to the deployment verified on the dev phone 2026-08-24 (journal/power: r5 armed tests, crash autopsies, measurements). Co-Authored-By: Claude Fable 5 --- aports/device/fp6-charging-mode/APKBUILD | 91 ++++++++++ .../fp6-charging-mode/charging-keys.service | 9 + .../device/fp6-charging-mode/charging-keys.sh | 111 ++++++++++++ .../fp6-charging-mode/charging-mode-generator | 41 +++++ .../charging-mode-initfs-hook.sh | 19 ++ .../charging-mode-suppress.conf | 7 + .../charging-monitor.service | 9 + .../fp6-charging-mode/charging-monitor.sh | 167 ++++++++++++++++++ .../fp6-charging-mode/charging-usbnet.service | 9 + .../fp6-charging-mode/charging-usbnet.sh | 16 ++ .../device/fp6-charging-mode/charging.target | 26 +++ build.sh | 3 +- 12 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 aports/device/fp6-charging-mode/APKBUILD create mode 100644 aports/device/fp6-charging-mode/charging-keys.service create mode 100755 aports/device/fp6-charging-mode/charging-keys.sh create mode 100755 aports/device/fp6-charging-mode/charging-mode-generator create mode 100755 aports/device/fp6-charging-mode/charging-mode-initfs-hook.sh create mode 100644 aports/device/fp6-charging-mode/charging-mode-suppress.conf create mode 100644 aports/device/fp6-charging-mode/charging-monitor.service create mode 100755 aports/device/fp6-charging-mode/charging-monitor.sh create mode 100644 aports/device/fp6-charging-mode/charging-usbnet.service create mode 100755 aports/device/fp6-charging-mode/charging-usbnet.sh create mode 100644 aports/device/fp6-charging-mode/charging.target diff --git a/aports/device/fp6-charging-mode/APKBUILD b/aports/device/fp6-charging-mode/APKBUILD new file mode 100644 index 0000000..5d97b0d --- /dev/null +++ b/aports/device/fp6-charging-mode/APKBUILD @@ -0,0 +1,91 @@ +# Offline charging mode for the FP6 (Android-style charger boot), built +# 2026-08-24 — source of truth + design + test evidence: fp6 repo +# work/charging-mode/ and journal/power/. +# +# The FP6 ABL appends androidboot.mode=charger when a charger insertion +# powers the phone on. Without this package, that boot goes all the way to +# the full UI and at low battery sags back into shutdown — the dead-battery +# bootloop. With it, a systemd generator diverts charger boots to a minimal +# charging.target: ADSP-owned charging runs (incl. USB-PD), panel dark +# (splash killed ~6-10 s in via the initramfs hook), modem/cdsp remoteprocs +# stopped, radios/sensors suppressed, CPU capped. Verified net >= breakeven +# even on a 100 mA-classified workstation USB port; strongly positive on +# real chargers. +# +# User interactions in charging mode: +# power key reboot into a normal boot (warm reboot drops the ABL flag) +# volume-up toggle on-screen battery status (%/rate/input/ETA) +# volume-down enable ssh over the USB cable (debug; static 172.16.42.1) +# unplug orderly poweroff +# +# Suspend duty-cycling exists behind /etc/charging-mode/enable-suspend but +# is NOT shipped enabled: resume from deep suspend intermittently kills the +# UFS link (tracked in journal/power, track 3). Do not enable until fixed. +# Escape hatch: touch /etc/charging-mode/disable to make the generator +# inert. Units install under /etc/systemd/system (same paths the dev phone +# runs, byte-identical to the tested deployment). +maintainer="Jorijn van der Graaf " +pkgname=fp6-charging-mode +pkgver=1 +pkgrel=0 +pkgdesc="Fairphone 6 offline charging mode (charger-boot detection + minimal charging target)" +url="https://forgejo.catcrafts.net/Catcrafts/fp6-img" +arch="noarch" +license="MIT" +options="!check" +source=" + charging-mode-generator + charging.target + charging-monitor.service + charging-monitor.sh + charging-usbnet.service + charging-usbnet.sh + charging-keys.service + charging-keys.sh + charging-mode-suppress.conf + charging-mode-initfs-hook.sh +" + +package() { + install -Dm755 "$srcdir"/charging-mode-generator \ + "$pkgdir"/usr/lib/systemd/system-generators/charging-mode-generator + install -Dm644 "$srcdir"/charging.target \ + "$pkgdir"/etc/systemd/system/charging.target + install -Dm644 "$srcdir"/charging-monitor.service \ + "$pkgdir"/etc/systemd/system/charging-monitor.service + install -Dm755 "$srcdir"/charging-monitor.sh \ + "$pkgdir"/usr/lib/charging-mode/charging-monitor.sh + install -Dm644 "$srcdir"/charging-usbnet.service \ + "$pkgdir"/etc/systemd/system/charging-usbnet.service + install -Dm755 "$srcdir"/charging-usbnet.sh \ + "$pkgdir"/usr/lib/charging-mode/charging-usbnet.sh + install -Dm644 "$srcdir"/charging-keys.service \ + "$pkgdir"/etc/systemd/system/charging-keys.service + install -Dm755 "$srcdir"/charging-keys.sh \ + "$pkgdir"/usr/lib/charging-mode/charging-keys.sh + # radios/sensors out of charging mode: conditional drop-ins, inert in + # normal boots (the generator creates the /run/charging-mode marker + # only on charger boots). Drop-ins for units a variant image may not + # ship are harmless. + for u in bluetooth iio-sensor-proxy hkdm; do + install -Dm644 "$srcdir"/charging-mode-suppress.conf \ + "$pkgdir"/etc/systemd/system/$u.service.d/zz-charging-mode.conf + done + # early splash/panel kill — lands in the initramfs via the + # postmarketos-initramfs trigger on /usr/share/mkinitfs + install -Dm755 "$srcdir"/charging-mode-initfs-hook.sh \ + "$pkgdir"/usr/share/mkinitfs/hooks/20-charging-mode.sh +} + +sha512sums=" +28027696cb1f5d2b1f5d1e09d7f381e89c223b7f10e54f7f8f6e1d75de2b0640b4db81a14263deda69c9c1d992614df0449f9078fec654e56c67c659649301c6 charging-mode-generator +e0640c43c7900269000fb0854f8a113258572ee35d6072e96c5e919329a785de9a17308002099343046d2c5657638780708c34728d1445f82a27505e329fd55a charging.target +1b330faa2dbd9e4d1505f58db7883f52de656c51aac93012c3e91aed7797fccada9eb67b116a9d932754588475e282fa6fb57287bb6681431e50b4a51aa7003a charging-monitor.service +d05b1a5e3a13de4cfa0d09fb87842da740d13d4f557894adab4d9f0d9d3ea47d61d17fd75beee3fddf2297a64b7d9d0e0ebdefa76952df4445daf169946428e5 charging-monitor.sh +504c9cf3bb0d511851bc82ae5cf715b9be6c289ff9611c7387b787d68404254a702af1fc705376f6e72314bf17094acb61a5d66279a3bcc5fbda2f2e9d1f2454 charging-usbnet.service +89f3cd78969787cfccc4da26dadacc395d51f5d2e0f4e9565988eb4a34bb98c2b3ca55550d21229e43bc1abe0191394ea0c9b0b68f7f39f5dc1a5e05ca8340e7 charging-usbnet.sh +24cda032576251a7ac2ebc67a4bb1e40b8e245a6280cba083a7d8a2b76513fbb728f1f78ad62fc95f77b12880c80d963226b9ffc049997487e4fd7619b7dc8a4 charging-keys.service +8905a1dfb7a73a846fe6b9f4c530a28b326dded44e992c4f6ca1976250b966f231d67639190570a055628f01637debd951dfb7bf937640364424ed97afe517e2 charging-keys.sh +127cbcd35a581c048eb9e9c31df9044a3a62c3223914032f413e58da58422c3871faa09b4ac5ecd8f4237b33a448c43a91b181830faa62eb753d7111d1efa66b charging-mode-suppress.conf +1e9ab3c137cda062de1cbb7eea59677337a6b59eeed7e7f74bc7826146af8c6d26e0ac394984d4ccec3b0a1017f196f09f3fdba1728218e3c28022892c06685d charging-mode-initfs-hook.sh +" diff --git a/aports/device/fp6-charging-mode/charging-keys.service b/aports/device/fp6-charging-mode/charging-keys.service new file mode 100644 index 0000000..da7242d --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-keys.service @@ -0,0 +1,9 @@ +[Unit] +Description=Charging-mode key handler (volume-down enables ssh) +# Only ever pulled by charging.target. + +[Service] +Type=exec +ExecStart=/usr/lib/charging-mode/charging-keys.sh +Restart=on-failure +RestartSec=5 diff --git a/aports/device/fp6-charging-mode/charging-keys.sh b/aports/device/fp6-charging-mode/charging-keys.sh new file mode 100755 index 0000000..f6a8d3b --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-keys.sh @@ -0,0 +1,111 @@ +#!/bin/sh +# Charging-mode key handler (r4): +# volume-down (pmic_resin) -> enable ssh debug mode (inhibits suspend) +# volume-up (gpio-keys) -> toggle the on-screen battery status display +# The display costs nothing while off: panel + pipeline stay powered down +# and suspend cycling continues. While visible, /run/charging-mode-display +# inhibits suspend (the monitor checks it); toggling off (or the safety +# timeout) blanks everything again and cycling resumes. +# The power key is taken (logind: reboot to normal boot). + +log() { echo "charging-keys: $*"; } + +BAT=/sys/class/power_supply/qcom-battmgr-bat +USB=/sys/class/power_supply/qcom-battmgr-usb +DISPLAY_MARKER=/run/charging-mode-display +DISPLAY_TIMEOUT=600 # safety auto-off, seconds + +find_dev() { + for e in /sys/class/input/event*; do + n=$(cat "$e/device/name" 2>/dev/null) + case "$n" in + $1) echo "/dev/input/${e##*/}"; return 0 ;; + esac + done + return 1 +} + +read_key() { + # echoes the key code of a key RELEASE event (or nothing). Release, not + # press: when a key wakes the phone from suspend, the press can be + # consumed as the wake event while the release is still delivered — so + # triggering on release works both awake and as a wake key. + # input_event on arm64: u64 sec, u64 usec, u16 type, u16 code, s32 value + # od -tu2 words: 1-8 time, 9 type, 10 code, 11-12 value (lo, hi) + _d="$1" + set -- $(dd if="$_d" bs=24 count=1 2>/dev/null | od -An -v -tu2) + [ $# -ge 11 ] || return 1 + [ "$9" = "1" ] && [ "${11}" = "0" ] && echo "${10}" + return 0 +} + +enable_ssh() { + [ -e /run/charging-mode-ssh ] && return + : > /run/charging-mode-ssh + log "volume-down pressed: enabling usbnet+sshd, suspend cycling inhibited" + systemctl start systemd-user-sessions.service charging-usbnet.service sshd.service +} + +draw_status() { + cap=$(cat "$BAT/capacity" 2>/dev/null) + cur=$(cat "$BAT/current_now" 2>/dev/null) + cc=$(cat "$BAT/charge_counter" 2>/dev/null) + full=$(cat "$BAT/charge_full" 2>/dev/null) + [ -n "$full" ] || full=4415000 + uv=$(cat "$USB/voltage_now" 2>/dev/null) + ui=$(cat "$USB/current_now" 2>/dev/null) + eta=$(awk -v f="$full" -v c="$cc" -v i="$cur" 'BEGIN { + if (i > 10000) { h = (f - c) / i; printf "%dh%02dm", int(h), (h - int(h)) * 60 } + else printf "--" + }' 2>/dev/null) + { + printf '\033[2J\033[H\n\n' + printf ' CHARGING %s%%\n\n' "$cap" + printf ' battery %d mA\n' "$(( ${cur:-0} / 1000 ))" + printf ' input %d mV %d mA\n' "$(( ${uv:-0} / 1000 ))" "$(( ${ui:-0} / 1000 ))" + printf ' full in %s\n' "$eta" + } > /dev/tty0 2>/dev/null +} + +display_off() { + rm -f "$DISPLAY_MARKER" + printf '\033[2J\033[H' > /dev/tty0 2>/dev/null + for b in /sys/class/backlight/*/brightness; do echo 0 > "$b" 2>/dev/null; done + for f in /sys/class/graphics/fb*/blank; do { echo 4 > "$f" || echo 1 > "$f"; } 2>/dev/null; done + log "status display OFF" +} + +display_on() { + : > "$DISPLAY_MARKER" + log "volume-up pressed: status display ON" + for f in /sys/class/graphics/fb*/blank; do echo 0 > "$f" 2>/dev/null; done + for b in /sys/class/backlight/*/brightness; do echo 400 > "$b" 2>/dev/null; done + ( + shown=0 + while [ -e "$DISPLAY_MARKER" ] && [ "$shown" -lt "$DISPLAY_TIMEOUT" ]; do + draw_status + sleep 5 + shown=$((shown + 5)) + done + display_off + ) & +} + +VOLDOWN_DEV=$(find_dev "*resin*") +VOLUP_DEV=$(find_dev "gpio-keys") + +[ -n "$VOLDOWN_DEV" ] && log "volume-down (ssh) on $VOLDOWN_DEV" \ + || log "no resin device - ssh enable unavailable this boot" +[ -n "$VOLUP_DEV" ] && log "volume-up (display) on $VOLUP_DEV" \ + || log "no gpio-keys device - status display unavailable this boot" + +if [ -n "$VOLDOWN_DEV" ]; then + ( while :; do [ "$(read_key "$VOLDOWN_DEV")" = "114" ] && enable_ssh; done ) & +fi +if [ -n "$VOLUP_DEV" ]; then + ( while :; do + [ "$(read_key "$VOLUP_DEV")" = "115" ] || continue + if [ -e "$DISPLAY_MARKER" ]; then display_off; else display_on; fi + done ) & +fi +wait diff --git a/aports/device/fp6-charging-mode/charging-mode-generator b/aports/device/fp6-charging-mode/charging-mode-generator new file mode 100755 index 0000000..6de67b4 --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-mode-generator @@ -0,0 +1,41 @@ +#!/bin/sh +# systemd system generator: divert a charger-triggered boot to charging.target. +# +# The FP6 ABL appends "androidboot.mode=charger" to the kernel cmdline when +# the PMIC PON reason is a charger insertion (confirmed on-device 2026-08-24, +# journal/power). On such boots we swap the default target for the minimal +# charging.target instead of booting to the full UI. +# +# This must NEVER break a normal boot: every failure path falls through to +# exit 0 and the stock default.target. +# +# Escape hatch: /etc/charging-mode/disable — while it exists this generator +# is inert (deploy.sh creates it; arming = removing it). +# +# The CHARGING_MODE_* env overrides exist only for side-effect-free dry-runs +# on a live system; systemd invokes generators with a clean environment. + +[ -e "${CHARGING_MODE_DISABLE_FILE:-/etc/charging-mode/disable}" ] && exit 0 + +grep -Eq '(^| )androidboot\.mode=charger( |$)' /proc/cmdline || exit 0 + +# $2 = early generator dir (highest priority in the unit search path that a +# generator can write to — beats /etc/systemd/system). Old single-arg +# invocation falls back to $1. +EARLY_DIR="${2:-$1}" +[ -n "$EARLY_DIR" ] && [ -d "$EARLY_DIR" ] || exit 0 + +ln -sf /etc/systemd/system/charging.target "$EARLY_DIR/default.target" || exit 0 + +# Marker for the ConditionPathExists=!/run/charging-mode drop-ins that keep +# radios/sensors (bluetooth, iio-sensor-proxy, hkdm) out of charging mode. +# Only created after the default.target diversion succeeded, so the +# suppressions can never leak into a normal boot. +: > "${CHARGING_MODE_MARKER:-/run/charging-mode}" 2>/dev/null + +# Power key exits charging mode via reboot: a warm reboot PONs as HARD_RESET, +# so ABL does not re-append mode=charger and the next boot is a normal boot. +LOGIND_DIR="${CHARGING_MODE_LOGIND_DIR:-/run/systemd/logind.conf.d}" +mkdir -p "$LOGIND_DIR" 2>/dev/null && printf '[Login]\nHandlePowerKey=reboot\nHandlePowerKeyLongPress=reboot\n' > "$LOGIND_DIR/10-charging-mode.conf" + +exit 0 diff --git a/aports/device/fp6-charging-mode/charging-mode-initfs-hook.sh b/aports/device/fp6-charging-mode/charging-mode-initfs-hook.sh new file mode 100755 index 0000000..5f2858f --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-mode-initfs-hook.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# FP6 offline charging (work/charging-mode): on charger-triggered boots, +# kill the boot splash and panel as early as software can — this hook runs +# right after splash_start in the initramfs (~6-10 s after power-on), vs +# the charging monitor's blank at ~16-20 s. The 0-5 s bootloader logo is +# ABL's and untouchable. Executed via `sh` by run_hooks (subshell, exit is +# safe); written to fall through harmlessly on any failure — a normal boot +# must never be affected. + +if grep -Eq '(^| )androidboot\.mode=charger( |$)' /proc/cmdline 2>/dev/null; then + command -v plymouth >/dev/null 2>&1 && plymouth quit 2>/dev/null + for b in /sys/class/backlight/*/brightness; do + [ -w "$b" ] && echo 0 > "$b" 2>/dev/null + done + for f in /sys/class/graphics/fb*/blank; do + [ -w "$f" ] && { echo 4 > "$f" || echo 1 > "$f"; } 2>/dev/null + done +fi +exit 0 diff --git a/aports/device/fp6-charging-mode/charging-mode-suppress.conf b/aports/device/fp6-charging-mode/charging-mode-suppress.conf new file mode 100644 index 0000000..a5ba1de --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-mode-suppress.conf @@ -0,0 +1,7 @@ +# Installed by work/charging-mode (FP6 offline charging) as a drop-in for +# units that are pointless while charging (radios, sensors, hotkeys). +# Inert in normal boots: the generator creates /run/charging-mode only on +# charger-triggered boots, and only after the default.target diversion +# succeeded. +[Unit] +ConditionPathExists=!/run/charging-mode diff --git a/aports/device/fp6-charging-mode/charging-monitor.service b/aports/device/fp6-charging-mode/charging-monitor.service new file mode 100644 index 0000000..d686e27 --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-monitor.service @@ -0,0 +1,9 @@ +[Unit] +Description=Offline charging monitor +After=basic.target + +[Service] +Type=exec +ExecStart=/usr/lib/charging-mode/charging-monitor.sh +Restart=on-failure +RestartSec=5 diff --git a/aports/device/fp6-charging-mode/charging-monitor.sh b/aports/device/fp6-charging-mode/charging-monitor.sh new file mode 100755 index 0000000..6366140 --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-monitor.sh @@ -0,0 +1,167 @@ +#!/bin/sh +# Offline charging monitor (prototype r3). Runs only inside charging.target. +# +# - waits for the ADSP-backed battmgr supplies (charging + USB-PD live on +# the ADSP, which never sleeps — AP suspend does not touch the PD contract) +# - blanks + powers down the display pipeline, stops modem/cdsp remoteprocs, +# caps CPU frequency, nudges UFS auto-hibernate +# - unplug -> orderly poweroff (debounced ~6 s; a poweroff while plugged +# re-wakes via the USB_CHARGER PON trigger) +# - power key -> logind reboots to a NORMAL boot (generator drop-in) +# - suspend cycling (rock-bottom mode): only when /etc/charging-mode/ +# enable-suspend exists AND ssh debug mode is off (/run/charging-mode-ssh, +# volume-down) AND the status display is off (/run/charging-mode-display, +# volume-up toggle). Arms an RTC wakealarm before every suspend and never +# suspends without a verified alarm. +# +# Deliberately NO rfkill anywhere: systemd-rfkill would persist a soft-block +# into normal boots (the 2026-08-23 wifi trap). +# +# Failsafe: if battmgr never appears, reboot — that PONs as HARD_RESET, so +# the phone comes back in a NORMAL boot and is never stuck dark. + +BAT=/sys/class/power_supply/qcom-battmgr-bat +USB=/sys/class/power_supply/qcom-battmgr-usb +WAKEALARM=/sys/class/rtc/rtc0/wakealarm +SUSPEND_SECS=45 + +log() { echo "charging-monitor: $*"; } + +log "offline charging mode active (prototype r5)" + +i=0 +while [ ! -r "$USB/online" ]; do + i=$((i + 1)) + if [ "$i" -gt 180 ]; then + log "battmgr never appeared after ${i}s - failsafe reboot to normal boot" + exec systemctl reboot + fi + sleep 1 +done +log "battmgr up after ${i}s" + +for b in /sys/class/backlight/*/brightness; do + [ -w "$b" ] && echo 0 > "$b" 2>/dev/null +done +log "panel blanked" + +# release the boot splash and power the display pipeline down (best effort — +# brightness 0 alone leaves the DSI/DPU scanning out a black frame) +if command -v plymouth >/dev/null 2>&1 && plymouth --ping 2>/dev/null; then + plymouth quit 2>/dev/null && log "plymouth quit" +fi +for f in /sys/class/graphics/fb*/blank; do + [ -w "$f" ] || continue + { echo 4 > "$f" || echo 1 > "$f"; } 2>/dev/null && log "fb powerdown via $f" +done + +# radios/compute are pointless while charging — stop modem and cdsp +# remoteprocs (adsp stays: it IS the charger; wpss left alone near the +# WCN-combo trap). The stop write can block past a short timeout yet still +# succeed — judge by the resulting state, not the write's exit code. +for r in /sys/class/remoteproc/remoteproc*; do + name=$(cat "$r/name" 2>/dev/null) + case "$name" in + *mpss*|*modem*|*mss*|*cdsp*) + timeout 25 sh -c "echo stop > '$r/state'" 2>/dev/null + if [ "$(cat "$r/state" 2>/dev/null)" = "offline" ]; then + log "stopped remoteproc $name" + else + log "remoteproc $name did not stop (state=$(cat "$r/state" 2>/dev/null)) - continuing" + fi + ;; + esac +done + +# cap CPU frequency to the minimum, then offline every core except cpu0 +# (a little @ 441.6 MHz) — emptying the two big clusters (cpu4-6, cpu7) +# lets their cluster rails power-collapse entirely. Charging mode always +# ends in a reboot, so no undo needed. +for p in /sys/devices/system/cpu/cpufreq/policy*; do + cat "$p/cpuinfo_min_freq" > "$p/scaling_max_freq" 2>/dev/null +done +for c in /sys/devices/system/cpu/cpu[1-9]*; do + [ -w "$c/online" ] && echo 0 > "$c/online" 2>/dev/null +done +log "cpu freq capped to min, online cpus: $(cat /sys/devices/system/cpu/online 2>/dev/null)" +# NOTE: do NOT touch UFS auto_hibern8 — the 2026-08-24 PD-boot crash +# (ufshcd hibern8 exit failed, dead rootfs at t=34s) happened with our +# auto_hibern8=5000 write + suspend-thrash; the write is the prime suspect +# and its benefit was never measured. + +# battmgr power_supply events are wakeup sources, and a PPS charger streams +# telemetry ~1/s — that turned suspend cycling into 1.2 s suspend-thrash on +# the PD brick. Drop the psy wakeup flags: the RTC alarm is our only +# intended waker (unplug is then detected at the next wake, <=45 s). +for w in /sys/class/power_supply/qcom-battmgr-*/device/power/wakeup \ + /sys/class/power_supply/qcom-battmgr-*/power/wakeup; do + [ -w "$w" ] && echo disabled > "$w" 2>/dev/null && log "wakeup disabled: $w" +done + +suspend_enabled=false +[ -e /etc/charging-mode/enable-suspend ] && suspend_enabled=true +log "suspend cycling: $suspend_enabled (flag /etc/charging-mode/enable-suspend)" + +check_unplug() { + # debounced: three reads ~3 s apart must all say offline + n=0 + while [ "$(cat "$USB/online" 2>/dev/null || echo 1)" = "0" ]; do + n=$((n + 1)) + if [ "$n" -ge 3 ]; then + log "charger removed (capacity $(cat "$BAT/capacity" 2>/dev/null)%) - powering off" + exec systemctl poweroff + fi + sleep 3 + done +} + +sample() { + log "sample: cap $(cat "$BAT/capacity" 2>/dev/null)% status $(cat "$BAT/status" 2>/dev/null) bat $(cat "$BAT/current_now" 2>/dev/null)uA cc $(cat "$BAT/charge_counter" 2>/dev/null)uAh usb $(cat "$USB/voltage_now" 2>/dev/null)uV $(cat "$USB/current_now" 2>/dev/null)uA icl $(cat "$USB/input_current_limit" 2>/dev/null)uA online $(cat "$USB/online" 2>/dev/null)" +} + +suspending() { + $suspend_enabled && [ ! -e /run/charging-mode-ssh ] && [ ! -e /run/charging-mode-display ] +} + +tick=0 +thrash=0 +while :; do + check_unplug + # when suspend-cycling, log a full sample at EVERY wake — the persistent + # journal is the offline logger for charger tests where no ssh path + # exists (e.g. the PD brick occupying the USB port); in awake mode log + # once a minute + if suspending || [ $((tick % 12)) -eq 0 ]; then + sample + fi + tick=$((tick + 1)) + + if suspending && [ -w "$WAKEALARM" ]; then + echo 0 > "$WAKEALARM" 2>/dev/null + echo "+$SUSPEND_SECS" > "$WAKEALARM" 2>/dev/null + if [ -n "$(cat "$WAKEALARM" 2>/dev/null)" ]; then + t0=$(date +%s) + echo mem > /sys/power/state 2>/dev/null || { log "suspend write failed - falling back to sleep"; sleep 30; } + dur=$(( $(date +%s) - t0 )) + log "slept ${dur}s" + # thrash guard: instant resumes back-to-back nearly killed UFS + # on the PD brick (2026-08-24) — three short cycles in a row + # means something wakes us constantly; stop suspending and stay + # in awake mode (still fine: any real charger out-supplies it) + if [ "$dur" -lt 10 ]; then + thrash=$((thrash + 1)) + if [ "$thrash" -ge 3 ]; then + log "suspend thrash (3 consecutive <10s cycles) - disabling suspend cycling this session" + suspend_enabled=false + fi + else + thrash=0 + fi + else + log "could not arm RTC wakealarm - NOT suspending (sleep fallback)" + sleep 30 + fi + else + sleep 5 + fi +done diff --git a/aports/device/fp6-charging-mode/charging-usbnet.service b/aports/device/fp6-charging-mode/charging-usbnet.service new file mode 100644 index 0000000..bdc43ba --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-usbnet.service @@ -0,0 +1,9 @@ +[Unit] +Description=Static USB gadget network for charging mode +# Only ever pulled by charging.target — replaces NetworkManager's +# "Developer Mode" profile there so no wifi stack starts in charging mode. + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/lib/charging-mode/charging-usbnet.sh diff --git a/aports/device/fp6-charging-mode/charging-usbnet.sh b/aports/device/fp6-charging-mode/charging-usbnet.sh new file mode 100755 index 0000000..6ab338b --- /dev/null +++ b/aports/device/fp6-charging-mode/charging-usbnet.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Give the USB gadget interface its usual static address (the one the NM +# "Developer Mode" profile would assign) without NetworkManager. The +# workstation side self-configures 172.16.42.2 via its udev rule, so +# `ssh fp6usb4` works in charging mode. + +IF="" +for i in $(seq 1 30); do + IF=$(ls /sys/class/net 2>/dev/null | grep '^usb' | head -1) + [ -n "$IF" ] && break + sleep 1 +done +[ -n "$IF" ] || exit 0 + +ip addr replace 172.16.42.1/16 dev "$IF" +ip link set "$IF" up diff --git a/aports/device/fp6-charging-mode/charging.target b/aports/device/fp6-charging-mode/charging.target new file mode 100644 index 0000000..92aba23 --- /dev/null +++ b/aports/device/fp6-charging-mode/charging.target @@ -0,0 +1,26 @@ +# Offline charging mode: minimal boot target for charger-triggered boots. +# Reached only via charging-mode-generator (never enabled, never the static +# default). basic.target gives us udev coldplug -> qcom_q6v5_pas module -> +# ADSP boot -> pmic_glink/battmgr (all builtin), i.e. battery telemetry and +# the ADSP-owned charging state machine, with no UI stack and no greeter. +# +# systemd-logind handles the power key (generator drops HandlePowerKey=reboot +# into /run for this boot only). + +[Unit] +Description=Offline charging mode +Requires=basic.target +After=basic.target rescue.service rescue.target +Conflicts=rescue.service rescue.target +Wants=charging-monitor.service systemd-logind.service +AllowIsolate=yes + +# Observability (r3): ssh is OFF by default — volume-down enables it +# (charging-keys starts systemd-user-sessions + charging-usbnet + sshd, USB +# cable only, no NetworkManager/wifi ever). systemd-user-sessions is needed +# at that moment because we bypass multi-user.target and sshd would +# otherwise refuse unprivileged logins via pam_nologin. +# Radios/sensors are kept out via ConditionPathExists drop-ins on +# bluetooth/iio-sensor-proxy/hkdm (marker: /run/charging-mode) and the +# monitor stops the modem + cdsp remoteprocs. +Wants=charging-keys.service diff --git a/build.sh b/build.sh index a574c3f..788a2b6 100755 --- a/build.sh +++ b/build.sh @@ -118,6 +118,7 @@ cp -r aports/device/linux-postmarketos-qcom-milos "$WORK/pmaports/device/testing cp -r aports/device/soc-fairphone-fp6-audio "$WORK/pmaports/device/" cp -r aports/device/callaudioshim "$WORK/pmaports/device/" cp -r aports/device/fp6-device-tweaks "$WORK/pmaports/device/" +cp -r aports/device/fp6-charging-mode "$WORK/pmaports/device/" cp -r aports/device/catcrafts-fp6-repo "$WORK/pmaports/device/" cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/" cp -r aports/modem/imsd "$WORK/pmaports/modem/" @@ -200,7 +201,7 @@ aports = $WORK/pmaports device = fairphone-fp6 ui = plasma-mobile systemd = always -extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fp6-device-tweaks,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel +extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel EOF # All five source tarballs are generated locally above, so every checksum