diff --git a/oc-patches/services/auto-dismiss-load-dialog/README.md b/oc-patches/services/auto-dismiss-load-dialog/README.md new file mode 100644 index 0000000..b99da21 --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/README.md @@ -0,0 +1,85 @@ +# auto-dismiss-load-dialog + +Auto-dismisses the "Load filament complete" modal that appears on the +Centauri Carbon's touchscreen at the end of a filament-load cycle. The +print queue is blocked while the dialog is up, so without this you have +to walk to the printer, tap OK, and walk back to your computer to start +the next print. + +The unload-complete dialog **does not** appear (no auto-dismiss needed +there); only loads produce a blocking confirmation. + +## How it works + +Three pieces, all installed under `/opt`: + +| File | Role | +|---|---| +| `/opt/sbin/synth-tap` | bash + `xxd` script that synthesises one touchscreen tap by writing `input_event` structs to `/dev/input/event1` | +| `/opt/sbin/auto-dismiss-daemon` | `tail -F`s `/board-resource/log1`, watches for the load-complete signal, then calls `synth-tap` | +| `/opt/etc/init.d/S99auto-dismiss` | entware service script (start/stop/restart/status), auto-started on boot via `rc.unslung` | + +Detection signal — three log-line states from `/board-resource/log1`: + +``` +[app][...]:feed state change : 0 -> 1 ← load OR unload starts; reset +[gcode][...]:single_command ← cycle is a load (load-only gcode) +[gcode][...]:single_command ← gcode complete; dialog appears. + If is_load, tap. +``` + +`M729` was identified as the cleanest load-only discriminator by +diffing the load and unload gcode sequences. Unloads run +`G1 E-60 F240` + `SET_MIN_EXTRUDE_TEMP S0/RESET`; loads run +`G1 E120 F240` + `M729`. `M82` (set absolute extrude) fires at the +end of *both* cycles, but only loads precede it with `M729`, so the +is_load gate keeps the unload cycle untapped. + +Trigger choice came from a live capture: `feed state change : 1 -> 0` +fires *after* the dialog is dismissed (it's downstream of the tap), +so it's useless as a trigger — would create chicken-and-egg. `M82` +fires at the moment the dialog appears. + +Tap synthesis matches the gt9xxnew_ts driver's recorded sequence +(captured from a real user tap on V0.3.0-o): + +``` +EV_KEY BTN_TOUCH=1 +EV_ABS ABS_MT_POSITION_X = 289 ← OK button on the load-complete dialog +EV_ABS ABS_MT_POSITION_Y = 182 +EV_ABS ABS_MT_TOUCH_MAJOR = 20 +EV_ABS ABS_MT_WIDTH_MAJOR = 20 +EV_ABS ABS_MT_TRACKING_ID = 0 +EV_SYN SYN_MT_REPORT +EV_SYN SYN_REPORT +sleep 150 ms +EV_KEY BTN_TOUCH=0 +EV_SYN SYN_REPORT +``` + +## Configuration + +The daemon reads three env vars (with defaults): + +| Var | Default | Notes | +|---|---|---| +| `LOG` | `/board-resource/log1` | path to `app`'s log file | +| `TAP_X` | `289` | OK-button X (gt9xxnew coordinate space) | +| `TAP_Y` | `182` | OK-button Y | +| `SETTLE_S` | `0.7` | settle delay between detection and tap | + +Override by editing `/opt/etc/init.d/S99auto-dismiss` to set them +before starting the daemon. + +## Footprint + +- ~6 KB on disk +- Runs one bash process tailing one log file; idle CPU is rounding + error +- Only writes to `/dev/input/event1` when `M729` + `feed state + change : 1 -> 0` are observed in sequence + +## Tested on + +Firmware V0.3.0-o (OpenCentauri based on stock 1.1.40), Centauri +Carbon (CC1). diff --git a/oc-patches/services/auto-dismiss-load-dialog/S99auto-dismiss b/oc-patches/services/auto-dismiss-load-dialog/S99auto-dismiss new file mode 100755 index 0000000..443dd52 --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/S99auto-dismiss @@ -0,0 +1,68 @@ +#!/bin/sh +# /opt/etc/init.d/S99auto-dismiss — entware service for auto-dismiss-daemon. +# Same convention as the bundled S40sshd. + +prefix="/opt" +PATH=${prefix}/bin:${prefix}/sbin:/sbin:/bin:/usr/sbin:/usr/bin + +DAEMON=${prefix}/sbin/auto-dismiss-daemon +PIDFILE=${prefix}/var/run/auto-dismiss.pid +LOGFILE=${prefix}/var/log/auto-dismiss.log + +start() { + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "auto-dismiss already running (pid $(cat "$PIDFILE"))" + return 0 + fi + mkdir -p "$(dirname "$PIDFILE")" "$(dirname "$LOGFILE")" + # Truncate log on (re)start so each run is self-contained + : >"$LOGFILE" + echo "starting auto-dismiss..." + # No nohup/setsid in busybox — detach via subshell + daemon's own + # `trap '' HUP`. Closes stdin/out/err to log file; subshell exits + # immediately so the daemon gets reparented to init. + ( "$DAEMON" >>"$LOGFILE" 2>&1 "$PIDFILE" ) +} + +stop() { + # The PIDFILE-only path was naive: if the file got out of sync + # with reality (stale PID after a failed start, kill-not-taking + # because the daemon was in a tail-F'ing disk wait, an out-of-band + # spawn from a previous boot's S-script, etc.) leftover daemons + # would survive `restart` and pile up. + # + # Belt-and-braces: name-based lookup with `pidof` plus a SIGKILL + # fallback for anything that didn't honour SIGTERM within a second. + # No `pkill` in this busybox, but pidof + kill works everywhere. + if [ -f "$PIDFILE" ]; then + kill "$(cat "$PIDFILE")" 2>/dev/null || true + rm -f "$PIDFILE" + fi + PIDS=$(pidof auto-dismiss-daemon 2>/dev/null || true) + if [ -n "$PIDS" ]; then + kill $PIDS 2>/dev/null || true + sleep 1 + STRAG=$(pidof auto-dismiss-daemon 2>/dev/null || true) + [ -n "$STRAG" ] && kill -9 $STRAG 2>/dev/null + fi + echo "auto-dismiss stopped" +} + +restart() { stop; sleep 1; start; } + +status() { + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "auto-dismiss running (pid $(cat "$PIDFILE"))" + return 0 + fi + echo "auto-dismiss not running" + return 3 +} + +case "$1" in + start) start ;; + stop) stop ;; + restart) restart ;; + status) status ;; + *) echo "Usage: $0 {start|stop|restart|status}"; exit 1 ;; +esac diff --git a/oc-patches/services/auto-dismiss-load-dialog/auto-dismiss-daemon b/oc-patches/services/auto-dismiss-load-dialog/auto-dismiss-daemon new file mode 100755 index 0000000..843369e --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/auto-dismiss-daemon @@ -0,0 +1,77 @@ +#!/opt/bin/bash +# auto-dismiss-daemon — auto-dismiss the "Load complete" dialog on the +# Centauri Carbon's touchscreen by watching the printer's log for the +# load-completion signal and synthesising a tap on /dev/input/event1. +# +# Designed for OpenCentauri (no Klipper/Moonraker process; the proprietary +# `app` daemon writes to /board-resource/log1 with Klipper-derived module +# names). +# +# Detection logic (V0.3.0-o, verified by live capture): +# "feed state change : 0 -> 1" → cycle started; reset is_load +# "single_command" → cycle is a load (M729 fires only in +# the load gcode, not unload) +# "single_command" → load gcode finished; dialog appears +# NOW. If is_load, settle then tap. +# +# We deliberately do NOT trigger on "feed state change : 1 -> 0": +# that line fires AFTER a tap dismisses the dialog (it's downstream of +# the action we're trying to take), making it useless as a trigger. +# Both load and unload cycles emit M82, but only loads precede it +# with M729, so the is_load gate keeps unload cycles untapped. +# +# Tap coordinates default to (289, 182), captured on firmware V0.3.0-o. + +set -u + +# Ignore SIGHUP so we survive the parent SSH session closing on install. +# init.d will use SIGTERM to stop us, so HUP isn't needed for that. +trap '' HUP + +LOG=${LOG:-/board-resource/log1} +TAP_X=${TAP_X:-289} +TAP_Y=${TAP_Y:-182} +SETTLE_S=${SETTLE_S:-0.7} # let the dialog finish drawing +SYNTH_TAP=${SYNTH_TAP:-/opt/sbin/synth-tap} + +log() { logger -t auto-dismiss -- "$*"; } + +if [ ! -r "$LOG" ]; then + log "ERROR: cannot read $LOG; exiting" + exit 1 +fi +if [ ! -x "$SYNTH_TAP" ]; then + log "ERROR: $SYNTH_TAP not executable; exiting" + exit 1 +fi + +log "starting (tap=$TAP_X,$TAP_Y log=$LOG)" + +is_load=0 + +while read -r line; do + case "$line" in + *"feed state change : 0 -> 1"*) + # New cycle starting (load or unload). Reset the flag. + is_load=0 + ;; + *"single_command"*) + # Load-specific gcode. Only loads emit M729. + is_load=1 + ;; + *"single_command"*) + # M82 fires immediately after the extrude completes — the + # exact moment the load-complete dialog appears. Both load + # and unload cycles emit M82, so the is_load gate is what + # makes this load-only. + if [ "$is_load" = "1" ]; then + log "load M82 (gcode complete); tapping ($TAP_X,$TAP_Y) after ${SETTLE_S}s" + sleep "$SETTLE_S" + "$SYNTH_TAP" "$TAP_X" "$TAP_Y" || log "WARN: synth-tap exited $?" + is_load=0 + fi + ;; + esac +done < <(tail -n 0 -F "$LOG" 2>/dev/null) + +log "tail exited; daemon shutting down" diff --git a/oc-patches/services/auto-dismiss-load-dialog/patch.sh b/oc-patches/services/auto-dismiss-load-dialog/patch.sh new file mode 100755 index 0000000..186e80c --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/patch.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# +# Auto-dismiss the load-filament-complete dialog. +# +# Installs: +# /opt/sbin/synth-tap — touchscreen tap synthesiser +# /opt/sbin/auto-dismiss-daemon — log-tailing daemon +# /opt/etc/init.d/S99auto-dismiss — entware service script +# +# All three live under /opt (the entware bind mount), so this patch +# stages them under ${SQUASHFS_ROOT}/app/auto-dismiss-load-dialog/ and +# extends bootstrap.sh / rc.local at boot to copy them into /opt. +# (We can't write directly under ./opt at firmware-build time because +# /opt is bind-mounted from /user-resource at boot.) + +if [ $UID -ne 0 ]; then + echo "Error: Please run as root." + exit 1 +fi + +project_root="$REPOSITORY_ROOT" +source "$project_root/TOOLS/helpers/utils.sh" "$project_root" + +check_tools "cat install" + +set -x +set -e + +cd "$SQUASHFS_ROOT" + +STAGE_DIR=./app/auto-dismiss-load-dialog +mkdir -p "$STAGE_DIR" + +cat "$CURRENT_PATCH_PATH/synth-tap" > "$STAGE_DIR/synth-tap" +cat "$CURRENT_PATCH_PATH/auto-dismiss-daemon" > "$STAGE_DIR/auto-dismiss-daemon" +cat "$CURRENT_PATCH_PATH/S99auto-dismiss" > "$STAGE_DIR/S99auto-dismiss" +chmod 755 "$STAGE_DIR/synth-tap" "$STAGE_DIR/auto-dismiss-daemon" "$STAGE_DIR/S99auto-dismiss" + +# Hook into rc.local: after the bootstrap_oc block, copy our files into +# /opt and start the service. Idempotent — only adds the hook once. +RC_LOCAL=./etc/rc.local +HOOK_BEGIN='# BEGIN: auto-dismiss-load-dialog' +HOOK_END='# END: auto-dismiss-load-dialog' + +if ! grep -q "$HOOK_BEGIN" "$RC_LOCAL"; then + # Insert before the final 'exit 0', if present; otherwise append. + TMP=$(mktemp) + awk -v begin="$HOOK_BEGIN" -v end="$HOOK_END" ' + /^exit 0/ && !inserted { + print begin + print "if [ -d /opt/sbin ] && [ -d /opt/etc/init.d ] && [ -f /app/auto-dismiss-load-dialog/auto-dismiss-daemon ]; then" + print " cp -f /app/auto-dismiss-load-dialog/synth-tap /opt/sbin/synth-tap" + print " cp -f /app/auto-dismiss-load-dialog/auto-dismiss-daemon /opt/sbin/auto-dismiss-daemon" + print " cp -f /app/auto-dismiss-load-dialog/S99auto-dismiss /opt/etc/init.d/S99auto-dismiss" + print " chmod 755 /opt/sbin/synth-tap /opt/sbin/auto-dismiss-daemon /opt/etc/init.d/S99auto-dismiss" + print " /opt/etc/init.d/S99auto-dismiss start &" + print "fi" + print end + print "" + inserted = 1 + } + { print } + END { + if (!inserted) { + print begin + print "[ -f /app/auto-dismiss-load-dialog/auto-dismiss-daemon ] && /opt/etc/init.d/S99auto-dismiss start &" + print end + } + } + ' "$RC_LOCAL" > "$TMP" + cat "$TMP" > "$RC_LOCAL" + rm -f "$TMP" +fi + +echo "Installed auto-dismiss-load-dialog patch." diff --git a/oc-patches/services/auto-dismiss-load-dialog/patch.toml b/oc-patches/services/auto-dismiss-load-dialog/patch.toml new file mode 100644 index 0000000..ddbe6f0 --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/patch.toml @@ -0,0 +1,5 @@ +id = "auto_dismiss_load_dialog" +name = "Auto-dismiss the load-filament-complete dialog" +execution_policy = "Always" +compatible_versions = ["*"] +after = ["bootstrap_oc"] diff --git a/oc-patches/services/auto-dismiss-load-dialog/synth-tap b/oc-patches/services/auto-dismiss-load-dialog/synth-tap new file mode 100755 index 0000000..3bad1e5 --- /dev/null +++ b/oc-patches/services/auto-dismiss-load-dialog/synth-tap @@ -0,0 +1,67 @@ +#!/opt/bin/bash +# synth-tap — synthesize a single touchscreen tap on /dev/input/event1. +# +# Centauri Carbon's Goodix gt9xxnew_ts driver speaks Linux multitouch +# protocol type A. A tap is just two bursts of events written to the +# input device: a "down" packet describing where the finger landed, +# followed ~150 ms later by an "up" packet. +# +# Usage: synth-tap [X] [Y] +# defaults: 289 182 (the "OK" button on the load-complete dialog) + +set -eu + +X=${1:-289} +Y=${2:-182} +DEV=/dev/input/event1 + +# Codes from on this kernel +readonly EV_SYN=0 +readonly EV_KEY=1 +readonly EV_ABS=3 +readonly SYN_REPORT=0 +readonly SYN_MT_REPORT=2 +readonly BTN_TOUCH=$((0x14a)) +readonly ABS_MT_TOUCH_MAJOR=$((0x30)) +readonly ABS_MT_WIDTH_MAJOR=$((0x32)) +readonly ABS_MT_POSITION_X=$((0x35)) +readonly ABS_MT_POSITION_Y=$((0x36)) +readonly ABS_MT_TRACKING_ID=$((0x39)) + +# Encode one input_event as 32 hex chars (16 bytes, little-endian): +# struct input_event { +# __u32 tv_sec; # 4 bytes +# __u32 tv_usec; # 4 bytes (kernel rewrites these on input, zeros are fine) +# __u16 type; # 2 bytes +# __u16 code; # 2 bytes +# __s32 value; # 4 bytes +# }; total 16 bytes +ev_hex() { + local typ=$1 code=$2 val=$3 + local uval=$(( val & 0xFFFFFFFF )) # mask to unsigned 32 (handles negatives too) + printf '0000000000000000%02x%02x%02x%02x%02x%02x%02x%02x' \ + $(( typ & 0xff )) $(( (typ >> 8) & 0xff )) \ + $(( code & 0xff )) $(( (code >> 8) & 0xff )) \ + $(( uval & 0xff )) $(( (uval >> 8) & 0xff )) \ + $(( (uval >> 16) & 0xff )) $(( (uval >> 24) & 0xff )) +} + +# DOWN burst — one open/write cycle +{ + ev_hex $EV_KEY $BTN_TOUCH 1 + ev_hex $EV_ABS $ABS_MT_POSITION_X "$X" + ev_hex $EV_ABS $ABS_MT_POSITION_Y "$Y" + ev_hex $EV_ABS $ABS_MT_TOUCH_MAJOR 20 + ev_hex $EV_ABS $ABS_MT_WIDTH_MAJOR 20 + ev_hex $EV_ABS $ABS_MT_TRACKING_ID 0 + ev_hex $EV_SYN $SYN_MT_REPORT 0 + ev_hex $EV_SYN $SYN_REPORT 0 +} | xxd -r -p > "$DEV" + +sleep 0.15 + +# UP burst — recorded sequence shows just BTN_TOUCH=0 + SYN_REPORT +{ + ev_hex $EV_KEY $BTN_TOUCH 0 + ev_hex $EV_SYN $SYN_REPORT 0 +} | xxd -r -p > "$DEV"