Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/README.md
Original file line number Diff line number Diff line change
@@ -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<M729> ← cycle is a load (load-only gcode)
[gcode][...]:single_command<M82> ← 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).
68 changes: 68 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/S99auto-dismiss
Original file line number Diff line number Diff line change
@@ -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 </dev/null & echo $! >"$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
77 changes: 77 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/auto-dismiss-daemon
Original file line number Diff line number Diff line change
@@ -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<M729>" → cycle is a load (M729 fires only in
# the load gcode, not unload)
# "single_command<M82>" → 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<M729>"*)
# Load-specific gcode. Only loads emit M729.
is_load=1
;;
*"single_command<M82>"*)
# 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"
75 changes: 75 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/patch.sh
Original file line number Diff line number Diff line change
@@ -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."
5 changes: 5 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/patch.toml
Original file line number Diff line number Diff line change
@@ -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"]
67 changes: 67 additions & 0 deletions oc-patches/services/auto-dismiss-load-dialog/synth-tap
Original file line number Diff line number Diff line change
@@ -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 <linux/input-event-codes.h> 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"