#!/bin/sh # Monitorink — boucle d'affichage e-ink sur Kobo Libra 2. # # Overlay sur https://github.com/usetrmnl/trmnl-kobo : réutilise ses binaires ARM # (bin/fbink, bin/busybox_kobo) et ses helpers WiFi (scripts/*.sh). On remplace l'appel # API TRMNL par un simple fetch de notre image de dashboard. # Lancé par monitorink.sh (via NickelMenu). Logs -> ../monitorink.log BASE="$(dirname "$0")" cd "$BASE" || exit 1 IMAGE_URL="${MONITORINK_URL:-https://monitorink.homelab.nestor-server.fr/image.png}" # Endpoints du refresh partiel, dérivés de l'URL image (.../image.png -> .../frame.meta|frame.png). BASE_URL="${IMAGE_URL%/image.png}" META_URL="$BASE_URL/frame.meta" FRAME_URL="$BASE_URL/frame.png" CLIENT="${MONITORINK_CLIENT:-kobo}" REFRESH="${MONITORINK_REFRESH:-600}" TMP="/tmp/monitorink.png" FBINK="./bin/fbink/fbink" BUSYBOX="./bin/busybox_kobo" log() { echo "[$(date '+%H:%M:%S')] $*"; sync; } read_battery() { # Renvoie "CAP|CHG" (ex. "85|0"), CHG=1 si en charge. Vide si introuvable. # On lit capacity + status dans le même dossier /sys/class/power_supply/*. for d in /sys/class/power_supply/*/; do [ -r "${d}capacity" ] || continue cap=$(cat "${d}capacity" 2>/dev/null) chg=0 if [ -r "${d}status" ]; then case "$(cat "${d}status" 2>/dev/null)" in Charging|Full) chg=1 ;; esac fi echo "${cap}|${chg}" return 0 done echo "" } bat_query() { # Renvoie "bat=CAP&chg=CHG" pour pousser la batterie au backend (vide si introuvable). bat="$(read_battery)" if [ -n "$bat" ]; then cap="${bat%%|*}"; chg="${bat##*|}" echo "bat=${cap}&chg=${chg}" fi } http_get() { # $1=url, $2=fichier de sortie ("-" = stdout). busybox wget puis fallback curl. url="$1"; out="$2" if [ "$out" = "-" ]; then "$BUSYBOX" wget -q -T 30 -O - "$url" 2>/dev/null && return 0 command -v curl >/dev/null 2>&1 && curl -fsSL -m 30 "$url" 2>/dev/null && return 0 else "$BUSYBOX" wget -q -T 30 -O "$out" "$url" 2>/dev/null && return 0 command -v curl >/dev/null 2>&1 && curl -fsSL -m 30 -o "$out" "$url" 2>/dev/null && return 0 fi return 1 } fetch_meta() { # Récupère la ligne "MODE X Y W H SEQ" du backend (avec batterie + client). Vide si KO. # Au 1er cycle après un (re)démarrage (FIRST=1), on demande reset=1 : l'écran a été effacé # par le reboot, on force un full refresh côté serveur pour éviter un partiel sur base erronée. murl="$META_URL?client=$CLIENT" [ "${FIRST:-0}" = 1 ] && murl="$murl&reset=1" q="$(bat_query)"; [ -n "$q" ] && murl="$murl&$q" http_get "$murl" - } # Ferme les FD hérités pour ne pas bloquer l'éjection USB. exec 3>&- 2>/dev/null log "boucle démarrée — BASE=$BASE URL=$IMAGE_URL refresh=${REFRESH}s" log "fbink présent: $([ -x "$FBINK" ] && echo oui || echo NON) ; busybox: $([ -x "$BUSYBOX" ] && echo oui || echo NON)" display_full() { # Full refresh : clear + waveform complète (le "flash" e-ink), efface le ghosting. "$FBINK" -g file="$TMP",valign=CENTER,halign=CENTER -c -f log "display full rc=$?" } display_partial() { # Refresh partiel : dessine le crop à l'offset (x,y) ; ni -c ni -f -> seule cette zone est # rafraîchie, en waveform partielle (sans flash). $1=x $2=y (coords portrait, origine HG). "$FBINK" -g file="$TMP",x="$1",y="$2" log "display partial x=$1 y=$2 rc=$?" } offline() { log "hors ligne" "$FBINK" -pmh "Monitorink hors ligne ($(date '+%H:%M'))" } show_frame() { # Récupère le crop/full image stocké côté serveur et l'affiche selon le mode. # $1=mode $2=x $3=y if ! http_get "$FRAME_URL?client=$CLIENT" "$TMP"; then log "frame.png KO (mode=$1)" [ "$1" = "full" ] && offline return 1 fi log "frame.png OK ($(wc -c < "$TMP" 2>/dev/null) octets)" if [ "$1" = "partial" ]; then display_partial "$2" "$3" else display_full fi } frontlight_off() { # Éteint le rétroéclairage (frontlight). Le nœud sysfs exact dépend du modèle Kobo, # donc on écrit 0 dans tous les contrôleurs présents. for b in /sys/class/backlight/*/brightness; do [ -w "$b" ] && echo 0 > "$b" 2>/dev/null done } DIAG_DONE=0 suspend_diag() { # DIAGNOSTIC (one-shot) : quand un suspend échoue sur batterie, capture la raison # du veto noyau dans le log. À retirer une fois la cause identifiée. [ "$DIAG_DONE" = 1 ] && return DIAG_DONE=1 log "===== DIAG suspend (one-shot) =====" for d in /sys/class/power_supply/*/; do n=$(basename "$d") log " $n: status=$(cat "${d}status" 2>/dev/null) online=$(cat "${d}online" 2>/dev/null) present=$(cat "${d}present" 2>/dev/null)" done log " wakeup_count=$(cat /sys/power/wakeup_count 2>/dev/null) state-extended=$(cat /sys/power/state-extended 2>/dev/null)" # IRQ responsable du DERNIER réveil — la clé du mystère. log " pm_wakeup_irq=$(cat /sys/power/pm_wakeup_irq 2>/dev/null) last_resume_reason=$(cat /sys/kernel/debug/wakeup/last_resume_reason 2>/dev/null)" # Quels devices d'entrée sont armés comme source de réveil (1=oui). log " -- input devices (name | wakeup) --" for inp in /sys/class/input/input*/; do nm=$(cat "${inp}name" 2>/dev/null) wk=$(cat "${inp}power/wakeup" 2>/dev/null) [ -n "$nm" ] && log " $(basename "$inp"): \"$nm\" wakeup=$wk" done # debugfs (souvent non monté) -> wakeup_sources triés par nb d'évènements. mount -t debugfs none /sys/kernel/debug 2>/dev/null if [ -r /sys/kernel/debug/wakeup_sources ]; then log " -- top wakeup_sources (event_count) --" awk 'NR>1 && ($3+0)>0 {print $3"\t"$1}' /sys/kernel/debug/wakeup_sources 2>/dev/null \ | sort -rn | head -8 | while IFS= read -r l; do log " $l"; done fi # Les IRQ qui montent = source matérielle qui spamme (touch elan, gpio, etc.). log " -- /proc/interrupts (lignes non nulles) --" awk 'NR>1 && ($2+0)>0 {print $0}' /proc/interrupts 2>/dev/null \ | sort -t: -k2 -rn 2>/dev/null | head -12 | while IFS= read -r l; do log " $l"; done log " -- dmesg : lignes PM/wakeup/elan/gpio (40 dernières) --" dmesg 2>/dev/null | grep -iE 'PM:|wakeup|abort|suspend|elan|gpio|irq' | tail -40 \ | while IFS= read -r l; do log " $l"; done log "===== /DIAG =====" } suspend_for() { # Suspend rtcwake (-m mem), réveil RTC après "secs". # # PIÈGE EPDC : juste après un refresh e-ink, la haute tension VEE du panneau n'est pas # encore redescendue ; le pilote EPDC (20f4000.epdc) refuse alors de suspendre # ("waiting for VEE stable ... please retry suspend later", error -2) et le noyau avorte # TOUT le suspend. Sans gestion, on tombait dans un sleep CPU-allumé -> batterie vidée. # Parade (recommandée par le noyau lui-même) : laisser VEE se décharger, puis RÉESSAYER # le suspend jusqu'à ce qu'il prenne. secs="$1" sync sleep 8 # laisse l'EPDC couper ses rails (VEE) ~10s après le refresh attempt=0 while [ "$attempt" -lt 6 ]; do attempt=$((attempt + 1)) sync echo 1 > /sys/power/state-extended 2>/dev/null start=$(date +%s) "$BUSYBOX" rtcwake -a -s "$secs" -m mem 2>/dev/null elapsed=$(( $(date +%s) - start )) echo 0 > /sys/power/state-extended 2>/dev/null log "rtcwake tentative=$attempt elapsed=${elapsed}s" # elapsed grand = on a réellement dormi (réveil RTC ou bouton) -> terminé. [ "$elapsed" -ge 15 ] && return # Échec immédiat (EPDC/VEE pas prêt) : on attend un peu et on retente. sleep 3 done # Toujours pas suspendu après les retries (cas anormal) -> diag + repli sleep pour ne pas # marteler le backend, sans laisser le CPU tourner inutilement plus que "secs". log "suspend impossible après $attempt tentatives -> diag + sleep" suspend_diag sleep "$secs" } has_ip() { ip addr show 2>/dev/null | grep -o 'inet [0-9.]*' | grep -qv '127.0'; } wifi_up() { ./scripts/enable-wifi.sh >/dev/null 2>&1 ./scripts/force-wifi-connection.sh >/dev/null 2>&1 # Attend l'association + bail DHCP (jusqu'à ~24 s). i=0 while [ "$i" -lt 12 ]; do ./scripts/obtain-ip.sh >/dev/null 2>&1 has_ip && return 0 sleep 2; i=$((i + 1)) done return 1 } wifi_down() { # Coupe la radio avant le suspend (helpers trmnl, env Nickel siphonné en amont). ./scripts/release-ip.sh >/dev/null 2>&1 ./scripts/disable-wifi.sh >/dev/null 2>&1 } # MODE PROD : frontlight éteint, WiFi cyclé (off pendant le suspend), rtcwake mem. frontlight_off FIRST=1 # 1er cycle après lancement -> demande un full refresh (reset=1) au backend while true; do log "--- itération ---" frontlight_off # réaffirme après chaque réveil ./scripts/ledToggle.sh on 2>/dev/null wifi_up meta="$(fetch_meta)" if [ -z "$meta" ]; then # WiFi peut-être tombé : on tente une reconnexion puis un re-essai. log "meta KO -> reconnexion WiFi" wifi_up meta="$(fetch_meta)" fi if [ -n "$meta" ]; then # shellcheck disable=SC2086 set -- $meta # MODE X Y W H SEQ mode="$1"; mx="$2"; my="$3" log "meta: mode=$mode x=$mx y=$my w=$4 h=$5 seq=$6" case "$mode" in noop) log "aucun changement -> pas de refresh" ;; partial) show_frame partial "$mx" "$my" ;; *) show_frame full ;; # full ou valeur inattendue -> full refresh sûr esac FIRST=0 # meta obtenue : le reset n'a plus lieu d'être pour les cycles suivants else log "meta ECHEC" offline fi ./scripts/ledToggle.sh off 2>/dev/null wifi_down # coupe la radio avant le suspend suspend_for "$REFRESH" # rtcwake -m mem, WiFi éteint pendant ~5 min done