Reboot Kobo: 3 appuis bouton de page (evdev EV_KEY) au lieu du triple-tap tactile

L'ancien triple-tap via finger_trace dessinait des points noirs (outil de demo
FBInk), ne respawnait pas (mort definitif si le process tombait) et le tactile ne
reveille pas l'appareil. Le power, lui, n'emet que des scancodes MSC_SCAN parasites
(etat de charge USB). Les boutons de page emettent des EV_KEY propres (codes 193/194).

reboot_watcher.sh: lit l'evdev (FD persistant, pas de perte d'evenements), declenche
sur 3 press EV_KEY < 3 s, boucle de respawn. Plus de finger_trace.

Refresh: full force au (re)demarrage (reset=1 cote client -> oubli de prev_image cote
serveur) pour eviter un refresh partiel pose sur un ecran efface par le reboot.
This commit is contained in:
jerem
2026-06-15 19:38:15 +02:00
parent c7395d1c37
commit 5925b0f9d2
7 changed files with 122 additions and 32 deletions

View File

@@ -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) @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, # 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. # 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. # 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)) 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']}" line = f"{info['mode']} {info['x']} {info['y']} {info['w']} {info['h']} {info['seq']}"
return PlainTextResponse(line, headers={"Cache-Control": "no-store"}) return PlainTextResponse(line, headers={"Cache-Control": "no-store"})

View File

@@ -42,11 +42,17 @@ def _changed_bbox(prev: Image.Image, cur: Image.Image) -> tuple[int, int, int, i
return diff.getbbox() 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 """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()) state = _clients.setdefault(client, _ClientState())
async with state.lock: async with state.lock:
if reset:
state.prev_image = None # -> force_full ci-dessous
cur = await render.render_image() cur = await render.render_image()
full_w, full_h = cur.size full_w, full_h = cur.size
state.seq += 1 state.seq += 1

View File

@@ -174,7 +174,7 @@
</div> </div>
<footer> <footer>
<span>Monitorink · 3 taps = redémarrer</span> <span>Monitorink · 3 appuis bouton page = redémarrer</span>
{% if kobo.ok %}<span class="{% if kobo.low %}stale{% endif %}">{% if kobo.charging %}⚡{% else %}🔋{% endif %} Kobo {{ kobo.percent }}%{% if kobo.stale %} ·&nbsp;?{% endif %}</span>{% endif %} {% if kobo.ok %}<span class="{% if kobo.low %}stale{% endif %}">{% if kobo.charging %}⚡{% else %}🔋{% endif %} Kobo {{ kobo.percent }}%{% if kobo.stale %} ·&nbsp;?{% endif %}</span>{% endif %}
<span class="{% if stale %}stale{% endif %}">maj {{ updated }}{% if stale %} · DONNÉE PÉRIMÉE{% endif %}</span> <span class="{% if stale %}stale{% endif %}">maj {{ updated }}{% if stale %} · DONNÉE PÉRIMÉE{% endif %}</span>
</footer> </footer>

View File

@@ -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`), - **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 **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. 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. - **Redémarrer / restaurer Nickel** : **3 appuis rapides (< 3 s) sur un bouton de page**
- Pour **arrêter** : maintenir le bouton power (le device se réveille sous Nickel). (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). - Logs : `/tmp/monitorink.log` (effacés au reboot).
## À valider sur l'appareil (cf. plan, tâche 8) ## À 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 - Stabilité de `rtcwake -m mem` sur FW 4.38.23171 (sinon le fallback `state-extended` prend le
relais ; en dernier recours `sleep`). relais ; en dernier recours `sleep`).
- Cycle WiFi : confirmer qu'au réveil la reconnexion DHCP aboutit en < ~24 s (`has_ip` OK). - 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. - Autonomie réelle sur 24 h.

View File

@@ -53,7 +53,8 @@ fi
# Synchronise l'horloge RTC (sinon rtcwake calcule mal le réveil). # Synchronise l'horloge RTC (sinon rtcwake calcule mal le réveil).
hwclock -w -u 2>/dev/null 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" & sh "$BASE/reboot_watcher.sh" &
# Boucle bloquante. À la sortie (STOP tue monitorinkloop.sh), on reboot pour restaurer Nickel. # Boucle bloquante. À la sortie (STOP tue monitorinkloop.sh), on reboot pour restaurer Nickel.

View File

@@ -65,7 +65,10 @@ http_get() {
fetch_meta() { fetch_meta() {
# Récupère la ligne "MODE X Y W H SEQ" du backend (avec batterie + client). Vide si KO. # 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" murl="$META_URL?client=$CLIENT"
[ "${FIRST:-0}" = 1 ] && murl="$murl&reset=1"
q="$(bat_query)"; [ -n "$q" ] && murl="$murl&$q" q="$(bat_query)"; [ -n "$q" ] && murl="$murl&$q"
http_get "$murl" - http_get "$murl" -
} }
@@ -170,6 +173,7 @@ wifi_down() {
# MODE PROD : frontlight éteint, WiFi cyclé (off pendant le suspend), rtcwake mem. # MODE PROD : frontlight éteint, WiFi cyclé (off pendant le suspend), rtcwake mem.
frontlight_off frontlight_off
FIRST=1 # 1er cycle après lancement -> demande un full refresh (reset=1) au backend
while true; do while true; do
log "--- itération ---" log "--- itération ---"
frontlight_off # réaffirme après chaque réveil frontlight_off # réaffirme après chaque réveil
@@ -195,6 +199,7 @@ while true; do
partial) show_frame partial "$mx" "$my" ;; partial) show_frame partial "$mx" "$my" ;;
*) show_frame full ;; # full ou valeur inattendue -> full refresh sûr *) show_frame full ;; # full ou valeur inattendue -> full refresh sûr
esac esac
FIRST=0 # meta obtenue : le reset n'a plus lieu d'être pour les cycles suivants
else else
log "meta ECHEC" log "meta ECHEC"
offline offline

View File

@@ -1,33 +1,92 @@
#!/bin/sh #!/bin/sh
# Monitorink — redémarrage par TRIPLE-TAP (restaure Nickel / relance proprement). # Monitorink — reboot logiciel par TRIPLE-APPUI sur un BOUTON FRONTAL (restaure Nickel).
# Lancé en arrière-plan par monitorink.sh (après que Nickel a été tué -> écran tactile libre). # Lancé en arrière-plan par monitorink.sh. Remplace l'ancien triple-tap finger_trace qui
# Utilise finger_trace (FBInk) pour détecter les touchers ; pas de mapping de coordonnées. # 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")" BASE="$(dirname "$0")"
cd "$BASE" || exit 1 cd "$BASE" || exit 1
LOG="$BASE/monitorink.log" LOG="$BASE/monitorink.log"
FT="./bin/fbink/finger_trace"
FBINK="./bin/fbink/fbink" 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; } # Repère le périphérique input des boutons (gpio-keys). Repli : event0.
echo "[watcher] triple-tap reboot actif" >> "$LOG"; sync 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 graceful_reboot() {
last=0 echo "[watcher] TRIPLE-APPUI frontal -> reboot logiciel" >> "$LOG"; sync
"$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 "$FBINK" -c -pmh "Redemarrage..." 2>/dev/null
sleep 1; sync sleep 1; sync
reboot 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 fi
;; echo "[watcher] écoute $DEV" >> "$LOG"; sync
esac
# 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 done