Backend : endpoints /frame.meta (ligne 'MODE X Y W H SEQ') + /frame.png qui servent un crop de la zone modifiée (diff PIL par client) ou l'image pleine. Full refresh forcé tous les N cycles (MONITORINK_FULL_EVERY=12, ~1h) ou si la zone change sur plus de 60% de l'écran. Mode 'noop' quand rien ne change. Anti-429 : l'usage Claude est mis en cache (MONITORINK_USAGE_TTL=120s) avec repli sur la dernière valeur connue en cas d'erreur transitoire. Kobo : monitorinkloop.sh récupère meta puis png et fait un fbink partiel (-g file=,x=,y=) sans flash, full refresh (-c -f) en mode full. Refresh 5 min.
207 lines
6.9 KiB
Bash
Executable File
207 lines
6.9 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.
|
|
murl="$META_URL?client=$CLIENT"
|
|
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
|
|
}
|
|
|
|
suspend_for() {
|
|
# Séquence Kobo éprouvée (trmnl-kobo) : state-extended=1 AVANT rtcwake, puis
|
|
# suspend manuel (echo mem) si rtcwake n'a pas vraiment suspendu.
|
|
secs="$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 ))
|
|
log "rtcwake elapsed=${elapsed}s"
|
|
if [ "$elapsed" -le 10 ]; then
|
|
log "suspend manuel via echo mem"
|
|
sleep 1; sync; sleep 2
|
|
m_start=$(date +%s)
|
|
echo mem > /sys/power/state 2>/dev/null
|
|
log "echo mem elapsed=$(( $(date +%s) - m_start ))s (≈${secs}=OK, ≈0=USB branché/suspend bloqué)"
|
|
fi
|
|
echo 0 > /sys/power/state-extended 2>/dev/null
|
|
# Repli : si rien n'a réellement suspendu (USB branché, etc.), on attend le temps
|
|
# restant en sleep pour ne pas boucler en continu (martèlement backend + batterie).
|
|
total=$(( $(date +%s) - start ))
|
|
if [ "$total" -lt "$secs" ]; then
|
|
rem=$(( secs - total ))
|
|
log "suspend incomplet (${total}s) -> sleep ${rem}s"
|
|
sleep "$rem"
|
|
fi
|
|
}
|
|
|
|
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
|
|
|
|
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
|
|
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
|