Files
Monitorink/kobo/monitorinkloop.sh
jerem ca4febbc44 Refresh e-ink: multi-régions + full toutes les 2h (basé temps)
Le full refresh apparaissait trop souvent: getbbox() renvoyait un seul
rectangle englobant tous les pixels modifiés, donc météo (haut) + heure de
MaJ (ailleurs) qui changeaient au même cycle produisaient un rectangle
quasi plein écran -> ratio > partial_max_ratio -> full forcé.

- frame.py: détection des bandes horizontales modifiées disjointes
  (_changed_regions), refresh partiel serré par zone. Full basé sur le
  temps écoulé (last_full_at + time.monotonic) au lieu d'un compteur de
  cycles. État pngs/regions en liste, get_png(client, region).
- config.py: full_refresh_interval_minutes (MONITORINK_FULL_INTERVAL_MIN,
  défaut 120). Suppression de partial_max_ratio.
- app.py: /frame.meta renvoie un bloc multi-ligne "MODE SEQ N" + N régions
  "i x y w h"; /frame.png?region=i.
- monitorinkloop.sh: display_meta parse le bloc et fait N fbink partiels.
2026-06-16 14:06:49 +02:00

245 lines
9.0 KiB
Bash
Executable File

#!/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'))"
}
fetch_region() {
# Récupère le PNG de la région $1 stocké côté serveur dans $TMP. 0 = OK.
if http_get "$FRAME_URL?client=$CLIENT&region=$1" "$TMP"; then
log "frame.png OK region=$1 ($(wc -c < "$TMP" 2>/dev/null) octets)"
return 0
fi
log "frame.png KO region=$1"
return 1
}
display_meta() {
# Parse le bloc meta multi-ligne et affiche selon le mode :
# MODE SEQ N
# i x y w h (N lignes ; i = index région pour /frame.png?region=i)
# Lecture ligne-à-ligne : read consomme l'en-tête, le while lit les régions restantes (même
# stdin). Le sous-shell de pipe convient : chaque région est fetch + affichée sur place.
printf '%s\n' "$1" | {
read mode seq n
log "meta: mode=$mode seq=$seq regions=$n"
case "$mode" in
noop) log "aucun changement -> pas de refresh" ;;
partial)
while read idx x y w h; do
[ -n "$idx" ] || continue
fetch_region "$idx" && display_partial "$x" "$y"
done
;;
*) # full ou valeur inattendue -> full refresh sûr (région 0 = image pleine)
if fetch_region 0; then display_full; else offline; fi
;;
esac
}
}
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() {
# Filet (one-shot) : si le suspend échoue malgré les retries (anormal), capture une fois
# l'état alim + la raison du veto noyau (dmesg) pour diagnostic.
[ "$DIAG_DONE" = 1 ] && return
DIAG_DONE=1
log "===== DIAG suspend ====="
for d in /sys/class/power_supply/*/; do
log " $(basename "$d"): status=$(cat "${d}status" 2>/dev/null) online=$(cat "${d}online" 2>/dev/null)"
done
log " -- dmesg (PM/epdc/suspend) --"
dmesg 2>/dev/null | grep -iE 'PM:|epdc|suspend|abort|wakeup' | tail -15 \
| 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 12 # laisse l'EPDC couper ses rails (VEE) : ~12s suffisent pour
# réussir dès la 1re tentative (mesuré), retry = filet
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 --- batterie=$(read_battery)" # CAP|CHG, pour suivre le drain
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
display_meta "$meta"
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