uzbek-plus/b17/aurora-modem

405 lines
28 KiB
Bash

#!/bin/sh
# Aurora (JZ08AU, MSM8916) modem lifecycle controller - B6P production build of the B6N-proven controller (b6j/aurora-modem).
# aurora-modem start | disconnect | stop | status | addroute <ipv4>
# start: [rmtfs -r -P -s -v -> MSS -> wwan0qmi0 (+2 s port-race guard) -> SIM ready -> DMS online (one SET, retried only on DeviceNotReady)
# -> registration state machine (registered + PS attached, deadline REG_TIMEOUT, default 600 s)] (skipped when rmtfs+MSS already run)
# -> udevd/dbus/polkitd/ModemManager (reused if running) -> ONE simple-connect -> bearer IPv4 on wwanN + /32 routes only (no default route).
# disconnect: simple-disconnect, our routes, flush+down all wwan*, wait BAM-DMUX suspended. MSS, rmtfs, MM and radio stay up; 'start' reconnects.
# stop: disconnect -> stop MM (+qmi-proxy) -> BAM-DMUX suspended -> SIGTERM rmtfs (rmtfs writes 'stop' -> MSS offline) -> rmtfs exits last.
# Never writes eMMC/NV: rmtfs -r keeps EFS writes in RAM; refuses to start unless modemst1/2, fsc, fsg, persist, mmcblk0 are getro=1 and /firmware is ro.
# Does not need a valid wall clock (all timing from /proc/uptime). No automatic MSS/rmtfs retry.
# B7C: time bootstrap. At fixed points of start (QMI_READY, SIM_READY, RADIO_ONLINE, each REG transition, REGISTERED, BEARER_CONNECTED) logs QMI DMS
# + 3GPP NITZ. The first valid DMS (>= 2026, >= image build, plausible, |DMS-NITZ| < 2 s; NITZ required unless TIME_REQUIRE_NITZ=0) steps
# CLOCK_REALTIME once with date -s (never backwards, never RTC). After START OK, a background SNTP check (VNIIFTRI, query only) logs the residual
# and steps only if |offset| >= TIME_NTP_STEP s. No slew/discipline: STA_UNSYNC stays set, so the kernel RTC_SYSTOHC 11-min RTC write never engages.
# B17: the USB-management ping (480s 172.16.42.2) during registration is OFF unless MGMT_CHECK=1 in $CONF — the board must
# work standalone (Wi-Fi router on battery/charger, no host). The post-connect "mgmt route still via usb0" check runs only if usb0 has IPv4.
# Exit: 0 ok, 1 failure/timeout (start rolls back with a full stop unless MODEM_KEEP_ON_FAIL=1), 2 usage/config error.
CONF=${MODEM_CONF:-/etc/aurora/modem.conf}
D=/run/aurora-modem; mkdir -p $D
LOG=$D/lifecycle.log; STATE=$D/state; ROUTES=$D/routes; BEARER=$D/bearer
RPNAME=4080000.remoteproc
BAM=/sys/bus/platform/devices/4080000.remoteproc:bam-dmux
Q="qmicli -p -d /dev/wwan0qmi0"
# --- timeouts (s); override in $CONF
T_RMTFS=5 T_MSS=60 T_QMI_DEV=30 T_QMI=45 T_SIM=60 T_ONLINE=30 T_REG=180 T_MM_DETECT=150 T_MM_REG=60 T_CONNECT=150
T_DISCONNECT=60 T_DISABLE=30 T_MM_EXIT=30 T_BAM_IDLE=15 T_MSS_STOP=30 T_RMTFS_EXIT=15
APN= USER= PASSWORD= AUTH= IP_TYPE=ipv4 REG_TIMEOUT=600 ROUTES= KEEP=${MODEM_KEEP_ON_FAIL:-0}
MGMT_CHECK=0 TIME_BOOTSTRAP=1 TIME_REQUIRE_NITZ=1 TIME_MAX_DMS_NITZ=2 TIME_NTP="ntp1.vniiftri.ru ntp2.vniiftri.ru ntp3.vniiftri.ru" TIME_NTP_STEP=1
[ -f "$CONF" ] && . "$CONF"
MODEM_APN=$APN MODEM_USER=$USER MODEM_PASSWORD=$PASSWORD MODEM_AUTH=$AUTH MODEM_IP_TYPE=${IP_TYPE:-ipv4} MODEM_TEST_ROUTES=$ROUTES T_REG=${REG_TIMEOUT:-600}
MODEM_KEEP_ON_FAIL=${MODEM_KEEP_ON_FAIL:-$KEEP}
mmc() { mmcli "$@" 2>/dev/null | tr -d '\033' | sed 's/\[[0-9;]*m//g'; } # mmcli colours values even without a tty
now() { cut -d' ' -f1 /proc/uptime; }
log() { m="[$(now)] $*"; echo "$m" >> $LOG; echo "$m"; echo "<5>[aurora-modem] $*" > /dev/kmsg 2>/dev/null; }
setstate() { o=$(cat $STATE 2>/dev/null); echo "$1" > $STATE; log "STATE ${o:-OFF} -> $1"; }
die() { log "FAIL: $*"; exit 1; }
# wait_for TIMEOUT INTERVAL cmd... : poll a real condition, no blind sleeps
wait_for() { _to=$1; _iv=$2; shift 2; _t0=$(now)
while :; do "$@" && return 0
[ "$(awk -v a="$(now)" -v b="$_t0" -v t="$_to" 'BEGIN{print (a-b>=t)}')" = 1 ] && return 1
sleep $_iv; done; }
since() { awk -v a="$(now)" -v b="$1" 'BEGIN{printf "%.1f", a-b}'; }
rp() { for r in /sys/class/remoteproc/remoteproc*; do [ "$(cat $r/name 2>/dev/null)" = $RPNAME ] && echo $r && return; done; }
mss_state() { r=$(rp); [ -n "$r" ] && cat $r/state || echo none; }
mss_is() { [ "$(mss_state)" = "$1" ]; }
rmtfs_pid() { pidof rmtfs; }
mm_pid() { pidof ModemManager; }
bam_pm() { cat $BAM/power/runtime_status 2>/dev/null || echo none; }
bam_suspended() { [ "$(bam_pm)" = suspended ]; }
qmi_dev() { [ -e /dev/wwan0qmi0 ] && [ -e /sys/class/net/wwan0 ]; }
link_up() { [ $(( $(cat /sys/class/net/$1/flags) & 1 )) -ne 0 ]; }
mm_idx() { mmc -L | grep -oE "Modem/[0-9]+" | head -1 | cut -d/ -f2; }
mm_has_modem() { [ -n "$(mm_idx)" ]; }
mm_state() { mmc -m "$(mm_idx)" | awk -F': ' '/\| +state: /{gsub(/ /,"",$2); print $2; exit}'; }
# MM initialized: proven session 22 pass 1 connected from 'disabled' (Simple.Connect enables the modem itself)
mm_usable() { case "$(mm_state)" in disabled|enabling|enabled|searching|registered|connected|connecting) return 0;; esac; return 1; }
qmi_ok() { timeout 20 $Q --dms-get-operating-mode > $D/qmi-mode.txt 2>&1 && grep -q "Mode:" $D/qmi-mode.txt; }
qmi_reg() { timeout 20 $Q --nas-get-serving-system > $D/qmi-ss.txt 2>&1
grep -q "Registration state: 'registered'" $D/qmi-ss.txt && grep -q "PS: 'attached'" $D/qmi-ss.txt; }
sim_ready() { timeout 20 $Q --uim-get-card-status > $D/qmi-uim.txt 2>&1
grep -q "Card state: 'present'" $D/qmi-uim.txt && grep -q "Application state: 'ready'" $D/qmi-uim.txt; }
set_online() { timeout 20 $Q --dms-set-operating-mode=online > $D/qmi-set.txt 2>&1; r=$?; cat $D/qmi-set.txt >> $LOG
[ $r = 0 ] && return 0; grep -q "DeviceNotReady" $D/qmi-set.txt || { echo hard > $D/qmi-set.err; return 0; }; return 1; }
# this mmcli has no --list-bearers ("no actions specified"); use 'Bearer | paths:' of mmcli -m, not the initial (attach) bearer
bearers() { mmc -m "$(mm_idx)" | grep -v "initial bearer" | grep -oE "Bearer/[0-9]+" | cut -d/ -f2; }
any_bearer_connected() { for b in $(bearers); do mmc -b $b | grep -q "connected: yes" && return 0; done; return 1; }
qv() { grep -m1 -oE "$1: *'[^']*'" $2 2>/dev/null | sed -E "s/.*'([^']*)'/\1/"; }
# B6M registration state machine. rc 0 = registered+PS attached, 1 = REGISTRATION_TIMEOUT, 2 = local failure (reason in $D/reg-fail)
reg_sm() {
T=$D/reg-trans.tsv; t0=$1; last=; n=0; nd=0; rm -f $D/reg-fail; D0=$(dmesg | wc -l)
printf 'uptime t_online reg ps status true_status domain plmn earfcn pci si_tac si_eci cl_tac cl_eci rsrp rsrq snr
' > $T
lf() { echo "$*" > $D/reg-fail; return 2; }
while :; do n=$((n+1))
mss_is running || { lf "MSS state $(mss_state)"; return 2; }
[ -e /dev/wwan0qmi0 ] || { lf "wwan0qmi0 disappeared"; return 2; }
[ -n "$(rmtfs_pid)" ] || { lf "rmtfs died"; return 2; }
[ "$(bam_pm)" = error ] && { lf "BAM-DMUX runtime_status=error"; return 2; }
dmesg | tail -n +$((D0+1)) | grep -qE 'crash detected|fatal error|[Ww]atchdog|wdog|Oops|BUG:|Failed to resume' && { lf "dmesg: $(dmesg | tail -n +$((D0+1)) | grep -m1 -E 'crash detected|fatal error|[Ww]atchdog|wdog|Oops|BUG:|Failed to resume')"; return 2; }
if [ $((n % 5)) = 1 ]; then
sim_ready || { lf "SIM not ready: $(grep -E 'Card state|Application state' $D/qmi-uim.txt | head -2 | tr -s ' \t\n' ' ')"; return 2; }
if [ "$MGMT_CHECK" = 1 ]; then
ping -c1 -W2 172.16.42.2 >/dev/null 2>&1 || { sleep 1; ping -c1 -W2 172.16.42.2 >/dev/null 2>&1 || { lf "USB management (usb0 ping) lost"; return 2; }; }
fi
fi
timeout 20 $Q --nas-get-serving-system > $D/qmi-ss.txt 2>&1
timeout 20 $Q --nas-get-system-info > $D/qmi-si-all.txt 2>&1; sed -n '/LTE service:/,/SIM reject/p' $D/qmi-si-all.txt > $D/qmi-si.txt
RG=$(qv 'Registration state' $D/qmi-ss.txt); PS=$(qv PS $D/qmi-ss.txt); ST=$(qv Status $D/qmi-si.txt); TS=$(qv 'True Status' $D/qmi-si.txt)
DM=$(qv Domain $D/qmi-si.txt); PL=$(qv MCC $D/qmi-si.txt)-$(qv MNC $D/qmi-si.txt); SIT=$(qv 'Tracking Area Code' $D/qmi-si.txt); SIE=$(qv 'Cell ID' $D/qmi-si.txt)
key="$RG|$PS|$ST|$TS|$DM|$PL|$SIT|$SIE"
if [ "$key" != "$last" ]; then
timeout 20 $Q --nas-get-cell-location-info > $D/qmi-cl.txt 2>&1; timeout 20 $Q --nas-get-signal-info > $D/qmi-sig.txt 2>&1
[ "$RG" = registration-denied ] && [ "${last%%|*}" != registration-denied ] && nd=$((nd+1))
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' $(now) $(since $t0) "$RG" "$PS" "$ST" "$TS" "$DM" "$PL" \
"$(qv 'EUTRA Absolute RF Channel Number' $D/qmi-cl.txt)" "$(qv 'Serving Cell ID' $D/qmi-cl.txt)" "$SIT" "$SIE" \
"$(qv 'Tracking Area Code' $D/qmi-cl.txt)" "$(qv 'Global Cell ID' $D/qmi-cl.txt)" "$(qv RSRP $D/qmi-sig.txt)" "$(qv RSRQ $D/qmi-sig.txt)" "$(qv SNR $D/qmi-sig.txt)" >> $T
log "REG t+$(since $t0)s: $RG / PS $PS / $ST ($TS, $DM) $PL pci=$(qv 'Serving Cell ID' $D/qmi-cl.txt) si=$SIT/$SIE cl=$(qv 'Tracking Area Code' $D/qmi-cl.txt)/$(qv 'Global Cell ID' $D/qmi-cl.txt)"
last=$key; tprobe "REG:$RG/$PS"
fi
echo $nd > $D/reg-denied-count
[ "$RG" = registered ] && [ "$PS" = attached ] && return 0
[ "$(awk -v a="$(now)" -v b="$t0" -v t="$T_REG" 'BEGIN{print (a-b>=t)}')" = 1 ] && { lf "REGISTRATION_TIMEOUT after ${T_REG}s ($RG, PS $PS)"; return 1; }
sleep 3
done
}
bearer_field() { awk -F': ' -v k="$1" '$1 ~ " "k"$" {gsub(/ /,"",$2); print $2; exit}' $BEARER.txt; }
# --- B7C time bootstrap. Variables are _t-prefixed: tprobe runs inside reg_sm (n, T, last, t0 belong to it).
NTPBB=/usr/libexec/aurora/busybox; GPS_EPOCH=315964800; Y2026=1767225600
time_floor() { _tf=$(cat /etc/aurora/build-epoch 2>/dev/null); case "$_tf" in ''|*[!0-9]*) _tf=$Y2026;; esac; [ $_tf -lt $Y2026 ] && _tf=$Y2026; echo $_tf; }
# tprobe PHASE: read DMS + NITZ (raw kept in $D/time-probe-N.*), validate, log; first VALID sample while the clock is still pre-build -> tset
tprobe() {
[ "$TIME_BOOTSTRAP" = 1 ] || return 0
_tn=$(( $(cat $D/time-probes 2>/dev/null || echo 0) + 1 )); echo $_tn > $D/time-probes; _tp=$D/time-probe-$_tn
_u0=$(now); timeout 5 $Q --dms-get-time > $_tp.dms 2>&1; _u1=$(now); timeout 5 $Q --nas-get-network-time > $_tp.nitz 2>&1; _u2=$(now)
_ms=$(sed -n "s/.*System time: '\([0-9]*\).*/\1/p" $_tp.dms); _nd=$(qv '3GPP Date' $_tp.nitz); _nt=$(qv '3GPP Time' $_tp.nitz)
_ne=; [ -n "$_nd" ] && [ -n "$_nt" ] && _ne=$(date -u -d "$_nd $_nt" +%s 2>/dev/null); _clk=$(date -u +%s)
# -> "VERDICT dms_utc dms_uptime dms-nitz reason"; dms-nitz compares DMS projected to the NITZ read instant with whole-second NITZ
_v=$(awk -v ms="$_ms" -v g=$GPS_EPOCH -v u0=$_u0 -v u1=$_u1 -v u2=$_u2 -v ne="$_ne" -v fl=$(time_floor) -v y=$Y2026 -v rn=$TIME_REQUIRE_NITZ -v mx=$TIME_MAX_DMS_NITZ 'BEGIN{
if (ms !~ /^[0-9]+$/) { print "INVALID - - - dms-unreadable"; exit }
dms=ms/1000+g; at=(u0+u1)/2; r="ok"; d="-"
if (dms < y) r="year<2026"; else if (dms < fl) r="before-build"; else if (dms > fl+5*365*86400) r="implausible"
if (ne ~ /^[0-9]+$/) { dd=dms+((u1+u2)/2-at)-ne; d=sprintf("%+.2f", dd); if (r=="ok" && (dd<-mx || dd>mx)) r="dms-nitz-mismatch" }
else if (r=="ok" && rn==1) r="no-nitz"
printf "%s %.3f %.2f %s %s\n", (r=="ok" ? "VALID" : "INVALID"), dms, at, d, r }')
set -- "$1" $_v
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' $_tn "$1" $_u0 "${_ms:--}" "$3" "${_nd:--}T${_nt:--}" "$5" $_clk "$2/$6" >> $D/time.tsv
log "TIME probe $_tn [$1]: dms_ms=${_ms:-none} dms_utc=$3 nitz=${_nd:-none} ${_nt} dms-nitz=$5 clock=$_clk -> $2 ($6)"
[ "$2" = VALID ] && [ ! -f $D/time-set ] && [ $_clk -lt $(time_floor) ] && tset "$1" $3 $4 $5
return 0; }
# tset PHASE DMS_UTC DMS_UPTIME DIFF: step CLOCK_REALTIME once, landing on a whole UTC second (uptime-based wait, no fixed bias correction)
tset() {
_N=$(awk -v d=$2 -v a=$3 -v u=$(now) 'BEGIN{printf "%d", int(d+(u-a))+1}')
_w=$(awk -v N=$_N -v d=$2 -v a=$3 -v u=$(now) 'BEGIN{w=N-(d+(u-a)); printf "%.3f", (w>0 ? w : 0)}')
sleep $_w; _old=$(date -u +%s); [ $_N -gt $_old ] || { log "TIME SET skipped: target $_N not after clock $_old (never backwards)"; return 0; }
date -u -s @$_N > /dev/null 2>&1; _rc=$?
[ $_rc = 0 ] && echo "$_N $(now) $1 qmi-dms" > $D/time-set
log "TIME SET rc=$_rc source=qmi-dms phase=$1 old=$_old ($(date -u -d @$_old '+%F %T')) new=$_N ($(date -u '+%F %T')) uptime=$(now) dms-nitz=$4 validation=VALID rtc=untouched"; }
# after START OK, background: resolve VNIIFTRI via bearer DNS, /32 routes via addroute (removed by stop/disconnect), ntpd -w (query only)
time_sntp() {
[ "$TIME_BOOTSTRAP" = 1 ] && [ -x $NTPBB ] && [ -n "$TIME_NTP" ] || { log "SNTP: skipped (bootstrap=$TIME_BOOTSTRAP bin=$([ -x $NTPBB ] && echo yes || echo no))"; return 0; }
. $D/ipcfg 2>/dev/null || return 0; _dns=${DNS%%,*}; _ips=
for _h in $TIME_NTP; do _ip=$(nslookup $_h $_dns 2>/dev/null | awk '/^Name:/{f=1} f && /^Address/{print $2; exit}')
case "$_ip" in [0-9]*.[0-9]*.[0-9]*.[0-9]*) addroute $_ip && _ips="$_ips $_ip";; *) log "SNTP: $_h not resolved via $_dns";; esac; done
[ -n "$_ips" ] || return 0
_a=; for _ip in $_ips; do _a="$_a -p $_ip"; done
timeout 15 $NTPBB ntpd -w -d $_a > $D/sntp.txt 2>&1
_b=$(awk '/reply from/{o="";d=""; for(i=1;i<=NF;i++){ if($i~/^offset:/) o=substr($i,8); if($i~/^delay:/) d=substr($i,7) } if(o!="" && (bd=="" || d+0<bd+0)){b=o; bd=d; s=$4}}
END{ if (b!="") print b, bd, s }' $D/sntp.txt)
log "SNTP check ($_ips): $(grep -c 'reply from' $D/sntp.txt) replies; best offset delay server: ${_b:-none} (offset > 0 = clock behind)"
[ -n "$_b" ] || return 0
if [ "$(awk -v o=${_b%% *} -v s=$TIME_NTP_STEP 'BEGIN{print (o>=s || o<=-s)}')" = 1 ]; then
_old=$(date -u +%s); _N=$(awk -v c=$_old -v o=${_b%% *} 'BEGIN{printf "%d", c+o+0.5}'); date -u -s @$_N > /dev/null 2>&1
log "SNTP STEP rc=$? old=$_old new=$_N (|offset| >= ${TIME_NTP_STEP}s) rtc=untouched"
else log "SNTP: |offset| < ${TIME_NTP_STEP}s - clock kept (no slew: STA_UNSYNC stays, RTC_SYSTOHC never engages)"; fi; }
check_safety() {
for p in modemst1 modemst2 fsc fsg persist; do
d=/dev/disk/by-partlabel/$p; [ -e $d ] || return 1
[ "$(blockdev --getro $(readlink -f $d))" = 1 ] || { log "SAFETY: $p not RO"; return 1; }
done
if [ "$(blockdev --getro /dev/mmcblk0)" != 1 ]; then # B12P: the disk flag may be lifted only for the Alpine userdata mount
_ud=$(readlink -f /dev/disk/by-partlabel/userdata 2>/dev/null)
for x in /dev/mmcblk0p* /dev/mmcblk0boot0 /dev/mmcblk0boot1; do [ "$x" = "$_ud" ] && continue
[ "$(blockdev --getro $x)" = 1 ] || { log "SAFETY: mmcblk0 not RO ($x rw)"; return 1; }; done
fi
grep -q " /firmware .* ro," /proc/mounts || { log "SAFETY: /firmware not mounted ro"; return 1; }
[ "$(cat /sys/module/firmware_class/parameters/path)" = /firmware/image ] || { log "SAFETY: firmware path"; return 1; }
return 0
}
connect_args() {
for v in "$MODEM_APN" "$MODEM_USER" "$MODEM_PASSWORD" "$MODEM_AUTH" "$MODEM_IP_TYPE"; do
case "$v" in *[,\"\'=]*) echo "BAD"; return;; esac; done
a="apn=$MODEM_APN"
[ -n "$MODEM_USER" ] && a="$a,user=$MODEM_USER"
[ -n "$MODEM_PASSWORD" ] && a="$a,password=$MODEM_PASSWORD"
[ -n "$MODEM_AUTH" ] && a="$a,allowed-auth=$MODEM_AUTH"
echo "$a,ip-type=$MODEM_IP_TYPE"
}
addroute() { # /32 via the bearer interface, recorded so stop removes exactly what we added
ip=$1; . $D/ipcfg 2>/dev/null || { echo "no bearer ipcfg"; return 1; }
grep -qx "$ip" $ROUTES 2>/dev/null && return 0
ip route add $ip/32 dev $IF src $A && echo $ip >> $ROUTES
}
diag() { # snapshot before rollback
{ echo "### diag $(now) state=$(cat $STATE 2>/dev/null) mss=$(mss_state) rmtfs=$(rmtfs_pid) mm=$(mm_pid) bam=$(bam_pm)"
[ -n "$(mm_pid)" ] && { mmc -L; mmc -m "$(mm_idx)"; }
cat $D/qmi-mode.txt $D/qmi-ss.txt 2>/dev/null; dmesg | tail -40; } > $D/diag-$(now).txt 2>&1
}
fail_start() { log "FAIL in $(cat $STATE 2>/dev/null): $*"; diag
if [ "$MODEM_KEEP_ON_FAIL" = 1 ]; then log "MODEM_KEEP_ON_FAIL=1: leaving stack as is"
else log "rollback: full stop"; do_stop; fi
exit 1; }
do_start() {
T0=$(now); log "=== START (conf $CONF, apn '${MODEM_APN}', ip-type $MODEM_IP_TYPE)"
[ -n "$MODEM_APN" ] || { log "config: MODEM_APN empty - refusing (no APN guessing)"; exit 2; }
CA=$(connect_args); [ "$CA" = BAD ] && { log "config: forbidden character in APN/user/password/auth"; exit 2; }
check_safety || { log "FAIL: safety gate (RO) - nothing started"; exit 1; }
[ -n "$(rp)" ] || { log "FAIL: remoteproc $RPNAME not found"; exit 1; }
# OFF -> RMTFS_READY -> MPSS_BOOTING -> MPSS_RUNNING
if [ -n "$(rmtfs_pid)" ] && mss_is running; then log "rmtfs $(rmtfs_pid) + MSS running already - reuse"
elif [ -n "$(rmtfs_pid)" ] || ! mss_is offline; then
log "FAIL: inconsistent (rmtfs='$(rmtfs_pid)' MSS=$(mss_state)); run stop first"; exit 1
else
: > $ROUTES; rm -f $D/ipcfg
n0=$(grep -c 'registering services' $D/rmtfs.log 2>/dev/null)
LD_PRELOAD=/usr/lib/linebuf.so rmtfs -r -P -s -v >> $D/rmtfs.log 2>&1 &
echo $! > $D/rmtfs.pid
wait_for $T_RMTFS 0.2 sh -c "kill -0 $(cat $D/rmtfs.pid) 2>/dev/null && [ \$(grep -c 'registering services' $D/rmtfs.log) -gt ${n0:-0} ]" \
|| fail_start "rmtfs did not start (pid $(cat $D/rmtfs.pid))"
setstate RMTFS_READY
setstate MPSS_BOOTING
t=$(now); wait_for $T_MSS 0.2 mss_is running || fail_start "MSS not running after ${T_MSS}s (state $(mss_state))"
log "MSS running after $(since $t)s"
fi
setstate MPSS_RUNNING
# -> QMI_READY
t=$(now); wait_for $T_QMI_DEV 0.2 qmi_dev || fail_start "wwan0qmi0/wwan0 missing after ${T_QMI_DEV}s"
log "wwan0qmi0 + wwan0 present after $(since $t)s; +2 s (JZ02 rpmsg_wwan_ctrl port-race guard, proven start_post)"
sleep 2
[ "$(bam_pm)" = error ] && fail_start "BAM-DMUX runtime_status=error"
# -> RADIO_ONLINE -> REGISTERED (Aurora-proven: qmicli online + NAS poll before ModemManager)
if [ -n "$(mm_pid)" ]; then
log "ModemManager already running - skip qmicli, use MM state"
setstate QMI_READY
else
t=$(now); wait_for $T_QMI 1 qmi_ok || fail_start "QMI DMS not answering after ${T_QMI}s"
setstate QMI_READY; log "DMS $(grep -o "Mode: '[^']*'" $D/qmi-mode.txt) after $(since $t)s"; tprobe QMI_READY
t=$(now); wait_for $T_SIM 1 sim_ready || fail_start "SIM/USIM not ready after ${T_SIM}s ($(grep -E "Card state|Application state" $D/qmi-uim.txt | head -2 | tr -s ' \t\n' ' '))"
setstate SIM_READY; log "UIM card present, USIM app ready after $(since $t)s"; tprobe SIM_READY
if ! grep -q "Mode: 'online'" $D/qmi-mode.txt; then
log "SET: $Q --dms-set-operating-mode=online (retried only on DeviceNotReady, <= ${T_ONLINE}s)"
rm -f $D/qmi-set.err; t=$(now); wait_for $T_ONLINE 1 set_online || fail_start "SET online: DeviceNotReady for ${T_ONLINE}s"
[ -f $D/qmi-set.err ] && fail_start "SET online failed: $(grep -i error $D/qmi-set.txt | head -1)"
qmi_ok && grep -q "Mode: 'online'" $D/qmi-mode.txt || fail_start "mode not online after SET"
log "DMS online after $(since $t)s"
fi
setstate RADIO_ONLINE; tprobe RADIO_ONLINE
t=$(now); reg_sm $t; rr=$?
[ $rr = 2 ] && fail_start "LOCAL FAILURE during registration: $(cat $D/reg-fail)"
[ $rr = 1 ] && fail_start "$(cat $D/reg-fail)"
log "registered: $(grep -oE "(MCC|MNC|Description): '[^']*'" $D/qmi-ss.txt | tr '\n' ' ')after $(since $t)s"
setstate REGISTERED; tprobe REGISTERED
killall qmi-proxy 2>/dev/null; wait_for 5 0.2 sh -c '! pidof qmi-proxy >/dev/null'
fi
# -> MM_READY (mm-up order: udevd, trigger, settle, dbus, polkitd, ModemManager)
pidof udevd >/dev/null || { udevd --daemon; udevadm trigger --action=add; udevadm settle --timeout=15; log "udevd started"; }
if ! pidof dbus-daemon >/dev/null; then
mkdir -p /run/dbus /var/lib/dbus /var/lib/polkit; rm -f /run/dbus/pid; dbus-uuidgen --ensure
dbus-daemon --system --fork || fail_start "dbus-daemon"; log "dbus started"; fi
pidof polkitd >/dev/null || { /usr/lib/polkit-1/polkitd --no-debug >> $D/polkitd.log 2>&1 & log "polkitd started"; }
if [ -z "$(mm_pid)" ]; then
N=$(ls $D/mm-*.log 2>/dev/null | wc -l)
ModemManager --debug > $D/mm-$N.log 2>&1 &
echo $! > $D/mm.pid; log "ModemManager started pid $! (log mm-$N.log)"
fi
t=$(now); wait_for $T_MM_DETECT 2 mm_has_modem || fail_start "MM: no modem after ${T_MM_DETECT}s"
log "MM modem $(mm_idx) detected after $(since $t)s"
mmc -m "$(mm_idx)" > $D/mm-modem.txt
grep -q "plugin: qcom-soc" $D/mm-modem.txt || fail_start "MM plugin is not qcom-soc"
grep -q "primary port: wwan0qmi0" $D/mm-modem.txt || fail_start "MM primary port is not wwan0qmi0"
setstate MM_READY
t=$(now); wait_for $T_MM_REG 2 mm_usable || fail_start "MM state '$(mm_state)' not usable after ${T_MM_REG}s"
log "MM state $(mm_state) after $(since $t)s"
# -> BEARER_CONNECTED (exactly one simple-connect)
if any_bearer_connected; then log "bearer already connected - reuse"
else
log "mmcli -m $(mm_idx) --simple-connect=\"$(echo "$CA" | sed 's/password=[^,]*/password=***/')\""
timeout $T_CONNECT mmcli -m "$(mm_idx)" --simple-connect="$CA" >> $LOG 2>&1 || fail_start "simple-connect failed/timeout"
fi
BR=; for b in $(bearers); do mmc -b $b | grep -q "connected: yes" && BR=$b; done
[ -n "$BR" ] || fail_start "no connected bearer after simple-connect"
mmc -m "$(mm_idx)" > $D/mm-modem.txt
grep -qE "registration: (home|roaming)" $D/mm-modem.txt || fail_start "MM registration not home/roaming after connect"
log "MM state $(mm_state), $(grep -oE "registration: [a-z]+" $D/mm-modem.txt)"
mmc -b $BR > $BEARER.txt
IF=$(bearer_field interface); A=$(bearer_field address); PF=$(bearer_field prefix); GW=$(bearer_field gateway)
DNS=$(bearer_field dns); MTU=$(bearer_field mtu)
echo "$IF" | grep -qE '^wwan[0-7]$' && [ -n "$A" ] && [ -n "$PF" ] || fail_start "bearer $BR bad ipv4 config IF=$IF A=$A PF=$PF"
printf 'BR=%s\nIF=%s\nA=%s\nPF=%s\nGW=%s\nDNS=%s\nMTU=%s\n' "$BR" "$IF" "$A" "$PF" "$GW" "$DNS" "$MTU" > $D/ipcfg
log "bearer $BR: $IF $A/$PF gw $GW dns $DNS mtu $MTU"
ip addr flush dev $IF; ip addr add $A/$PF dev $IF || fail_start "ip addr add"
[ -n "$MTU" ] && ip link set $IF mtu $MTU
ip link set $IF up || fail_start "ip link set $IF up (bam-dmux runtime PM?)"
link_up $IF || fail_start "$IF not UP"
for h in $(echo "$DNS" | tr ',' ' ') $MODEM_TEST_ROUTES; do addroute $h || fail_start "route $h"; done
if ip -4 addr show usb0 2>/dev/null | grep -q "inet "; then
ip route get 172.16.42.2 2>&1 | grep -q "dev usb0" || fail_start "management route no longer via usb0"
fi
ip route | grep -q "^default" && log "WARNING: a default route exists (not created by us)"
setstate BEARER_CONNECTED
log "=== START OK in $(since $T0)s ($IF $A/$PF, routes: $(tr '\n' ' ' < $ROUTES))"
tprobe BEARER_CONNECTED; ( time_sntp ) &
}
do_stop() {
T0=$(now); log "=== STOP (from $(cat $STATE 2>/dev/null || echo OFF): mss=$(mss_state) rmtfs=$(rmtfs_pid) mm=$(mm_pid) bam=$(bam_pm))"
rc=0
# 1. disconnect bearer(s)
if [ -n "$(mm_pid)" ] && mm_has_modem; then
timeout $T_DISCONNECT mmcli -m "$(mm_idx)" --simple-disconnect >> $LOG 2>&1; log "simple-disconnect rc=$? (bearers: $(bearers | tr '\n' ' '))"
wait_for 15 1 sh -c "! sh $0 _anyconn" && log "no bearer connected" || { log "WARNING: bearer still connected"; rc=1; }
fi
# 2. our /32 routes, 3. flush + DOWN every wwan*
for h in $(cat $ROUTES 2>/dev/null); do ip route del $h/32 2>/dev/null; done; : > $ROUTES
for d in /sys/class/net/wwan*; do [ -e "$d" ] || continue; w=${d##*/}
ip addr flush dev $w 2>/dev/null; ip link set $w down 2>/dev/null
link_up $w && { log "ERROR: $w still UP"; rc=1; }; done
rm -f $D/ipcfg
ip route | grep -q " dev wwan" && { log "ERROR: routes via wwan remain: $(ip route | grep ' dev wwan' | tr '\n' ';')"; rc=1; }
log "wwan links down, addresses/routes removed"
[ -n "$(cat $STATE 2>/dev/null)" ] && setstate WWAN_DOWN
# 4. stop ModemManager (+ its qmi-proxy)
if [ -n "$(mm_pid)" ]; then
kill -TERM $(mm_pid); wait_for $T_MM_EXIT 0.5 sh -c '! pidof ModemManager >/dev/null' \
|| { log "MM did not exit in ${T_MM_EXIT}s - SIGKILL"; kill -KILL $(mm_pid) 2>/dev/null; rc=1; }
log "ModemManager stopped"
fi
pidof qmi-proxy >/dev/null && { killall qmi-proxy; wait_for 5 0.2 sh -c '! pidof qmi-proxy >/dev/null'; log "qmi-proxy stopped"; }
setstate MM_STOPPED
# 5. BAM-DMUX idle
if [ -e $BAM ] && ! mss_is offline; then
t=$(now); if wait_for $T_BAM_IDLE 0.5 bam_suspended; then log "BAM-DMUX suspended after $(since $t)s"
else log "ERROR: BAM-DMUX '$(bam_pm)' not suspended after ${T_BAM_IDLE}s - NOT stopping MSS"; exit 1; fi
fi
# 6. MSS stop via rmtfs -s (SIGTERM -> pwrite "stop" to remoteproc state -> exit), 7. rmtfs exits last
P=$(rmtfs_pid)
if [ -n "$P" ]; then
t=$(now); kill -TERM $P; log "SIGTERM rmtfs $P (rmtfs stops MSS, then exits)"
wait_for $T_MSS_STOP 0.2 mss_is offline && log "MSS offline after $(since $t)s (rmtfs alive: $(kill -0 $P 2>/dev/null && echo yes || echo no))" \
|| { log "ERROR: MSS '$(mss_state)' after ${T_MSS_STOP}s"; rc=1; }
setstate MPSS_STOPPED
wait_for $T_RMTFS_EXIT 0.2 sh -c "! kill -0 $P 2>/dev/null" && log "rmtfs exited after $(since $t)s" \
|| { log "rmtfs still alive - second SIGTERM"; kill -TERM $P; wait_for 5 0.2 sh -c "! kill -0 $P 2>/dev/null" || { kill -KILL $P; rc=1; }; }
elif ! mss_is offline && [ -n "$(rp)" ]; then
log "MSS $(mss_state) without rmtfs - echo stop > state"; echo stop > $(rp)/state
wait_for $T_MSS_STOP 0.2 mss_is offline || rc=1
fi
mss_is offline || mss_is none || { log "ERROR: MSS state $(mss_state)"; rc=1; }
setstate OFF
log "=== STOP done rc=$rc in $(since $T0)s (mss=$(mss_state) rmtfs='$(rmtfs_pid)' mm='$(mm_pid)' bam=$(bam_pm))"
return $rc
}
do_disconnect() { # B6N: bearer down only; MSS, rmtfs, MM (and DMS radio) stay up. Next 'start' reuses them and only simple-connects.
T0=$(now); log "=== DISCONNECT (from $(cat $STATE 2>/dev/null || echo OFF): mss=$(mss_state) rmtfs=$(rmtfs_pid) mm=$(mm_pid) bam=$(bam_pm))"; rc=0
[ -n "$(mm_pid)" ] && mm_has_modem || { log "DISCONNECT: no MM modem"; return 1; }
timeout $T_DISCONNECT mmcli -m "$(mm_idx)" --simple-disconnect >> $LOG 2>&1; log "simple-disconnect rc=$? (bearers: $(bearers | tr '\n' ' '))"
wait_for 15 1 sh -c "! sh $0 _anyconn" && log "no bearer connected" || { log "ERROR: bearer still connected"; rc=1; }
for h in $(cat $ROUTES 2>/dev/null); do ip route del $h/32 2>/dev/null; done; : > $ROUTES
for d in /sys/class/net/wwan*; do [ -e "$d" ] || continue; w=${d##*/}
ip addr flush dev $w 2>/dev/null; ip link set $w down 2>/dev/null
link_up $w && { log "ERROR: $w still UP"; rc=1; }; done
rm -f $D/ipcfg
ip route | grep -q " dev wwan" && { log "ERROR: routes via wwan remain: $(ip route | grep ' dev wwan' | tr '\n' ';')"; rc=1; }
t=$(now); wait_for $T_BAM_IDLE 0.5 bam_suspended && log "BAM-DMUX suspended after $(since $t)s" || { log "ERROR: BAM-DMUX '$(bam_pm)' not suspended"; rc=1; }
setstate MM_IDLE
log "=== DISCONNECT done rc=$rc in $(since $T0)s (mss=$(mss_state) rmtfs='$(rmtfs_pid)' mm='$(mm_pid)' mm_state=$(mm_state) bam=$(bam_pm))"
return $rc
}
do_status() {
r=$(rp)
echo "controller: $(cat $STATE 2>/dev/null || echo OFF)"
echo "remoteproc: $RPNAME $(mss_state) ($r) fw=$(cat $r/firmware 2>/dev/null)"
echo "rmtfs: pid='$(rmtfs_pid)' writes_logged=$(grep -c 'write ' $D/rmtfs.log 2>/dev/null)"
echo "qmi dev: $(ls /dev/wwan0qmi0 2>/dev/null || echo absent) qmi-proxy='$(pidof qmi-proxy)'"
echo "daemons: udevd='$(pidof udevd)' dbus='$(pidof dbus-daemon)' polkitd='$(pidof polkitd)' ModemManager='$(mm_pid)'"
if [ -n "$(mm_pid)" ] && mm_has_modem; then
mmc -m "$(mm_idx)" | grep -E "\| +(state|power state|registration|operator name|access tech|signal quality|plugin|primary port): " \
| sed -E 's/^[^|]*\| */ mm /'
for b in $(bearers); do echo " bearer $b: $(mmc -b $b | grep -E 'connected:|interface:|address:' | tr -s ' |' ' ' | tr '\n' ';')"; done
else echo " mm: n/a"; fi
for d in /sys/class/net/wwan*; do [ -e "$d" ] || continue; w=${d##*/}
link_up $w && echo "wwan: $w UP $(ip -4 addr show $w | awk '/inet /{print $2}')"; done
echo "wwan up: $(n=0; for d in /sys/class/net/wwan*; do [ -e $d ] && link_up ${d##*/} && n=$((n+1)); done; echo $n) of $(ls -d /sys/class/net/wwan* 2>/dev/null | wc -l)"
echo "routes: $(ip route | grep ' dev wwan' | tr '\n' ';')"
echo "bam-dmux: $(bam_pm) active_ms=$(cat $BAM/power/runtime_active_time 2>/dev/null) suspended_ms=$(cat $BAM/power/runtime_suspended_time 2>/dev/null) control=$(cat $BAM/power/control 2>/dev/null)"
echo "usb0: $(ip -4 addr show usb0 | awk '/inet /{print $2}') route-to-host: $(ip route get 172.16.42.2 2>&1 | head -1)"
echo "emmc: $(awk '{print "writes_completed="$5" sectors_written="$7}' /sys/block/mmcblk0/stat)"
echo "time: $(date -u '+%F %T') UTC; set: $(cat $D/time-set 2>/dev/null || echo no); probes: $(cat $D/time-probes 2>/dev/null || echo 0)"
echo "dmesg-errs: $(dmesg | grep -cE 'Failed to resume|Resource temporarily|crash detected|fatal error|[Ww]atchdog|Oops|Kernel panic|Unable to handle')"
}
case "$1" in
start) do_start;;
stop) do_stop; exit $?;;
disconnect) do_disconnect; exit $?;;
status) do_status;;
addroute) [ -n "$2" ] || exit 2; addroute "$2";;
_anyconn) any_bearer_connected;;
_mmstate) mm_state;;
*) echo "usage: $0 start|stop|status|addroute <ipv4>"; exit 2;;
esac