#!/bin/sh
# =============================================================================
#  P2Block S11 Fan-Inversion Fix  v1.0.0
#  https://p2block.com/tools/s11-fan-fix   (download, checksum, instructions)
# -----------------------------------------------------------------------------
#  Verify before running:   sha256sum -c s11-fan-fix.sh.sha256
#  Published by P2Block, the non-custodial BLAKE2b (BTCB2) mining pool.
#  USE AT YOUR OWN RISK. No warranty. Only run on units you own. See the page
#  above for symptoms, safety notes and recovery steps.
# =============================================================================
# =============================================================================
#  S11 Fan-Inversion Fix  --  standalone one-shot repair tool
# =============================================================================
#  WHAT THIS IS
#  ------------
#  Some Innosilicon S11 units ship with a firmware bug that INVERTS the fan
#  duty cycle on affected control boards: the fan controller computes
#  `speed = 100 - speed`, so a board that thinks it is running the fans at 10%
#  is actually running them at 90% (and vice-versa). On these units the fans
#  ROAR when the chips are cool and go QUIET as the chips get hot -- the exact
#  opposite of what you want. Left alone, an affected unit can overheat.
#
#  This tool patches the bug out of BOTH fan controllers on the miner
#  (`cgminer` and `dm-monitor`) by changing a single CPU instruction from the
#  inverting form back to a no-op, so the fan control loop works correctly.
#  The change takes effect on the miner's next reboot (this tool reboots it
#  for you at the end).
#
#  HOW IT WORKS (transparency)
#  ---------------------------
#  The S11 web interface runs as root and has a command channel we use to run
#  a small, auditable shell script ON the miner. That on-device script (shown
#  in full below, in the ONDEVICE heredoc -- read it) does the patch the SAFE
#  way: it copies each binary, patches the COPY, byte-verifies the copy, and
#  only then atomically renames it over the original. It never writes a running
#  program in place and never stops any service, so the hardware watchdog keeps
#  being fed and the unit stays up until you reboot.
#
#  SAFETY
#  ------
#   * It is SELF-VERIFYING. Before changing anything it reads the exact 4 bytes
#     at each known offset. It only patches if it finds the exact buggy
#     instruction. If the bytes are already fixed it reports "already patched"
#     and changes nothing. If the bytes are anything unexpected (different
#     firmware build, already-modified binary) it ABORTS and changes nothing.
#     => Safe to run on any S11 of this firmware, and safe to run twice.
#   * It checks for free space before copying and cleans up on any failure.
#   * Nothing is committed unless every patched copy passes byte verification.
#
#  REQUIREMENTS
#  ------------
#   * This machine: a POSIX shell, `curl`, and `od`+`tr` (standard on
#     Linux/macOS). No Python, no extra installs.
#   * The miner: reachable on your LAN, powered on, admin web login known.
#   * Firmware: the S11 build with cgminer/dm-monitor at the offsets below.
#     (If yours differs, the tool aborts safely without changing anything.)
#
#  USAGE
#  -----
#     ./s11-fan-fix.sh <miner-ip> [admin-password]
#
#   e.g.  ./s11-fan-fix.sh 192.168.1.170
#         ./s11-fan-fix.sh 192.168.1.170 mysecret
#
#   If no password is given it tries the common defaults (admin, dragonadmin).
#   You can also set S11_USER / S11_PASS in the environment.
#
#  RECOVERY (if something ever goes wrong)
#  ---------------------------------------
#   Because the patch is verified before commit, a failed run leaves the unit
#   untouched. In the unlikely event a unit will not boot after a change, you
#   can always re-flash the stock firmware .swu through the miner's own web UI
#   upgrade page -- that restores the factory binaries.
# =============================================================================

set -eu
VERSION="1.0.0"
[ "${1:-}" = "--version" ] && { echo "p2block-s11-fan-fix $VERSION"; exit 0; }

# ---- settings ---------------------------------------------------------------
S11_USER="${S11_USER:-admin}"
IP="${1:-}"
PW_ARG="${2:-}"

if [ -z "$IP" ]; then
  echo "usage: $0 <miner-ip> [admin-password]" >&2
  echo "   e.g. $0 192.168.1.170" >&2
  exit 2
fi

# Password candidates: CLI arg, then S11_PASS env, then common defaults.
PW_LIST=""
[ -n "$PW_ARG" ]           && PW_LIST="$PW_LIST $PW_ARG"
[ -n "${S11_PASS:-}" ]     && PW_LIST="$PW_LIST $S11_PASS"
PW_LIST="$PW_LIST admin dragonadmin"

BASE="http://$IP"
CURL="curl -s -S -m 30"

say()  { printf '%s\n' "$*" >&2; }
step() { printf '\n==> %s\n' "$*" >&2; }
die()  { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

command -v curl >/dev/null 2>&1 || die "curl is required but not found on this machine."
command -v od   >/dev/null 2>&1 || die "od is required but not found on this machine."

# =============================================================================
#  The on-device fix script. READ THIS -- it is exactly what runs on the miner.
#  Human progress goes to stderr; a lone "127.0.0.1" on stdout means SUCCESS.
#  (Mirror of fan-fix-net.sh. cgminer offset 244352 / dm-monitor 120432;
#   buggy bytes 64 30 63 12  ->  fixed bytes 00 00 a0 e1 = nop.)
# =============================================================================
ONDEVICE=$(cat <<'ODEOF'
#!/bin/sh
set -eu
log() { echo "$@" >&2; }
TARGETS="/usr/bin/cgminer:244352 /usr/bin/dm-monitor:120432"
W=/config/.fanfix
printf '\144\060\143\022' > "$W.pre"    # 64 30 63 12 = buggy (rsbne r3,r3,#100)
printf '\000\000\240\341' > "$W.post"   # 00 00 a0 e1 = fixed (nop)
read4() { dd if="$1" bs=1 skip="$2" count=4 2>/dev/null > "$W.cur"; }

# pre-flight: verify ALL targets before touching ANY
need=0
for t in $TARGETS; do
  bin=${t%:*}; off=${t#*:}
  [ -f "$bin" ] || { log "ABORT: $bin not found"; exit 1; }
  read4 "$bin" "$off"
  if cmp -s "$W.cur" "$W.post"; then log "OK   $bin: already patched"; continue; fi
  if ! cmp -s "$W.cur" "$W.pre"; then
    log "ABORT: $bin @ $off unexpected bytes (wrong build / already modified). No changes."
    exit 1
  fi
  log "OK   $bin: buggy, will fix"; need=1
done
[ "$need" = 0 ] && { log "Nothing to do -- both already patched."; echo 127.0.0.1; exit 0; }

# make rootfs writable + ensure room for copies
mount -o remount,rw / 2>/dev/null || true
free=$(df -k / 2>/dev/null | awk 'NR==2{print $4+0}') || free=0
if [ "${free:-0}" -lt 8192 ]; then
  log "ABORT: only ${free} KB free on rootfs; need headroom. No changes."
  mount -o remount,ro / 2>/dev/null || true
  exit 1
fi

# build + verify patched COPIES (no rename yet)
built=""
for t in $TARGETS; do
  bin=${t%:*}; off=${t#*:}
  read4 "$bin" "$off"; cmp -s "$W.cur" "$W.post" && continue
  new="$bin.fanfix.new"
  cp -p "$bin" "$new"   # -p preserves mode/owner (cat> would drop +x -> boot loop)
  chmod 755 "$new"      # belt-and-suspenders: guarantee executable
  printf '\000\000\240\341' | dd of="$new" bs=1 seek="$off" count=4 conv=notrunc 2>/dev/null
  dd if="$new" bs=1 skip="$off" count=4 2>/dev/null > "$W.cur"
  if ! cmp -s "$W.cur" "$W.post"; then
    log "ABORT: patched copy of $bin failed verify. No rename performed."
    rm -f "$new"; for b in $built; do rm -f "$b.fanfix.new"; done
    sync; mount -o remount,ro / 2>/dev/null || true; exit 1
  fi
  built="$built $bin"; log "built + verified patched copy for $bin"
done

# commit: atomic rename each verified copy over the original
for bin in $built; do mv "$bin.fanfix.new" "$bin"; log "committed $bin"; done
sync; mount -o remount,ro / 2>/dev/null || true
log "SUCCESS -- patched via atomic rename. Reboot to load."
echo 127.0.0.1
ODEOF
)

# =============================================================================
#  Delivery. The on-device script is hex-encoded (hex can never contain a
#  blocked token), written to /config on the miner, decoded there with the
#  miner's own `xxd`, then run. Success = the miner echoes 127.0.0.1 back.
# =============================================================================

# Hex-encode the script. `$(...)` strips the trailing newline; printf adds one
# back, so the on-device file always ends in exactly one newline (deterministic).
HEX=$(printf '%s\n' "$ONDEVICE" | od -An -v -tx1 | tr -d ' \n')
[ -n "$HEX" ] || die "internal: failed to hex-encode the on-device script."

JWT=""

# POST a command through the miner's root web API. Prints the response text.
inject() { # $1 = payload
  $CURL -H "Authorization: Bearer $JWT" \
        --data-urlencode "check_url=$1" \
        "$BASE/api/checkUrl"
}

# ---- 1. authenticate --------------------------------------------------------
step "Connecting to $IP and logging in ..."
for pw in $PW_LIST; do
  resp=$($CURL --data-urlencode "username=$S11_USER" \
               --data-urlencode "password=$pw" \
               "$BASE/api/auth" 2>/dev/null) || continue
  tok=$(printf '%s' "$resp" | sed -n 's/.*"jwt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
  if [ -n "$tok" ]; then JWT="$tok"; break; fi
done
[ -n "$JWT" ] || die "login failed. Check the IP is reachable and the admin password is correct
       (pass it as the 2nd argument: $0 $IP <password>)."
say "Logged in."

# ---- 2. confirm the command channel works -----------------------------------
step "Verifying admin command channel ..."
chk=$(inject '$(echo 127.0.0.1)' 2>/dev/null || true)
case "$chk" in
  *127.0.0.1*) say "Command channel OK." ;;
  *) die "the admin command channel did not respond as expected. This firmware may
       differ from the one this tool supports; nothing was changed." ;;
esac

# ---- 3. upload the hex-encoded script to /config ----------------------------
step "Uploading fix script to the miner ..."
up=$(inject "x\$(printf %s $HEX>/config/ff.sh.hex)" 2>/dev/null || true)
case "$up" in
  *__ERROR__*|"") die "upload failed (no response from miner)." ;;
esac

# ---- 4. decode it on the miner (uses the miner's own xxd; no python3) --------
step "Decoding fix script on the miner ..."
inject "x\$(xxd -r -p /config/ff.sh.hex>/config/ff.sh)" >/dev/null 2>&1 || \
  die "decode step failed."

# ---- 5. run it. A lone 127.0.0.1 echoed back == success ---------------------
#  NB: NO 'x' prefix on this call -- the script's stdout (127.0.0.1) must BE the
#  ping target so the reply comes back readable. A prefix would make the target
#  "x127.0.0.1", which can't resolve and would make every run look like failure.
step "Applying the patch (verify -> patch copy -> verify -> atomic rename) ..."
run=$(inject '$(sh /config/ff.sh 2>/config/ff.log)' 2>/dev/null || true)
case "$run" in
  *127.0.0.1*) : ;;   # success signal present
  *) die "the fix did not report success, so it aborted safely and changed NOTHING.
       This usually means the binaries were an unexpected build or already modified.
       Miner response: $(printf '%s' "$run" | tr -d '\r' | head -c 200)" ;;
esac
say "Patch applied and byte-verified on the miner."

# ---- 6. reboot so the fix loads ---------------------------------------------
step "Rebooting the miner to load the fix ..."
$CURL -H "Authorization: Bearer $JWT" "$BASE/api/reboot" >/dev/null 2>&1 || true
say "Reboot requested (the connection may drop as the unit goes down -- that's normal)."

cat >&2 <<DONE

=============================================================================
 DONE. The fan-inversion fix is applied and the miner is rebooting.
 (P2Block S11 Fan-Inversion Fix v$VERSION - https://p2block.com/tools/s11-fan-fix)
=============================================================================
 * Give it ~30-60 seconds to come back up.
 * Then watch the fans under load: as the chips warm up the fans should now
   speed UP (not slow down). Reported fan duty and airflow now agree.
 * Safe to re-run this tool anytime -- it reports "already patched" and makes
   no changes on a unit that's already fixed.

 SECURITY NOTE: the admin web interface that let this run also lets anyone on
 your LAN take the unit over with the admin login. Before reselling or exposing
 a unit, change its admin password and keep miners on an isolated network.

 Mining BTCB2 with this unit? P2Block pays every miner straight from the
 coinbase, 1% with your own gateway or 2% on our stratum: https://p2block.com
=============================================================================
DONE
