#!/bin/sh
#
# S82dcentrald - Start DCENTos mining daemon (am2-s19pro variant)
#
# This init script overlays on top of the shared board/zynq init script
# (via overlay-on-overlay in the defconfig). It adds am2-specific logic:
#
#   1. Reads /etc/dcentos/platform + /etc/dcentos/board_target, keeping the
#      broad AM2 carrier identity separate from the exact ASIC protocol.
#   2. Leaves this BM1398 image management-only; only exact BM1362 S19j target
#      stamps may inject --s19j-hybrid and its serial work recipe.
#   3. Scrubs inherited BM1362/lab environment before the explicit persistent
#      operator override in /data/dcentrald-env.
#   4. Leaves I2C ownership to dcentrald. The production .139 path can use
#      gpio_bitbang PSU transport and must not force-bind xiic-i2c from init.
#
# Variant scope: this overlay stamps am2-s19pro. Carrier-level AM2 fan safety is
# retained, but ASIC-specific dispatch requires a separate exact-family match.
#
# SAFETY: unchanged from shared script — command fans to PWM 30 on crash/timeout.
# NEVER PWM 127. Home mining.
#
# D-Central Technologies - DCENTos
#

DAEMON="/usr/local/bin/dcentrald"
PIDFILE="/var/run/dcentrald.pid"
CHILD_PIDFILE="/var/run/dcentrald-child.pid"
EXPECTFILE="/var/run/dcentrald.expected_exit.pid"
LOGFILE="/tmp/dcentrald.log"
FAN_BASE=0x42800000
# AM2 persistent fan-custodian pidfile (see am2_start_fan_custodian below).
FANHOLD_PIDFILE="/var/run/dcentrald-fanhold.pid"
FANHOLD_READYFILE="/var/run/dcentrald-fanhold.ready"
DEFAULT_COMPAT="/etc/default/dcentos-compat"
PERSISTENT_COMPAT="/data/dcentos-compat"
API_PORT=8080
SCRIPT="$0"
SESSION_LATCH_HELPER="/usr/libexec/dcentos/dcentrald-session-latch.sh"
PROCESS_IDENTITY_HELPER="/usr/libexec/dcentos/dcentrald-process-identity.sh"
FAN_CUSTODY_HELPER="/usr/libexec/dcentos/dcentrald-fan-custody.sh"
CRASH_LATCH_FILE="/data/dcent/dcentrald-hardware-session.crash-latched"
START_STOP_DAEMON=start-stop-daemon
FAN_CUSTODIAN_DAEMON=$DAEMON
FAN_CUSTODY_READY_WAIT=10
ZYNQ_LAUNCHER_TEST_MODE=0
if [ "${DCENT_TEST_ONLY_ZYNQ_LAUNCHER:-0}" = 1 ]; then
    ZYNQ_LAUNCHER_TEST_MODE=1
    DAEMON=${DCENT_TEST_DAEMON:-$DAEMON}
    LOGFILE=${DCENT_TEST_LOGFILE:-$LOGFILE}
    PIDFILE=${DCENT_TEST_PIDFILE:-$PIDFILE}
    CHILD_PIDFILE=${DCENT_TEST_CHILD_PIDFILE:-$CHILD_PIDFILE}
    EXPECTFILE=${DCENT_TEST_EXPECTFILE:-$EXPECTFILE}
    FANHOLD_PIDFILE=${DCENT_TEST_FANHOLD_PIDFILE:-$FANHOLD_PIDFILE}
    FANHOLD_READYFILE=${DCENT_TEST_FANHOLD_READYFILE:-$FANHOLD_READYFILE}
    PROCESS_IDENTITY_HELPER=${DCENT_TEST_PROCESS_IDENTITY_HELPER:-$PROCESS_IDENTITY_HELPER}
    FAN_CUSTODY_HELPER=${DCENT_TEST_FAN_CUSTODY_HELPER:-$FAN_CUSTODY_HELPER}
    START_STOP_DAEMON=${DCENT_TEST_START_STOP_DAEMON:-$START_STOP_DAEMON}
    FAN_CUSTODIAN_DAEMON=${DCENT_TEST_FAN_CUSTODIAN_DAEMON:-$DAEMON}
    FAN_CUSTODY_READY_WAIT=${DCENT_TEST_FAN_READY_WAIT:-$FAN_CUSTODY_READY_WAIT}
    case "$DAEMON:$LOGFILE:$PIDFILE:$CHILD_PIDFILE:$EXPECTFILE:$FANHOLD_PIDFILE" in
        /*:/*:/*:/*:/*:/*) ;;
        *) echo "  [FAIL] test-only Zynq launcher paths must be absolute" >&2; exit 1 ;;
    esac
    case "$START_STOP_DAEMON" in
        /*) ;;
        *) echo "  [FAIL] test-only start-stop-daemon path must be absolute" >&2; exit 1 ;;
    esac
    case "$FANHOLD_READYFILE:$FAN_CUSTODY_HELPER:$FAN_CUSTODIAN_DAEMON" in
        /*:/*:/*) ;;
        *) echo "  [FAIL] test-only fan-custody paths must be absolute" >&2; exit 1 ;;
    esac
fi
DCENT_PROCESS_IDENTITY_TEST_AUTHORITY=0
if [ "$ZYNQ_LAUNCHER_TEST_MODE" -eq 1 ] \
    && [ "$DAEMON" != /usr/local/bin/dcentrald ]; then
    DCENT_PROCESS_IDENTITY_TEST_AUTHORITY=1
fi
readonly DCENT_PROCESS_IDENTITY_TEST_AUTHORITY
PROCESS_IDENTITY_AVAILABLE=0
if [ -r "$PROCESS_IDENTITY_HELPER" ] && . "$PROCESS_IDENTITY_HELPER"; then
    PROCESS_IDENTITY_AVAILABLE=1
fi
FAN_CUSTODY_AVAILABLE=0
if [ "$PROCESS_IDENTITY_AVAILABLE" -eq 1 ] \
    && [ -r "$FAN_CUSTODY_HELPER" ] && . "$FAN_CUSTODY_HELPER"; then
    FAN_CUSTODY_AVAILABLE=1
fi
FAN_TRANSITION_LOCK_HELD=0

dcent_release_fan_transition_lock() {
    if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
        dcent_fan_lock_release "${FANHOLD_READYFILE}.lock" || true
        FAN_TRANSITION_LOCK_HELD=0
    fi
}

dcent_acquire_fan_transition_lock() {
    [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ] || return 1
    dcent_fan_lock_acquire "${FANHOLD_READYFILE}.lock" 10 || return 1
    FAN_TRANSITION_LOCK_HELD=1
    trap 'dcent_release_fan_transition_lock' 0
    trap 'exit 1' 1 2 15
}

# Use persistent config if available, otherwise default.
#
# Config search order (highest priority first):
#   1. /data/dcentrald.toml             — operator override (persists across upgrades)
#   2. /etc/dcentrald/xil_override.toml — baked am2 milestone-path override
#                                          (zynq-bm3-am2 only; see post-build.sh,
#                                          source: dcentrald/configs/dcentrald_s19jpro_xil.toml)
#   3. /etc/dcentrald.toml              — shared default (post-build.sh stamps
#                                          dcentrald_s19jpro_am2.toml here today)
if [ -f /data/dcentrald.toml ]; then
    CONFIG="/data/dcentrald.toml"
elif [ -f /etc/dcentrald/xil_override.toml ]; then
    CONFIG="/etc/dcentrald/xil_override.toml"
else
    CONFIG="/etc/dcentrald.toml"
fi

migrate_legacy_api_port() {
    [ -f "$CONFIG" ] || return 0

    # Rewrite legacy dcentrald API http_port 80 to 8080 so S80dashboard owns :80.
    if ! grep -Eq '^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$' "$CONFIG" 2>/dev/null; then
        return 0
    fi

    TMPFILE="${CONFIG}.tmp.$$"
    if awk '
        BEGIN { in_api = 0 }
        /^[[:space:]]*\[[^]]+\][[:space:]]*$/ {
            in_api = ($0 ~ /^[[:space:]]*\[api\][[:space:]]*$/)
        }
        in_api && /^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$/ {
            sub(/80/, "8080")
        }
        { print }
    ' "$CONFIG" > "$TMPFILE" 2>/dev/null; then
        if mv "$TMPFILE" "$CONFIG" 2>/dev/null; then
            echo "  [MIGRATE] dcentrald API http_port 80 to 8080 in $CONFIG"
        else
            rm -f "$TMPFILE"
            echo "  [WARN] Could not migrate legacy dcentrald API port in $CONFIG"
        fi
    else
        rm -f "$TMPFILE"
        echo "  [WARN] Could not inspect dcentrald API port in $CONFIG"
    fi
}

# Platform + board target are written by post-build.sh.
#   /etc/dcentos/platform     — "zynq-bm3-am2" (am2-s19jpro), "am1-s9" (S9)
#   /etc/dcentos/board_target — "am2-s19j"     (am2-s19jpro), "am1-s9" (S9)
# Both keys are consulted so a half-stamped image still routes correctly.
PLATFORM=""
[ -r /etc/dcentos/platform ] && PLATFORM=$(cat /etc/dcentos/platform 2>/dev/null)
BOARD_TARGET=""
[ -r /etc/dcentos/board_target ] && BOARD_TARGET=$(cat /etc/dcentos/board_target 2>/dev/null)
if [ "$ZYNQ_LAUNCHER_TEST_MODE" -eq 1 ]; then
    PLATFORM=${DCENT_TEST_PLATFORM:-$PLATFORM}
    BOARD_TARGET=${DCENT_TEST_BOARD_TARGET:-$BOARD_TARGET}
fi

# Extra CLI args populated per platform below.
EXTRA_ARGS=""

# am2 detection — either marker matches. Empty/unmatched values fall through
# to the S9 (am1-s9) launch shape unchanged.
IS_AM2=0
case "$PLATFORM" in
    zynq-bm3-am2) IS_AM2=1 ;;
esac
case "$BOARD_TARGET" in
    am2-s17|am2-s17p|am2-s17pro|am2-s17plus|am2-t17|am2-t17plus|am2-s19|am2-s19j|am2-s19jpro|am2-s19jpro-zynq|am2-s19pro|am2-t19) IS_AM2=1 ;;
esac

# The AM2 carrier identity is not an ASIC protocol identity. Only the exact
# BM1362 S19j targets may select the hybrid engine and its proven serial recipe;
# S17/BM1397 and S19 Pro/BM1398 images must remain management-only until their
# own engines are admitted.
IS_BM1362_HYBRID_TARGET=0
case "$BOARD_TARGET" in
    am2-s19j|am2-s19jpro|am2-s19jpro-zynq) IS_BM1362_HYBRID_TARGET=1 ;;
esac

# Scrub inherited protocol/lab state before the explicit /data override below.
# This makes an incomplete board stamp fail closed instead of inheriting a
# BM1362 wire recipe merely because it uses the same AM2 carrier.
unset DCENT_AM2_SKIP_FAST_UART
unset DCENT_AM2_SERIAL_WORK_DISPATCH
unset DCENT_AM2_SKIP_115200_PER_CHIP
unset DCENT_AM2_TRUST_DEGRADED_FW
unset DCENT_AM2_FORCE_FAST_UART
unset DCENT_AM2_FAST_UART_VALUE
unset DCENT_AM2_FAST_UART_BAUD
unset DCENT_AM2_FASTUART_SWITCH_HOST_FIRST
unset DCENT_AM2_WORK_TX_BOSMODE

if [ "$IS_BM1362_HYBRID_TARGET" = "1" ]; then
    EXTRA_ARGS="$EXTRA_ARGS --s19j-hybrid"
    export DCENT_AM2_SKIP_FAST_UART=1
    export DCENT_AM2_SERIAL_WORK_DISPATCH=1
    export DCENT_AM2_SKIP_115200_PER_CHIP=1
fi

# Operator escape hatch for live debugging on a flashed unit — sourced AFTER
# the milestone defaults so /data overrides win. Format: standard sh export
# lines. Never shipped in the image (/data/ is persistent storage).
if [ -r /data/dcentrald-env ]; then
    . /data/dcentrald-env
fi

# gpio907/PWR_CONTROL polarity normalization for am2-s19|am2-s19pro|am2-t19.
#
# Scrub any inherited value, then pin exactly one marker. Both halves matter:
#
#   * The `unset` prevents a stale or hostile environment (e.g. an operator
#     `/data/dcentrald-env`, sourced just above) from choosing the polarity the
#     terminal safe-off path will use.
#   * The `export` is REQUIRED for safe-off to run at all on this image. With
#     neither marker set, `dcentrald --safe-off` refuses outright ("AM2
#     PWR_CONTROL polarity unknown or conflicting", main.rs). As of 2026-07-27
#     this script DOES define `am2_pwr_control_polarity_known()` and a fan-only
#     fallback (see below), so an unset marker now degrades honestly instead of
#     silently removing the power cut — but the marker is still exported,
#     because fan-only is a degradation, not a power cut.
#
# HONEST RISK — polarity EVIDENCED BY FAMILY, declaration deliberately UNCHANGED
# (Wave 7, 2026-07-27). A six-lane investigation with per-lane adversarial
# refutation concluded gpio907 is ACTIVE-LOW (0 = rail ON): 4 lanes CONFIRMED at
# HIGH confidence, 0 refuted. Evidence:
#   * S17 Pro factory jig (UNSTRIPPED, Bitmain's own symbols): power_on writes
#     "0", power_off writes "1" on gpio907.
#   * Same convention across the 7007 class (S17, T17/T17e/S17e, DR5, S11).
#   * Live `.25` register dump: gpio907 value=0 while bosminer held the rail up.
#   * NOTHING anywhere writes the sysfs `active_low` attribute on this path.
#
# The S19 Pro leg is FAMILY-INFERRED, not model-exact: no decoded S19 Pro jig
# artifact resolves the pin yet. A held candidate exists and is decodable at the
# desk — knowledge-base/repos/amtc-s19pro-jig/single_board_test_bm1398 — which
# would upgrade this to model-exact. Until then this image is the WEAKER of the
# two evidence cases, and `.129` is a live S19 Pro that would be affected.
#
# Under the CURRENT export a terminal safe-off writes 0 — the jig's power-ON
# level — and the readback tautologically agrees, minting a FALSE safe-off proof.
# The export is nonetheless kept, deliberately, per operator decision: flipping
# it is a fleet-visible change to the only power-cut path on this image. The flip
# is OPERATOR-GATED. See docs/dev/2026-07-27-pwr-control-polarity/POLARITY_EVIDENCE.md.
#
# Do NOT simply delete this block; that reintroduces the silent no-cut.
unset DCENT_AM2_PWR_CONTROL_ACTIVE_LOW
unset DCENT_AM2_PWR_CONTROL_ACTIVE_HIGH
case "$BOARD_TARGET" in
    am2-s19|am2-s19pro|am2-t19) export DCENT_AM2_PWR_CONTROL_ACTIVE_HIGH=1 ;;
esac

bosminer_pic_bootstrap_enabled() {
    BOSMINER_PIC_BOOTSTRAP=0
    [ -f "$DEFAULT_COMPAT" ] && . "$DEFAULT_COMPAT"
    [ -f "$PERSISTENT_COMPAT" ] && . "$PERSISTENT_COMPAT"

    case "$BOSMINER_PIC_BOOTSTRAP" in
        1|true|TRUE|yes|YES|on|ON|enabled|ENABLED)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

compat_bootstrap_model_allowed() {
    BOSMINER_PIC_BOOTSTRAP_ALLOW_MODEL="am1-s9"
    [ -f "$DEFAULT_COMPAT" ] && . "$DEFAULT_COMPAT"
    [ -f "$PERSISTENT_COMPAT" ] && . "$PERSISTENT_COMPAT"

    [ "$BOSMINER_PIC_BOOTSTRAP_ALLOW_MODEL" = "am1-s9" ] || return 1
    [ -d /sys/class/uio ] || return 1

    UIO_COUNT=$(ls -d /sys/class/uio/uio* 2>/dev/null | wc -l)
    [ "$UIO_COUNT" -gt 0 ] || return 1
    [ "$UIO_COUNT" -le 14 ] || return 1

    for name_file in /sys/class/uio/*/name; do
        [ -f "$name_file" ] || continue
        if grep -q "board-control" "$name_file" 2>/dev/null; then
            return 1
        fi
    done

    return 0
}

# am2 preflight: ensure the kernel xiic-i2c driver is bound to the FPGA AXI IIC
# controller at 41600000.i2c and that /dev/i2c-0 exists. Mirrors the Rust-side
# logic in dcentrald_hal; duplicating at shell level guarantees the char device
# is present even if the binary is a stale build that skipped the call.
#
# Safe on am1-s9 because we gate the caller on PLATFORM=zynq-bm3-am2.
ensure_i2c0_kernel_bound() {
    # Already bound + visible → nothing to do.
    [ -c /dev/i2c-0 ] && return 0

    # Bind xiic-i2c (no-op if already bound, logs to stderr we silence).
    if [ -d /sys/bus/platform/drivers/xiic-i2c ]; then
        echo 41600000.i2c > /sys/bus/platform/drivers/xiic-i2c/bind 2>/dev/null
        # Give udev a beat to pick it up and create the node.
        sleep 1
    fi

    # Fallback: if udev/mdev didn't materialize /dev/i2c-0, create it manually.
    # Major 89 = i2c-dev, minor 0 = bus 0. Standard kernel assignment.
    if [ ! -c /dev/i2c-0 ]; then
        mknod /dev/i2c-0 c 89 0 2>/dev/null
        chmod 600 /dev/i2c-0 2>/dev/null
    fi

    if [ -c /dev/i2c-0 ]; then
        echo "  [OK] /dev/i2c-0 ready (xiic-i2c bound)"
    else
        echo "  [WARN] /dev/i2c-0 still missing — dcentrald devmem fallback must cover it"
    fi
}

# Safety: command fans to the home cap. Tach/RPM is the physical noise proof.
# PIC watchdog cuts voltage within 10-60s if dcentrald is dead.
# NEVER PWM 127 — home mining, noise is unacceptable.
# AUDIT FIX (2026-05-29, feedback_am2_never_leave_unit_without_fan_manager.md):
# On AM2 (XIL Zynq) the fan PWM is held only while a process owns the uio16 mmap
# AND keeps re-commanding it. The old `$DAEMON --set-fan N` wrote the PWM once
# and EXITED, so the board's fan IP drifted the fans back to full speed — a home
# unit blasts and the documented AM2 safety remediation silently failed. The fix
# is a BACKGROUNDED persistent custodian (`dcentrald --hold-fan N`) that stays
# resident and re-asserts the PWM every ~5 s (mmap-hold alone is not enough). We
# kill any prior custodian first so only one owns the mmap. AM2 ONLY ($IS_AM2);
# S9/am1 fan registers hold via devmem so that branch is unchanged. PWM is only
# ever driven DOWN (<= 30).
am2_start_fan_custodian() {
    _pwm="$1"
    FAN_CUSTODY_RESULT=1
    if [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ]; then
        if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
            dcent_fan_custodian_start_locked "$FAN_CUSTODIAN_DAEMON" "$_pwm" \
                "$FANHOLD_PIDFILE" "$FANHOLD_READYFILE" "$START_STOP_DAEMON" \
                "$LOGFILE" "$FAN_CUSTODY_READY_WAIT"
        else
            dcent_fan_custodian_start "$FAN_CUSTODIAN_DAEMON" "$_pwm" \
            "$FANHOLD_PIDFILE" "$FANHOLD_READYFILE" "$START_STOP_DAEMON" \
                "$LOGFILE" "$FAN_CUSTODY_READY_WAIT"
        fi
        FAN_CUSTODY_RESULT=$?
    fi
    if [ "$FAN_CUSTODY_RESULT" -eq 0 ]; then
        echo "$(date): SAFETY: verified am2 fan custody at PWM $_pwm (pid $(cat "$FANHOLD_PIDFILE" 2>/dev/null))" >> "$LOGFILE"
        return 0
    fi
    echo "$(date): WARN: am2 fan custodian (--hold-fan $_pwm) failed readiness/custody validation" >> "$LOGFILE"
    return 1
}

am2_stop_fan_custodian() {
    [ "$FAN_CUSTODY_AVAILABLE" -eq 1 ] || return 1
    if [ "$FAN_TRANSITION_LOCK_HELD" -eq 1 ]; then
        dcent_fan_custodian_stop_locked "$FAN_CUSTODIAN_DAEMON" "$FANHOLD_PIDFILE" \
            "$FANHOLD_READYFILE" 5 2
    else
        dcent_fan_custodian_stop "$FAN_CUSTODIAN_DAEMON" "$FANHOLD_PIDFILE" \
            "$FANHOLD_READYFILE" 5 2
    fi
}

# PWR_CONTROL polarity helpers, ported from am2-s19jpro (2026-07-27, Wave 7).
# These are the "CORRECT FIX" this script's own HONEST RISK block prescribed.
#
# am2_pwr_control_polarity_known(): true iff exactly one polarity marker is
# exported. When NEITHER is set, the emergency path must degrade to fan-only
# and say so, rather than calling `--safe-off` (which refuses outright with no
# marker) and leaving the unit with no response at all.
#
# am2_power_cut_proven(): reads the raw gpio907 level and interprets it against
# the DECLARED polarity. It is a *declaration-relative* check, not physical rail
# proof — under a wrong declaration it returns a wrong answer, which is exactly
# why the declaration matters and why the polarity question above is still open.
am2_pwr_control_polarity_known() {
    [ "${DCENT_AM2_PWR_CONTROL_ACTIVE_LOW:-0}" = "1" ] || [ "${DCENT_AM2_PWR_CONTROL_ACTIVE_HIGH:-0}" = "1" ]
}

am2_power_cut_proven() {
    [ -r /sys/class/gpio/gpio907/value ] || return 1
    _v=$(cat /sys/class/gpio/gpio907/value 2>/dev/null)
    if [ "${DCENT_AM2_PWR_CONTROL_ACTIVE_LOW:-0}" = "1" ]; then
        [ "$_v" = "1" ]
    elif [ "${DCENT_AM2_PWR_CONTROL_ACTIVE_HIGH:-0}" = "1" ]; then
        [ "$_v" = "0" ]
    else
        return 1
    fi
}

fan_safety_override() {
    if [ "$IS_AM2" = "1" ]; then
        if [ -x "$DAEMON" ]; then
            # cut-hash-before-noise: only drop to the quiet floor once the power
            # cut is evidenced. Unproven cut stays at 30 so a still-energized
            # board keeps real airflow.
            FAN_PWM=30
            if am2_power_cut_proven; then
                FAN_PWM=10
            fi
            # Persistent custodian, NOT the exit-immediately --set-fan one-shot.
            am2_start_fan_custodian "$FAN_PWM"
            return $?
        else
            echo "$(date): WARN: am2 dcentrald binary absent at $DAEMON; fan cap not applied" >> $LOGFILE
            return 1
        fi
    elif command -v devmem > /dev/null 2>&1; then
        devmem $((FAN_BASE + 0x10)) 32 30 2>/dev/null || return 1
        devmem $((FAN_BASE + 0x14)) 32 30 2>/dev/null || return 1
        echo "$(date): SAFETY: fans commanded to PWM 30 (dcentrald exited)" >> $LOGFILE
        return 0
    fi
    return 1
}

case "$1" in
    start)
        EXTERNAL_MEDIA_MARKER=${DCENTOS_EXTERNAL_MEDIA_MARKER:-/etc/dcentos/external-media-ephemeral-root}
        if [ -e "$EXTERNAL_MEDIA_MARKER" ] || [ -L "$EXTERNAL_MEDIA_MARKER" ]; then
            echo "  [SKIP] dcentrald hardware owner: external-media boot remains safe-idle/management-only"
            exit 0
        fi
        if [ ! -x "$DAEMON" ]; then
            echo "  [SKIP] dcentrald: binary not found at $DAEMON"
            exit 0
        fi
        dcent_acquire_fan_transition_lock || {
            echo "  [FAIL] Cannot serialize dcentrald owner admission"
            exit 1
        }

        if [ "$IS_AM2" = "1" ] \
            && { [ -e "$FANHOLD_PIDFILE" ] || [ -e "$FANHOLD_READYFILE" ] \
                || [ -e "${FANHOLD_READYFILE}.pending" ]; }; then
            am2_stop_fan_custodian || {
                echo "  [FAIL] Prior fan custodian cannot be verified and stopped"
                exit 1
            }
            echo "  [OK] exact prior am2 fan custodian stopped before owner admission"
        fi

        RUNNING_PIDS=$(pidof dcentrald 2>/dev/null || true)
        if [ -n "$RUNNING_PIDS" ]; then
            echo "  [FAIL] Refusing a second dcentrald owner (PID(s): $RUNNING_PIDS)"
            exit 1
        fi
        if [ ! -r "$SESSION_LATCH_HELPER" ] \
            || [ "$PROCESS_IDENTITY_AVAILABLE" -ne 1 ]; then
            echo "  [FAIL] Persistent session or exact-process identity helper is missing"
            fan_safety_override
            exit 1
        fi
        SESSION_TOKEN=$(/bin/sh "$SESSION_LATCH_HELPER" prepare 2>> "$LOGFILE") || {
            echo "  [FAIL] Hardware-session admission is blocked; see $LOGFILE"
            fan_safety_override
            exit 1
        }

        # W1.5 (2026-05-07): re-assert tight perms on /data/dcent/ + auth.json
        # before launching the daemon. dcentrald will also auto-correct via
        # verify_auth_file_perms() at startup, but doing it here closes the
        # window between mount(/data) and the first daemon write.
        if [ -d /data/dcent ]; then
            chmod 0700 /data/dcent 2>/dev/null
        fi
        if [ -f /data/dcent/auth.json ]; then
            chmod 0600 /data/dcent/auth.json 2>/dev/null
        fi

        # Do not force xiic-i2c from init. The Rust AM2 path binds kernel I2C
        # only when its configured transport requires it.

        # Legacy compatibility mode only. Native DCENT_OS cold boot is the
        # default product path, so only wait for S15pic_boot when the explicit
        # compat bootstrap is enabled and the board is positively allowlisted.
        if bosminer_pic_bootstrap_enabled && compat_bootstrap_model_allowed && [ -x /usr/bin/bosminer ] && [ ! -f /tmp/pic_init_done ]; then
            echo "  [WAIT] dcentrald: waiting for legacy PIC bootstrap..."
            for i in $(seq 1 120); do
                [ -f /tmp/pic_init_done ] && break
                [ -f /tmp/pic_init_failed ] && break
                sleep 1
            done
            if [ -f /tmp/pic_init_done ]; then
                :
            elif [ -f /tmp/pic_init_failed ]; then
                echo "  [WARN] S15pic_boot did not confirm PIC init - starting dcentrald native path"
            else
                echo "  [WARN] PIC init flag not found after 120s - starting anyway"
            fi
        fi

        echo "Starting dcentrald mining daemon..."
        migrate_legacy_api_port
        echo "  Config:   $CONFIG"
        echo "  Platform: ${PLATFORM:-unknown}"
        echo "  API: REST/WebSocket :$API_PORT"
        echo "  Dashboard: HTTP :80 via S80dashboard"
        [ -n "$EXTRA_ARGS" ] && echo "  Extra args:$EXTRA_ARGS"
        if [ "$IS_AM2" = "1" ]; then
            echo "  AM2 milestone env: DCENT_AM2_SKIP_FAST_UART=${DCENT_AM2_SKIP_FAST_UART:-unset} DCENT_AM2_SERIAL_WORK_DISPATCH=${DCENT_AM2_SERIAL_WORK_DISPATCH:-unset} DCENT_AM2_SKIP_115200_PER_CHIP=${DCENT_AM2_SKIP_115200_PER_CHIP:-unset}"
        fi

        # Do not kill all Python interpreters here.
        # S81mcp starts immediately before us and must keep running.

        # NOTE (updated 2026-03-26): dcentrald uses devmem AXI IIC bypass for I2C.
        # It unbinds the kernel xiic-i2c driver internally on startup.
        # The kernel driver is NOT used - devmem provides direct register access
        # with proper clock timing (THIGH/TLOW=993 for 100kHz).

        # Kill any orphaned dcentrald from previous manual runs.
        # Expected exits are marked so the wrapper does not misclassify replacement
        # as a crash and blast the fans.
        # Reap ALL live dcentrald PIDs, not just the first: two stale energized
        # daemons would otherwise survive and fight the new daemon over the single
        # /dev/i2c-0 owner + PWR_CONTROL + chain UART (single-I2C-owner violation).
        # Mirrors the base zynq/amlogic/am3-bb wrappers.
        # A live owner is refused above. Never kill-and-replace an orphan from
        # the start path: its hardware disposition is unknown.

        set -- $EXTRA_ARGS
        set -- "$DAEMON" --config "$CONFIG" "$@"
        if ! "$START_STOP_DAEMON" -S -b -m -p "$PIDFILE" \
            -x /bin/sh -- "$SESSION_LATCH_HELPER" supervise "$SESSION_TOKEN" \
            "$SCRIPT" "$LOGFILE" "$CHILD_PIDFILE" "$EXPECTFILE" "$@"; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" supervisor-launch-failed \
                >> "$LOGFILE" 2>&1 || true
            fan_safety_override
            echo "  [FAIL] dcentrald supervisor did not start"
            exit 1
        fi
        echo "  [OK] dcentrald started (REST/WebSocket :$API_PORT, CGMiner :4028)"
        echo "  Log: $LOGFILE"
        ;;

    stop)
        echo "Stopping dcentrald mining daemon..."
        # Send SIGTERM for graceful shutdown. dcentrald keeps PIC heartbeats alive
        # while disabling voltage, then cools fans. Full sequence takes ~12s:
        #   0.5s nonce drain + voltage disable + 2s heartbeat stop + 2s cap discharge
        #   + 5s fan cooldown = ~12s total
        # NEVER SIGKILL before voltage is off - kills heartbeat thread, PIC watchdog
        # fires, corrupts I2C bus, requires PSU power cycle.
        if [ "$PROCESS_IDENTITY_AVAILABLE" -ne 1 ]; then
            echo "  [FAIL] Exact-process identity helper is unavailable; refusing stop mutations"
            exit 1
        fi
        dcent_stop_managed_session "$CHILD_PIDFILE" "$EXPECTFILE" "$DAEMON" \
            "$PIDFILE" "$CRASH_LATCH_FILE" "$SCRIPT" "$LOGFILE" \
            30 5 30 || exit $?
        echo "  [OK] dcentrald stopped after exact owner-death and terminal-custody evidence"
        ;;

    restart)
        "$0" stop || exit $?
        sleep 2
        exec "$0" start
        ;;

    safety)
        dcent_acquire_fan_transition_lock || exit 1
        if [ -e "$FANHOLD_PIDFILE" ] || [ -e "$FANHOLD_READYFILE" ] \
            || [ -e "${FANHOLD_READYFILE}.pending" ]; then
            am2_stop_fan_custodian || exit 1
        fi
        if dcent_dcentrald_may_execute; then
            echo "$(date): SAFETY-FAIL: refusing a competing safety writer while dcentrald may execute" >> "$LOGFILE"
            exit 1
        fi
        # G22 (no-brick safety, gap-swarm 2026-05-28): the emergency safety response
        # must CUT HASH POWER, not just cap fans. Delegate to the daemon's audited
        # per-platform one-shot `--safe-off` (cuts hash power, then quiets fans —
        # cut-hash-before-noise) instead of hardcoding GPIO/devmem here. Falls back to
        # the fan-only override if the binary is absent, polarity is unknown, or
        # the cut fails, so an emergency NEVER ends up doing nothing.
        if [ "$IS_AM2" = "1" ] && ! am2_pwr_control_polarity_known; then
            echo "$(date): SAFETY-FAIL: AM2 PWR_CONTROL polarity unknown; fan-only fallback is not a power-cut receipt" >> "$LOGFILE"
            fan_safety_override || true
            exit 1
        fi
        if [ ! -x "$DAEMON" ]; then
            echo "$(date): SAFETY-FAIL: $DAEMON absent; PSU cut unavailable" >> "$LOGFILE"
            exit 1
        fi
        if ! "$DAEMON" --safe-off >> "$LOGFILE" 2>&1; then
            echo "$(date): SAFETY-FAIL: $DAEMON --safe-off failed; fan-only fallback is not a power-cut receipt" >> "$LOGFILE"
            fan_safety_override || true
            exit 1
        fi
        if ! fan_safety_override; then
            echo "$(date): SAFETY-FAIL: power cut returned evidence but persistent fan custody failed" >> "$LOGFILE"
            exit 1
        fi
        echo "$(date): SAFETY: power-cut command/readback and persistent fan custody returned evidence; physical rail state remains separately classified" >> "$LOGFILE"
        ;;

    status)
        if "$START_STOP_DAEMON" -K -p "$PIDFILE" -t -q 2>/dev/null; then
            PID=$(cat "$PIDFILE" 2>/dev/null)
            echo "dcentrald is running (PID $PID)"
        else
            echo "dcentrald is not running"
        fi
        ;;

    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

exit 0
