diff --git a/backend/app.py b/backend/app.py index 5ad7eb4..49a3037 100644 --- a/backend/app.py +++ b/backend/app.py @@ -48,12 +48,15 @@ async def image(fresh: int = 0, bat: int | None = None, chg: int = 0) -> Respons @app.get("/frame.meta", response_class=PlainTextResponse) -async def frame_meta(client: str = "kobo", bat: int | None = None, chg: int = 0) -> Response: +async def frame_meta( + client: str = "kobo", bat: int | None = None, chg: int = 0, reset: int = 0 +) -> Response: # Refresh partiel : rend l'image, calcule la zone modifiée vs le dernier frame de ce client, # et renvoie une ligne "MODE X Y W H SEQ" triviale à parser en shell busybox. # MODE ∈ {full, partial, noop}. Le PNG correspondant est récupéré via /frame.png. + # reset=1 (1er cycle après un (re)démarrage Kobo) -> oublie l'état et force un full refresh. kobo.record(bat, bool(chg)) - info = await frame.compute_frame(client) + info = await frame.compute_frame(client, reset=bool(reset)) line = f"{info['mode']} {info['x']} {info['y']} {info['w']} {info['h']} {info['seq']}" return PlainTextResponse(line, headers={"Cache-Control": "no-store"}) diff --git a/backend/frame.py b/backend/frame.py index 967f0f3..68e9945 100644 --- a/backend/frame.py +++ b/backend/frame.py @@ -42,11 +42,17 @@ def _changed_bbox(prev: Image.Image, cur: Image.Image) -> tuple[int, int, int, i return diff.getbbox() -async def compute_frame(client: str) -> dict: +async def compute_frame(client: str, reset: bool = False) -> dict: """Rend le dashboard, calcule le diff vs l'image précédente du client, met à jour son état - et renvoie {mode, x, y, w, h, seq}. Le PNG correspondant est stocké pour /frame.png.""" + et renvoie {mode, x, y, w, h, seq}. Le PNG correspondant est stocké pour /frame.png. + + reset=True (envoyé par la Kobo au 1er cycle après un (re)démarrage) oublie l'image + précédente : l'écran a été effacé par le reboot, un diff partiel se poserait sur une base + erronée. On force alors un full refresh propre.""" state = _clients.setdefault(client, _ClientState()) async with state.lock: + if reset: + state.prev_image = None # -> force_full ci-dessous cur = await render.render_image() full_w, full_h = cur.size state.seq += 1 diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html index a07e7c5..e080842 100644 --- a/backend/templates/dashboard.html +++ b/backend/templates/dashboard.html @@ -174,7 +174,7 @@ diff --git a/kobo/README.md b/kobo/README.md index fd4bf42..1b8bc71 100644 --- a/kobo/README.md +++ b/kobo/README.md @@ -45,8 +45,19 @@ afin de ne pas réinventer (et risquer de casser) la gestion WiFi/suspend spéci - **Mode prod** (réglages par défaut) : refresh **5 min** (`MONITORINK_REFRESH=300`), **rétroéclairage éteint** (frontlight à 0), et **WiFi cyclé** — la radio est rallumée le temps du fetch puis coupée pendant le suspend `rtcwake -m mem` pour économiser la batterie. -- Un **swipe** sur l'écran réveille le device et force un rafraîchissement. -- Pour **arrêter** : maintenir le bouton power (le device se réveille sous Nickel). +- **Redémarrer / restaurer Nickel** : **3 appuis rapides (< 3 s) sur un bouton de page** + (frontal). `reboot_watcher.sh` lit l'evdev et déclenche un reboot logiciel propre sur 3 press + EV_KEY (codes 193/194). Sur **batterie** le device est en suspend profond et les boutons + frontaux ne le réveillent pas : faire d'abord **un appui power** (réveil), puis les 3 appuis + dans la fenêtre d'éveil. Filet de secours : appui power **long** = reboot matériel. + - Pourquoi pas le bouton power directement : sur ce Kobo (mx6sll) le power n'émet que des + scancodes bruts `MSC_SCAN`, parasités par l'état de charge USB (peu fiable). Les boutons + frontaux émettent des `EV_KEY` propres. + - `reboot_watcher.sh` tourne en arrière-plan avec une boucle de respawn (il ne reste jamais + mort). Calage/debug : lancer avec `MONITORINK_PWR_DEBUG=1` pour logger chaque évènement + (`type/code/val`), puis repasser à `0`. +- `finger_trace` (ancien déclencheur triple-tap tactile) n'est **plus utilisé** : on peut le + retirer de `bin/fbink/`. - Logs : `/tmp/monitorink.log` (effacés au reboot). ## À valider sur l'appareil (cf. plan, tâche 8) @@ -57,4 +68,9 @@ afin de ne pas réinventer (et risquer de casser) la gestion WiFi/suspend spéci - Stabilité de `rtcwake -m mem` sur FW 4.38.23171 (sinon le fallback `state-extended` prend le relais ; en dernier recours `sleep`). - Cycle WiFi : confirmer qu'au réveil la reconnexion DHCP aboutit en < ~24 s (`has_ip` OK). +- Boutons (reboot) : sur ce Kobo, `find_button_dev` retient `/dev/input/event3` et les boutons + de page émettent `EV_KEY` codes 193/194 (struct `input_event` 16 o). Sur un autre modèle, + recaler via `MONITORINK_PWR_DEBUG=1` (codes/format dans le log). **Sur batterie**, vérifier + le geste réveil-power → 3 appuis frontaux dans la fenêtre d'éveil (les frontaux ne réveillent + pas du suspend). - Autonomie réelle sur 24 h. diff --git a/kobo/monitorink.sh b/kobo/monitorink.sh index 51118d6..f2ed837 100755 --- a/kobo/monitorink.sh +++ b/kobo/monitorink.sh @@ -53,7 +53,8 @@ fi # Synchronise l'horloge RTC (sinon rtcwake calcule mal le réveil). hwclock -w -u 2>/dev/null -# Watcher triple-tap -> reboot (en arrière-plan, écran tactile libre car Nickel est mort). +# Watcher bouton power -> reboot logiciel (en arrière-plan, Nickel mort). +# Premier calage : préfixer par MONITORINK_PWR_DEBUG=1 pour logger device/format dans le log. sh "$BASE/reboot_watcher.sh" & # Boucle bloquante. À la sortie (STOP tue monitorinkloop.sh), on reboot pour restaurer Nickel. diff --git a/kobo/monitorinkloop.sh b/kobo/monitorinkloop.sh index 33c0076..8f7fbdc 100755 --- a/kobo/monitorinkloop.sh +++ b/kobo/monitorinkloop.sh @@ -65,7 +65,10 @@ http_get() { 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" - } @@ -170,6 +173,7 @@ wifi_down() { # 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 @@ -195,6 +199,7 @@ while true; do 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 diff --git a/kobo/reboot_watcher.sh b/kobo/reboot_watcher.sh index 5cc80d4..f5141a7 100755 --- a/kobo/reboot_watcher.sh +++ b/kobo/reboot_watcher.sh @@ -1,33 +1,92 @@ #!/bin/sh -# Monitorink — redémarrage par TRIPLE-TAP (restaure Nickel / relance proprement). -# Lancé en arrière-plan par monitorink.sh (après que Nickel a été tué -> écran tactile libre). -# Utilise finger_trace (FBInk) pour détecter les touchers ; pas de mapping de coordonnées. +# Monitorink — reboot logiciel par TRIPLE-APPUI sur un BOUTON FRONTAL (restaure Nickel). +# Lancé en arrière-plan par monitorink.sh. Remplace l'ancien triple-tap finger_trace qui +# barbouillait l'écran de points noirs. Appui long sur power = reboot matériel = filet de +# secours ultime. +# +# Pourquoi les boutons frontaux et pas le power : sur ce Kobo (mx6sll) le bouton power n'émet +# que des scancodes bruts EV_MSC/MSC_SCAN, parasités par l'état de charge USB (impossible à +# distinguer de façon fiable). Les boutons frontaux, eux, émettent des EV_KEY PROPRES +# (type=1) codes 193 (KEY_F23) et 194 (KEY_F24), press(val=1)/release(val=0) nets, sans aucun +# parasite. On compte 3 appuis (press) sur l'un OU l'autre en moins de 3 s. +# +# NB suspend : les boutons frontaux ne réveillent pas l'appareil du suspend profond (seul le +# power le fait). En usage sur secteur (USB) l'appareil ne suspend pas vraiment -> le watcher +# tourne en continu et le triple-appui marche tout le temps. Sur batterie, réveiller d'abord +# par un appui power, puis faire les 3 appuis frontaux dans la fenêtre d'éveil. BASE="$(dirname "$0")" cd "$BASE" || exit 1 LOG="$BASE/monitorink.log" -FT="./bin/fbink/finger_trace" FBINK="./bin/fbink/fbink" +BB="./bin/busybox_kobo" +DEBUG="${MONITORINK_PWR_DEBUG:-0}" # 1 = logge chaque évènement pour caler device/codes +BTN_A=193 # bouton frontal 1 (KEY_F23) +BTN_B=194 # bouton frontal 2 (KEY_F24) -[ -x "$FT" ] || { echo "[watcher] finger_trace absent" >> "$LOG"; exit 0; } -echo "[watcher] triple-tap reboot actif" >> "$LOG"; sync +# Repère le périphérique input des boutons (gpio-keys). Repli : event0. +find_button_dev() { + for ev in /dev/input/event*; do + [ -e "$ev" ] || continue + case "$(cat "/sys/class/input/${ev##*/}/device/name" 2>/dev/null)" in + *gpio*key*|*power*|*Power*|*pmic*|*keys*) echo "$ev"; return 0 ;; + esac + done + [ -e /dev/input/event0 ] && echo /dev/input/event0 +} -count=0 -last=0 -"$FT" 2>/dev/null | while read -r line; do - case "$line" in - *UP*|*RELEASE*|*Release*) - now=$(date +%s) - [ $((now - last)) -gt 3 ] && count=0 - count=$((count + 1)) - last=$now - echo "[watcher] tap $count/3" >> "$LOG"; sync - if [ "$count" -ge 3 ]; then - echo "[watcher] TRIPLE-TAP -> reboot" >> "$LOG"; sync - "$FBINK" -c -pmh "Redemarrage..." 2>/dev/null - sleep 1; sync - reboot - fi - ;; - esac +graceful_reboot() { + echo "[watcher] TRIPLE-APPUI frontal -> reboot logiciel" >> "$LOG"; sync + "$FBINK" -c -pmh "Redemarrage..." 2>/dev/null + sleep 1; sync + reboot +} + +echo "[watcher] reboot triple-appui frontal actif" >> "$LOG"; sync + +# Boucle de respawn : si le lecteur meurt (réinit input au réveil, fd fermé), on le relance. +# Le watcher ne reste jamais mort -> le reboot logiciel est toujours disponible. +while true; do + DEV="$(find_button_dev)" + if [ -z "$DEV" ]; then + echo "[watcher] aucun input bouton trouvé, retry 5s" >> "$LOG"; sync + sleep 5; continue + fi + echo "[watcher] écoute $DEV" >> "$LOG"; sync + + # IMPORTANT : ouvrir le périphérique UNE seule fois (FD 4) et lire séquentiellement. + # Rouvrir à chaque évènement (dd if=$DEV) crée un gap qui fait perdre des évènements. + # FD persistant = flux continu, zéro perte, zéro buffering. + if ! exec 4< "$DEV"; then + echo "[watcher] ouverture $DEV KO -> retry 5s" >> "$LOG"; sync + sleep 5; continue + fi + + # Lit le flux evdev par enregistrements de 16 octets (struct input_event 32 bits : + # timeval 8o + type 2o + code 2o + value 4o). od -> 16 octets décimaux (little-endian). + count=0 + last=0 + while rec="$("$BB" dd bs=16 count=1 <&4 2>/dev/null | "$BB" od -An -tu1)"; do + [ -z "$rec" ] && break # fd fermé/EOF -> respawn + # shellcheck disable=SC2086 + set -- $rec # $1..$16 + [ "$#" -ge 13 ] || continue + type=$(( $9 + ${10} * 256 )) + code=$(( ${11} + ${12} * 256 )) + val="${13}" + [ "$DEBUG" = 1 ] && { echo "[ft] type=$type code=$code val=$val" >> "$LOG"; sync; } + # On ne compte que les PRESS (val=1) EV_KEY (type=1) d'un bouton frontal. + [ "$type" -eq 1 ] && [ "$val" -eq 1 ] || continue + [ "$code" -eq "$BTN_A" ] || [ "$code" -eq "$BTN_B" ] || continue + now=$(date +%s) + [ $((now - last)) -gt 3 ] && count=0 # reset si > 3 s depuis le dernier appui + count=$((count + 1)) + last=$now + echo "[watcher] appui $count/3" >> "$LOG"; sync + [ "$count" -ge 3 ] && graceful_reboot + done + + exec 4<&- + echo "[watcher] lecteur input arrêté -> respawn" >> "$LOG"; sync + sleep 1 done