3

I need a function that will display the output of several live data streams in the terminal across multiple lines. The ideal result will print to the terminal like this:

Measurement 1: #### 
Measurement 2: ####

Where #### update in 1s intervals, but the rest of the text stays as it is on the screen.

This problem has been solved for a single line of output here, but I can't figure out how to extend to multiple lines.

Here's what I have so far:

function doit {
  while :;
  do
    i=$(current reading from sensor 1)
    j=$(current reading from sensor 2)
    echo -ne  "Measurement 1: $i\nMeasurement 2: $j"'\r';
    sleep 1;
  done
}

This causes successive output to only write over the second line on the terminal, creating a new block of output on the terminal after each iteration of the loop.

Another solution presented here was:

function doit {
  while :;
  do
    i=$(current reading from sensor 1)
    j=$(current reading from sensor 2)
    echo -ne  "Measurement 1: $i\nMeasurement 2: $j"'\r';
    sleep 1;
    tput cuu1
    tput el
    tput cuu1
    tput el
  done
}

This is close, but clears all text displayed on the terminal after each loop, which creates an unpleasant visual as blocks of text flash on and off the screen. This will have to work so that someone could monitor the readings closely. I'll need to scale this up to 5 or more lines of output. Thanks for any suggestions and looking forward to seeing some solutions!

Community
  • 1
  • 1

4 Answers4

1

Here's a version which tries to avoid flashing the screen by sending the delete sequences at the end of each line (which normally won't erase anything, but will help if the previous line was longer). I used $RANDOM to represent the "get a measurement" action, and I since that is a number, I use a %5d format so that the numbers are aligned similarly on each cycle (which also helps avoid display artifacts).

function doit {
  local up2=$(tput cuu 2)$(tput cr) up=
  local endl=$(tput el)$'\n'
  while :; do
    local i=$RANDOM
    local j=$RANDOM
    printf "%sMeasurement 1: %5d%sMeasurement 2: %5d%s" "$up" "$i" "$endl" "$j" "$endl"
    up=$up2
    sleep 1;
  done
}
rici
  • 234,347
  • 28
  • 237
  • 341
0

Unless you have other things on the screen that need to be displayed, you could just clear the screen each cycle. In this case, you don't need -n with the echo statement. You don't need the tput commands either.

#!/bin/bash
function doit {
  while :;
  do
  i=$(current reading from sensor 1)
  j=$(current reading from sensor 2)
  clear
  echo -e  "Measurement 1: $i\nMeasurement 2: $j"'\r';
  sleep 1;
  done
}
Guest
  • 124
  • 7
0

The intuitive approach of just storing and restoring the cursor position with tput sc and tput rc breaks if we're too close to the bottom of the screen and the output causes scrolling.

Instead of trying to figure out what line we're currently on, we can just move the cursor up again by the number of lines we've printed with tput cuu n; after this, we erase to the end of the screen with tput ed.

If it takes a while to read out the sensors, this would flicker (simulated with sleep 0.1), so we erase the lines right before printing the new ones, but after reading out the sensors. Because this must not happen the first time around, we set a flag notfirst after the first time printing to indicate that clearlines should be called.

All in all:

#!/bin/bash

clearlines () {
    local nlines=$1
    tput cuu $nlines
    tput ed
}

display () {
    while true; do
        # Get "sensor measurements"
        sleep 0.1   # Simulate lag
        local vars=($RANDOM $RANDOM $RANDOM $RANDOM $RANDOM)

        # If this isn't the first output, clear the previous one
        if [[ -n $notfirst ]]; then
            clearlines ${#vars[@]}
        fi

        # Loop over "measurements" to print
        local var
        local i=0
        for var in "${vars[@]}"; do
            printf 'Measurement %d: %d\n' $(( ++i )) "${var}"
        done
        sleep 1

        # Set flag
        local notfirst=1
    done
}

display
Community
  • 1
  • 1
Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
  • I just realize that this might cause the flickering you describe in your second approach... there isn't one for me, but that might depend on your terminal emulator. – Benjamin W. Jan 24 '17 at 02:49
  • It doesn't flicker because $RANDOM takes very little time. If you're actually reaching out to some external device which has returns measurements, possibly over a slow comms line or network, then blanking before acquiring the new data is likely to cause a noticeable blink. Also, `tput cuu $i` is the same as doing `i` `tput cuu1`s, but quite a bit quicker. You could use `tput cuu $i; tput ed` to move up `$i` lines and clear to the end of the screen (instead of blanking each line in turn), but I still prefer my version. – rici Jan 24 '17 at 03:13
  • @rici Good points, I'll update. (And I also like your solution better ;)) – Benjamin W. Jan 24 '17 at 04:02
0

It's already been pointed out that the delay caused by whatever's gathering your sensor data may be a flicker factor or might cause other display weirdness.

So here's an alternate take on this, in minimal form.

#!/usr/bin/env/ bash

# This generates random sensor data and stores it in predictably named temp files,
# after a short random delay.
function gather() {
    while sleep $(( $RANDOM % 3 )); do
        printf '%2d\n' $(( $RANDOM % 100 )) > /tmp/t.$$.$1
    done
}

main() {

    # Background our data gatherers...
    gather i &
    gather j &

    # Print a couple of blank lines, since our format starts with "up"
    printf '\n\n'

    # and loop.
    while sleep 1; do
        # gather our randomly generated sensor data...
        i=$( < /tmp/t.$$.i )
        j=$( < /tmp/t.$$.j )
        # and print it.
        printf "$fmt" "$i" "$j"
    done
}

cleol=$(tput el)
up2=$(tput cuu1; tput cuu1)
fmt="${up2}Measurement 1: %d${cleol}\nMeasurement 2: %d${cleol}\n"

main

Cursor movement is fun. :-)

But the point of all this is that the loop that is responsible for writing your output is VERY light-weight. It gets data from static text files, and it prints. Then repeats. All the heavy lifting is done in the backgrounded gather() function, which updates the text files at a frequency that may be different from your display update frequency.

Note that as written, there's a race condition where your "read" might hit the file while the "write" is happening, but before data have been written. To minimize that, you could work around the race with something like this:

function gather() {
    while sleep 1; do
        value=$(
          # This simulates the delay of a slow sensor read.
          sleep $(( $RANDOM % 3 ))
          printf '%2d\n' $(( $RANDOM % 100 ))
        )
        printf '%d\n' "$value" > /tmp/t.$$.$1
    done
}

This is better, and may be "good enough", but writing to a temporary file like this is not an atomic filesystem operation, so the possibility of a race condition still exists.

In the following variation, we simulate a longish delay while data are read, but the output from the read command goes to a different temporary file, which get symlinked to upon a completion of the sensor read.

function gather() {
    local n=0 item="${1//[^[:alnum:]]/}"
    while :; do
        old="$(readlink /tmp/t.$$.$item)"
        (
          # This simulates the delay of a slow sensor read.
          sleep $(( $RANDOM % 3 ))
          printf '%d\n' $(( $RANDOM % 100 ))
        ) > /tmp/t.$$.$item.$n
        test -s /tmp/t.$$.$item.$n &&
          ln -sfn /tmp/t.$$.$item.$n /tmp/t.$$.$item &&
        test -f "$old" &&
          rm -f "$old"
        ((n++))
        sleep 1
    done
}

Everything in the parentheses is what you'd run to generate your sensor output. And if you're afraid of running out of values for $n (but not afraid of proton decay) you could switch to $RANDOM instead of using the incrementing counter.

Have you thought of using an existing montoring system for whatever it is you're monitoring? It seems to me like problems like this have been solved already. :-)


UPDATE...

And just because I don't use my time well, here's an improved main() function and the rest of the script:

function main() {

    # Background our data gatherers...
    local -a pids=()
    for thing in "${things[@]}"; do
        gather $thing &
        pids+=($!); echo "Backgrounded $thing at $!"
    done
    trap "echo 'Quitting...'; kill $(printf '%s ' "${pids[@]}"); exit 0" 2
    trap "echo 'Aborting...'; kill $(printf '%s ' "${pids[@]}"); exit 1" 1 3 15

    # Wait for gatherers to have at least one round of data
    sleep 2

    # and loop.
    local -A v=()
    while sleep 1; do
        # gather our randomly generated sensor data...
        for thing in "${things[@]}"; do
            v[$thing]=$( < /tmp/t.$$.$thing )
        done
        # and print it.
        printf "$fmt" "${v[@]}"
    done
}

things=( i j )                # the list of things to monitor
cleol=$(tput el)              # you know what this is already
up2=$(tput cuu1; tput cuu1)   # this too
fmt="${up2}Measurement 1: %d${cleol}\nMeasurement 2: %d${cleol}\n"

main

This puts your list of monitored items in a bash array, $things[], and backgrounds as required. It traps errors and breaks properly. The one thing it doesn't do is adapt your $fmt to the size of the array. Or make coffee.

ghoti
  • 45,319
  • 8
  • 65
  • 104