#!/bin/sh # # lcdd.sh -- drive the Whistle InterJet's front-panel LCD and keypad # from NetBSD. # # The panel is a 20x8 character LCD with a keypad, on com1 (/dev/dty01) # at 57600. lcdd paints a status screen, runs a front-panel menu, and has # test commands used to map the panel's control codes. Run it with no # arguments for usage. It needs only the NetBSD base system. # # Panel hazards: # ESC W arms a watchdog that RESETS THE MACHINE after 10 seconds of # serial silence. Never sent. # ESC X powers the machine off. Sent only by "lcdd.sh poweroff --yes". # ^A ^B ^C graphics commands that consume the bytes after them. lcd_blit # bounds-checks column and page; "diag" also sends them as tests. # ^D sets the LEDs. Only diag's LED tests send it, and they need # --lights. # # The panel is write-only: nothing can read back what it shows. # # Run stty on the port only while this script holds it open: the first # open of a com(4) port resets it to 9600. -ixon, because the keypad # shares the line and an XOFF would stop output for good. # # Exit 0 clean, 1 error, 2 usage. Status on stderr, readings on stdout. # # Author: Royce Williams # SPDX-License-Identifier: MIT # # Copyright (c) 2026 Royce Williams # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. set -u self_id() { _si_f=$0 case "$_si_f" in /*) ;; *) _si_f=$PWD/$_si_f ;; esac if command -v md5 >/dev/null 2>&1; then _si_h=$(md5 -q "$_si_f" 2>/dev/null) else _si_h=$(md5sum "$_si_f" 2>/dev/null | cut -d' ' -f1) fi printf '%s - %s\n' "${_si_f##*/}" "${_si_h:-unknown}" } VERSION=0.1.0 DEV=/dev/dty01 SPEED=57600 INTERVAL=30 WIDTH=20 ROWS=8 BANNER_HOLD=3 PIDFILE=/var/run/lcdd.pid DRYRUN=0 VERBOSE=0 SYSLOG=1 ASSUME_YES=0 DO_LIGHTS=0 DO_KEYPAD=0 KEYPAD_WAIT=30 INVHOLD=4 DIAG_HOLD=5 DIAG_LIST=0 DIAG_SEL='' GLYPHS=1 GLYPH_NAME=enter GCELL_C=7 GCELL_R=3 DO_DUMP=0 BLIT_BLANK=0 DO_PROBE=0 MENU=0 IDLE=60 FLASH_HOLD=3 SMASHBALL=/usr/local/bin/smashball DIAG_TAIL='' BANNER=' WhistleJet' MSG_TITLE='Message' MSG_FOOTER='' SPLASH_TEXT='' SPLASH_ROW7='' KEY_CHUNK=3 ALLOW_WRITE=0 PROG=${0##*/} C_CLEAR='\f' C_INVON='\017' C_INVOFF='\016' C_TAB='\t' C_BS='\b' C_POWEROFF='\033X' WAIT_SECS=90 C_GCOL='\001' C_GPAGE='\002' C_GBLIT='\003' G_ENTER='\030\074\176\377\030\030\030\030\030\030\037\037' G_CANCEL='\030\074\146\303\201\245\231\231\245\201\377\000' G_CELLS=2 TEXT_X0=6 ROW8_TEXT=' Press for menu' ROW8_GLYPH_PX=48 ROW8_PMIN=42 ROW8_PMAX=60 cell_px() { CELL_PX=$(( TEXT_X0 + 6 * $1 )); } C_EOL='\r\n' usage() { cat >&2 <<'USAGE' usage: lcdd.sh [options] [options] Options may appear on EITHER side of the command: "lcdd probe --keypad" and "lcdd --keypad probe" are the same. Anything left over after the command is an error, except the text operands of "msg". commands: probe interactive panel test; pauses between steps for the operator keys map the keypad and nothing else. Each byte is printed here AND shown on the panel, because the machine is usually across the room from the console. X twice finishes. Use this rather than "probe --keypad" once the display side is known good. diag [TEST|GROUP ...] THE CONTROL-CODE SUITE: every code the panel is known to honour, re-confirmed; the open questions (do ^B/^A move the TEXT cursor? which pixel grid is text on?); and every other byte below 0x20 except ESC. One frame per test, from ^N ^L. The console shows the bytes in hex and what to look for, and asks on /dev/tty. A table of results goes to stdout; exit 1 if a known fact FAILED. No operand runs all of it; TEST is an id such as T03 or G01, GROUP one of text graphics sweep leds shades. --list prints the catalogue and opens nothing. --yes asks nothing and holds each frame --hold SECS (default 5). LED tests need --lights; a run that lit any ends with every LED off. "diag --lights shades" shows green and amber side by side, each LED in each state once. start daemonize and refresh the status screen clear clear the panel and paint the idle screen, then exit blit send ONE raw ^C run: "lcdd blit COL PAGE HEX", e.g. "lcdd --blank blit 48 3 ff818181818181". The primitive under "glyph", exposed for testing what the panel does with a run. USE --blank: without it the run lands on whatever the panel already held, and this display keeps its contents across a reboot -- a one-column mark is then easy to read as part of a character that was already there. Make the FIRST byte differ from the rest (ff then 81) so that any stray copy of it is identifiable as the first byte. row8 try a glyph position on the real status row 8. No argument prints the table of pixel columns with the gap either side and touches nothing; "lcdd row8 51" puts that one on the panel. The glyph need NOT land on a character boundary -- that is what a blit buys, and 51 is the only position with equal gaps. status WHICH lcdd IS RUNNING, and is it the one rc.d knows about? Reports the file, the pidfile, and every lcdd daemon on the system. Kills nothing. Exit 1 if anything disagrees. Run this FIRST when a new file on disk changes nothing on the panel -- an untracked daemon left by an interrupted start keeps repainting from the code it began with. glyph BLIT TEST, never yet run on hardware. Draw one of Whistle's own key symbols on the glass with the graphics commands decoded from paneld, inside a frame built to show whether it landed on the character cell it was aimed at. Takes "enter" (default) or "cancel". See --cell and --dump. invtest sweep safe control codes looking for inverted text. Never sends ESC (watchdog) or ^A/^B/^C (consume arguments). Found: ^O (0x0F) turns inversion ON. invoff sweep for the code that turns inversion OFF. Each frame forces inversion on first, so a stuck panel cannot corrupt the reading. menu drive the front-panel menu once, in the foreground, and exit when it idles out or the operator leaves it. For testing; the deployed path is "start --menu". msg TEXT... paint one message frame and exit. For rc.d marker scripts and one-off notices. Each TEXT operand starts a new row and is word-wrapped from there. Refuses while the daemon owns the panel. splash TEXT [ROW7] paint the shutdown splash with TEXT centred on row 4, and ROW7 centred on row 7, then exit. rc.d/lcdd and rc.d/lcdhalt use it at shutdown. Refuses while the daemon owns the panel. poweroff TURN THE MACHINE OFF by sending ESC X to the panel -- the command WhistleWare's own kernel sent on RB_POWEROFF, and the one the BIOS sends when mains is gone for good. Needs --yes. Syncs twice first, but the ORDERLY path is "shutdown -p now", which unmounts before rc.d/lcdhalt sends the same two bytes. THE PANEL TAKES TENS OF SECONDS. --wait SECS sets how long to wait for the power to go (default 90); --wait 0 sends and returns at once. power the power-fail screen from powerfaild's state file, in the foreground, with X to hold its shutdown. The daemon raises it by itself when powerfaild sends SIGUSR2; this is for testing. Refuses while the daemon owns the panel. options: -d DEV serial device (default /dev/dty01, com1 callout) -i SECS refresh interval (default 30) -p FILE pidfile (default /var/run/lcdd.pid) -b SECS hold the startup banner (default 3) -n dry run: print the byte sequences, open no device -v progress to stderr --yes probe: do not pause between steps. diag: ask nothing, hold each frame for --hold seconds, record "unobserved". --lights diag: run the L tests, ^D with one mask bit at a time, the value 3 in each field, the shade check, then 00. However the run ends, it ends with every LED off; what was lit before cannot be restored. --list diag: print the catalogue of tests and open nothing. --hold S diag --yes: seconds each frame stays up (default 5). --keypad probe: run the KEYPAD test INSTEAD of the display walk -- the same thing as "lcdd keys". It does NOT mean "as well as": the keypad is the only readback the panel has, and testing it has nothing to do with the seven display screens. --menu start: watch the keypad and raise the front-panel menu when a key arrives. Default off. --glyphs draw chassis-key symbols with a blit instead of spelling them. Default ON. With it on, the row 7 legends draw the chassis keys: "[arrow] = menu" on the status screen, the Enter arrow blitted into two blank cells; with it off, "Enter = menu" and "Cancel = ..." are spelled out instead. --no-glyphs spell the key names instead of drawing them. --cell C R glyph: character column and row to draw at (default 7 3). --glyph N glyph: which symbol, same as the operand. --probe glyph: write an X where the blit left the cursor. OFF by default -- writing a character straight after a run is what puts a stray mark in the left margin. --blank blit: clear the panel before the run, so that anything on the glass afterwards came from this command and nothing else. --dump glyph: write the wire bytes to stdout and open no device. Run this on the TARGET first: lcdd glyph --dump cancel | od -An -tx1 | wc -w -> 18 It proves the target's printf emits every byte, including the \000 the cancel symbol ends with. If that byte were dropped the ^C run would be short and the panel would eat the next byte. diag: the whole suite's wire bytes, asking nothing; the table goes to stderr. On the target: lcdd diag --dump --lights | od -An -v -tx1 | grep -c 1b -> 0 --idle N menu: seconds of silence before it drops back (default 60). --idle 0 means NEVER time out, for sitting in the menu while reading it. Do not combine 0 with "start --menu": the daemon must be able to return to the status screen by itself. --title T msg: the inverted title row (default "Message") --footer T msg: one unwrapped line on row 7, after the five wrapped rows. For a closing verdict, not for more prose. Trimmed and centred, as a title is. --banner T the inverted title row on the status frame, the idle screen and the menu (default " WhistleJet"). CLIPPED AT 20 COLUMNS, silently. For a II: " WhistleJet II". --allow-write menu: permit the one item that writes /etc/ifconfig.. Default off. Everything else in the menu is runtime-only. exit: 0 ok, 1 error, 2 usage USAGE exit 2 } msg() { printf '%s: %s\n' "$PROG" "$*" >&2; } vmsg() { [ "$VERBOSE" -eq 0 ] || msg "$@"; } lmsg() { msg "$@" [ "$SYSLOG" -eq 0 ] && return 0 [ "$DRYRUN" -eq 1 ] && return 0 logger -t "$PROG" -p daemon.notice "$*" 2>/dev/null return 0 } cmd_power() { require_no_daemon power pf_read || die "no power-fail state in $PF_STATE: is powerfaild running?" lcd_open case $PF_ST in good) softkeys '' '' '' '' menu_frame 'Power' '' 'Mains power is on.' '' \ 'powerfaild is' 'watching.' ;; *) PF_PENDING=1 pf_screen ;; esac lcd_close return 0 } require_no_daemon() { [ "$DRYRUN" -eq 0 ] || return 0 [ -f "$PIDFILE" ] || return 0 read -r _rnd_pid _rnd_junk < "$PIDFILE" || return 0 [ -n "${_rnd_pid:-}" ] || return 0 kill -0 "$_rnd_pid" 2>/dev/null || return 0 msg "lcdd is running as pid $_rnd_pid and owns the panel." msg "Stop it first: /etc/rc.d/lcdd stop" die "$1 needs the panel to itself" } die() { msg "$*"; exit 1; } lcd_open() { if [ "$DRYRUN" -eq 1 ]; then exec 3>&1 printf '[dry run] would open %s and set %s baud\n' "$DEV" "$SPEED" lcd_raw "$C_INVOFF" return 0 fi [ -e "$DEV" ] || die "$DEV does not exist (run: cd /dev && sh MAKEDEV tty1)" [ -c "$DEV" ] || die "$DEV is not a character device" exec 3<>"$DEV" || die "cannot open $DEV" stty "$SPEED" cs8 -parenb -cstopb clocal -crtscts -ixon -ixoff \ -onlcr -opost -echo -icanon \ -isig -brkint -icrnl -inlcr -igncr -ixany -imaxbel -iexten <&3 \ || die "stty failed on $DEV" lcd_raw "$C_INVOFF" vmsg "opened $DEV at $SPEED bps" } lcd_close() { exec 3>&- } lcd_raw() { if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] bytes: %s\n' "$1" return 0 fi printf "$1" >&3 } lcd_clear() { lcd_raw "$C_CLEAR"; } lcd_line() { if [ "$DRYRUN" -eq 1 ]; then if [ "${#1}" -gt "$WIDTH" ]; then msg "OVERLONG (${#1} cols): [$1]" fi printf '[dry run] |%-*.*s|\n' "$WIDTH" "$WIDTH" "$1" return 0 fi printf '%.*s' "$WIDTH" "$1" >&3 printf "$C_EOL" >&3 } lcd_line_pad() { if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%-*.*s|\n' "$WIDTH" "$WIDTH" "$1" return 0 fi printf '%-*.*s' "$WIDTH" "$WIDTH" "$1" >&3 printf "$C_EOL" >&3 } lcd_line_last() { if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%-*.*s| (no trailing newline -- row 8)\n' \ "$WIDTH" "$WIDTH" "$1" return 0 fi printf '%-*.*s' "$WIDTH" "$WIDTH" "$1" >&3 } glyph_len() { GLEN=0 _gl_t=$1 while [ -n "$_gl_t" ]; do _gl_rest=${_gl_t#\\???} if [ "$_gl_rest" = "$_gl_t" ]; then msg "glyph_len: malformed escape string near '$_gl_t'" GLEN=0 return 1 fi _gl_t=$_gl_rest GLEN=$(( GLEN + 1 )) done return 0 } blit_fmt() { BLIT_FMT='' glyph_len "$3" || return 1 if [ "$1" -lt 0 ] || [ "$1" -gt 127 ] || [ "$2" -lt 0 ] || [ "$2" -gt 7 ]; then msg "lcd_blit: column $1 or page $2 out of range" return 1 fi if [ $(( $1 + GLEN )) -gt 128 ]; then msg "lcd_blit: $GLEN bytes at column $1 runs past 128" return 1 fi _bl_hdr=$(printf '\\%03o\\%03o\\%03o\\%03o\\%03o\\%03o' 1 "$1" 2 "$2" 3 "$GLEN") BLIT_FMT="$_bl_hdr$3" return 0 } lcd_blit() { blit_fmt "$@" || return 1 lcd_raw "$BLIT_FMT" return 0 } _sk_cell() { if [ -n "$1" ]; then printf "${C_INVON}%-4.4s${C_INVOFF}" "$1" >&3 else printf ' ' >&3 fi } lcd_softkeys() { if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%-4.4s %-4.4s %-4.4s %-4.4s| (row 8, unset = plain)\n' \ "$1" "$2" "$3" "$4" return 0 fi _sk_cell "$1"; printf ' ' >&3 _sk_cell "$2"; printf ' ' >&3 _sk_cell "$3"; printf ' ' >&3 _sk_cell "$4" } SK1=''; SK2=''; SK3=''; SK4='' softkeys() { SK1=${1:-}; SK2=${2:-}; SK3=${3:-}; SK4=${4:-} if [ "$DRYRUN" -eq 1 ]; then for _sk_chk in "$SK1" "$SK2" "$SK3" "$SK4"; do [ "${#_sk_chk}" -le 4 ] || msg "BUG: soft-key label '$_sk_chk' is ${#_sk_chk} columns, paints as 4" done fi } CENTRED='' centre_str() { _cs_n=${#1} if [ "$_cs_n" -ge "$WIDTH" ]; then CENTRED=$1 return 0 fi _cs_pad='' _cs_want=$(( (WIDTH - _cs_n) / 2 )) while [ "${#_cs_pad}" -lt "$_cs_want" ]; do _cs_pad="$_cs_pad " done CENTRED="$_cs_pad$1" } LG_GLYPH='' LG_CELL=0 glyph_legend() { if [ "$GLYPHS" -eq 1 ]; then centre_str " $3" LG_CELL=$(( (WIDTH - ${#3} - 2) / 2 )) LG_GLYPH=$1 else centre_str "$2$3" LG_GLYPH='' fi } legend_blit() { [ -n "$LG_GLYPH" ] || return 0 cell_px "$LG_CELL" [ "$DRYRUN" -eq 0 ] || printf '[dry run] row 7: glyph blitted into columns %s-%s\n' \ $(( LG_CELL + 1 )) $(( LG_CELL + 2 )) lcd_blit "$CELL_PX" 6 "$LG_GLYPH" LG_GLYPH='' } lcd_row() { case $1 in *'~'[0-9]*) ;; *) lcd_line "$1"; return 0 ;; esac _lr_in=$1 _lr_vis='' _lr_inv='' while [ -n "$_lr_in" ]; do case $_lr_in in *'~'[0-9]*) _lr_pre=${_lr_in%%'~'[0-9]*} ;; *) _lr_pre=$_lr_in ;; esac _lr_in=${_lr_in#"$_lr_pre"} lr_put "$_lr_pre" 0 [ -n "$_lr_in" ] || break _lr_in=${_lr_in#?} _lr_d=${_lr_in%"${_lr_in#?}"} _lr_in=${_lr_in#?} lr_put "$_lr_d" 0 done if [ "$DRYRUN" -eq 1 ]; then [ "${#_lr_vis}" -le "$WIDTH" ] || msg "OVERLONG (${#_lr_vis} cols): [$_lr_vis]" printf '[dry run] |%-*.*s| inv %s\n' "$WIDTH" "$WIDTH" "$_lr_vis" "$_lr_inv" return 0 fi printf "$C_EOL" >&3 } lr_put() { [ -n "$1" ] || return 0 _lp_room=$(( WIDTH - ${#_lr_vis} )) [ "$2" -eq 0 ] || _lr_inv="$_lr_inv${_lr_inv:+,}$(( ${#_lr_vis} + 1 ))" _lr_vis="$_lr_vis$1" [ "$DRYRUN" -eq 0 ] || return 0 [ "$_lp_room" -gt 0 ] || return 0 if [ "$2" -eq 1 ]; then printf "${C_INVON}%.*s${C_INVOFF}" "$_lp_room" "$1" >&3 else printf '%.*s' "$_lp_room" "$1" >&3 fi } lcd_title() { lcd_raw "$C_INVON" lcd_line_pad "$1" lcd_raw "$C_INVOFF" } K_ENTER=13 K_DEL=9 K_BS=8 K_STAR=42 K_HASH=35 K_UP=94 K_DOWN=118 K_LEFT=60 K_RIGHT=62 KEY='' KEY_TENTHS='' KEY_RAW=0 lcd_getkey() { lcd_getkey_raw "$1" _gk_rc=$? if [ "$PF_PENDING" -eq 1 ]; then pf_screen KEY='' return 1 fi return $_gk_rc } lcd_getkey_raw() { KEY='' if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] key (1 char, empty = timeout, q = quit): ' >&2 IFS= read -r _kline || return 1 [ -n "$_kline" ] || return 1 _kc=${_kline%"${_kline#?}"} KEY=$(printf '%d' "'$_kc") return 0 fi if [ "$1" != "$KEY_TENTHS" ]; then stty min 0 time "$1" <&3 || return 1 KEY_TENTHS=$1 fi set -- $(dd bs=1 count=1 <&3 2>/dev/null | od -An -tu1) [ $# -ge 1 ] || return 1 KEY=$1 case $KEY in 00?) KEY=${KEY#00} ;; 0??) KEY=${KEY#0} ;; esac if [ "$KEY_RAW" -eq 0 ]; then case $KEY in 0|17|19) KEY=''; return 1 ;; esac fi return 0 } KEY_DIGIT='' wait_key() { _wk_left=$1 KEY='' while [ "$_wk_left" -gt 0 ]; do _wk_chunk=$_wk_left [ "$_wk_chunk" -le "$KEY_CHUNK" ] || _wk_chunk=$KEY_CHUNK lcd_getkey $(( _wk_chunk * 10 )) && return 0 [ "$PF_UNWIND" -eq 0 ] || return 1 _wk_left=$(( _wk_left - _wk_chunk )) done return 1 } key_describe() { if [ "$KEY" -ge 32 ] && [ "$KEY" -le 126 ]; then KEY_DESC="$KEY = 0x$(printf '%02x' "$KEY") '$(printf "\\$(printf '%03o' "$KEY")")'" else KEY_DESC="$KEY = 0x$(printf '%02x' "$KEY") ctrl" fi } key_is_digit() { KEY_DIGIT='' [ -n "$KEY" ] || return 1 [ "$KEY" -ge 48 ] 2>/dev/null || return 1 [ "$KEY" -le 57 ] || return 1 KEY_DIGIT=$(( KEY - 48 )) return 0 } SYSNAME='' OSVER='' PHYSMB='' KERNID='' CONSDEV='' CONSPEED='' IS_II=0 init_static() { SYSNAME=$(uname -n) SYSNAME=${SYSNAME%%.*} OSVER=$(uname -sr) set -- $(uname -v 2>/dev/null | awk '{ _id = ""; _bn = "" for (i = 1; i <= NF; i++) { if ($i ~ /^\(/) { _id = $i; gsub(/[()]/, "", _id) } if ($i ~ /^#[0-9]+:?$/) { _bn = $i; sub(/:$/, "", _bn) } } if (_id != "") print _id, _bn }') if [ $# -ge 1 ]; then KERNID=$1 if [ -n "${2:-}" ] && [ $(( ${#1} + ${#2} + 1 )) -le "$WIDTH" ]; then KERNID="$1 $2" fi else KERNID=unknown fi case $KERNID in INTERJET2*) IS_II=1 ;; *) IS_II=0 ;; esac set -- $(sysctl -n hw.physmem 2>/dev/null) if [ $# -ge 1 ] && [ "$1" -gt 0 ] 2>/dev/null; then PHYSMB=$(( $1 / 1048576 )) else PHYSMB='?' fi set -- $(sysctl -n kern.consdev 2>/dev/null) CONSDEV=${1:-} CONSDEV=${CONSDEV#/dev/} [ -n "$CONSDEV" ] || CONSDEV='?' if [ "$DRYRUN" -eq 1 ]; then CONSPEED='?' else set -- $(stty -f /dev/console speed 2>/dev/null) CONSPEED=${1:-} case "$CONSPEED" in ''|*[!0-9]*) CONSPEED='?' ;; esac fi } UPTIME_STR='' fmt_uptime() { _secs=$(( $2 - $1 )) [ "$_secs" -ge 0 ] || _secs=0 _d=$(( _secs / 86400 )) _h=$(( (_secs % 86400) / 3600 )) _m=$(( (_secs % 3600) / 60 )) if [ "$_d" -gt 0 ]; then UPTIME_STR=$(printf '%dd %02d:%02d' "$_d" "$_h" "$_m") else UPTIME_STR=$(printf '%02d:%02d' "$_h" "$_m") fi } refresh() { [ "$GOING_DOWN" -eq 0 ] || return 0 set -- $(sysctl -n vm.loadavg kern.boottime 2>/dev/null) if [ $# -eq 4 ]; then _l1=$1; _l5=$2; _l15=$3; _boot=$4 else _l1='?'; _l5='?'; _l15='?'; _boot=0 fi set -- $(date '+%s %Y-%m-%d %H:%M:%S') _now=$1; _date=$2; _clock=$3 if [ "$_boot" -gt 0 ]; then fmt_uptime "$_boot" "$_now" else UPTIME_STR='unknown' fi status_ip _ip=$STATUS_IP lcd_clear lcd_title "$BANNER" lcd_line "$_date $_clock" lcd_line "on: $SYSNAME" lcd_ip_row "$_ip" lcd_line "up: $UPTIME_STR" lcd_line "ld: $_l1 $_l5 $_l15" if [ "$MENU" -eq 1 ]; then glyph_legend "$G_ENTER" 'Enter' ' = menu' lcd_line "$CENTRED" else lcd_line '' fi lcd_line_last '' legend_blit } status_ip() { net_iface_list STATUS_IP='none' for _si_n in $NET_IFACES; do net_addr "$_si_n" || continue [ "$NET_IP" != 'none' ] || continue STATUS_IP=$NET_IP return 0 done return 0 } lcd_ip_row() { lcd_line "IP: $1" } idle_screen() { [ "$GOING_DOWN" -eq 0 ] || return 0 status_ip lcd_clear lcd_title "$BANNER" lcd_line '' lcd_line "on: $SYSNAME" lcd_ip_row "$STATUS_IP" lcd_line '' lcd_line '' centre_str 'lcdd stopped' lcd_line "$CENTRED" lcd_line_last '' lcd_raw "$C_INVOFF" } on_signal() { trap - INT TERM QUIT USR1 [ -z "$SLEEP_PID" ] || kill "$SLEEP_PID" 2>/dev/null if [ "${1:-}" = quiet ]; then lmsg "daemon: pid $$ caught USR1, stopping without the idle screen" else lmsg "daemon: pid $$ caught a signal, restoring idle screen" idle_screen fi lcd_close if [ "$DRYRUN" -eq 0 ] && [ -f "$PIDFILE" ]; then read -r _os_pid _os_junk < "$PIDFILE" 2>/dev/null if [ "${_os_pid:-}" = "$$" ]; then rm -f "$PIDFILE" else msg "pidfile names ${_os_pid:-nothing}, not me ($$) -- left alone" fi fi lmsg "daemon: pid $$ stopped after $ITER refreshes" exit 130 } ITER=0 SLEEP_PID='' lcd_title_c() { _tc_t=$1 while [ "${_tc_t# }" != "$_tc_t" ]; do _tc_t=${_tc_t# }; done while [ "${_tc_t% }" != "$_tc_t" ]; do _tc_t=${_tc_t% }; done _tc_n=${#_tc_t} if [ "$_tc_n" -ge "$WIDTH" ]; then _tc_p=0 else _tc_p=$(( (WIDTH - _tc_n) / 2 )) fi _tc_r=$(( WIDTH - _tc_p )) lcd_raw "$C_INVON" if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%*s%-*.*s|\n' "$_tc_p" '' "$_tc_r" "$_tc_r" "$_tc_t" else printf '%*s%-*.*s' "$_tc_p" '' "$_tc_r" "$_tc_r" "$_tc_t" >&3 printf "$C_EOL" >&3 fi lcd_raw "$C_INVOFF" } menu_frame() { [ "$GOING_DOWN" -eq 0 ] || return 0 _mf_title=$1; shift _mf_legend=$1; shift lcd_clear lcd_title_c "$_mf_title" _mf_i=0 while [ "$_mf_i" -lt 5 ]; do if [ $# -gt 0 ]; then lcd_row "$1" shift else lcd_line '' fi _mf_i=$(( _mf_i + 1 )) done if [ $# -gt 0 ] && [ "$DRYRUN" -eq 1 ]; then msg "BUG: menu_frame '$_mf_title' was given $# rows beyond the five" fi lcd_line "$_mf_legend" if [ -n "$SK1$SK2$SK3$SK4" ]; then lcd_softkeys "$SK1" "$SK2" "$SK3" "$SK4" else lcd_line_last '' fi softkeys '' '' '' '' legend_blit } MENU_EXIT=0 GOTO_HOME=0 home_caught() { [ "$GOTO_HOME" -eq 1 ] || return 1 GOTO_HOME=0 return 0 } stay_here() { [ "$MENU_EXIT" -eq 0 ] || return 1 [ "$GOTO_HOME" -eq 0 ] || return 1 return 0 } GOING_DOWN=0 lcd_final() { softkeys '' '' '' '' menu_frame "$@" GOING_DOWN=1 } PF_STATE=${LCDD_PF_STATE:-/var/run/powerfaild.state} PF_PIDFILE=${LCDD_PF_PID:-/var/run/powerfaild.pid} PF_PENDING=0 PF_ACTIVE=0 PF_UNWIND=0 PF_ST='' PF_SECS=0 pf_read() { PF_ST='' PF_SECS=0 [ -r "$PF_STATE" ] || return 1 read -r PF_ST PF_SECS _pf_junk < "$PF_STATE" 2>/dev/null || return 1 case $PF_ST in good|lost|held|down) ;; *) PF_ST=''; return 1 ;; esac case $PF_SECS in ''|*[!0-9]*) PF_SECS=0 ;; esac return 0 } pf_mmss() { _pm_s=$(( $1 % 60 )) [ "$_pm_s" -ge 10 ] || _pm_s="0$_pm_s" PF_MMSS="$(( $1 / 60 )):$_pm_s" } pf_drain() { [ "$DRYRUN" -eq 0 ] || return 0 _pd_raw=$KEY_RAW KEY_RAW=1 _pd_n=0 while [ "$_pd_n" -lt 512 ] && lcd_getkey_raw 0; do _pd_n=$(( _pd_n + 1 )) done KEY_RAW=$_pd_raw [ "$_pd_n" -eq 0 ] || lmsg "power: discarded $_pd_n stale byte(s) from the panel" } pf_hold() { _ph_pid='' [ -r "$PF_PIDFILE" ] && read -r _ph_pid _ph_junk < "$PF_PIDFILE" 2>/dev/null case $_ph_pid in ''|*[!0-9]*) lmsg "power: X pressed, but $PF_PIDFILE names no powerfaild" return 1 ;; esac if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] would send SIGUSR1 to powerfaild pid %s\n' "$_ph_pid" return 0 fi if kill -USR1 "$_ph_pid" 2>/dev/null; then lmsg "power: X pressed, hold sent to powerfaild $_ph_pid" else lmsg "power: X pressed, but powerfaild $_ph_pid did not take the signal" fi } pf_paint() { pf_mmss "$PF_SECS" centre_str "$PF_MMSS" _pp_left=$CENTRED softkeys '' '' '' '' case $PF_ST in lost) glyph_legend "$G_CANCEL" 'Cancel' ' = hold' menu_frame 'Power failure' "$CENTRED" \ 'Mains power is off.' 'Running on battery.' '' \ 'Shutting down in' "$_pp_left" ;; held) menu_frame 'Power failure' '' \ 'Shutdown held.' 'Restore mains power' \ 'or it shuts down in' '' "$_pp_left" ;; esac } pf_screen() { PF_PENDING=0 [ "$PF_ACTIVE" -eq 0 ] || return 0 [ "$GOING_DOWN" -eq 0 ] || return 0 pf_read || return 0 case $PF_ST in lost|held|down) ;; *) return 0 ;; esac PF_ACTIVE=1 PF_UNWIND=1 lmsg "power: $PF_ST $PF_SECS; power-fail screen up" pf_drain _ps_shown='' _ps_n=0 while :; do case $PF_ST in lost|held) if [ "$_ps_shown" != "$PF_ST $PF_SECS" ]; then pf_paint _ps_shown="$PF_ST $PF_SECS" fi ;; down) centre_str "$ACK_TEXT" lcd_final 'Power failure' '' 'Mains power is off.' \ 'Shutting down.' '' "$CENTRED" break ;; good) softkeys '' '' '' '' menu_frame 'Power restored' '' 'Mains power is on.' '' \ 'Shutdown cancelled.' sleep 2 break ;; *) softkeys '' '' '' '' menu_frame 'Power failure' '' 'The power monitor' \ 'has stopped.' sleep 2 break ;; esac if lcd_getkey_raw 10 && [ "$KEY" = "$K_DEL" ] && [ "$PF_ST" = lost ]; then pf_hold fi PF_PENDING=0 pf_read || PF_ST='' if [ "$DRYRUN" -eq 1 ]; then _ps_n=$(( _ps_n + 1 )) [ "$_ps_n" -lt 200 ] || { msg 'dry run: 200 frames, stopping'; break; } fi done PF_ACTIVE=0 lmsg "power: power-fail screen down (${PF_ST:-no state file})" } pf_check() { [ "$PF_PENDING" -eq 0 ] || pf_screen } ACK_TEXT='Please wait...' ACK_ROW=4 lcd_ack() { [ "$GOING_DOWN" -eq 0 ] || return 0 lcd_raw "$C_CLEAR" centre_str "$ACK_TEXT" _ak_i=1 while [ "$_ak_i" -lt "$ACK_ROW" ]; do if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%-*.*s|\n' "$WIDTH" "$WIDTH" '' else lcd_raw "$C_EOL" fi _ak_i=$(( _ak_i + 1 )) done if [ "$DRYRUN" -eq 1 ]; then printf '[dry run] |%-*.*s| (ack, row %s, no trailing newline)\n' \ "$WIDTH" "$WIDTH" "$CENTRED" "$ACK_ROW" return 0 fi printf '%.*s' "$WIDTH" "$CENTRED" >&3 } menu_wait() { if [ "$IDLE" -le 0 ]; then while :; do lcd_getkey $(( KEY_CHUNK * 10 )) && { lcd_ack; MENU_EXIT=0; return 0; } [ "$PF_UNWIND" -eq 0 ] || { MENU_EXIT=1; return 1; } if [ "$DRYRUN" -eq 1 ]; then MENU_EXIT=1; return 1; fi done fi _mw_left=$IDLE while [ "$_mw_left" -gt 0 ]; do _mw_chunk=$_mw_left [ "$_mw_chunk" -le "$KEY_CHUNK" ] || _mw_chunk=$KEY_CHUNK if lcd_getkey $(( _mw_chunk * 10 )); then lcd_ack MENU_EXIT=0 return 0 fi [ "$PF_UNWIND" -eq 0 ] || { MENU_EXIT=1; return 1; } if [ "$DRYRUN" -eq 1 ]; then MENU_EXIT=1 return 1 fi _mw_left=$(( _mw_left - _mw_chunk )) done MENU_EXIT=1 return 1 } menu_flash() { _fl_title=$1; shift centre_str "back in ${FLASH_HOLD}s" menu_frame "$_fl_title" "$CENTRED" "$@" [ "$DRYRUN" -eq 1 ] || sleep "$FLASH_HOLD" } menu_error() { while :; do softkeys 'BACK' '' '' 'HOME' menu_frame "$@" menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; esac done } FIT='' fit_addr() { case "$2" in none|'') FIT="$1 none"; return 0 ;; esac FIT="$1 $2/$3" [ "${#FIT}" -le "$WIDTH" ] || FIT="$1 $2" } NET_IFACES='' net_iface_list() { NET_IFACES='' set -- $(ifconfig -l 2>/dev/null) for _nl in "$@"; do case "$_nl" in lo[0-9]*|ppp[0-9]*|tun[0-9]*|gif[0-9]*|stf[0-9]*|pflog[0-9]*) continue ;; esac NET_IFACES="${NET_IFACES}${NET_IFACES:+ }$_nl" done } NET_IP='' NET_PLEN='' net_addr() { NET_IP='none' NET_PLEN='' set -- $(ifconfig "$1" inet 2>/dev/null | awk '$1 == "inet" { print $2; exit }') [ $# -ge 1 ] || return 1 NET_IP=${1%%/*} [ "$1" = "$NET_IP" ] || NET_PLEN=${1#*/} return 0 } NET_MAC='' net_mac() { set -- $(ifconfig "$1" 2>/dev/null | awk '$1 == "address:" { print $2; exit }') NET_MAC=${1:-none} } NET_LINK='' net_linkstate() { NET_LINK='??' set -- $(ifconfig "$1" 2>/dev/null) [ $# -ge 2 ] || return 1 case "$2" in *''*|*''*) ;; *) NET_LINK='dn'; return 0 ;; esac case "$2" in *''*|*''*) NET_LINK='up' ;; *) NET_LINK='cf' ;; esac return 0 } NET_GW='' net_gateway() { set -- $(netstat -rn -f inet 2>/dev/null | awk '$1 == "default" { print $2; exit }') NET_GW=${1:-none} } net_dhcp_running() { _dr_pf=$(dhcpcd -P "$1" 2>/dev/null) [ -n "$_dr_pf" ] || _dr_pf="/var/run/dhcpcd/$1.pid" [ -s "$_dr_pf" ] || [ -s /var/run/dhcpcd/pid ] } ENTRY='' input_number() { _in_title=$1 _in_max=$2 _in_dots=$3 _in_hint=$4 _in_act=${5:-OK} ENTRY='' while :; do softkeys 'BACK' '' "$_in_act" '' glyph_legend "$G_CANCEL" 'Cancel' ' = delete' menu_frame "$_in_title" "$CENTRED" \ '' \ " ${ENTRY}_" \ '' \ "$_in_hint" \ '' menu_wait || return 1 case "$KEY" in "$K_ENTER"|"$K_LEFT") [ -n "$ENTRY" ] || return 1 return 0 ;; "$K_UP") return 1 ;; "$K_DEL"|"$K_BS") [ -n "$ENTRY" ] || return 1 ENTRY=${ENTRY%?} ;; "$K_STAR") [ "$_in_dots" -eq 1 ] || continue case "$ENTRY" in ''|*.) continue ;; esac [ "${#ENTRY}" -lt "$_in_max" ] || continue ENTRY="$ENTRY." ;; *) key_is_digit || continue [ "${#ENTRY}" -lt "$_in_max" ] || continue ENTRY="$ENTRY$KEY_DIGIT" ;; esac done } ip_is_valid() { _iv_n=0 _iv_rest=$1 while [ -n "$_iv_rest" ]; do _iv_oct=${_iv_rest%%.*} if [ "$_iv_oct" = "$_iv_rest" ]; then _iv_rest='' else _iv_rest=${_iv_rest#*.} [ -n "$_iv_rest" ] || return 1 fi case "$_iv_oct" in ''|*[!0-9]*) return 1 ;; 0?*) return 1 ;; esac [ "${#_iv_oct}" -le 3 ] || return 1 [ "$_iv_oct" -le 255 ] || return 1 _iv_n=$(( _iv_n + 1 )) done [ "$_iv_n" -eq 4 ] } confirm_screen() { _cf_act=${4:-YES} centre_str 'no answer = NO' softkeys 'BACK' '' '' "$_cf_act" menu_frame "$1" "$CENTRED" \ "$2" \ "${3:-}" \ '' \ '' \ '' menu_wait || return 1 [ "$KEY" -eq "$K_ENTER" ] || [ "$KEY" -eq "$K_RIGHT" ] } scr_netstatus() { while :; do net_iface_list net_gateway _ns_a='' _ns_b='' set -- $NET_IFACES if [ $# -ge 1 ]; then net_addr "$1" fit_addr "$1" "$NET_IP" "${NET_PLEN:-?}" _ns_a=$FIT fi if [ $# -ge 2 ]; then net_addr "$2" fit_addr "$2" "$NET_IP" "${NET_PLEN:-?}" _ns_b=$FIT fi _ns_link='' for _nsi in $NET_IFACES; do net_linkstate "$_nsi" _ns_try="${_ns_link:-link} $_nsi:$NET_LINK" [ "${#_ns_try}" -le "$WIDTH" ] || break _ns_link=$_ns_try done softkeys 'BACK' '' '' 'HOME' menu_frame ' Network status' 'link = UP + RUNNING' \ "${_ns_a:-no interfaces}" \ "$_ns_b" \ "$_ns_link" \ "gw ${NET_GW}" \ "host $SYSNAME" menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; esac done } scr_netsetup() { _sn_if=$1 _sn_newip='' _sn_newplen='' while :; do net_addr "$_sn_if" if net_dhcp_running "$_sn_if"; then _sn_dhcp='on' else _sn_dhcp='off' fi softkeys 'BACK' '' '' 'HOME' menu_frame " Setup: $_sn_if" '' \ "now $NET_IP" \ "new ${_sn_newip:-(same)}" \ "pfx /${NET_PLEN:-?} -> /${_sn_newplen:-?}" \ '~1 addr ~2 pfx ~3 apply' \ "~4 dhcp:$_sn_dhcp ~5 save" menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; 49) if input_number ' IP address' 15 1 '0-9 digit * = .' 'OK'; then if ip_is_valid "$ENTRY"; then _sn_newip=$ENTRY else menu_flash ' Bad address' '' "$ENTRY" '' 'is not an IPv4' 'address. Ignored.' fi else stay_here || return 1 fi ;; 50) if input_number ' Prefix length' 2 0 '0-32, e.g. 24' 'OK'; then if [ "$ENTRY" -le 32 ] 2>/dev/null; then _sn_newplen=$ENTRY else menu_flash ' Bad prefix' '' "/$ENTRY" '' 'must be 0 to 32.' 'Ignored.' fi else stay_here || return 1 fi ;; 51) if [ -z "$_sn_newip" ] || [ -z "$_sn_newplen" ]; then menu_flash ' Nothing to do' '' 'Set both address' 'and prefix length' 'first.' continue fi if ! confirm_screen "Apply to $_sn_if?" "$_sn_newip" "prefix /$_sn_newplen" 'SET'; then stay_here || return 1 continue fi centre_str "$ACK_TEXT" menu_frame 'Applying' '' '' " $_sn_if:" \ " $_sn_newip/$_sn_newplen" '' "$CENTRED" [ "$DRYRUN" -eq 1 ] || sleep "$FLASH_HOLD" if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would: ifconfig $_sn_if inet $_sn_newip/$_sn_newplen" _sn_rc=0 else ifconfig "$_sn_if" inet "$_sn_newip/$_sn_newplen" 2>/dev/null _sn_rc=$? fi if [ "$_sn_rc" -eq 0 ]; then menu_flash ' Applied' '' 'Address set.' '' 'NOT saved -- use' 'item 5 to persist.' else menu_error ' FAILED' '' '' 'ifconfig refused' 'the address.' '' 'Nothing changed.' || return 1 fi ;; 52) if [ "$_sn_dhcp" = 'on' ]; then if ! confirm_screen 'Stop DHCP?' "$_sn_if releases its" 'DHCP address.' 'OFF'; then stay_here || return 1 continue fi if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would: dhcpcd -k $_sn_if" else dhcpcd -k "$_sn_if" >/dev/null 2>&1 fi else if ! confirm_screen 'Start DHCP?' "$_sn_if requests an" 'address by DHCP.' 'ON'; then stay_here || return 1 continue fi if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would: dhcpcd $_sn_if" else dhcpcd "$_sn_if" >/dev/null 2>&1 fi fi menu_flash ' DHCP' '' 'Requested.' '' 'Check Network' 'status for effect.' ;; 53) scr_netsave "$_sn_if" || return 1 ;; esac done } scr_netsave() { _sv_if=$1 _sv_file="/etc/ifconfig.$_sv_if" if [ "$ALLOW_WRITE" -eq 0 ]; then while :; do softkeys 'BACK' '' '' 'HOME' menu_frame ' Save disabled' '' \ 'Writing /etc is' 'off by default.' '' 'Restart lcdd with' '--allow-write.' menu_wait || return 1 case "$KEY" in "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$K_DOWN"|"$K_LEFT") ;; *) return 0 ;; esac done fi net_addr "$_sv_if" [ "$NET_IP" != 'none' ] || { menu_flash ' Nothing to save' '' "$_sv_if has no" 'address configured.' return 0 } if ! confirm_screen 'Save network cfg?' "$_sv_file" "$NET_IP/${NET_PLEN:-32}" 'SAVE'; then stay_here || return 1 return 0 fi if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would write $_sv_file: inet $NET_IP/${NET_PLEN:-32}" menu_flash ' Saved' '' '[dry run]' "$_sv_file" return 0 fi _sv_tmp="$_sv_file.new.$$" if printf 'inet %s/%s\n' "$NET_IP" "${NET_PLEN:-32}" >| "$_sv_tmp" 2>/dev/null && [ -s "$_sv_tmp" ] && chmod 0644 "$_sv_tmp" 2>/dev/null && mv "$_sv_tmp" "$_sv_file" 2>/dev/null then menu_flash ' Saved' '' "$_sv_file" '' "$NET_IP/${NET_PLEN:-32}" else rm -f "$_sv_tmp" menu_error ' Save FAILED' '' 'Could not write' "$_sv_file" '' \ 'Full, read-only,' 'or a bad sector?' || return 1 fi } scr_sysinfo() { _si_page=1 while :; do if [ "$_si_page" -eq 1 ]; then net_iface_list set -- $NET_IFACES NET_MAC='none' [ $# -lt 1 ] || net_mac "$1" set -- $(sysctl -n kern.boottime 2>/dev/null) if [ "${1:-0}" -gt 0 ] 2>/dev/null; then _si_boot=$(date -r "$1" '+%m/%d/%y %H:%M' 2>/dev/null) else _si_boot='unknown' fi softkeys 'BACK' '' 'NEXT' 'HOME' menu_frame 'System info 1 of 2' '' \ "host $SYSNAME" \ "$OSVER" \ "mem $PHYSMB MB" \ "boot $_si_boot" \ "hw ${NET_MAC}" else _si_kern=$KERNID set -- $(sysctl -n vm.loadavg 2>/dev/null) _si_load="${1:-?} ${2:-?} ${3:-?}" set -- $(df -k / 2>/dev/null | awk 'NR == 2 { print $4, $5 }') _si_pct=${2:-?} if [ "${1:-x}" -ge 0 ] 2>/dev/null; then _si_free="$(( $1 / 1024 ))MB" else _si_free='?' fi softkeys 'BACK' 'PREV' '' 'HOME' menu_frame 'System info 2 of 2' '' \ "$_si_kern" \ "load $_si_load" \ "/ $_si_pct (${_si_free} free)" \ "con $CONSDEV $CONSPEED" \ "lcdd $VERSION" fi menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$K_LEFT") _si_page=2 ;; "$K_DOWN") _si_page=1 ;; "$K_ENTER") if [ "$_si_page" -eq 1 ]; then _si_page=2; else _si_page=1; fi ;; 49) _si_page=1 ;; 50) _si_page=2 ;; esac done } scr_syscontrol() { _sc_halt=50 [ "$IS_II" -eq 0 ] || _sc_halt=49 while :; do softkeys 'BACK' '' '' 'HOME' if [ "$IS_II" -eq 1 ]; then menu_frame ' System control' '' \ '~1 Shut down (halt)' \ '' \ '' \ 'An InterJet cannot' \ 'power itself off.' else menu_frame ' System control' '' \ '~1 Restart' \ '~2 Shut down (halt)' \ '' \ 'An InterJet cannot' \ 'power itself off.' fi menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$_sc_halt") if ! confirm_screen 'Shut down?' 'Halts the system' 'right now.' 'HALT'; then stay_here || return 1 continue fi if [ "$DRYRUN" -eq 1 ]; then msg '[dry run] would: shutdown -h now' else shutdown -h now 'front panel' >/dev/null 2>&1 fi centre_str "$ACK_TEXT" lcd_final ' Halting' '' \ '' \ '' \ "$CENTRED" \ '' \ '' return 1 ;; 49) sys_restart || return 1 ;; esac done } sys_restart() { if ! confirm_screen 'Restart?' 'Reboots the system' 'right now.' 'BOOT'; then stay_here && return 0 return 1 fi if [ "$DRYRUN" -eq 1 ]; then msg '[dry run] would: shutdown -r now' else shutdown -r now 'front panel' >/dev/null 2>&1 fi centre_str "$ACK_TEXT" lcd_final ' Restarting' '' \ '' \ '' \ "$CENTRED" \ '' \ '' return 1 } scr_diag() { while :; do softkeys 'BACK' '' '' 'HOME' menu_frame ' Diagnostics' '' \ '~1 Ping test' \ '~2 Key test' \ '~3 Panel test' \ '~4 Glyph map' \ '' menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; 49) scr_ping || return 1 ;; 50) scr_keytest || return 1 ;; 51) scr_paneltest || return 1 ;; 52) scr_glyphs || return 1 ;; *) diag_hidden || return 1 ;; esac done } diag_hidden() { case "$KEY" in "$K_STAR") _dh_c='*' ;; "$K_HASH") _dh_c='#' ;; 48|49|5[0-7]) _dh_c=$(( KEY - 48 )) ;; *) return 0 ;; esac DIAG_TAIL=$DIAG_TAIL$_dh_c while [ ${#DIAG_TAIL} -gt 9 ]; do DIAG_TAIL=${DIAG_TAIL#?} done case "$DIAG_TAIL" in *'*#') scr_smashball ;; *) return 0 ;; esac } scr_smashball() { if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would run: $SMASHBALL --panel --idle $IDLE" return 0 fi if [ ! -x "$SMASHBALL" ]; then vmsg "no $SMASHBALL; the hidden code does nothing" return 0 fi lmsg "starting $SMASHBALL" "$SMASHBALL" --panel --idle "$IDLE" <&3 >&3 _sb_rc=$? [ "$_sb_rc" -ne 3 ] || return 1 [ "$_sb_rc" -le 2 ] || msg "$SMASHBALL exited $_sb_rc" return 0 } scr_ping() { _pg_last='' while :; do if ! input_number ' Ping address' 15 1 '0-9 digit * = .' 'PING'; then stay_here || return 1 return 0 fi if ! ip_is_valid "$ENTRY"; then menu_flash ' Bad address' '' "$ENTRY" '' 'is not an IPv4' 'address.' continue fi _pg_last=$ENTRY centre_str "$_pg_last" _pg_row=$CENTRED centre_str "$ACK_TEXT" menu_frame ' Ping' '' '' "$_pg_row" '' "$CENTRED" '' if [ "$DRYRUN" -eq 1 ]; then msg "[dry run] would: ping -c 3 -w 5 $_pg_last" _pg_sent=3; _pg_recv=3 else set -- $(ping -c 3 -w 5 "$_pg_last" 2>&1 | awk '/packets transmitted/ { print $1, $4; exit }') _pg_sent=${1:-0} _pg_recv=${2:-0} fi if [ "$_pg_recv" -gt 0 ] 2>/dev/null; then _pg_verdict='REACHABLE' else _pg_verdict='no reply' fi while :; do softkeys 'BACK' 'AGN' '' 'HOME' menu_frame ' Ping result' '' \ '' \ "$_pg_row" \ '' \ "sent $_pg_sent got $_pg_recv" \ "$_pg_verdict" menu_wait || return 1 case "$KEY" in "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$K_DOWN"|"$K_ENTER") break ;; "$K_LEFT") ;; *) return 0 ;; esac done done } scr_keytest() { _kt_prev='--' _kt_dels=0 while :; do glyph_legend "$G_CANCEL" 'Cancel' ' twice = exit' menu_frame ' Key test' "$CENTRED" \ "$_kt_prev" \ '' \ 'Press a key on' \ 'the front panel.' \ '' menu_wait || return 1 key_describe _kt_prev="byte $KEY_DESC" if [ "$KEY" -eq "$K_DEL" ] || [ "$KEY" -eq "$K_BS" ]; then _kt_dels=$(( _kt_dels + 1 )) [ "$_kt_dels" -lt 2 ] || return 0 else _kt_dels=0 fi done } scr_paneltest() { _pt_p=1 while :; do if [ "$_pt_p" -eq 1 ]; then softkeys '' '' 'NEXT' '' centre_str 'ruler above = 20' menu_frame '01234567890123456789' "$CENTRED" \ 'row 2' 'row 3' 'row 4' 'row 5' 'row 6' menu_wait || return 1 _pt_p=2 continue fi softkeys 'BACK' 'PREV' '' 'HOME' menu_frame ' Panel test' '' \ 'Title above should' 'be INVERTED and' 'full width.' '' 'Rows 1-5 visible?' menu_wait || return 1 case "$KEY" in "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$K_DOWN") _pt_p=1 ;; "$K_LEFT") ;; *) return 0 ;; esac done } HX='' hex_digit() { case "$1" in 10) HX=A ;; 11) HX=B ;; 12) HX=C ;; 13) HX=D ;; 14) HX=E ;; 15) HX=F ;; *) HX=$1 ;; esac } GROW='' glyph_row() { _gr_c=$1 _gr_end=$(( $1 + $2 )) if [ "$_gr_c" -lt 32 ] || [ "$_gr_end" -gt 256 ]; then msg "BUG: glyph_row $1 $2 leaves 0x20-0xff" GROW='' return 1 fi hex_digit $(( _gr_c / 16 )) GROW="$HX" if [ "$3" != 'dense' ]; then hex_digit $(( _gr_c % 16 )) GROW="$GROW$HX" fi GROW="$GROW:" while [ "$_gr_c" -lt "$_gr_end" ]; do _gr_g="\\$(( _gr_c / 64 ))$(( _gr_c / 8 % 8 ))$(( _gr_c % 8 ))" case "$3" in spaced) GROW="$GROW$_gr_g " ;; inverted) GROW="$GROW$C_INVON$_gr_g$C_INVOFF " ;; *) GROW="$GROW$_gr_g" ;; esac _gr_c=$(( _gr_c + 1 )) done [ "$3" != 'dense' ] || GROW="$GROW " GROW="$GROW|" return 0 } glyph_frame() { [ "$GOING_DOWN" -eq 0 ] || return 0 if [ "$1" = 'dense' ]; then _gf_per=16; _gf_rows='1 2 3 4 5 6 7' else _gf_per=8; _gf_rows='1 3 5 7' fi _gf_c=$(( 32 + $2 * 32 )) [ "$1" != 'dense' ] || _gf_c=$(( 32 + $2 * 112 )) lcd_clear _gf_r=1 while [ "$_gf_r" -le 7 ]; do case " $_gf_rows " in *" $_gf_r "*) glyph_row "$_gf_c" "$_gf_per" "$1" || return 1 lcd_raw "$GROW$C_EOL" _gf_c=$(( _gf_c + _gf_per )) ;; *) lcd_raw "$C_EOL" ;; esac _gf_r=$(( _gf_r + 1 )) done lcd_softkeys "$SK1" "$SK2" "$SK3" "$SK4" softkeys '' '' '' '' } scr_glyphpage() { _gp_n=7 [ "$1" != 'dense' ] || _gp_n=2 _gp_p=0 while :; do _gp_prev='' [ "$_gp_p" -eq 0 ] || _gp_prev='PREV' _gp_next='' [ "$_gp_p" -ge $(( _gp_n - 1 )) ] || _gp_next='NEXT' softkeys 'BACK' "$_gp_prev" "$_gp_next" 'HOME' glyph_frame "$1" "$_gp_p" || return 0 menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; "$K_DOWN") [ "$_gp_p" -eq 0 ] || _gp_p=$(( _gp_p - 1 )) ;; "$K_LEFT") [ "$_gp_p" -ge $(( _gp_n - 1 )) ] || _gp_p=$(( _gp_p + 1 )) ;; *) key_is_digit || continue [ "$KEY_DIGIT" -ge 1 ] && [ "$KEY_DIGIT" -le "$_gp_n" ] || continue _gp_p=$(( KEY_DIGIT - 1 )) ;; esac done } scr_glyphctl() { while :; do [ "$GOING_DOWN" -eq 0 ] || return 0 softkeys 'BACK' '' '' 'HOME' lcd_clear lcd_raw "1E:\\036 \\037 |$C_EOL" lcd_raw "$C_EOL" lcd_raw "1E:$C_INVON\\036$C_INVOFF $C_INVON\\037$C_INVOFF |$C_EOL" _gc_r=4 while [ "$_gc_r" -le 7 ]; do lcd_raw "$C_EOL" _gc_r=$(( _gc_r + 1 )) done lcd_softkeys "$SK1" "$SK2" "$SK3" "$SK4" softkeys '' '' '' '' menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; esac done } scr_glyphs() { while :; do softkeys 'BACK' '' '' 'HOME' menu_frame ' Glyph map' '' \ '~1 Spaced, 7 pages' \ '~2 Inverted cells' \ '~3 Dense, 2 pages' \ '~4 Codes 1E and 1F' \ '1-3: codes 20-FF' menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; 49) scr_glyphpage spaced || return 1 ;; 50) scr_glyphpage inverted || return 1 ;; 51) scr_glyphpage dense || return 1 ;; 52) scr_glyphctl || return 1 ;; esac done } menu_main() { GOTO_HOME=0 while :; do softkeys 'EXIT' '' '' '' menu_frame 'Main menu' '' \ '~1 Network status' \ '~2 Network setup' \ '~3 System info' \ '~4 System control' \ '~5 Diagnostics' menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; 49) scr_netstatus || home_caught || return 1 ;; 50) menu_pick_iface || home_caught || return 1 ;; 51) scr_sysinfo || home_caught || return 1 ;; 52) scr_syscontrol || home_caught || return 1 ;; 53) scr_diag || home_caught || return 1 ;; esac done } menu_pick_iface() { net_iface_list set -- $NET_IFACES case $# in 0) menu_flash ' Network setup' '' 'No configurable' 'interfaces found.' return 0 ;; 1) scr_netsetup "$1"; return $? ;; esac while :; do softkeys 'BACK' '' '' 'HOME' menu_frame ' Which interface' '' \ "${1:+~1 $1}" "${2:+~2 $2}" "${3:+~3 $3}" "${4:+~4 $4}" "${5:+~5 $5}" menu_wait || return 1 case "$KEY" in "$K_UP"|"$K_DEL") return 0 ;; "$K_RIGHT") GOTO_HOME=1; return 1 ;; esac key_is_digit || continue [ "$KEY_DIGIT" -ge 1 ] || continue [ "$KEY_DIGIT" -le 5 ] && [ "$KEY_DIGIT" -le $# ] || continue _pi_n=$KEY_DIGIT _pi_name='' for _pi_c in "$@"; do _pi_n=$(( _pi_n - 1 )) [ "$_pi_n" -gt 0 ] || { _pi_name=$_pi_c; break; } done [ -z "$_pi_name" ] || { scr_netsetup "$_pi_name" || return 1; } done } menu_poll() { _mp_left=$INTERVAL while [ "$_mp_left" -gt 0 ]; do _mp_chunk=$_mp_left [ "$_mp_chunk" -le "$KEY_CHUNK" ] || _mp_chunk=$KEY_CHUNK if lcd_getkey $(( _mp_chunk * 10 )); then lmsg "menu raised by byte $KEY" lcd_ack menu_main return 0 fi [ "$PF_UNWIND" -eq 0 ] || return 1 [ "$DRYRUN" -eq 0 ] || return 1 _mp_left=$(( _mp_left - _mp_chunk )) done return 1 } step() { printf '\n--- %s. %s\n' "$1" "$2" } confirm() { printf ' expect: %s\n' "$1" if [ "$ASSUME_YES" -eq 1 ] || [ "$DRYRUN" -eq 1 ]; then return 0 fi printf ' the panel should have changed BY NOW. Enter to go on, Ctrl-C to stop: ' read -r _ || probe_abort } probe_abort() { trap - INT TERM QUIT printf '\naborted at step %s; restoring the idle screen\n' "${PROBE_STEP:-?}" >&2 idle_screen lcd_close exit 130 } PROBE_STEP='' cmd_probe() { if [ "$DO_KEYPAD" -eq 1 ]; then msg "--keypad selects the keypad test only; same as: $PROG keys" cmd_keys return $? fi require_no_daemon 'probe' init_static printf '%s probe -- device %s, %s bps, %sx%s\n' \ "$PROG" "$DEV" "$SPEED" "$WIDTH" "$ROWS" printf 'The panel cannot be read back. Every check below except the\n' printf 'termios readback needs your eyes on the glass.\n' printf 'Each step WRITES FIRST and then asks -- so by the time you are\n' printf 'prompted, the bytes have already gone out.\n' PROBE_STEP=0 step 0 'device node' if [ "$DRYRUN" -eq 0 ]; then ls -lA "$DEV" || die "no such device: $DEV" fi lcd_open trap probe_abort INT TERM QUIT PROBE_STEP=1 step 1 'termios readback (the port, not the panel)' if [ "$DRYRUN" -eq 0 ]; then stty -a <&3 fi PROBE_STEP=2 step 2 'clear, then one line' lcd_clear lcd_line 'lcdd probe' confirm 'ONLY the text "lcdd probe", nothing else' PROBE_STEP=3 step 3 'column ruler and clipping' lcd_clear lcd_line '12345678901234567890' lcd_line 'ABCDEFGHIJKLMNOPQRSTUVWX' confirm 'a ruler ending in 0 at the right edge, then ABC..T cut at 20 (NOT wrapped)' PROBE_STEP=4 step 4 'row count' lcd_clear lcd_line 'row 1' lcd_line 'row 2' lcd_line 'row 3' lcd_line 'row 4' lcd_line 'row 5' lcd_line 'row 6' lcd_line 'row 7' lcd_line_last 'row 8' confirm 'rows 1 to 8, all visible, nothing scrolled off the top' PROBE_STEP=5 step 5 'inverted text' lcd_clear lcd_line 'normal' lcd_raw "$C_INVON" lcd_line 'inverted' lcd_raw "$C_INVOFF" lcd_line 'normal again' confirm 'the middle line reversed, the other two not' PROBE_STEP=6 step 6 'tab and backspace' lcd_clear lcd_raw "A${C_TAB}B${C_EOL}" lcd_raw "XY" lcd_raw "${C_BS}" lcd_raw "Z${C_EOL}" confirm 'line 1: A then B at a tab stop. line 2: XZ (the Z replacing the Y)' step 7 'LEDs NOT tested here -- run "lcdd diag --lights leds" for that' step 8 'keypad NOT tested here -- run "lcdd keys" for that' printf '\n--- not attempted, deliberately:\n' printf ' ESC W hardware watchdog -- resets the machine after 10s silence\n' printf ' ^A ^B ^C graphics mode -- firmware misbehaves if drawn off-screen\n' PROBE_STEP=9 step 9 'restoring the idle screen' idle_screen lcd_close trap - INT TERM QUIT printf '\nprobe complete. If steps 2-6 all looked right, "%s start" is safe.\n' "$PROG" return 0 } INV_SAFE="016:^N(SO) 006:^F(sheet) 005:^E 013:^K 020:^P 021:^Q 022:^R \ 023:^S 024:^T 025:^U 026:^V 027:^W 030:^X 031:^Y 032:^Z 034:^\\ 035:^]" cmd_keys() { require_no_daemon 'keys' KEY_RAW=1 init_static lcd_open trap probe_abort INT TERM QUIT PROBE_STEP='keys' printf '%s keys -- press keys on the FRONT PANEL; each byte is printed\n' "$PROG" printf 'here and shown on the panel. X (Cancel) twice in a row finishes,\n' printf 'as does %s seconds of silence. Ctrl-C is safe.\n\n' "$KEYPAD_WAIT" printf 'The six codes from the keypad cheat sheet -- * # and the four\n' printf 'softkeys -- are the ones worth walking.\n' printf 'The sheet is known wrong about ^F/^G on this hardware.\n\n' _k_n=0 _k_dels=0 _k_last='(none yet)' while :; do menu_frame ' Key test' 'X twice = done' \ "$_k_last" \ '' \ 'Press a key on' \ 'the front panel.' \ '' if ! wait_key "$KEYPAD_WAIT"; then printf '\n%s seconds of silence -- finishing.\n' "$KEYPAD_WAIT" break fi key_describe _k_n=$(( _k_n + 1 )) printf ' %3d byte %s\n' "$_k_n" "$KEY_DESC" _k_last="$KEY_DESC" if [ "$KEY" -eq "$K_DEL" ] || [ "$KEY" -eq "$K_BS" ]; then _k_dels=$(( _k_dels + 1 )) if [ "$_k_dels" -ge 2 ]; then printf '\nX twice -- finishing.\n' break fi else _k_dels=0 fi done printf '\n%s key(s) read. Record them, and say which\n' "$_k_n" printf 'PHYSICAL key produced each byte -- the byte alone maps nothing.\n' idle_screen lcd_close trap - INT TERM QUIT return 0 } cmd_invoff() { require_no_daemon 'invoff' init_static lcd_open trap probe_abort INT TERM QUIT printf '%s invoff -- hunting for the inverted-text OFF code\n' "$PROG" printf 'Row 1 must be INVERTED every time (that is the sanity check).\n' printf 'Rows 2-3 PLAIN means the candidate on row 1 is the off code.\n' for _c in 017:'^O(toggle?)' $INV_SAFE; do _oct=${_c%%:*} _lab=${_c#*:} PROBE_STEP="invoff $_oct $_lab" lcd_clear lcd_raw "$C_INVON" lcd_line_pad "code $_oct $_lab" lcd_raw "\\$_oct" lcd_line_pad 'OFF?' lcd_line_pad 'OFF?' printf '\n--- candidate \\%s %s\n' "$_oct" "$_lab" inv_pause done lcd_raw "$C_INVOFF" lcd_clear idle_screen lcd_close trap - INT TERM QUIT printf '\ninvoff complete. If the idle screen is still inverted, no safe\n' printf 'code turns it off and only a POWER CYCLE will clear the panel --\n' printf 'a reboot will not, this panel keeps its contents across one.\n' return 0 } cmd_invtest() { require_no_daemon 'invtest' init_static lcd_open trap probe_abort INT TERM QUIT printf '%s invtest -- hunting for the inverted-text code\n' "$PROG" printf 'Watch the panel. Row 2 vs row 3:\n' printf ' row 2 inverted -> that is the code\n' printf ' row 2 shifted right -> code not recognised, printed as a glyph\n' printf ' rows identical -> code consumed, no effect\n' for _c in '006:^F(doc:off)' '007:^G(doc:on)' '016:^N(SO)' '017:^O(SI)' \ '005:^E' '013:^K' '020:^P' '021:^Q' '022:^R' '023:^S' \ '024:^T' '025:^U' '026:^V' '027:^W' '030:^X' '031:^Y' \ '032:^Z' '034:^\' '035:^]' '036:^^' '037:^_'; do _oct=${_c%%:*} _lab=${_c#*:} PROBE_STEP="0x$(printf '%02X' "0$_oct") $_lab" lcd_raw '\006' lcd_clear lcd_line "code $_oct $_lab" lcd_raw "\\$_oct" lcd_line 'INVERT?' lcd_line 'INVERT?' lcd_raw '\006' printf '\n--- candidate \\%s %s\n' "$_oct" "$_lab" inv_pause done lcd_raw '\006' idle_screen lcd_close trap - INT TERM QUIT printf '\ninvtest complete. If no candidate inverted row 2, this firmware\n' printf 'has no text-inversion attribute and the BIOS banner is drawn in\n' printf 'graphics mode. Inversion is cosmetic -- do not chase it further.\n' return 0 } inv_pause() { if [ "$ASSUME_YES" -eq 1 ] || [ "$DRYRUN" -eq 1 ]; then [ "$DRYRUN" -eq 1 ] || sleep "$INVHOLD" return 0 fi printf ' Enter for next, Ctrl-C to stop: ' read -r _ || probe_abort } cmd_clear() { init_static lcd_open idle_screen lcd_close vmsg 'panel cleared' } msg_wrap() { _mw_g=$- set -f _mw_n=0 for _mw_para in "$@"; do _mw_row='' for _mw_w in $_mw_para; do if [ -z "$_mw_row" ]; then _mw_try=$_mw_w else _mw_try="$_mw_row $_mw_w" fi if [ "${#_mw_try}" -le "$WIDTH" ]; then _mw_row=$_mw_try continue fi lcd_line "$_mw_row" _mw_n=$(( _mw_n + 1 )) [ "$_mw_n" -lt 5 ] || break 2 _mw_row=$_mw_w done if [ -n "$_mw_row" ] && [ "$_mw_n" -lt 5 ]; then lcd_line "$_mw_row" _mw_n=$(( _mw_n + 1 )) fi done while [ "$_mw_n" -lt 5 ]; do lcd_line '' _mw_n=$(( _mw_n + 1 )) done case $_mw_g in *f*) ;; *) set +f ;; esac } cmd_msg() { [ $# -ge 1 ] || { msg 'msg needs some text to display'; usage; } if [ "$DRYRUN" -eq 0 ] && [ -f "$PIDFILE" ]; then read -r _oldpid _junk < "$PIDFILE" if [ -n "${_oldpid:-}" ] && kill -0 "$_oldpid" 2>/dev/null; then die "lcdd is running as pid $_oldpid and owns the panel" fi fi lcd_open lcd_clear lcd_title_c "$MSG_TITLE" msg_wrap "$@" if [ -n "$MSG_FOOTER" ]; then _cm_f=$MSG_FOOTER while [ "${_cm_f# }" != "$_cm_f" ]; do _cm_f=${_cm_f# }; done while [ "${_cm_f% }" != "$_cm_f" ]; do _cm_f=${_cm_f% }; done centre_str "$_cm_f" lcd_line "$CENTRED" fi lcd_close return 0 } cmd_splash() { if [ "$DRYRUN" -eq 0 ] && [ -f "$PIDFILE" ]; then read -r _oldpid _junk < "$PIDFILE" if [ -n "${_oldpid:-}" ] && kill -0 "$_oldpid" 2>/dev/null; then die "lcdd is running as pid $_oldpid and owns the panel" fi fi centre_str "$SPLASH_TEXT" _sp_row4=$CENTRED centre_str "$SPLASH_ROW7" _sp_row7=$CENTRED lcd_open printf '\016\f\017%-20.20s\r\n\016\r\n\r\n%s\r\n\r\n\r\n' \ "$BANNER" "$_sp_row4" >&3 [ -z "$SPLASH_ROW7" ] || printf '%.20s' "$_sp_row7" >&3 lcd_close return 0 } cmd_poweroff() { if [ "$DRYRUN" -eq 0 ] && [ "$ASSUME_YES" -eq 0 ]; then msg 'poweroff cuts power: OFF with mains absent, a power cycle with mains present.' msg 'Add --yes if that is what you mean.' msg 'For an orderly power-off use: shutdown -p now' exit 2 fi if [ "$DRYRUN" -eq 0 ] && [ -f "$PIDFILE" ]; then read -r _po_pid _po_junk < "$PIDFILE" if [ -n "${_po_pid:-}" ] && kill -0 "$_po_pid" 2>/dev/null; then die "lcdd is running as pid $_po_pid and owns the panel; stop it first" fi fi msg 'sending ESC X: panel power off' if [ "$DRYRUN" -eq 0 ]; then sync sync fi lcd_open lcd_raw "$C_POWEROFF" lcd_close [ "$DRYRUN" -eq 0 ] || return 0 if [ "$WAIT_SECS" -le 0 ]; then msg 'sent; the panel takes tens of seconds' return 0 fi msg "waiting up to ${WAIT_SECS}s for the power to go" _po_n=0 while [ "$_po_n" -lt "$WAIT_SECS" ]; do sleep 5 _po_n=$((_po_n + 5)) done msg "still running after ${WAIT_SECS}s: the power did not go." msg 'That does NOT by itself mean the panel ignored ESC X: it can answer' msg 'on DSR and CTS. Watch them with "dcdwatch --watch", and run' msg 'the control: "lcdd clear" opens and closes this same port WITHOUT' msg 'sending ESC X, so anything it moves was the port, not the command.' return 1 } cmd_menu() { if [ "$DRYRUN" -eq 0 ] && [ -f "$PIDFILE" ]; then read -r _oldpid _junk < "$PIDFILE" if [ -n "${_oldpid:-}" ] && kill -0 "$_oldpid" 2>/dev/null; then die "lcdd is running as pid $_oldpid and owns the panel; stop it first" fi fi trap on_signal INT TERM QUIT init_static lcd_open menu_main idle_screen lcd_close trap - INT TERM QUIT return 0 } start_banner() { lcd_clear lcd_title "$BANNER" lcd_line "on: $SYSNAME" lcd_line "$OSVER" lcd_line "$KERNID" lcd_line "mem: $PHYSMB MB" lcd_line '' centre_str 'lcdd starting...' lcd_line "$CENTRED" lcd_line_last '' } FB_FAILED=${LCDD_FB_FAILED:-/var/run/tkfirstboot.failed} scr_fbfailed() { [ "$MENU" -eq 1 ] || return 0 [ -r "$FB_FAILED" ] || return 0 _fb_1='' _fb_2='' { IFS= read -r _fb_1; IFS= read -r _fb_2; } < "$FB_FAILED" _fb_boot='BOOT' [ "$IS_II" -eq 0 ] || _fb_boot='' while :; do pf_check PF_UNWIND=0 [ "$GOING_DOWN" -eq 0 ] || return 0 softkeys '' '' 'OK' "$_fb_boot" menu_frame 'First boot failed' '' \ "$_fb_1" "$_fb_2" 'Retries next boot.' _fb_k='' while [ -z "$_fb_k" ]; do if lcd_getkey $(( KEY_CHUNK * 10 )); then case $KEY in "$K_LEFT"|"$K_DEL") _fb_k=ok ;; "$K_RIGHT") [ "$IS_II" -eq 1 ] || _fb_k=boot ;; esac elif [ "$PF_UNWIND" -eq 1 ]; then _fb_k=repaint elif [ "$DRYRUN" -eq 1 ]; then return 0 fi done case $_fb_k in ok) lcd_ack; return 0 ;; boot) lcd_ack sys_restart && continue return 0 ;; esac done } cmd_start() { if [ "$DRYRUN" -eq 1 ]; then vmsg "daemon flags:$(daemon_flags_str)" init_static lcd_open start_banner scr_fbfailed ITER=1 refresh return 0 fi if [ "${LCDD_CHILD:-0}" = "0" ]; then if [ -f "$PIDFILE" ]; then read -r _oldpid _junk < "$PIDFILE" if [ -n "${_oldpid:-}" ] && kill -0 "$_oldpid" 2>/dev/null; then die "already running as pid $_oldpid" fi vmsg "removing stale pidfile $PIDFILE" rm -f "$PIDFILE" fi DFLAGS=$(daemon_flags_str) vmsg "daemon flags:$DFLAGS" LCDD_CHILD=1 "$0" -d "$DEV" -i "$INTERVAL" -p "$PIDFILE" \ -b "$BANNER_HOLD" --idle "$IDLE" \ ${DFLAGS} start \ /dev/null 2>&1 & _child=$! printf '%s\n' "$_child" >| "$PIDFILE" \ || die "cannot write $PIDFILE" lmsg "start: parent $$ forked daemon $_child (dev=$DEV flags:${DFLAGS:-none})" return 0 fi lmsg "daemon: pid $$ ppid $PPID starting (dev=$DEV interval=$INTERVAL menu=$MENU)" trap '' HUP trap on_signal INT TERM QUIT trap 'on_signal quiet' USR1 trap 'PF_PENDING=1' USR2 trap '' PIPE init_static lcd_open start_banner sleep "$BANNER_HOLD" if pf_read; then case $PF_ST in lost|held|down) PF_PENDING=1 ;; esac fi scr_fbfailed while :; do ITER=$(( ITER + 1 )) pf_check PF_UNWIND=0 refresh if [ "$MENU" -eq 1 ]; then [ "$PF_PENDING" -eq 1 ] || menu_poll else if [ "$PF_PENDING" -eq 0 ]; then sleep "$INTERVAL" & SLEEP_PID=$! wait "$SLEEP_PID" [ "$PF_PENDING" -eq 0 ] || kill "$SLEEP_PID" 2>/dev/null fi fi done } cmd_blit() { case "${BLIT_COL:-}${BLIT_PAGE:-}" in *[!0-9]*|'') msg 'blit needs: COL PAGE HEX (e.g. blit 48 3 ff)'; exit 2 ;; esac case "${BLIT_HEX:-}" in '') msg 'blit needs hex bytes, e.g. "ff" or "183c7e"'; exit 2 ;; *[!0-9a-fA-F]*) msg "not hex: $BLIT_HEX"; exit 2 ;; esac _bl_h=$BLIT_HEX _bl_esc='' _bl_n=0 while [ -n "$_bl_h" ]; do _bl_rest=${_bl_h#??} _bl_pair=${_bl_h%"$_bl_rest"} if [ ${#_bl_pair} -ne 2 ]; then msg "odd number of hex digits in $BLIT_HEX" exit 2 fi _bl_esc="$_bl_esc\\$(printf '%03o' $(( 0x$_bl_pair )) )" _bl_h=$_bl_rest _bl_n=$(( _bl_n + 1 )) done require_no_daemon 'blit' lcd_open [ "$BLIT_BLANK" -eq 0 ] || lcd_clear vmsg "blit ${_bl_n} byte(s) at column $BLIT_COL page $BLIT_PAGE" lcd_blit "$BLIT_COL" "$BLIT_PAGE" "$_bl_esc" || { lcd_close; exit 1; } lcd_close return 0 } cmd_row8() { if [ -z "${ROW8_PX:-}" ]; then printf 'row 8 glyph position: "%s" with the glyph blitted over it\n\n' "$ROW8_TEXT" printf ' %-6s %-9s %-9s %s\n' px left right note _r8_p=$ROW8_PMIN while [ "$_r8_p" -le "$ROW8_PMAX" ]; do _r8_l=$(( _r8_p - ROW8_PMIN )) _r8_r=$(( ROW8_PMAX - _r8_p )) _r8_note='' [ "$_r8_l" -ne "$_r8_r" ] || _r8_note='balanced' [ $(( (_r8_p - TEXT_X0) % 6 )) -ne 0 ] || _r8_note="$_r8_note cell-aligned" printf ' %-6s %-9s %-9s %s\n' "$_r8_p" "$_r8_l" "$_r8_r" "$_r8_note" _r8_p=$(( _r8_p + 3 )) done printf '\nrun "lcdd row8 " to put one on the panel. 51 is balanced.\n' return 0 fi case "$ROW8_PX" in *[!0-9]*|'') msg "row8 takes a pixel column, $ROW8_PMIN to $ROW8_PMAX"; exit 2 ;; esac if [ "$ROW8_PX" -lt "$ROW8_PMIN" ] || [ "$ROW8_PX" -gt "$ROW8_PMAX" ]; then msg "pixel $ROW8_PX would overlap the text; use $ROW8_PMIN to $ROW8_PMAX" exit 2 fi require_no_daemon 'row8' init_static lcd_open lcd_clear lcd_title "$BANNER" lcd_line '' lcd_line "px=$ROW8_PX L=$(( ROW8_PX - ROW8_PMIN )) R=$(( ROW8_PMAX - ROW8_PX ))" lcd_line '' lcd_line '' lcd_line '' lcd_line '' lcd_line_last "$ROW8_TEXT" lcd_blit "$ROW8_PX" 7 "$G_ENTER" || { lcd_close; exit 1; } lcd_close return 0 } cmd_status() { _st_rc=0 printf 'file: %s\n' "$0" if command -v md5 >/dev/null 2>&1; then printf 'md5: %s\n' "$(md5 -q "$0" 2>/dev/null)" fi printf 'version: %s\n' "$VERSION" printf 'pidfile: %s' "$PIDFILE" if [ -f "$PIDFILE" ]; then read -r _st_pid _st_junk < "$PIDFILE" printf ' -> %s' "${_st_pid:-empty}" if [ -n "${_st_pid:-}" ] && kill -0 "$_st_pid" 2>/dev/null; then printf ' (alive)\n' else printf ' (DEAD OR STALE)\n' _st_rc=1 fi else _st_pid='' printf ' (absent)\n' _st_rc=1 fi PROG_PATH=$0 _st_n=0 _st_found='' _st_pgid=$(ps -p $$ -o pgid= 2>/dev/null | tr -d ' ') if [ -z "$_st_pgid" ]; then printf 'WARNING: cannot read this process group; the count below\n' >&2 printf 'may include this command own forks. Trust the pidfile.\n' >&2 _st_pgid=-1 fi _st_list=$(ps -axo pid= -o ppid= -o pgid= -o command= 2>/dev/null \ | grep "$PROG_PATH" \ | grep -v ' grep ' \ | awk -v g="$_st_pgid" '$3 != g { print $1 ":" $2 }') for _st_pair in $_st_list; do _st_p=${_st_pair%%:*} _st_par=${_st_pair##*:} _st_isk=0 for _st_other in $_st_list; do if [ "${_st_other%%:*}" = "$_st_par" ]; then _st_isk=1; break; fi done [ "$_st_isk" -eq 0 ] || continue _st_n=$(( _st_n + 1 )) if [ "$_st_p" = "${_st_pid:-}" ]; then _st_found="$_st_found $_st_p(pidfile,ppid=$_st_par)" else _st_found="$_st_found $_st_p(UNTRACKED,ppid=$_st_par)" _st_rc=1 fi done printf 'running: %d%s\n' "$_st_n" "$_st_found" if [ "$_st_n" -gt 1 ]; then printf '\nMORE THAN ONE lcdd IS RUNNING. The one rc.d does not know\n' printf 'about keeps repainting the panel from the code it started\n' printf 'with, so a new file on disk changes nothing. Kill the\n' printf 'UNTRACKED pid, then "service lcdd restart".\n' elif [ "$_st_n" -eq 0 ]; then if [ -n "${_st_pid:-}" ] && kill -0 "$_st_pid" 2>/dev/null; then printf '\nTWO READINGS DISAGREE AND THIS COMMAND CANNOT SETTLE IT.\n' printf 'The scan found no lcdd, but the pidfile names %s and\n' "$_st_pid" printf 'that pid is alive. An empty scan only means the filter\n' printf 'did not match; it cannot show what %s is. Ask the\n' "$_st_pid" printf 'system directly:\n\n' printf ' ps -p %s -o command=\n\n' "$_st_pid" printf 'If that names this script then the SCAN is broken and\n' printf 'the daemon is healthy. DO NOT remove %s on\n' "$PIDFILE" printf 'the strength of this output.\n' _st_rc=1 else printf '\nNo lcdd is running. Whatever is on the panel was left there\n' printf 'by a previous run -- this display survives a reboot.\n' fi fi return $_st_rc } cmd_glyph() { case "$GLYPH_NAME" in enter) _gl_esc=$G_ENTER ;; cancel) _gl_esc=$G_CANCEL ;; *) msg "unknown glyph: $GLYPH_NAME (try: enter, cancel)"; exit 2 ;; esac case "$GCELL_C$GCELL_R" in *[!0-9]*|'') msg "--cell takes two non-negative numbers"; exit 2 ;; esac if [ "$GCELL_R" -eq 2 ]; then msg "row 2 is the unblitted control; pick another row" exit 2 fi if [ "$GCELL_C" -gt $(( WIDTH - G_CELLS )) ] || [ "$GCELL_R" -gt 7 ]; then msg "--cell $GCELL_C $GCELL_R does not leave room for a ${G_CELLS}-cell glyph" exit 2 fi glyph_len "$_gl_esc" || exit 1 cell_px "$GCELL_C" _gl_px=$CELL_PX if [ "$DO_DUMP" -eq 1 ]; then _gl_hdr=$(printf '\\%03o\\%03o\\%03o\\%03o\\%03o\\%03o' 1 "$_gl_px" 2 "$GCELL_R" 3 "$GLEN") printf "$_gl_hdr$_gl_esc" return 0 fi lcd_open lcd_clear lcd_title 'glyph blit test' lcd_line '01234567890123456789' _gl_tail=$(( WIDTH - GCELL_C - G_CELLS )) _gl_dots='....................' _gl_row=$(printf '%.*s%*s%.*s' "$GCELL_C" "$_gl_dots" "$G_CELLS" '' "$_gl_tail" "$_gl_dots") lcd_line "$_gl_row" _gl_r=3 while [ "$_gl_r" -le 6 ]; do if [ "$_gl_r" -eq "$GCELL_R" ]; then lcd_line "$_gl_row" elif [ "$_gl_r" -eq 6 ]; then lcd_line 'r2 ctl; X=cursor' else lcd_line '' fi _gl_r=$(( _gl_r + 1 )) done lcd_line_last "$GLYPH_NAME $GCELL_C,$GCELL_R p$_gl_px n$GLEN" lcd_blit "$_gl_px" "$GCELL_R" "$_gl_esc" || { lcd_close; exit 1; } if [ "$DO_PROBE" -eq 1 ]; then lcd_raw 'X' fi lcd_close return 0 } DIAG_TEXT='T01 T02 T03 T04 T05 T06 T07 T08 T09 T10 T11 T12 T13 T14 T15 T16' DIAG_GFX='G01 G02 G03 G04 G05 G06 G07 G08' DIAG_SWEEP='S01 S02 S03 S04 S05 S06' DIAG_LEDS='L01 L02 L03 L04 L05 L06 L07 L08 L10 L11 L12 L13 L14 L15 L16 L09' DIAG_SHADES='L13 L14 L15 L16 L09' DG_LEDS_SENT=0 DG_REF=' [x]..............|' DG_TAIL='..............|' DG_N=0 DG_FAILS=0 dg_info_clear() { DG_NAME=''; DG_KIND=known; DG_WANT=''; DG_ALT='' DG_A=''; DG_B=''; DG_C=''; DG_D=''; DG_NOTE='' } dg_send() { case "$1" in '') return 0 ;; -*) msg "BUG: diag format starts with '-': $1"; return 1 ;; esac DG_WIRE="$DG_WIRE$1" if [ "$DRYRUN" -eq 1 ]; then return 0 fi printf "$1" >&3 } dg_stim() { DG_STIM="$DG_STIM$1" dg_send "$1" } dg_pad() { DG_PAD=$1 [ "${#DG_PAD}" -le "$WIDTH" ] || msg "BUG: diag text over $WIDTH columns: $1" while [ "${#DG_PAD}" -lt "$WIDTH" ]; do DG_PAD="$DG_PAD "; done } dg_title() { dg_pad "$DG_ID $DG_NAME" dg_send "\\017$DG_PAD\\r\\n\\016" } dg_row() { dg_send "$1\\r\\n"; } dg_last() { dg_send "$1"; } dg_oct() { DG_OCT=$(( ($1 / 64) * 100 + (($1 / 8) % 8) * 10 + $1 % 8 )) case ${#DG_OCT} in 1) DG_OCT=00$DG_OCT ;; 2) DG_OCT=0$DG_OCT ;; esac } dg_page() { case "$1" in *[!0-9]*|'') msg "dg_page: bad page '$1'"; return 1 ;; esac [ "$1" -le 7 ] || { msg "dg_page: page $1 out of range"; return 1; } dg_oct "$1" dg_stim "\\002\\$DG_OCT" } dg_col() { case "$1" in *[!0-9]*|'') msg "dg_col: bad column '$1'"; return 1 ;; esac [ "$1" -le 127 ] || { msg "dg_col: column $1 out of range"; return 1; } dg_oct "$1" dg_stim "\\001\\$DG_OCT" } dg_pos() { dg_page "$1" && dg_col "$2"; } dg_blit() { blit_fmt "$1" "$2" "$3" || return 1 dg_stim "$BLIT_FMT" } dg_byte() { dg_stim "$1[\\$2]$DG_TAIL"; } dg_bytes() { dg_title dg_row "$DG_REF" _dg_sep='' for _dg_pair in $1; do dg_stim "$_dg_sep" dg_byte "${_dg_pair%%:*}" "${_dg_pair#*:}" _dg_sep='\r\n' done } dg_T01() { case "$1" in info) DG_NAME='^L clear+home' DG_WANT='only "HOME T01", at the top left; the rest of the glass blank' DG_ALT='junk still there = ^L does not clear; HOME lower down = it does not home' ;; run) for _dg_r in 1 2 3 4 5 6; do dg_row 'T01 junk junk junk..'; done dg_last 'T01 junk junk junk..' dg_stim '\014HOME T01' ;; esac } dg_T02() { case "$1" in info) DG_NAME='^L keeps ^O' DG_WANT='row 1 inverse; rows 2 and 3 plain' DG_ALT='row 1 plain = ^L turns inversion off as well' ;; run) dg_pad 'T02 inverse after ^L' dg_stim "\\017\\014$DG_PAD" dg_send '\016\r\n' dg_row 'plain control row' dg_last 'want: row 1 inverse' ;; esac } dg_T03() { case "$1" in info) DG_NAME='CR no erase' DG_WANT='row 3 reads xyCDEFGH' DG_ALT='"xy" alone = CR erases the row; ABCDEFGHxy = CR ignored' ;; run) dg_title dg_row 'want: xyCDEFGH' dg_stim 'ABCDEFGH\rxy' ;; esac } dg_T04() { case "$1" in info) DG_NAME='LF keeps column?' DG_KIND=open DG_A='x on row 4 under the 5 of the ruler: LF keeps the column' DG_B='x at the start of row 4: LF also returns to column 0' ;; run) dg_title dg_row '01234567890123456789' dg_stim 'ABCDE\nx' ;; esac } dg_T05() { case "$1" in info) DG_NAME='LF row 8 SCROLLS' DG_WANT='the title has gone: the top row reads "row 2", the bottom "row 9"' DG_ALT='the title still on top = a LF on row 8 does not scroll' ;; run) dg_title for _dg_r in 2 3 4 5 6 7; do dg_row "row $_dg_r"; done dg_last 'row 8' dg_stim '\r\nrow 9' ;; esac } dg_T06() { case "$1" in info) DG_NAME='col 21+ dropped' DG_WANT='row 2 is A to T; row 3 reads "row 3 is this row"' DG_ALT='UVWX at the start of row 3 = text wraps at the edge' ;; run) dg_title dg_stim 'ABCDEFGHIJKLMNOPQRSTUVWX\r\n' dg_last 'row 3 is this row' ;; esac } dg_T07() { case "$1" in info) DG_NAME='^H no erase' DG_WANT='row 3 reads XaZ' DG_ALT='Xa = ^H erases as it goes back; XYZa or a glyph = not a backspace' ;; run) dg_title dg_row 'want: XaZ' dg_stim 'XYZ\010\010a' ;; esac } dg_T08() { case "$1" in info) DG_NAME='^H at column 0?' DG_KIND=open DG_A='b at the start of row 3: ^H stops at column 0' DG_B='b in place of the last dot of row 2: ^H wraps to the row above' DG_C='no b anywhere' ;; run) dg_title dg_row '....................' dg_stim '\010b' ;; esac } dg_T09() { case "$1" in info) DG_NAME='TAB 8-stop' DG_WANT='row 3: A under the 0, B under the 8, C under the 6 of 16' DG_ALT='B under the 4 = 4-stop tabs; ABC together = TAB ignored' ;; run) dg_title dg_row '01234567890123456789' dg_stim 'A\tB\tC' ;; esac } dg_T10() { case "$1" in info) DG_NAME='TAB past col 16?' DG_KIND=open DG_A='no Z anywhere: the TAB went past the edge' DG_B='Z in the last column, under the 9 of 19' DG_C='Z at the start of row 4: the TAB wrapped' DG_D='Z under the 8 of 18: the TAB moved one cell' ;; run) dg_title dg_row '01234567890123456789' dg_stim '0123456789ABCDEFG\tZ' ;; esac } dg_T11() { case "$1" in info) DG_NAME='^O sets' DG_WANT='rows 2 and 3 both solid inverse bars' DG_ALT='row 2 plain = ^O toggles, so two of them cancel' ;; run) dg_title dg_pad '^O twice: inverse' dg_stim "\\017\\017$DG_PAD" dg_send '\r\n\016' dg_pad '^O once: inverse' dg_send "\\017$DG_PAD\\r\\n\\016" dg_last 'want: r2+r3 inverse' ;; esac } dg_T12() { case "$1" in info) DG_NAME='^N ends inverse' DG_WANT='row 3: INV inverse, then " plain" not' DG_ALT='all of row 3 inverse = ^N does not turn it off' ;; run) dg_title dg_row 'want: INV then plain' dg_stim '\017INV\016 plain' ;; esac } dg_T13() { case "$1" in info) DG_NAME='inv written only' DG_WANT='row 2 inverse over its five letters only; row 3 a full-width bar' DG_ALT='row 2 full width as well = inversion paints the line, not cells' ;; run) dg_title dg_stim '\017short\016\r\n' dg_pad 'padded' dg_send "\\017$DG_PAD\\016\\r\\n" dg_last 'want: r2 short bar' ;; esac } dg_T14() { case "$1" in info) DG_NAME='1E 1F are glyphs' DG_WANT='rows 3 and 4: a glyph between the brackets, the bar under the row 2 bar' DG_ALT='brackets touching, bar one column left = the byte was consumed' ;; run) dg_bytes '1E:036 1F:037' ;; esac } dg_T15() { case "$1" in info) DG_NAME='11 is consumed' DG_WANT='row 3: [] touching, the bar one column LEFT of the row 2 bar' DG_ALT='the bar under the row 2 bar = 0x11 drew a glyph or a space' ;; run) dg_bytes '11:021' ;; esac } dg_T16() { case "$1" in info) DG_NAME='^F ^G do nothing' DG_WANT='rows 3 and 4: [] touching, the bar one column left of the row 2 bar' DG_ALT='the bar under the row 2 bar = the byte drew something' ;; run) dg_bytes '06:006 07:007' ;; esac } dg_G01() { case "$1" in info) DG_NAME='^B^A text up?' DG_KIND=open DG_A='ROW4 over the first four dots of row 4, on the cell grid: ^B/^A move the text cursor' DG_B='ROW4 after the ten dots of row 8: ^B/^A do not move the text cursor' DG_C='ROW4 on row 4 but off the cell grid, straddling two cells' DG_D='no ROW4 anywhere' ;; run) dg_title for _dg_r in 2 3 4 5 6 7; do dg_row '....................'; done dg_last '..........' dg_pos 3 "$TEXT_X0" dg_stim 'ROW4' ;; esac } dg_G02() { case "$1" in info) DG_NAME='^B^A to row 8?' DG_KIND=open DG_A='row 8 reads crW8-TEXT: positioned, and CR returns to row 8 column 0 -- what a boot-progress writer needs' DG_B='row 2 reads crrsor was here:ROW8, row 8 empty: ^B/^A ignored for text' DG_C='ROW8-TEXT on row 8 but cr at the start of row 2: CR returns to the old row' ;; run) dg_title dg_last 'cursor was here:' dg_pos 7 "$TEXT_X0" dg_stim 'ROW8-TEXT\rcr' ;; esac } dg_G03() { case "$1" in info) DG_NAME='home w/o clear?' DG_KIND=open DG_A='plain HOME over the start of the inverse title; every other row intact' DG_B='row 8 reads "row 8 intactHOME": the cursor did not move' DG_C='the panel was cleared' ;; run) dg_title for _dg_r in 2 3 4 5 6 7; do dg_row "row $_dg_r intact"; done dg_last 'row 8 intact' dg_pos 0 "$TEXT_X0" dg_stim 'HOME' ;; esac } dg_G04() { case "$1" in info) DG_NAME='text grid 6 or 4' DG_KIND=open DG_A='the H of "H<^A 6" sits exactly under the row 2 H: text cells start at pixel 6 (6c+6)' DG_B='the H of "H<^A 4" sits under it instead: the grid is selftest'"'"'s 4+6c' DG_C='all four H line up: the panel snaps ^A to a cell' DG_D='the four rows are not on rows 3-6 (compare G01)' DG_NOTE='note how many pixels each H sits from the row 2 H; "^A 0" also shows whether text can enter the margin' ;; run) dg_title dg_last 'H&2 printf ' stimulus, hex:\n' >&2 dg_hex "$DG_STIM" >&2 if [ "$VERBOSE" -eq 1 ]; then printf ' whole frame from ^N ^L, hex:\n' >&2 dg_hex "$DG_WIRE" >&2 fi if [ "$DG_KIND" = known ]; then printf ' expect: %s\n' "$DG_WANT" >&2 printf ' if not: %s\n' "$DG_ALT" >&2 else [ -z "$DG_A" ] || printf ' a: %s\n' "$DG_A" >&2 [ -z "$DG_B" ] || printf ' b: %s\n' "$DG_B" >&2 [ -z "$DG_C" ] || printf ' c: %s\n' "$DG_C" >&2 [ -z "$DG_D" ] || printf ' d: %s\n' "$DG_D" >&2 fi [ -z "$DG_NOTE" ] || printf ' note: %s\n' "$DG_NOTE" >&2 } dg_answer() { if [ "$DRYRUN" -eq 1 ] || [ "$DO_DUMP" -eq 1 ]; then dg_record unobserved '' return 0 fi if [ "$ASSUME_YES" -eq 1 ]; then sleep "$DIAG_HOLD" dg_record unobserved '' return 0 fi while :; do if [ "$DG_KIND" = known ]; then printf ' y = as expected, n = not, s = skip, q = quit: ' >&2 else _dg_ls='' [ -z "$DG_A" ] || _dg_ls="${_dg_ls}a/" [ -z "$DG_B" ] || _dg_ls="${_dg_ls}b/" [ -z "$DG_C" ] || _dg_ls="${_dg_ls}c/" [ -z "$DG_D" ] || _dg_ls="${_dg_ls}d/" printf ' %s = that outcome, n = none of these, s = skip, q = quit: ' \ "${_dg_ls%/}" >&2 fi read -r _dg_ans < /dev/tty || { printf '\n' >&2; _dg_ans=q; } _dg_out='' case "$_dg_ans" in y|Y) if [ "$DG_KIND" = known ]; then dg_record pass '' return 0 fi ;; n|N) printf ' what did you see? ' >&2 read -r _dg_seen < /dev/tty || _dg_seen='' if [ "$DG_KIND" = known ]; then dg_record FAIL "$_dg_seen" else dg_record other "$_dg_seen" fi return 0 ;; a) _dg_out=$DG_A ;; b) _dg_out=$DG_B ;; c) _dg_out=$DG_C ;; d) _dg_out=$DG_D ;; s|S) dg_record skip ''; return 0 ;; q|Q) dg_record skip 'quit here'; return 1 ;; esac if [ "$DG_KIND" = open ] && [ -n "$_dg_out" ]; then printf ' note (Enter for none): ' >&2 read -r _dg_seen < /dev/tty || _dg_seen='' dg_record "$_dg_ans" "$_dg_out${_dg_seen:+ -- $_dg_seen}" return 0 fi printf ' not an answer to this test\n' >&2 done } dg_one() { dg_info_clear "dg_$DG_ID" info case "$DG_ID" in L*) if [ "$DO_LIGHTS" -eq 0 ]; then dg_record skip 'needs --lights' return 0 fi ;; esac DG_WIRE='' DG_STIM='' dg_send '\016\014' if ! "dg_$DG_ID" run; then dg_record error 'frame not completed; see stderr' return 0 fi dg_report dg_answer } dg_finish() { if [ "$DG_LEDS_SENT" -eq 1 ] && [ "$DRYRUN" -eq 0 ]; then printf '\004\000' >&3 fi if [ "$DO_DUMP" -eq 1 ]; then printf '\016\014' >&3 dg_table >&2 return 0 fi [ "$DRYRUN" -eq 1 ] || printf '\016\014' >&3 idle_screen lcd_close dg_table } dg_abort() { trap - INT TERM QUIT printf '\n%s: interrupted at %s; restoring the panel\n' "$PROG" "${DG_ID:-?}" >&2 dg_finish exit 130 } cmd_diag() { dg_select || usage if [ "$DIAG_LIST" -eq 1 ]; then dg_list return 0 fi case "$DIAG_HOLD" in *[!0-9]*|'') msg '--hold takes whole seconds'; usage ;; esac if [ "$DO_DUMP" -eq 1 ]; then DRYRUN=0 exec 3>&1 else require_no_daemon 'diag' if [ "$DRYRUN" -eq 0 ] && [ "$ASSUME_YES" -eq 0 ]; then ( : < /dev/tty ) 2>/dev/null || die 'diag asks on /dev/tty and there is none; use --yes' fi init_static lcd_open fi trap dg_abort INT TERM QUIT set -f set -- $DG_RUN set +f printf '%s diag -- %s test(s), %s at %s bps\n' "$PROG" "$#" "$DEV" "$SPEED" >&2 printf 'The panel cannot be read back: you are the instrument. Each frame\n' >&2 printf 'is on the glass BEFORE its question is asked.\n' >&2 [ "$DO_LIGHTS" -eq 1 ] || printf 'L tests skipped: they need --lights.\n' >&2 for DG_ID in "$@"; do dg_one || break done dg_finish trap - INT TERM QUIT [ "$DG_FAILS" -eq 0 ] || return 1 return 0 } [ $# -gt 0 ] || usage DAEMON_FLAGS='MENU:--menu: ALLOW_WRITE:--allow-write: GLYPHS:--glyphs:--no-glyphs SYSLOG::--no-syslog' daemon_flags_str() { _dfs='' for _df in $DAEMON_FLAGS; do _dfvar=${_df%%:*} _dfrest=${_df#*:} _dfopt=${_dfrest%%:*} _dfoff=${_dfrest#*:} eval "_dfval=\$$_dfvar" if [ "$_dfval" -eq 0 ]; then [ -z "$_dfoff" ] || _dfs="$_dfs $_dfoff" else [ -z "$_dfopt" ] || _dfs="$_dfs $_dfopt" fi done printf '%s' "$_dfs" } CMD='' while [ $# -gt 0 ]; do case "$1" in -d) [ $# -ge 2 ] || usage; DEV=$2; shift 2 ;; -i) [ $# -ge 2 ] || usage; INTERVAL=$2; shift 2 ;; -p) [ $# -ge 2 ] || usage; PIDFILE=$2; shift 2 ;; -b) [ $# -ge 2 ] || usage; BANNER_HOLD=$2; shift 2 ;; -w) [ $# -ge 2 ] || usage; KEYPAD_WAIT=$2; shift 2 ;; -n) DRYRUN=1; shift ;; --no-syslog) SYSLOG=0; shift ;; -v) VERBOSE=1; shift ;; --yes) ASSUME_YES=1; shift ;; --lights) DO_LIGHTS=1; shift ;; --keypad) DO_KEYPAD=1; shift ;; --menu) MENU=1; shift ;; --glyphs) GLYPHS=1; shift ;; --no-glyphs) GLYPHS=0; shift ;; --dump) DO_DUMP=1; shift ;; --list) DIAG_LIST=1; shift ;; --hold) [ $# -ge 2 ] || usage; DIAG_HOLD=$2; shift 2 ;; --blank) BLIT_BLANK=1; shift ;; --probe) DO_PROBE=1; shift ;; --cell) [ $# -ge 3 ] || usage; GCELL_C=$2; GCELL_R=$3; shift 3 ;; --glyph) [ $# -ge 2 ] || usage; GLYPH_NAME=$2; shift 2 ;; --allow-write) ALLOW_WRITE=1; shift ;; --wait) [ $# -ge 2 ] || usage; WAIT_SECS=$2; shift 2 ;; --idle) [ $# -ge 2 ] || usage; IDLE=$2; shift 2 ;; --title) [ $# -ge 2 ] || usage; MSG_TITLE=$2; shift 2 ;; --footer) [ $# -ge 2 ] || usage; MSG_FOOTER=$2; shift 2 ;; --banner) [ $# -ge 2 ] || usage; BANNER=$2; shift 2 ;; -h|--help) usage ;; --) shift; break ;; -*) msg "unknown option: $1"; usage ;; *) if [ -z "$CMD" ]; then CMD=$1 shift else break fi ;; esac done [ -n "$CMD" ] || usage case "$CMD" in msg) ;; glyph) if [ $# -gt 0 ]; then GLYPH_NAME=$1; shift; fi [ $# -eq 0 ] || { msg "unexpected argument: $1"; usage; } ;; row8) if [ $# -gt 0 ]; then ROW8_PX=$1; shift; fi [ $# -eq 0 ] || { msg "unexpected argument: $1"; usage; } ;; diag) DIAG_SEL=$*; shift $# ;; splash) { [ $# -ge 1 ] && [ $# -le 2 ]; } || { msg 'usage: lcdd splash TEXT [ROW7]'; usage; } SPLASH_TEXT=$1; SPLASH_ROW7=${2-}; shift $# ;; blit) [ $# -eq 3 ] || { msg 'usage: lcdd blit COL PAGE HEX'; usage; } BLIT_COL=$1; BLIT_PAGE=$2; BLIT_HEX=$3; shift 3 ;; *) [ $# -eq 0 ] || { msg "unexpected argument: $1"; usage; } ;; esac case "$CMD" in probe|keys|menu|status|invtest|invoff|diag|power) self_id >&2 ;; esac case "$CMD" in probe) cmd_probe ;; keys) cmd_keys ;; invtest) cmd_invtest ;; invoff) cmd_invoff ;; start) cmd_start ;; menu) cmd_menu ;; poweroff) cmd_poweroff ;; power) cmd_power ;; msg) cmd_msg "$@" ;; splash) cmd_splash ;; clear) cmd_clear ;; glyph) cmd_glyph ;; status) cmd_status ;; row8) cmd_row8 ;; blit) cmd_blit ;; diag) cmd_diag ;; *) msg "unknown command: $CMD"; usage ;; esac