Displaying text on the HD-Fox T2 2:2 character display

/df

Well-Known Member
At least 5 years ago the CF acquired the /sbin/display binary. This is used in various scripts to display text to display text on the HDR-Fox T2, but has a fall-back interface for HD-Fox T2, as in /bin/tmenu and /etc/init.d/S89maintenance:
Code:
...
feedback()
{
      [ "$model" = HDR ] && display "$1" || display "\$$2"
}
...
When calling this the caller has to provide a string for display on the HDR and also a 4-character version, prefixed by $, for use on the HD.

The HD-Fox T2 has a display comprising 2 pairs of 7-segment displays separated by a double dot display (:). The segments are identified by the 7 least significant bits of a byte as below:
Code:
           0x1
  0x20             0x2
          0x40
  0x10             0x4
           0x8
The upper dot of the double dot is the top bit (0x80) of the 2nd segment byte; the lower dot is the top bit of the 4th segment byte.

Presumably a genius instrumented the HD humaxtv binary to identify the strange protocol used on the serial interface /dev/ttyS2 to the Micom front panel device. Doing the same to /sbin/display, like so
Code:
# strace -x /sbin/display '$abcd' 2>&1 | grep -E '^writ'
write(3, "\x05\x05\x0f\x77\x7c\x39\x5e\x9e", 8) = 8
#
reveals that each message begins with a byte of value 5 and concludes with a checksum byte equal to the modulo 256 sum of the bytes between. There are 2 further magic bytes after the first, with values 5 and 15; then each of 4 characters is sent as a segment bitmap (which may be 0 for no character) according to the layout shown above.

For the HDR, slightly different formats (eg it can display actual characters, which it does with code value 18) are used between the first and last bytes, but they all fall into this structure:
byte123...N
value5N-2code ...checksum
The HD will also accept strings of less than 4 characters using an appropriate count value; segments are updated from the left without changing the remaining segments.

Based on this, here is a shell script that sends an arbitrary string (subject to the limits of the 7-segment display) to the front panel, using scrolling.
Code:
#!/bin/sh 
# args [--loop] [--delay SEC] [--] [STRING...]

# functions

# if sleep only does integers (busybox /bin/sleep)
# would use command builtin, but CF /bin/sh is missing it
if ! sleep 0.1 2>/dev/null; then
                # round fractional time to nearest integer
                my_sleep() {
                        sleep "$(printf "%.0f" "$1")"
                }
else
                my_sleep() {
                        sleep "$@"
                }
fi

usage() { # progname
        printf "Usage: $1 [--loop|-l] [--delay|-d SEC] [--] [STRING...]\n"
        printf "Display STRING on the HD-Fox T2 front panel, scrolling left once\n"
        printf "per SEC (default %s) seconds. If --loop, scroll until cancelled.\n" "$char_delay"
}

# represent a number as a printf octal escape sequence
octal_esc() { # number
        printf "\\%#o" "$1"
}

# get the checksum byte to send to the HD Micom
hd_cksum() { # n1 [n2 .. n4] 
        # include bytes 2 (0x05) and 3 (0x0f) plus the 4 display bytes 
        printf "%d" $(( (20 + $1 + $2 + $3 + $4) % 256 ))
}

# send segments to Micom front panel serial device, blanking the rest
hd_out() { # ch1 [ch2 .. ch4]
        # any trailing null params -> 0
        set -- "${1:-0}" "${2:-0}" "${3:-0}" "${4:-0}"
        # 3 magic bytes, the 4 display bytes, checksum
        >/dev/ttyS2 printf "\x05\x05\x0f%b%b%b%b%b" \
                $(octal_esc $1) $(octal_esc $2) $(octal_esc $3) $(octal_esc $4) \
                $(octal_esc $(hd_cksum $1 $2 $3 $4))
}

# get the segment byte corresponding to a character as a decimal number
char2ch() { # char
        local ch
        
        # provide a few more char mappings than /sbin/display
        case $1 in 
        [Aa]) ch=119 ;;
        [Bb]) ch=124 ;;
        C) ch=57 ;;
        c) ch=88 ;;
        [Dd]) ch=94 ;;
        [Ee]) ch=121 ;;
        [Ff]) ch=113 ;;
        [Gg]) ch=61 ;;
        H) ch=118 ;;
        h) ch=116 ;;
        [1I]) ch=6 ;;
        i) ch=16 ;;
        [Jj]) ch=13 ;;
        [Kk]) ch=112 ;;
        [Ll]) ch=56 ;;
        # hard-to-map chars as 3 horizontal segments
        [MWX]) ch=73 ;;
        # or centre and bottom for lower case
        [mwx]) ch=72;;
        [Nn]) ch=84 ;;
        [O0]) ch=63 ;;
        o) ch=92 ;;
        [Pp]) ch=115 ;;
        [Qq]) ch=103 ;;
        [Rr]) ch=80 ;;
        [Ss5]) ch=109 ;;
        T) ch=7 ;;
        t) ch=120 ;;
        [UV]) ch=62 ;;
        [uv]) ch=28 ;;
        [Yy]) ch=110 ;;
        [Zz2]) ch=91 ;;
        3) ch=79 ;;
        4) ch=102 ;;
        6) ch=125 ;;
        7) ch=39 ;;
        8) ch=127 ;;
        9) ch=111 ;;
        [_.\;:,]) ch=8 ;;
        '|') ch=48 ;;
        [{[\(]) ch=35 ;;
        [}\)]]) ch=15 ;;
        -) ch=64 ;;
        =) ch=65 ;;
        /) ch=82 ;;
        @) ch=95 ;;
        '`') ch=96 ;;
        [\'\"]) ch=2 ;;
        [\]) ch=100 ;;
        ' ') ch=0 ;;
        !) ch=10 ;;
        # undefined chars show as top and bottom segments
        *) ch=9 ;;
        esac
        printf "%d" "$ch"
}

valid_secs() { # maybe_secs
        # if the entire param matches, return 0; else 1 
        echo "$1" | sed -rn 's/^[0-9]+(\.[0-9]+)?$//;t;q 1'
}

# defaults
# display speed/s
char_delay=0.5
# only display once
noloop=1

[ HD = "$(cat /etc/model)" ] ||
        { printf "This works on HD-Fox only\n"; usage "${0##*/}"; exit 1; } >&2
# parse args
while [ -n "$1" ]; do
        case "$1" in 
        --loop|-l)
                noloop=
                shift
                ;;
                
        --delay|-d)
                if valid_secs "$2"; then
                        char_delay="$2"
                        shift
                else
                        >&2 printf "\"%s\" is not a valid number of seconds, ignoring.\n" "$2"
                fi
                shift
                ;;
        -d[0-9]*)
                valid_secs "${1#-d}" &&
                        char_delay="${1#-d}"
                shift
                ;;
                
        --help|-h)
                usage "${0##*/}"
                exit
                ;;
                
        --)     shift
                break
                ;;
        *)      break ;;
        esac
done    
        
c1=
c2=
c3=
c4=
while true; do
        # space between loops
        str="$*${noloop:- }"
        
        while [ -n "$str" ]; do
                ch="${str}"
                str="${ch#?}"
                c4="$(char2ch "${ch%"$str"}")"
                if [ -n "$c4" ]; then
                        hd_out "$c1" "$c2" "$c3" "$c4"
                        c1="$c2"
                        c2="$c3"
                        c3="$c4"
                        c4=
                        my_sleep "$char_delay"
                fi
        done
        [ -n "$noloop" ] && break
done
while [ -z "$c1" -a -n "$c4" ]; do
        c1="$c2"
        c2="$c3"
        c3="$c4"
        c4=
        hd_out "$c1" "$c2" "$c3" "$c4"
        my_sleep "$char_delay"
done
In case anyone wonders what the point is ... First, the shell script saves 3k over the binary even allowing for some extra work for HDR support; second, I have a back burner HD fix-disk project that might benefit.
 
Last edited:
reveals that each message begins with a byte of value 5 and concludes with a checksum byte equal to the modulo 256 sum of the bytes between. There are 2 further magic bytes after the first, with values 5 and 15; then each of 4 characters is sent as a segment bitmap (which may be 0 for no character) according to the layout shown above.

The sequence is 0x5 (request) 0x5 (length of command and data) 0xf (command) <data> <checksum byte>

Be careful of experimenting with commands because it is possible to break a front panel this way.
One other useful command is 0x15 which turns the chasing snakes on and off.

For example, display turns them off with 0x5 0x2 0x15 0x0 0x17

The character mapping in display is here for reference:

Code:
/*

8    .    -    1
2    |     |    2
4     -
1    |     |    4
        -    8
*/
static char hdcodes[] = {
    '\x3f',        /* 0 */
    '\x06',        /* 1 */
    '\x5b',        /* 2 */
    '\x4f',        /* 3 */
    '\x66',        /* 4 */
    '\x6d',        /* 5 */
    '\xfd',        /* 6 */
    '\x27',        /* 7 */
    '\x7f',        /* 8 */
    '\x6f',        /* 9 */
    '\x77',        /* A */
    '\x7c',        /* b */
    '\x39',        /* C */
    '\x5e',        /* d */
    '\x79',        /* E */
    '\x71',        /* F */
    '\x3d',        /* G */
    '\x76',        /* H */
    '\x30',        /* i */
    '\x0d',        /* j */
    '\x70',        /* K */
    '\x38',        /* L */
    '\x49',        /* M */
    '\x54',        /* n */
    '\x3f',        /* O */
    '\x73',        /* p */
    '\x67',        /* q */
    '\x50',        /* r */
    '\x6d',        /* s */
    '\x78',        /* t */
    '\x3e',        /* u */
    '\x3e',        /* v */
    '\x49',        /* w */
    '\x49',        /* x */
    '\x6e',        /* y */
    '\x5b',        /* z */
};
 
I had the HD display get stuck, but didn't identify the exact trigger: perhaps accidentally sending an HDR message or a corrupt message? Reboot from the telnet menu didn't help. Pressing the reset button did.
 
Back
Top