Created
October 10, 2024 06:46
-
-
Save netscylla/3a06e7a8abb31b71799e8531fcd3859a to your computer and use it in GitHub Desktop.
modified pistar-lastqso to function on WPSD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# shellcheck source=/dev/null disable=SC2206,SC2034 | |
# | |
# pistar-lastqso | |
# Written by: Ken Cormack (KE8DPF), [email protected] | |
# Copyright: © 2024, Ken Cormack | |
# github: https://github.com/kencormack/pistar-lastqso | |
# QRZ page: https://www.qrz.com/db/KE8DPF | |
# Modified 2024-10-08 Andy (2W0APD) for WPSD | |
########################################################### | |
# INITIALIZATION | |
########################################################### | |
set -o allexport | |
VERSION=3.26 | |
#---------- | |
if [[ "${EUID}" -eq 0 ]] | |
then | |
echo | |
echo "ERROR:" >&2 | |
echo " Execution as \"root\" not required." >&2 | |
echo " Please run as the normal \"pi-star\" userid." >&2 | |
echo | |
exit | |
fi | |
#---------- | |
# This condition occurs during development only. A typical | |
# user will never encounter this. | |
if [[ -n "$(pgrep -o "build_cache")" ]] | |
then | |
echo | |
echo "ERROR:" >&2 | |
echo " A cache build is in progress." >&2 | |
echo | |
exit | |
fi | |
#---------- | |
# Turn on privileged mode. In this mode, the $BASH_ENV and $ENV files | |
# are not processed, shell functions are not inherited from the | |
# environment, and the SHELLOPTS, BASHOPTS, CDPATH and GLOBIGNORE | |
# variables, if they appear in the environment, are ignored. | |
set -p | |
# Make globs that don't get expanded cause errors, rather than getting | |
# passed to the command with the * intact. | |
shopt -s failglob | |
# Ensure that ERR traps are inherited by functions, command substitutions, | |
# and subshell environments. | |
set -o errtrace | |
# Ensure that DEBUG and RETURN traps are inherited by functions, command substitutions, | |
# and subshell environments. (The -E causes errors within functions to bubble up.) | |
set -E -o functrace | |
#---------- | |
# Save the current terminal configuration | |
STTY="$(stty -g)" | |
# Turn off character/word/line erase, and | |
# turn off echo of chars as they are typed | |
stty cbreak -echo | |
#---------- | |
# NOTE #1: Originally written for pi-star >= 4.1.4, for DMR mode ONLY. | |
# I developed this first for DMR, using a TYT MD-UV390GPS. I later | |
# obtained a Yaesu FT-70DR and added support for YSF. Then, by simply | |
# configuring pi-star and watching the logs, network-traffic-only (no RF | |
# traffic) was added for NXDN, D-Star, and P25. When a D-Star user sent | |
# me a sample of his logs, showing D-Star RF log entries, D-Star RF | |
# support was added. At this writing, support for NXDN, P25, and M17 RF | |
# voice traffic have also been added. | |
# NOTE #2: I've tried to write this script with readability in mind. | |
# A year after having last touched a script, I like to look at it and | |
# quickly understand what it's doing, without a lot of head scratching. | |
# Comments are applied liberally throughout, and "clever but cryptic" | |
# one-liners that are not immediately understandable without reading | |
# (and then re-reading) various man pages, are generally avoided. The | |
# priority is understandability, rather than clever elegance or raw | |
# performance, within reason. I did, however, undertake a performance | |
# minded sweep through the code, replacing well over a hundred calls | |
# to external tools like awk, cut, grep and such, with bash variable | |
# expansion mechanisms. This saves tremendous overhead in loops such | |
# as this script, particularly on a Pi Zero. | |
# NOTE #3: I cut my teeth on Sequent hardware and SVR3, 35+ years ago. | |
# Over three decades, I've worked on nearly all commercial *nix variants. | |
# Until Linux, none of them came with BASH, and KSH was my defacto shell. | |
# At heart, I'm a KSH guy. Consequently, you may find "old fashioned" | |
# ksh-like syntax in here (out of habbit), where a "modern"(?) bash-ism | |
# might exist, but that I simply forgot to use. | |
# NOTE #4: This was an effort to A.) eliminate the necessity of a second | |
# computer from which to simply monitor traffic via use of an HDMI | |
# console on the pi-star node - although it works terrifically via SSH, | |
# B.) provide a great deal more information about the traffic being | |
# observed than what pi-star's own dashboard provides, C.) be as well | |
# documented as possible to allow anyone to customize the code as they | |
# wish, and finally, D.) have my stuff be independent of Andy's great | |
# work (ie: not steal any of his dashboard code.) | |
#---------- | |
# NOTES ON GETTING RID OF SOME CUTS | |
#${var#*SubStr} # drops substring from start of string up to first occurrence of `SubStr` | |
#${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr` | |
#${var%SubStr*} # drops substring from last occurrence of `SubStr` to end of string | |
#${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string | |
# VAR="${VAR#*=}" same as: cut -f2 -d"=" | |
# VAR="${VAR%,*}" same as: cut -f1 -d"," | |
# If a string contains multiple fields, these do not extract *just* a single "hit". | |
# | |
# For that, make use of an array... | |
# IFS='/'; CUT_ARRAY=(${STRING TO PICK APART}); unset IFS; #IFS is the field separator | |
# SPECIFIC_NEEDED_DATA="${CUT_ARRAY[0]}" #[0] is first field, [1] is second, [2] is third, etc. | |
# unset CUT_ARRAY | |
# This allows selection of a single field, without contaminating that field with | |
# overlap from another field | |
#---------- | |
# POSSIBLE FUTURE ENHANCEMENTS TO CACHE UPDATING | |
# Fast in-place edits using sed: | |
# deleting records... | |
# sed -i '/^${callsign}:/d' ${file} | |
# updating records... | |
# sed -i "s/^${callsign}:.*/${callsign}:${new_data}/" "${file}" | |
# The potential complication is whether multiple instances | |
# of the script are running, each with it's own cache files, | |
# which currently then get re-merged and saved in $HOME. This | |
# would put stale records, as well as the updated records, | |
# back into the saved/merged copies, resulting in multiple | |
# records with no way to determine which record is then correct. | |
# Some thinking will be required. Easiest fix would be to | |
# disallow multiple concurrent instances of the script. | |
#---------- | |
# For whatever reason, "umask 0077" isn't locking down permissions | |
# on files written to /tmp, yet symbolically setting it does. | |
# I'll need to look into that a bit more. | |
umask u=rw,g=,o= | |
PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" | |
# Start the Elapsed Time clock | |
SECONDS="0" | |
#---------- | |
# Get the full path to this script | |
# for the -i|--info section | |
MY_PID="$(pgrep -o -f "${BASH} $(readlink -f "${0}")")" | |
#---------- | |
# For bash's built-in "time" command | |
# This gives us just the "real" time, | |
# in seconds, for the "time" command | |
# that clocks dxcc.pl execution time. | |
TIMEFORMAT='%R' | |
#---------- | |
# Any equal signs passed on the cmdline are replaced w/ spaces, | |
# so that an option, and it's parameter (if any), are two separate | |
# elements in an array. The array gets passed to fnPARSE_CMDLINE. | |
# | |
# Gather what's passed on the cmdline... | |
# Strip any "=" signs separating an option and it's parameter... | |
# Place the groomed elements into an array | |
MY_CMDLINE="${*//=/ }" | |
read -r -a ARG_LIST <<< "${MY_CMDLINE}" | |
ARG_LIST_LENGTH="${#ARG_LIST[@]}" | |
#---------- | |
# Get the tty we're using. Tmp filenames will be suffixed to keep | |
# multiple concurrent sessions from stomping on each other's files. | |
MY_TTY="$(tty | sed -u -e 's/\///g' -e 's/dev//')" | |
#---------- | |
# SET UP THE DEBUG FILE DESCRIPTOR | |
# If it doesn't already exist, setup the file descriptor for debugging | |
[[ -e /proc/self/fd/3 ]] || exec 3> /dev/null | |
# If no redirection has been applied by the user, "${DEBUG_LOG}" will be "/dev/null". | |
# But if the user has redirected fd 3 to a file, then "${DEBUG_LOG}" will contain the | |
# name (with full path), to the file. | |
DEBUG_LOG="$(readlink /proc/self/fd/3 2>/dev/null)" | |
echo "LASTQSO v${VERSION} - DEBUGGING LOG" >&3 | |
echo -e "==================================\n" >&3 | |
# FOR DEBUGGING: THE DEBUG TRAP | |
# This trap writes each line to be executed, just before it is executed, to the debug log. | |
trap 'echo -e "line#: ${LINENO}...\t${BASH_COMMAND}" >&3' DEBUG | |
# FOR DEBUGGING: THE ERR TRAP | |
# This trap will log all non-0 return codes. | |
trap 'echo -e "NON-0: LINE ${LINENO}: RETURN CODE: ${?}\t${BASH_COMMAND}" >&3' ERR | |
# FOR DEBUGGING: THE RETURN TRAP | |
# This trap logs the completion of each function, upon return. | |
trap 'echo -e "leave: ${FUNCNAME[0]} -> back to ${FUNCNAME[1]}\n" >&3' RETURN | |
#---------- | |
# SET UP THE PROFILING FILE DESCRIPTOR | |
# For a rough profiling of this script | |
[[ -e /proc/self/fd/4 ]] || exec 4> /dev/null | |
# If no redirection has been applied by the user, "${PROFILING_LOG}" will be "/dev/null". | |
# But if the user has redirected fd 4 to a file, then "${PROFILING_LOG}" will contain the | |
# name (with full path), to the file. | |
PROFILING_LOG="$(readlink /proc/self/fd/4 2>/dev/null)" | |
echo "LASTQSO v${VERSION} - PROFILING LOG" >&4 | |
echo -e "==================================\n" >&4 | |
BASH_XTRACEFD="4" | |
# Using PS4, "\011" is a tab, and "\t" is a current hh:mm:ss timestamp | |
# (I don't need subsecond accuracy here.) | |
PS4='+\011\t ${LINENO}\011' | |
# Enclose any section of code we want to profile with "set -x"..."set +x". | |
# Profiling data will be sent to the fd whenever "set -x" is in effect. | |
# Turning it on now, for example, would basically profile everything from | |
# here down. Use sparingly, as it can seriously impact performance | |
# (and don't forget to turn it off when done). | |
# set -x | |
#---------- | |
# Defaults that can be overridden with cmdline options... | |
# | |
# Timestamps now default to 24Hr format. To go back | |
# to 12Hr am/pm timestamps, use "-12|--12hr" option. | |
MIL_TIME="1" | |
# | |
# Don't force immediate download of user.csv file. | |
# Can be overridden with "-c|--csv" cmdline option | |
GET_CSV_NOW="0" | |
# | |
# Don't force immediate download of cty.dat file. | |
# Can be overridden with "-d|--dat" cmdline option | |
GET_DAT_NOW="0" | |
# | |
# Call upon dxcc.pl to resolve QTH country when | |
# the callsign can't be found in either the DMR | |
# user.csv or NXDN NXDN.csv files. Can be overridden | |
# with "-D|--DXCC" cmdline option. | |
USE_DXCC="1" | |
# | |
# Show errors, whenever they appear in the MMDVM log. | |
# Can be overridden with "-e|--errors" cmdline option | |
SHOW_LOG_ERRORS="1" | |
# | |
# Maidenhead lookups on US callsigns disabled by default. | |
# Can be activated with "-g|--grid" cmdline option. | |
GRID="0" | |
# | |
# If callsign is US-based, FCC database queries to | |
# determine if the license is Novice, Technician, General, | |
# Advanced, or Extra levels, are disabled by default. | |
# Can be activated with "-F|--FCC" cmdline option. | |
FCC="0" | |
# | |
# *IF* your display is greater than 130 columns wide, | |
# *AND* you use the "-t|--top" option, *AND* you add | |
# the "-i|--info" option, a few helpful details are | |
# added to the top-right corner of the display. This | |
# is helpful if you are modifying this script. Other | |
# than that, it serves little use. | |
SHOW_DEV_INFO="0" | |
# | |
# By default, display logo screen. | |
# Can be disabled with "-l|--logo" cmdline option | |
LOGO="1" | |
# | |
# Display Callsigns & TGs/DG-IDs, etc., in large font. | |
# Can be overridden with "-n|--nobig" cmdline option | |
USE_BANNER="1" | |
# | |
# By default, the script will query Brandmeister ONLY ONCE | |
# at startup, to determine currently linked talkgroups. | |
# This "-p|--poll" option tells the script to frequently | |
# poll Brandmeister to refresh the linked TG list. This is | |
# most helpful to people who like to bounce around between | |
# dynamically linked TGs, but can slow the script down a bit | |
# between each QSO, as that is when the polling to refresh | |
# the list will occur. This delay will be more noticable on | |
# Raspberry Pi Zero-based hotspots. The quad-core models | |
# (including the new Pi Zero 2) won't generally notice a delay. | |
BM_POLL="0" | |
# | |
# By default, the script will parse the current "live" log | |
# using "tail". But if testing, "-r|--replay" will use the | |
# "cat" command instead, to replay a user-specified log. | |
REPLAY="0" | |
# | |
# Don't display the config and history section at top of screen. | |
# Can be overridden with "-t|--top" cmdline option | |
TOP="0" | |
# | |
# Allow figlet to auto-wrap as needed. | |
# Can be overridden with "-w|--wrap" cmdline option | |
NO_WRAP="0" | |
#---------- | |
# Define some ways to make things bold, use different colors, control | |
# scrolling and cursor positioning, and so on. See "man tput" for | |
# descriptions of each tput option used here. (tput is much more | |
# predictable across terminal types than cryptic ANSI escape sequences.) | |
BELL="$(tput bel)" # Sound a bell | |
FLASH="$(tput flash)" # Flash the screen | |
# Not all term types (including the pi console's "TERM=linux") support | |
# tput's alternate screen buffers. If not, just clear the screen. | |
if infocmp -1 | grep -q "smcup=" && infocmp -1 | grep -q "rmcup=" | |
then | |
SMCUP="$(tput smcup)" # Set alternate screen buffer | |
RMCUP="$(tput rmcup)" # Restore primary screen buffer | |
HAS_BUFFER="1" | |
else | |
SMCUP="$(tput clear)" | |
RMCUP="$(tput clear)" | |
HAS_BUFFER="0" | |
fi | |
CUR_HOME=$(tput cup 0 0) # Send the cursor home, to line 0, column 0 | |
CUR_SC="$(tput sc)" # Save cursor position | |
CUR_RC="$(tput rc)" # Restore cursor position | |
CUR_OFF="$(tput civis)" # Turn cursor off | |
CUR_ON="$(tput cnorm)" # Turn cursor on | |
CLR_EL="$(tput el)" # Clear to end of line | |
CLR_ED="$(tput ed)" # Clear to end of display | |
INS_LINE="$(tput il1)" # Insert a line above current row (pushes everything down) | |
#DEL_LINE="$(tput dl1)" # Delete a line below current row (pulls everything up) | |
SGR0="$(tput sgr0)" # Undo text attributes (bold, etc.) | |
BOLD="$(tput bold)" # Make text bold | |
DIM="$(tput dim)" # Half-intensity | |
REV="$(tput rev)" # Reverse text fg/bg colors | |
SMUL="$(tput smul)" # Start Underline text | |
RMUL="$(tput rmul)" # End Underline text | |
#BLINK="$(tput blink)" # Blink text | |
CUR_UP="$(tput cuu1)" # Cursor up, one line | |
#CUR_DN="$(tput cud1)" # Cursor down, one line | |
WRAP_OFF="$(tput rmam)" # Disable line-wrap | |
WRAP_ON="$(tput smam)" # Enable line-wrap | |
# Forground colors | |
BLA="$(tput setaf 0)" # Black | |
RED="$(tput setaf 1)" # Red | |
GRE="$(tput setaf 2)" # Green | |
YEL="$(tput setaf 3)" # Yellow | |
BLU="$(tput setaf 4)" # Blue | |
MAG="$(tput setaf 5)" # Magenta | |
CYA="$(tput setaf 6)" # Cyan | |
WHI="$(tput setaf 7)" # White | |
DEF="$(tput setaf 9)" # Default | |
# Background colors | |
# Reserved for possible future use | |
# BG_BLA="$(tput setab 0)" # Black | |
# BG_RED="$(tput setab 1)" # Red | |
# BG_GRE="$(tput setab 2)" # Green | |
# BG_YEL="$(tput setab 3)" # Yellow | |
# BG_BLU="$(tput setab 4)" # Blue | |
# BG_MAG="$(tput setab 5)" # Magenta | |
# BG_CYA="$(tput setab 6)" # Cyan | |
# BG_WHI="$(tput setab 7)" # White | |
# BG_DEF="$(tput setab 9)" # Default | |
DMR_COLOR="${BOLD}${WHI}" | |
YSF_COLOR="${BOLD}${YEL}" | |
NXDN_COLOR="${BOLD}${CYA}" | |
DSTAR_COLOR="${BOLD}${GRE}" | |
P25_COLOR="${BOLD}${MAG}" | |
M17_COLOR="${BOLD}${RED}" | |
OTHER_COLOR="${BOLD}${BLU}" | |
TICK="✓" | |
CROSS="✗" | |
#---------- | |
# ANSI STUFF - for things that tput can't address. | |
# Not every terminal emulator or term type may support ANSI | |
# escape sequences, though most common ones do. YMMV | |
# The ESCape character | |
ESC=$(echo -n $'\e' 2>/dev/null) | |
# "tput sgr0" fails to restore terminal settings under certain | |
# circumstances. This may(?) be a better choice, upon exit. | |
TERM_RESET="$(echo -ne "${ESC}[0m" 2>/dev/null)" | |
# Double-height, double-width character set. | |
# 3 is top half of charset, 4 is bottom half. | |
# Not every terminal type/emulation supports these. | |
LARGE_TOP="$(echo -n "${ESC}#3" 2>/dev/null)" | |
LARGE_BOT="$(echo -n "${ESC}#4" 2>/dev/null)" | |
# 5 sets line single-width (lines are set this way when cleared by ESC [ J) | |
NORM_FONT="$(echo -n "${ESC}"#5 2>/dev/null)" | |
# Single-height, double-width character set | |
# 6 sets line double-width, with normal height | |
WIDE_FONT="$(echo -n "${ESC}"#6 2>/dev/null)" | |
#---------- | |
# Stuff dealing with pi-star files | |
# | |
MY_CALLSIGN="$(grep -A 8 "^\[General\]" /etc/mmdvmhost | grep "^Callsign")" | |
MY_CALLSIGN="${MY_CALLSIGN#*=}" | |
# Some DMR-related files we'll be consulting... | |
# These are supplied with, and updated by, pistar | |
DMRIDS="$(grep -A 2 "^\[DMR Id Lookup\]" /etc/mmdvmhost | grep "^File")" | |
DMRIDS="${DMRIDS#*=}" | |
TGLIST="$(grep "^TGLISTBM=" /usr/local/sbin/wpsd-hostfile-update)" | |
TGLIST="${TGLIST#*=}" | |
# Unlike the two files above, all of pi-star's references to this file are | |
# hard-coded, so I am safe to set this variable directly to the filename. | |
DMRHOSTS="/usr/local/etc/DMR_Hosts.txt" | |
# Users can create this next file to contain any DMR talkgroup | |
# names that are blank/empty in the pistar-supplied TGLIST above. | |
# If all your talk groups are covered, then you don't need to | |
# worry about this file. | |
MY_LIST="/usr/local/etc/MY_LIST.txt" | |
# This is the master DMR user.csv file from radioid.net. | |
# We download this, and update it if older than 7 days. | |
USERCSV="/usr/local/etc/user.csv" | |
# This is the NXDN.csv file supplied with, and updated by pi-star. | |
# It is identical in structure to the DMR user.csv file | |
NXDNCSV="/usr/local/etc/NXDN.csv" | |
# This is the cty.dat file from www.country-files.com. | |
# It is used by dxcc.pl, to determine the country that issued | |
# a callsign, based on the callsign's prefix. We download | |
# and update it if older than 30 days. | |
CTYDAT="/usr/local/etc/cty.dat" | |
#---------- | |
if [[ "${REPLAY}" -eq 1 ]] | |
then | |
WORKING_LOG="${REPLAY_LOG}" | |
else | |
# Find the latest log | |
LOGDIR="$(grep -A 4 "^\[Log\]" /etc/mmdvmhost | grep "^FilePath=")" | |
LOGDIR="${LOGDIR#*=}" | |
LOGROOT="$(grep -A 4 "^\[Log\]" /etc/mmdvmhost | grep "^FileRoot=")" | |
LOGROOT="${LOGROOT#*=}" | |
CANDIDATE_FILES=("${LOGDIR}/${LOGROOT}"*.log) NEWEST=${CANDIDATE_FILES[0]} | |
for F in "${CANDIDATE_FILES[@]}" | |
do | |
if [[ "${F}" -nt "${NEWEST}" ]] | |
then | |
NEWEST="${F}" | |
fi | |
done | |
WORKING_LOG="${NEWEST}" | |
fi | |
#---------- | |
# BM_API_KEY="$(grep "^apikey=" /etc/bmapi.key 2>/dev/null | cut -f2 -d"=")" | |
BM_API_KEY="$(grep "^apikey=" /etc/bmapi.key 2>/dev/null)" | |
BM_API_KEY="${BM_API_KEY#*=}" | |
HOTSPOT_ID="$(grep -A 13 "^\[DMR\]" /etc/mmdvmhost | grep "^Id=")" | |
HOTSPOT_ID="${HOTSPOT_ID#*=}" | |
#---------- | |
# This tmpfile preserves the counters manipulated in functions | |
# and loops, that are needed by (but not otherwise "handed up" to) | |
# calling functions and processes. | |
COUNT_FILE="/tmp/lastqso-count-${MY_TTY}" | |
# The various counters that will be stored and recalled | |
# to/from the above file | |
DMR_COUNT="0" | |
YSF_COUNT="0" | |
NXDN_COUNT="0" | |
DSTAR_COUNT="0" | |
P25_COUNT="0" | |
M17_COUNT="0" | |
QSO_COUNT="0" | |
KERCHUNK_COUNT="0" | |
ERROR_COUNT="0" | |
WARNING_COUNT="0" | |
# This tracks when logging is cycled | |
LOG_RESTARTS="0" | |
#---------- | |
HISTORY_FILE="/tmp/lastqso-history-${MY_TTY}" | |
# When we query Brandmeister to learn a hotspot's TGs, | |
# the findings are written to this tmpfile. | |
BM_TG_FILE="/tmp/lastqso-tgs-${MY_TTY}" | |
# We make sure any file left behind by a previous run is | |
# deleted, so that we build a fresh copy with this run. | |
rm "${BM_TG_FILE}" 2>/dev/null || : | |
# When dxcc.pl is called to find the country that issued | |
# a callsign, the result will be stored in this cache | |
# file. Subsequent appearances of that callsign can then | |
# quickly be recalled from cache, saving 3-5 seconds each | |
# time that callsign appears. Unlike the counter-tracking, | |
# history, and talk-group temp files employed by this | |
# script, this file will NOT be deleted upon program exit. | |
# This makes an already populated file immediately available, | |
# if the script is stopped by the user and then restarted. | |
DXCC_CACHE_FILE="/tmp/lastqso-dxcc-cache-${MY_TTY}" | |
touch "${DXCC_CACHE_FILE}" | |
# Lookups for the Maidenhead Grid Squares of US-based | |
# callsigns are performed via an online query to a | |
# remote host. These queries are normally very fast. | |
# But there's little point in continually querying the | |
# remote server each time we see the same callsign. | |
# So, we'll cache any learned results, in a manner | |
# similar to caching of the dxcc lookups. | |
GRID_CACHE_FILE="/tmp/lastqso-grid-cache-${MY_TTY}" | |
touch "${GRID_CACHE_FILE}" | |
# Ditto, for the FCC license class. | |
LICENSE_CACHE_FILE="/tmp/lastqso-license-cache-${MY_TTY}" | |
touch "${LICENSE_CACHE_FILE}" | |
#---------- | |
# Finding a way to get the screen width of a window that can | |
# be dynamically re-sized is funky, in bash. Neither tput, | |
# stty, bash's own $LINES and $COLUMNS, or other such tools | |
# work to read a terminal's screen size, from within a script. | |
# So, I have to call on a few lines of python. | |
# | |
# The advantage to using the "here doc" is that I don't need | |
# to host, bundle, distribute and install a trivial external | |
# python script alongside pistar-lastqso. Everything needed | |
# stays within the bash script. | |
# | |
# Note to self: | |
# My 40" 1080P screen is... | |
# 61 lines x 228 columns - HDMI-connected to Pi | |
# 45 lines x 157 columns - SSH (PuTTY Max Windowed-mode) | |
# 49 lines x 160 columns - SSH (Putty Full Screen-mode) | |
# My Windows laptop screen with PuTTY is... | |
# 44 lines x 168 columns - SSH (PuTTY Max Windowed Mode) | |
# 48 lines x 170 columns - SSH (Putty Full Screen-mode) | |
export LINES COLUMNS | |
PY3_SCRIPT=$(cat <<'EOF' | |
import os | |
ts = os.get_terminal_size(2) | |
ts.lines | |
ts.columns | |
print (ts.lines, ts.columns, sep=' ') | |
EOF | |
) | |
# The above is called like this, whenever needed... | |
read -r LINES COLUMNS <<< "$(python3 -c "${PY3_SCRIPT}")" 2>/dev/null | |
#---------- | |
# MISC stuff... | |
# For grepping stuff that includes tabs | |
GTAB=$'\t' | |
# This padding is appended to most of the displayed values at top of | |
# the "-t" screen, and then truncated to an appropriate length. This | |
# acts to clear out any artifacts of previous data that are some- | |
# times left behind when the user changes the size of an SSH window | |
# or the data in that field changes. | |
#TOP_PAD=".........................................................." | |
TOP_PAD=" " | |
# Disable line-wrap | |
# Prevents unexpectedly long fields from busting up the screen. | |
# (There's one entry in user.csv that is over 170 characters long!) | |
echo -en "${WRAP_OFF}" | |
########################################################### | |
# FUNCTIONS | |
########################################################### | |
#---------- | |
fnCHOP_CALL() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Call with: | |
# VAR_TO_CHOP="$(fnCHOP_CALL "${VAR_TO_CHOP}")" | |
# Attempt to isolate the call when we see things | |
# like "XX0XXX/BILL", reducing it to "XX0XXX". | |
# Any non-alphanumeric separator the person has | |
# inserted to append their name (often seen in | |
# YSF, such as a slash, dash, underscore or | |
# whatever), is changed to a known value we can | |
# parse on. In this case, I change it to an | |
# underscore. Then, I chop off that underscore | |
# along with anything that follows it, leaving | |
# only the callsign. | |
CHOP_CALL="${1}" | |
CHOP_CALL="${CHOP_CALL//[^[:alnum:]]/_}" | |
CHOP_CALL="${CHOP_CALL%%_*}" | |
echo "${CHOP_CALL}" | |
# TODO: Someday, I need to figure out a way to | |
# handle cases where no non-alphanumeric char | |
# was used, and the person just appended their | |
# name directly onto the call ("XX0XXXBILL"). | |
# But that's for another day (maybe). | |
return | |
} | |
#---------- | |
fnSECONDS2TIME() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
local T="${SECONDS}" | |
local D=$((T/60/60/24)) | |
local H=$((T/60/60%24)) | |
local M=$((T/60%60)) | |
local S=$((T%60)) | |
[[ $D -ge 0 ]] && printf '%02d days ' $D | |
[[ $H -ge 0 ]] && printf '%02d hrs ' $H | |
[[ $M -ge 0 ]] && printf '%02d mins ' $M | |
# [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' | |
# printf '%d secs\n' $S | |
return | |
} | |
#---------- | |
fnWRITE_COUNTER_FILE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
echo "# | |
DMR_COUNT=${DMR_COUNT} | |
YSF_COUNT=${YSF_COUNT} | |
NXDN_COUNT=${NXDN_COUNT} | |
DSTAR_COUNT=${DSTAR_COUNT} | |
P25_COUNT=${P25_COUNT} | |
M17_COUNT=${M17_COUNT} | |
# | |
QSO_COUNT=${QSO_COUNT} | |
KERCHUNK_COUNT=${KERCHUNK_COUNT} | |
# | |
ERROR_COUNT=${ERROR_COUNT} | |
WARNING_COUNT=${WARNING_COUNT}" \ | |
> "${COUNT_FILE}" | |
return | |
} | |
#----------------------------------- | |
fnCOPY_CACHE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
#---------- | |
# Combine any current caches with anything previously saved. | |
# Put the groomed file in place as the live cache. | |
# Save a copy to the home directory. Do this, only if the | |
# active and saved files are different. If they are the | |
# same, skip the merge/sort/write/copy/delete operations. | |
if ! cmp --silent -- "${DXCC_CACHE_FILE}" /home/pi-star/.lastqso-dxcc-cache 2>/dev/null | |
then | |
cat "${DXCC_CACHE_FILE}" /home/pi-star/.lastqso-dxcc-cache 2>/dev/null \ | |
| sed 's/United States of America/United States/' \ | |
| LC_ALL=C sort -u >/tmp/.cache_tmp 2>/dev/null | |
cat /tmp/.cache_tmp > "${DXCC_CACHE_FILE}" 2>/dev/null | |
sudo mount -o remount,rw / 2>/dev/null | |
cat /tmp/.cache_tmp > /home/pi-star/.lastqso-dxcc-cache 2>/dev/null | |
rm /tmp/.cache_tmp 2>/dev/null | |
fi | |
if ! cmp --silent -- "${GRID_CACHE_FILE}" /home/pi-star/.lastqso-grid-cache 2>/dev/null | |
then | |
cat "${GRID_CACHE_FILE}" /home/pi-star/.lastqso-grid-cache 2>/dev/null \ | |
| grep -F -v "Unknown" \ | |
| LC_ALL=C sort -u >/tmp/.cache_tmp 2>/dev/null | |
cat /tmp/.cache_tmp > "${GRID_CACHE_FILE}" 2>/dev/null | |
sudo mount -o remount,rw / 2>/dev/null | |
cat /tmp/.cache_tmp > /home/pi-star/.lastqso-grid-cache 2>/dev/null | |
rm /tmp/.cache_tmp 2>/dev/null | |
fi | |
if ! cmp --silent -- "${LICENSE_CACHE_FILE}" /home/pi-star/.lastqso-license-cache 2>/dev/null | |
then | |
cat "${LICENSE_CACHE_FILE}" /home/pi-star/.lastqso-license-cache 2>/dev/null \ | |
| LC_ALL=C sort -u >/tmp/.cache_tmp 2>/dev/null | |
cat /tmp/.cache_tmp > "${LICENSE_CACHE_FILE}" 2>/dev/null | |
sudo mount -o remount,rw / 2>/dev/null | |
cat /tmp/.cache_tmp > /home/pi-star/.lastqso-license-cache 2>/dev/null | |
rm /tmp/.cache_tmp 2>/dev/null | |
fi | |
# When I'm testing/developing, occasionally the script spots log | |
# entries it denotes as "unparsed". Save those for me to look | |
# at, to determine if the script should parse or ignore them. | |
if [[ -n "${IS_DEV}" ]] | |
then | |
sudo mount -o remount,rw / 2>/dev/null | |
mv /tmp/unparsed_DMR /home/pi-star 2>/dev/null || : | |
mv /tmp/unparsed_YSF /home/pi-star 2>/dev/null || : | |
mv /tmp/unparsed_NXDN /home/pi-star 2>/dev/null || : | |
mv /tmp/unparsed_DSTAR /home/pi-star 2>/dev/null || : | |
mv /tmp/unparsed_P25 /home/pi-star 2>/dev/null || : | |
mv /tmp/unparsed_M17 /home/pi-star 2>/dev/null || : | |
fi | |
#---------- | |
sync;sync;sync | |
return | |
} | |
# For possible future use | |
#----------------------------------- | |
fnCENTER() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
read -r LINES COLUMNS <<< "$(python3 -c "${PY3_SCRIPT}")" 2>/dev/null | |
LENGTH=${#STRING} | |
tput cup $((LINES / 2)) $(((COLUMNS / 2) - (LENGTH / 2))) | |
echo "${STRING}" | |
return | |
} | |
# Look up Maidenhead Grid Square for US Callsigns, | |
# based on address of record, in the FCC database. | |
#----------------------------------- | |
fnGET_US_GRIDSQUARE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
US_CALL="${1}" | |
US_CALL="$(fnCHOP_CALL "${US_CALL}")" | |
# Search the cache. | |
GRIDSQUARE="$(grep "^${US_CALL}:" "${GRID_CACHE_FILE}")" | |
if [[ -n "${GRIDSQUARE}" ]] | |
then | |
# If the cache delivered, we're done. | |
GRIDSQUARE="${GRIDSQUARE^^}" | |
GRIDSQUARE=" (${GRIDSQUARE#*:})" | |
else | |
# Otherwise, we need to query the remote host. | |
read -r GRIDSQUARE <<< "$(curl -s --fail --max-time 2 "https://callook.info/${US_CALL}/json" 2>/dev/null | jq -r '.location.gridsquare' 2>/dev/null | tr '\n' ' ')" | |
GRIDSQUARE="${GRIDSQUARE^^}" | |
# Dont cache an unknown or null response | |
if [[ "${GRIDSQUARE}" =~ [A-Z][A-Z][0-9][0-9][A-Z][A-Z] ]] | |
then | |
echo "${US_CALL}:${GRIDSQUARE}" >> "${GRID_CACHE_FILE}" | |
GRIDSQUARE=" (${GRIDSQUARE})" | |
fi | |
fi | |
return | |
} | |
# Look up FCC License level for US Callsigns | |
#----------------------------------- | |
fnGET_US_LICENSE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
US_CALL="${1}" | |
US_CALL="$(fnCHOP_CALL "${US_CALL}")" | |
# Search the cache. | |
LICENSE="$(grep "^${US_CALL}:" "${LICENSE_CACHE_FILE}")" | |
if [[ -n "${LICENSE}" ]] | |
then | |
# If the cache delivered, we're done. | |
LICENSE="${LICENSE^^}" | |
LICENSE="${LICENSE#*:}" | |
else | |
# Otherwise, we need to query the remote host. | |
read -r TYPE CLASS <<< "$(curl --silent --fail --max-time 2 "https://callook.info/${US_CALL}/json" 2>/dev/null \ | |
| grep -v "^<" | jq -r '.type, .current.operClass' 2>/dev/null \ | |
| tr '\n' ' ')" | |
if [[ "${TYPE}" =~ "CLUB" ]] | |
then | |
CLASS="CLUB" | |
fi | |
# Dont cache an "Unknown" or null response | |
LICENSE="${CLASS^^}" | |
case "${LICENSE}" in | |
NOVICE|TECHNICIAN|GENERAL|ADVANCED|EXTRA|CLUB) | |
echo "${US_CALL}:${LICENSE}" >> "${LICENSE_CACHE_FILE}" | |
;; | |
*) | |
LICENSE="" | |
;; | |
esac | |
fi | |
return | |
} | |
#----------------------------------- | |
fnGET_MODES() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
#---------- | |
# Primary modes | |
DMR_ENABLED="$(grep -A 2 "^\[DMR\]" /etc/mmdvmhost | grep "^Enable")" | |
DMR_ENABLED="${DMR_ENABLED#*=}" | |
YSF_ENABLED="$(grep -A 2 "^\[System Fusion\]" /etc/mmdvmhost | grep "^Enable")" | |
YSF_ENABLED="${YSF_ENABLED#*=}" | |
DSTAR_ENABLED="$(grep -A 2 "^\[D-Star\]" /etc/mmdvmhost | grep "^Enable")" | |
DSTAR_ENABLED="${DSTAR_ENABLED#*=}" | |
NXDN_ENABLED="$(grep -A 2 "^\[NXDN\]" /etc/mmdvmhost | grep "^Enable")" | |
NXDN_ENABLED="${NXDN_ENABLED#*=}" | |
P25_ENABLED="$(grep -A 2 "^\[P25\]" /etc/mmdvmhost | grep "^Enable")" | |
P25_ENABLED="${P25_ENABLED#*=}" | |
M17_ENABLED="$(grep -A 2 "^\[M17\]" /etc/mmdvmhost | grep "^Enable")" | |
M17_ENABLED="${M17_ENABLED#*=}" | |
#POCSAG_ENABLED="$(grep -A 2 "^\[POCSAG\]" /etc/mmdvmhost | grep "^Enable")" | |
#POCSAG_ENABLED="${POCSAG_ENABLED#*=}" | |
#---------- | |
# Cross-modes | |
DMR2YSF_ENABLED="$(grep -A 1 "^\[Enabled\]" /etc/dmr2ysf | grep "^Enabled")" | |
DMR2YSF_ENABLED="${DMR2YSF_ENABLED#*=}" | |
DMR2NXDN_ENABLED="$(grep -A 1 "^\[Enabled\]" /etc/dmr2nxdn | grep "^Enabled")" | |
DMR2NXDN_ENABLED="${DMR2NXDN_ENABLED#*=}" | |
YSF2DMR_ENABLED="$(grep -A 1 "^\[Enabled\]" /etc/ysf2dmr | grep "^Enabled")" | |
YSF2DMR_ENABLED="${YSF2DMR_ENABLED#*=}" | |
YSF2NXDN_ENABLED="$(grep -A 1 "^\[Enabled\]" /etc/ysf2nxdn | grep "^Enabled")" | |
YSF2NXDN_ENABLED="${YSF2NXDN_ENABLED#*=}" | |
YSF2P25_ENABLED="$(grep -A 1 "^\[Enabled\]" /etc/ysf2p25 | grep "^Enabled")" | |
YSF2P25_ENABLED="${YSF2P25_ENABLED#*=}" | |
return | |
} | |
#----------------------------------- | |
fnCLEANUP() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Delete everything except the dxcc and grid cache files | |
rm "${BM_TG_FILE}" \ | |
"${COUNT_FILE}" \ | |
"${HISTORY_FILE}" 2>/dev/null || : | |
fnCOPY_CACHE | |
return | |
} | |
#----------------------------------- | |
fnCLOSE_FDS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Close the debugging and profiling file descriptors | |
exec 3>&- 2>/dev/null | |
set +x 2>/dev/null | |
exec 4>&- 2>/dev/null | |
return | |
} | |
#----------------------------------- | |
# Double high, double wide character set | |
fnLARGE_FONT() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
echo -n "${SGR0}" | |
case ${MY_FONT} in | |
5) | |
echo -e "${LARGE_TOP}${*}\n${LARGE_BOT}${*}" | |
;; | |
6) | |
echo -e "${WIDE_FONT}${*}" | |
;; | |
*) | |
echo "${*}" | |
;; | |
esac | |
echo -n "${CLR_ED}" | |
return | |
} | |
#----------------------------------- | |
# Because bash can't do simple floating-point comparisons.... | |
fnCOMPARE_FLOAT_LT() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Look for this to return "<" if FLOAT1 is less than FLOAT2 | |
awk -v FLOAT1="${1}" -v FLOAT2="${2}" 'BEGIN {printf "%s " (FLOAT1<FLOAT2?"<":">=") " %s\n", FLOAT1, FLOAT2}' | |
return | |
} | |
#----------------------------------- | |
# Because bash can't do simple floating-point comparisons.... | |
fnCOMPARE_FLOAT_GT() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Look for this to return ">" if FLOAT1 is greater than FLOAT2 | |
awk -v FLOAT1="${1}" -v FLOAT2="${2}" 'BEGIN {printf "%s " (FLOAT1>FLOAT2?">":"<=") " %s\n", FLOAT1, FLOAT2}' | |
return | |
} | |
#----------------------------------- | |
# Gratuitous eye-candy | |
# UTF-8 has richer, more predictable box-drawing characters | |
# than old fashioned terminfo acsc characters. The caveat is | |
# that the terminal must support UTF-8. | |
# | |
fnDEFINE_BOXCHARS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Unicode box-drawing characters. We'll use a few of these. | |
# 0 1 2 3 4 5 6 7 8 9 A B C D E F | |
# U+250x ─ ━ │ ┃ ┌ ┏ | |
# U+251x ┐ ┓ └ ┗ ┘ ┛ ├ ┝ | |
# U+252x ┠ ┣ ┤ ┥ ┨ ┫ ┬ ┯ | |
# U+253x ┰ ┳ ┴ ┷ ┸ ┻ ┼ ┿ | |
# U+254x ╂ ╋ | |
# U+255x ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ | |
# U+256x ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ | |
# Unicode block elements. | |
# 0 1 2 3 4 5 6 7 8 9 A B C D E F | |
# U+258x ▀ ▄ █ ▌ | |
# U+259x ▐ ░ ▒ ▓ | |
if [[ "$(locale charmap)" =~ "UTF-8" ]] | |
then | |
# UTF-8 single-line box-drawing characters | |
SBOX_BR="$(printf "\u2518")" # ┘ bottom-right corner | |
SBOX_TR="$(printf "\u2510")" # ┐ top-right corner | |
SBOX_TL="$(printf "\u250C")" # ┌ top-left corner | |
SBOX_BL="$(printf "\u2514")" # └ bottom-left corner | |
SBOX_CC="$(printf "\u253C")" # ┼ center cross | |
SBOX_HL="$(printf "\u2500")" # ─ horizontal line | |
SBOX_LT="$(printf "\u251C")" # ├ left-side "T" (right-pointing) | |
SBOX_RT="$(printf "\u2524")" # ┤ right-side "T" (left-pointing) | |
SBOX_BT="$(printf "\u2534")" # ┴ bottom "T" (up-pointing) | |
SBOX_TT="$(printf "\u252C")" # ┬ top "T" (down-pointing) | |
SBOX_VL="$(printf "\u2502")" # │ vertical line | |
# UTF-8 double-line box-drawing characters | |
DBOX_BR="$(printf "\u255D")" # ╝ bottom-right corner | |
DBOX_TR="$(printf "\u2557")" # ╗ top-right corner | |
DBOX_TL="$(printf "\u2554")" # ╔ top-left corner | |
DBOX_BL="$(printf "\u255A")" # ╚ bottom-left corner | |
DBOX_CC="$(printf "\u256C")" # ╬ center cross | |
DBOX_HL="$(printf "\u2550")" # ═ horizontal line | |
DBOX_LT="$(printf "\u2560")" # ╠ left-side "T" (right-pointing) | |
DBOX_RT="$(printf "\u2563")" # ╣ right-side "T" (left-pointing) | |
DBOX_BT="$(printf "\u2569")" # ╩ bottom "T" (up-pointing) | |
DBOX_TT="$(printf "\u2566")" # ╦ top "T" (down-pointing) | |
DBOX_VL="$(printf "\u2551")" # ║ vertical line | |
# UTF-8 mixed outer double/inner single-line box-drawing characters | |
DSBOX_LT="$(printf "\u255F")" # ╟ mixed dbl/sgl-line left-side "T" (right-pointing) | |
DSBOX_RT="$(printf "\u2562")" # ╢ mixed dbl/sgl-line right-side "T" (left-pointing) | |
DSBOX_BT="$(printf "\u2567")" # ╧ bottom "T" (up-pointing) | |
DSBOX_TT="$(printf "\u2564")" # ╤ top "T" (down-pointing) | |
# UTF-8 mixed outer single/inner double-line box-drawing characters | |
SDBOX_LT="$(printf "\u255E")" # ╞ mixed sgl/dbl-line left-side "T" (right-pointing) | |
SDBOX_RT="$(printf "\u2561")" # ╡ mixed sgl/dbl-line right-side "T" (left-pointing) | |
SDBOX_BT="$(printf "\u2568")" # ╨ bottom "T" (up-pointing) | |
SDBOX_TT="$(printf "\u2565")" # ╥ top "T" (down-pointing) | |
TOP_HALF_SHADOW="$(printf "\u2580")" # ▀ top half-height shadow | |
BOT_HALF_SHADOW="$(printf "\u2584")" # ▀ bottom half-height shadow | |
FULL_SHADOW="$(printf "\u2588")" # █ full-height shadow | |
else | |
# if no UTF-8, use ascii characters to draw a poor-man's box | |
SBOX_BR="+" | |
SBOX_TR="+" | |
SBOX_TL="+" | |
SBOX_BL="+" | |
SBOX_CC="+" | |
SBOX_HL="-" | |
SBOX_LT="+" | |
SBOX_RT="+" | |
SBOX_BT="+" | |
SBOX_TT="+" | |
SBOX_VL="|" | |
DBOX_BR="+" | |
DBOX_TR="+" | |
DBOX_TL="+" | |
DBOX_BL="+" | |
DBOX_CC="+" | |
DBOX_HL="=" | |
DBOX_LT="+" | |
DBOX_RT="+" | |
DBOX_BT="+" | |
DBOX_TT="+" | |
DBOX_VL="|" | |
DSBOX_LT="+" | |
DSBOX_RT="+" | |
DSBOX_BT="+" | |
DSBOX_TT="+" | |
SDBOX_LT="+" | |
SDBOX_RT="+" | |
SDBOX_BT="+" | |
SDBOX_TT="+" | |
TOP_HALF_SHADOW="#" | |
BOT_HALF_SHADOW="#" | |
FULL_SHADOW="#" | |
fi | |
DBL_LINE="$(printf "${DBOX_HL}%.0s" {1..76})" | |
return | |
} | |
#----------------------------------- | |
# Usage: fnDRAW_BOX BOX_TOP BOX_LEFT BOX_HEIGHT BOX_WIDTH BOX_STYLE BOX_FILL BOX_SHADOW | |
# BOX_TOP and BOX_LEFT = the position of the upper left corner of the box | |
# BOX_HEIGHT = the number of rows of the box | |
# BOX_WIDTH = the number of columns of the box | |
# BOX_STYLE = single [1] or double-line [2] box-drawing characters | |
# BOX_FILL = preserve [0] or erase [1] anything within the box | |
# BOX_SHADOW = no drop-shadow [0] or add a drop-shadow [1] | |
fnDRAW_BOX() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
#---------- | |
BOX_TOP="${1}" | |
BOX_LEFT="${2}" | |
BOX_HEIGHT="${3}" | |
BOX_WIDTH="${4}" | |
BOX_STYLE="${5}" | |
BOX_FILL="${6}" | |
BOX_SHADOW="${7}" | |
BOX_BOTTOM=$((BOX_TOP + BOX_HEIGHT)) | |
BOX_RIGHT=$((BOX_LEFT + BOX_WIDTH)) | |
#---------- | |
if [[ "${BOX_STYLE}" = "1" ]] | |
then | |
# single-line characters | |
BOX_TL="${SBOX_TL}" | |
BOX_TR="${SBOX_TR}" | |
BOX_HL="${SBOX_HL}" | |
BOX_VL="${SBOX_VL}" | |
BOX_BL="${SBOX_BL}" | |
BOX_BR="${SBOX_BR}" | |
else | |
# double-line characters | |
BOX_TL="${DBOX_TL}" | |
BOX_TR="${DBOX_TR}" | |
BOX_HL="${DBOX_HL}" | |
BOX_VL="${DBOX_VL}" | |
BOX_BL="${DBOX_BL}" | |
BOX_BR="${DBOX_BR}" | |
fi | |
#---------- | |
# Top line of the box | |
if [[ "${BOX_WIDTH}" -gt 2 ]] | |
then | |
tput cup "${BOX_TOP}" "${BOX_LEFT}" | |
for ((I=BOX_LEFT; I<=BOX_RIGHT; I++)) | |
do | |
printf -- "${BOX_HL}%0.s" | |
done | |
fi | |
#---------- | |
# Top-left corner | |
tput cup "${BOX_TOP}" "${BOX_LEFT}" | |
echo -n "${BOX_TL}" | |
# Top-right corner | |
tput cup "${BOX_TOP}" $((BOX_LEFT + BOX_WIDTH)) | |
echo -n "${BOX_TR}" | |
BOX_CURRENT_LINE=$((BOX_TOP + 1)) | |
#---------- | |
# preserve or erase what's between the sides of the box | |
COUNT="1" | |
PADDING="" | |
if [[ "${BOX_FILL}" = "0" ]] | |
then | |
PAD="$(tput cuf1)" # Move cursor one space to the right | |
else | |
PAD=" " # one space | |
fi | |
until [[ "${COUNT}" -eq "${BOX_WIDTH}" ]] | |
do | |
PADDING="${PADDING}${PAD}" | |
((COUNT++)) | |
done | |
#---------- | |
# sides of the box | |
until [[ "${BOX_CURRENT_LINE}" -eq "${BOX_BOTTOM}" ]] | |
do | |
tput cup "${BOX_CURRENT_LINE}" "${BOX_LEFT}" | |
echo -n "${BOX_VL}${PADDING}${BOX_VL}" | |
# if shadow, print the block | |
if [[ "${BOX_SHADOW}" -eq 1 ]] | |
then | |
echo -n "${FULL_SHADOW}" | |
fi | |
((BOX_CURRENT_LINE++)) | |
done | |
#---------- | |
# Bottom line of the box | |
if [[ "${BOX_WIDTH}" -gt 2 ]] | |
then | |
tput cup "${BOX_BOTTOM}" "${BOX_LEFT}" | |
for ((I=BOX_LEFT; I<=BOX_RIGHT; I++)) | |
do | |
printf -- "${BOX_HL}%0.s" | |
done | |
fi | |
#---------- | |
# Bottom-left corner | |
tput cup "${BOX_BOTTOM}" "${BOX_LEFT}" | |
echo -n "${BOX_BL}" | |
# Bottom-right corner | |
tput cup "${BOX_BOTTOM}" "${BOX_RIGHT}" | |
echo -n "${BOX_BR}" | |
# if shadow, print the row of blocks | |
if [[ "${BOX_SHADOW}" -eq 1 ]] | |
then | |
tput cup "${BOX_CURRENT_LINE}" "$((BOX_RIGHT + 1))" | |
echo -n "${FULL_SHADOW}" | |
tput cup $((BOX_BOTTOM + 1)) $((BOX_LEFT + 2)) | |
for ((I=BOX_LEFT; I<="$((BOX_RIGHT -1))"; I++)) | |
do | |
printf "${TOP_HALF_SHADOW}%0.s" | |
done | |
fi | |
return | |
} | |
#----------------------------------- | |
fnTOP_DATA() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# This function deals with the optional non-scrolling zone, | |
# created when the -t|--top option is specified on the cmdline. | |
#---------- | |
if [[ "${REPLAY}" -eq 1 ]] | |
then | |
WORKING_LOG="${REPLAY_LOG}" | |
fi | |
MY_LOG="$(basename "${WORKING_LOG}")" | |
#---------- | |
# GATHER LINE 0 DATA... | |
# This script's version | |
# Pad, and truncate to fit | |
MY_VER="${IS_DEV}${VERSION}${IS_DEV}" | |
# Datestamp of the user.csv file | |
CSV_DATE="$(stat --print=%y /usr/local/etc/user.csv 2>/dev/null)" | |
CSV_DATE="${CSV_DATE%%" "*}" | |
# Datestamp of the cty.dat file | |
DAT_DATE="$(stat --print=%y /usr/local/etc/cty.dat 2>/dev/null)" | |
DAT_DATE="${DAT_DATE%%" "*}" | |
#---------- | |
# GATHER LINE 1 DATA... | |
# Hotspot TX & RX frequencies | |
# <rant> I hate that bash can't do simple floating-point math without external help. </rant> | |
TX="$(printf %.4f "$(echo print "$(grep -A 9 "\[Info\]" /etc/mmdvmhost | grep "^TXFrequency=" 2>/dev/null | cut -f2 -d"=" | sed -u 's/Hz//')"/1000000 | perl)")" | |
RX="$(printf %.4f "$(echo print "$(grep -A 9 "\[Info\]" /etc/mmdvmhost | grep "^RXFrequency=" 2>/dev/null | cut -f2 -d"=" | sed -u 's/Hz//')"/1000000 | perl)")" | |
# Details about the modem | |
DESCRIPTION="$(grep "description:" /var/log/pi-star/MMDVM* | tail -1)" | |
# Firmware version | |
IFS=' '; CUT_ARRAY=(${DESCRIPTION}); unset IFS; | |
FW="${CUT_ARRAY[8]}" | |
unset CUT_ARRAY | |
# Isolate the field that contains "MHz" within the record | |
# If we can't find it, set it to "unknown" | |
TCXO="$(printf %2.4f "$(echo "${DESCRIPTION}" | awk '/MHz/ {for(i=NF;i>=1;i--) {if($i~/MHz/) {$0=i":"$i}} print}' | cut -f3 -d":" | sed -u 's/MHz//')")" | |
TCXO="${TCXO:-'unknown'}" | |
#---------- | |
# GATHER LINE 2 DATA... | |
# CPU_TEMP is fetched in fnNOSCROLL_ZONE, where it can be updated frequently, | |
# rather than here in this function. | |
MODEM_PORT="$(grep -A 25 "^\[Modem\]" /etc/mmdvmhost | grep "^Port")" | |
IFS='/'; CUT_ARRAY=(${MODEM_PORT}); unset IFS; | |
MODEM_PORT="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
PLATFORM="$(grep Platform /etc/pistar-release|cut -f 2 -d '=')" | |
#---------- | |
# GATHER LINE 3 DATA... | |
# elapsed time since launch, and load average, are fetched in fnNOSCROLL_ZONE, | |
# where it can be updated frequently, rather than in this function. | |
#---------- | |
# GATHER LINE 4 DATA... | |
# brandmeister static and dynamic talkgroups are fetched in fnNOSCROLL_ZONE, | |
# where they can be updated frequently, rather than in this function. | |
if [[ "${DMR_ENABLED}" = "1" ]] | |
then | |
DMR_MASTER="$(grep -A 10 "^\[DMR Network\]" /etc/mmdvmhost | grep "^Address")" | |
DMR_MASTER="${DMR_MASTER#*=}" | |
DMR_MASTER_NAME="$(grep "${GTAB}${DMR_MASTER}${GTAB}" /usr/local/etc/DMR_Hosts.txt | awk '{ print $1 }' | sed -u 's/_/ /g')" | |
else | |
if [[ "${YSF2DMR_ENABLED}" = "1" ]] | |
then | |
DMR_MASTER="DMR2YSF ($(grep -A 5 "^\[DMR Network 3\]" /etc/dmrgateway | grep "^Name"))" | |
DMR_MASTER="${DMR_MASTER#*=}" | |
DMR_MASTER="${DMR_MASTER%)*}" | |
DMR_MASTER_NAME="${DMR_MASTER}" | |
else | |
DMR_MASTER_NAME="null" | |
fi | |
fi | |
if [[ "${YSF_ENABLED}" = "1" ]] | |
then | |
YSF_STARTUP="$(grep -A 5 "^\[Network\]" /etc/ysfgateway | grep "^Startup")" | |
YSF_STARTUP="${YSF_STARTUP#*=}" | |
else | |
if [[ "${DMR2YSF_ENABLED}" = "1" ]] | |
then | |
YSF_STARTUP="YSF2DMR ($(grep -A 5 "^\[Network\]" /etc/ysfgateway | grep "^Startup"))" | |
YSF_STARTUP="${YSF_STARTUP#*=}" | |
YSF_STARTUP="${YSF_STARTUP%)*}" | |
else | |
YSF_STARTUP="null" | |
fi | |
fi | |
#---------- | |
# NXDN reflector is gathered in fnNOSCROLL_ZONE | |
# P25 reflector is gathered in fnNOSCROLL_ZONE | |
# D-Star reflector is gathered in fnNOSCROLL_ZONE | |
return | |
} | |
#----------------------------------- | |
fnDEV_INFO() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# If running on a big screen, this prints some info in the | |
# top-right corner of the screen, on the non-scrolling lines, | |
# when -t|--top is used. | |
if [[ "${COLUMNS}" -lt 130 ]] || [[ "${SHOW_DEV_INFO}" -eq 0 ]] | |
then | |
return | |
fi | |
# If null, set to "n/a" | |
BG_PID="${BG_PID:-'n/a'}" | |
#---------- | |
DEV_STRING[1]=" Options: ${MY_CMDLINE}" | |
DEV_STRING[2]=" Debugging: 3> $(basename "${DEBUG_LOG}") Profiling: 4> $(basename "${PROFILING_LOG}")" | |
DEV_STRING[3]=" ${TERM} SIZE=${LINES}x${COLUMNS} TTY=${MY_TTY} PID=${MY_PID} BG_PID=${BG_PID}" | |
DEV_STRING[4]=" Modes/X-Modes: YSF=${YSF_ENABLED} DMR=${DMR_ENABLED} NXDN=${NXDN_ENABLED} DSTAR=${DSTAR_ENABLED} P25=${P25_ENABLED} M17=${M17_ENABLED}" | |
DEV_STRING[5]=" YSF2DMR=${YSF2DMR_ENABLED} YSF2NXDN=${YSF2NXDN_ENABLED} YSF2P25=${YSF2P25_ENABLED} DMR2YSR=${DMR2YSF_ENABLED} DMR2NXDN=${DMR2NXDN_ENABLED}" | |
DEV_STRING[6]=" Traffic: DMR=${DMR_COUNT} YSF=${YSF_COUNT} NXDN=${NXDN_COUNT} DSTAR=${DSTAR_COUNT} P25=${P25_COUNT} M17=${M17_COUNT}" | |
DEV_STRING[7]=" DXCC=$(grep -c . "${DXCC_CACHE_FILE}") Grid=$(grep -c . "${GRID_CACHE_FILE}") Lic=$(grep -c . "${LICENSE_CACHE_FILE}") K=${KERCHUNK_COUNT} E=${ERROR_COUNT} W=${WARNING_COUNT} R=${LOG_RESTARTS}" | |
#---------- | |
# In replay mode, text is bright yellow. | |
# In normal mode, text is pale cyan. | |
if [[ "${REPLAY}" -eq 1 ]] | |
then | |
echo -n "${BOLD}${YEL}" | |
else | |
echo -n "${DIM}${CYA}" | |
fi | |
#---------- | |
DEV_INDEX="1" | |
until [[ "${DEV_INDEX}" -gt 7 ]] | |
do | |
# tput references lines, beginning at 0. The array begins | |
# at 1. So we subtract 1 from the array index, to get the | |
# line number needed for tput. | |
X=$(( "${DEV_INDEX}" - 1 )) | |
# Subtract the length of the string from the width of the | |
# screen, to find the column at which to begin printing. | |
Y=$(( ("${COLUMNS}" - "${#DEV_STRING[DEV_INDEX]}") - 1 )) | |
# Print each line, flush right, at right edge of screen. | |
tput cup "${X}" "${Y}" | |
echo -n "${DEV_STRING[DEV_INDEX]}" | |
((DEV_INDEX++)) | |
done | |
echo -n "${SGR0}" | |
return | |
} | |
#----------------------------------- | |
fnNOSCROLL_ZONE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
if [[ "${TOP}" = "0" ]] | |
then | |
return | |
fi | |
# Save the current cursor position, and turn the cursor off | |
echo -n "${CUR_SC}${CUR_OFF}" | |
# Set the whole screen as scrollable | |
tput csr 0 $((LINES - 1)) | |
# Go to line 0 | |
echo -n "${CUR_HOME}" | |
# Define the scrolling region of the screen as the line just | |
# below the no-scroll region, to the bottom of screen. | |
# (Screen lines are numbered beginning with line 0.) | |
tput csr "${BELOW_NOSCROLL}" $((LINES - 1)) | |
#---------- | |
# Fetch BM TGs for line 5 | |
if [[ "$(grep "${GTAB}${DMR_MASTER}${GTAB}" "${DMRHOSTS}")" =~ "BM_" ]] && [[ "${DMR_ENABLED}" -eq 1 ]] | |
then | |
#---------- | |
# Query BM about your hotspot's TGs | |
# If BM_POLL=0, the query is performed only once. | |
# If BM_POLL=1, it's performed every time. | |
if [[ ! -f "${BM_TG_FILE}" ]] || [[ "${BM_POLL}" -eq 1 ]] | |
then | |
curl -s --fail --max-time 2 -X 'GET' "https://api.brandmeister.network/v2/device/${HOTSPOT_ID}/profile" -H 'accept: application/json' > "${BM_TG_FILE}" | |
fi | |
#---------- | |
BM_TG_LIST="" | |
BM_TG_COUNT="0" | |
# Parse the JSON for any static TGs | |
while read -r TALKGROUP | |
do | |
if [[ -n "${TALKGROUP}" ]] | |
then | |
BM_TG_LIST="${BM_TG_LIST} ${TALKGROUP}" | |
((BM_TG_COUNT++)) | |
fi | |
done <<< "$(jq -r '.staticSubscriptions[] .talkgroup' "${BM_TG_FILE}" 2>/dev/null)" | |
# Parse again, for any dynamic TGs | |
while read -r TALKGROUP | |
do | |
if [[ -n "${TALKGROUP}" ]] | |
then | |
# We indicate a dynamic TG by marking it with an asterisk | |
BM_TG_LIST="${BM_TG_LIST} ${TALKGROUP}*" | |
((BM_TG_COUNT++)) | |
fi | |
done <<< "$(jq -r '.dynamicSubscriptions[] .talkgroup' "${BM_TG_FILE}" 2>/dev/null)" | |
# Get the number of TGs linked (static and dynamic, combined), | |
# and remove quote marks from the resulting list of talkgroups | |
BM_TG_COUNT="$(printf %02d "${BM_TG_COUNT}")" | |
if [[ "${BM_TG_COUNT}" -eq 0 ]] | |
then | |
BM_TG_LIST=" No Talkgroups Linked" | |
else | |
BM_TG_LIST="${BM_TG_LIST//\"}" | |
fi | |
#---------- | |
# See if the total characters is wider than what we can show | |
# within the line-length limit of the no-scroll region. | |
BM_TG_LENGTH="${#BM_TG_LIST}" | |
if [[ "${BM_TG_LENGTH}" -gt 28 ]] | |
then | |
# If so, take only the first 28 chars and add an elipsis (...) | |
# to show we are aware there's more, but it just won't fit. | |
# | |
# Most people (including the BM admins) would recommend against | |
# too many statically linked talkgroups. Dynamically linked | |
# talkgroups should suffice for most people's needs. | |
BM_TG_LIST="${BM_TG_LIST:0:26}..." | |
fi | |
BM_TG_LIST="${BM_TG_LIST}${TOP_PAD}" | |
BM_TG_LIST="${BM_TG_LIST:0:29}" | |
BM_TG_LINE="${DIM}${WHI}TALKGROUPS(${DMR_COLOR}${BM_TG_COUNT}${SGR0}${DIM}${WHI}):${SGR0}${DMR_COLOR}${BM_TG_LIST}${SGR0}" | |
else | |
BM_TG_LINE="${DIM}${WHI}TALKGROUPS: No API available${SGR0}" | |
fi | |
#---------- | |
# Print line 0 | |
DAT_DATE="${DAT_DATE}${TOP_PAD}" | |
DAT_DATE="${DAT_DATE:0:11}" | |
CSV_DATE="${CSV_DATE}${TOP_PAD}" | |
CSV_DATE="${CSV_DATE:0:11}" | |
MY_VER="${MY_VER}${TOP_PAD}" | |
MY_VER="${MY_VER:0:7}" | |
MY_LOG="${MY_LOG}${TOP_PAD}" | |
MY_LOG="${MY_LOG:0:23}" | |
echo -en "${DIM}${WHI}LASTQSO:${SGR0} ${VER_CHECK_COLOR}${MY_VER}${SGR0}\r" | |
tput cuf 16 | |
echo -en "${DIM}${WHI}DAT:${SGR0} ${WHI}${BOLD}${DAT_DATE}${SGR0}\r" | |
tput cuf 32 | |
echo -en "${DIM}${WHI}CSV:${SGR0} ${WHI}${BOLD}${CSV_DATE}${SGR0}\r" | |
tput cuf 48 | |
echo "${DIM}${WHI}LOG:${SGR0} ${WHI}${BOLD}${MY_LOG}${SGR0}" | |
#---------- | |
# Print line 1 | |
TX="${TX}${TOP_PAD}" | |
TX="${TX:0:12}" | |
RX="${RX}${TOP_PAD}" | |
RX="${RX:0:12}" | |
FW="${FW}${TOP_PAD}" | |
FW="${FW:0:24}" | |
TCXO="${TCXO}${TOP_PAD}" | |
TCXO="${TCXO:0:10}" | |
echo -en "${DIM}${WHI}TX:${SGR0} ${WHI}${BOLD}${TX}${SGR0}\r" | |
tput cuf 16 | |
echo -en "${DIM}${WHI}RX:${SGR0} ${WHI}${BOLD}${RX}${SGR0}\r" | |
tput cuf 32 | |
echo -en "${DIM}${WHI}TCXO:${SGR0} ${WHI}${BOLD}${TCXO}${SGR0}\r" | |
tput cuf 48 | |
echo "${DIM}${WHI}FW:${SGR0} ${WHI}${BOLD}${FW}${SGR0}" | |
#---------- | |
# Print line 2 | |
CPU_TEMP="$(vcgencmd measure_temp)" | |
CPU_TEMP="${CPU_TEMP#*=}" | |
CPU_TEMP="${CPU_TEMP/\'/°}" | |
CPU_TEMP_INT="${CPU_TEMP%.*}" | |
if [[ "${CPU_TEMP_INT}" -lt 50 ]] | |
then | |
CPU_TEMP_COLOR="${GRE}" | |
fi | |
if [[ "${CPU_TEMP_INT}" -ge 50 ]] | |
then | |
CPU_TEMP_COLOR="${YEL}" | |
fi | |
if [[ "${CPU_TEMP_INT}" -ge 69 ]] | |
then | |
CPU_TEMP_COLOR="${RED}" | |
fi | |
CPU_TEMP="${CPU_TEMP}${TOP_PAD}" | |
CPU_TEMP="${CPU_TEMP:0:10}" | |
MODEM_PORT="${MODEM_PORT}${TOP_PAD}" | |
MODEM_PORT="${MODEM_PORT:0:10}" | |
PLATFORM="${PLATFORM}${TOP_PAD}" | |
PLATFORM="${PLATFORM:0:34}" | |
echo -en "${DIM}${WHI}Temp:${SGR0} ${CPU_TEMP_COLOR}${BOLD}${CPU_TEMP}${SGR0}\r" | |
tput cuf 16 | |
echo -en "${DIM}${WHI}Port:${SGR0} ${WHI}${BOLD}${MODEM_PORT}${SGR0}\r" | |
tput cuf 32 | |
echo "${DIM}${WHI}Platform:${SGR0} ${WHI}${BOLD}${PLATFORM}${SGR0}" | |
#---------- | |
# Print line 3 | |
ELAPSED="$(fnSECONDS2TIME)" | |
ELAPSED="${ELAPSED}${TOP_PAD}" | |
ELAPSED="${ELAPSED:0:23}" | |
echo -en "${DIM}${WHI}Elapsed:${SGR0} ${WHI}${BOLD}${ELAPSED}${SGR0}\r" | |
LOAD_AVG="$(uptime | awk '{ print $(NF-2)" "$(NF-1)" "$NF }')" | |
LOAD_AVG="${LOAD_AVG}${TOP_PAD}" | |
LOAD_AVG="${LOAD_AVG:0:20}" | |
tput cuf 32 | |
echo "${DIM}${WHI}Load Avg (1,5,15 mins):${SGR0} ${WHI}${BOLD}${LOAD_AVG}${SGR0}" | |
#---------- | |
# Get Reflectors for line 4 | |
NXDN_REFLECTOR="null" | |
if [[ "${NXDN_ENABLED}" = "1" ]] | |
then | |
SEARCH="$(grep --no-filename "reflector " /var/log/pi-star/NXDNGateway*.log | tail -1)" | |
IFS=' '; CUT_ARRAY=(${SEARCH}); unset IFS; | |
NXDNSWITCHED="${CUT_ARRAY[6]}" | |
NXDNSTATIC="${CUT_ARRAY[7]}" | |
unset CUT_ARRAY | |
if [[ "${SEARCH}" =~ "Switched to reflector" ]] | |
then | |
NXDN_REFLECTOR="${NXDNSWITCHED}" | |
else | |
if [[ "${SEARCH}" =~ "Statically linked to reflector" ]] | |
then | |
NXDN_REFLECTOR="${NXDNSTATIC}" | |
fi | |
fi | |
fi | |
P25_REFLECTOR="null" | |
if [[ "${P25_ENABLED}" = "1" ]] | |
then | |
SEARCH="$(grep --no-filename "reflector " /var/log/pi-star/P25Gateway*.log | tail -1)" | |
IFS=' '; CUT_ARRAY=(${SEARCH}); unset IFS; | |
P25LINKED="${CUT_ARRAY[6]}" | |
P25STATIC="${CUT_ARRAY[7]}" | |
unset CUT_ARRAY | |
if [[ "${SEARCH}" =~ "Switched to reflector" ]] | |
then | |
P25_REFLECTOR="${P25LINKED}" | |
else | |
if [[ "${SEARCH}" =~ "Statically linked to reflector" ]] | |
then | |
P25_REFLECTOR="${P25STATIC}" | |
fi | |
fi | |
fi | |
DSTAR_REFLECTOR="null" | |
if [[ "${DSTAR_ENABLED}" = "1" ]] | |
then | |
SEARCH="$(grep --no-filename "link to" /var/log/pi-star/ircDDBGateway*.log | tail -1)" | |
if [[ "${SEARCH}" =~ "established" ]] | |
then | |
DSTAR_REFLECTOR="${SEARCH:39:8}" | |
fi | |
fi | |
M17_STARTUP="null" | |
if [[ "${M17_ENABLED}" = "1" ]] | |
then | |
M17_STARTUP="$(grep -A 5 "^\[Network\]" /etc/m17gateway | grep "^Startup")" | |
M17_STARTUP="${M17_STARTUP#*=}" | |
fi | |
#---------- | |
# Print line 4 | |
YSF_STARTUP="${YSF_STARTUP}${TOP_PAD}" | |
YSF_STARTUP="${YSF_STARTUP:0:11}" | |
NXDN_REFLECTOR="${NXDN_REFLECTOR}${TOP_PAD}" | |
NXDN_REFLECTOR="${NXDN_REFLECTOR:0:11}" | |
DSTAR_REFLECTOR="${DSTAR_REFLECTOR}${TOP_PAD}" | |
DSTAR_REFLECTOR="${DSTAR_REFLECTOR:0:11}" | |
P25_REFLECTOR="${P25_REFLECTOR}${TOP_PAD}" | |
P25_REFLECTOR="${P25_REFLECTOR:0:9}" | |
M17_STARTUP="${M17_STARTUP}${TOP_PAD}" | |
M17_STARTUP="${M17_STARTUP:0:9}" | |
echo -en "${DIM}${WHI}YSF:${SGR0}${YSF_COLOR}${YSF_STARTUP}${SGR0}\r" | |
tput cuf 16 | |
echo -en "${DIM}${WHI}NXDN:${SGR0}${NXDN_COLOR}${NXDN_REFLECTOR}${SGR0}\r" | |
tput cuf 32 | |
echo -en "${DIM}${WHI}DSTAR:${SGR0}${DSTAR_COLOR}${DSTAR_REFLECTOR}${SGR0}\r" | |
tput cuf 48 | |
echo -en "${DIM}${WHI}P25:${SGR0}${P25_COLOR}${P25_REFLECTOR}${SGR0}\r" | |
tput cuf 62 | |
echo "${DIM}${WHI}M17:${SGR0}${M17_COLOR}${M17_STARTUP}${SGR0}" | |
#---------- | |
# Print line 5 | |
DMR_MASTER_NAME="${DMR_MASTER_NAME}${TOP_PAD}" | |
DMR_MASTER_NAME="${DMR_MASTER_NAME:0:19}" | |
echo -en "${DIM}${WHI}DMR MASTER:${SGR0} ${DMR_COLOR}${DMR_MASTER_NAME}${SGR0}\r" | |
# Print the talkgroups | |
tput cuf 32 | |
echo "${BM_TG_LINE}" | |
#---------- | |
# The history column headings | |
if [[ "${HIST_MAX}" -gt 0 ]] | |
then | |
echo "${REV}${CYA}Previous ${SBOX_VL} From ${SBOX_VL} To ${SBOX_VL} Secs ${SBOX_VL} BER% ${SBOX_VL} Src ${SBOX_VL} Loss% or S-Meter ${SGR0}${WHI}" | |
else | |
echo "${REV}${CYA} History not enabled - 0 lines specified on the commandline ${SGR0}${WHI}" | |
fi | |
#---------- | |
# Load the current history... | |
. "${HISTORY_FILE}" 2>/dev/null | |
#---------- | |
# DISPLAY (AND STORE) THE COMPLETE CURRENT HISTORY | |
# Print the QSO history lines... newest through oldest | |
# Save the history to a file, as well, for auto-restarts. | |
rm "${HISTORY_FILE}" 2>/dev/null | |
HIST_COUNT="1" | |
until [[ "${HIST_COUNT}" -gt "${HIST_MAX}" ]] | |
do | |
echo "${SGR0}${HISTORY[HIST_COUNT]}${CLR_EL}" | |
echo -e "HISTORY[${HIST_COUNT}]=\"${HISTORY[HIST_COUNT]}\"" >> "${HISTORY_FILE}" | |
((HIST_COUNT++)) | |
done | |
#---------- | |
# Print red line and clear the remainder of the line | |
echo "${RED}${DBL_LINE}${WHI}${SGR0}${CLR_EL}" | |
#---------- | |
# If the -i|--info block is to be displayed, do it here. | |
if [[ "${SHOW_DEV_INFO}" = "1" ]] | |
then | |
fnDEV_INFO | |
fi | |
#---------- | |
# Now return the cursor back to where it was, and turn it back on | |
echo -n "${CUR_RC}${CUR_ON}" | |
return | |
} | |
#----------------------------------- | |
fnRESET_SCROLLING() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
if [[ "${TOP}" = "0" ]] | |
then | |
return | |
fi | |
# Save the current cursor position, and turn the cursor off | |
echo -n "${CUR_SC}${CUR_OFF}" | |
# Define the entire screen as the scrolling region | |
tput csr 0 $((LINES - 1)) | |
# Now return the cursor back to where it was. | |
echo -n "${CUR_RC}${CUR_ON}" | |
return | |
} | |
#----------------------------------- | |
fnUSAGE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
echo "USAGE: Valid options and parameters include: | |
Short Form: | |
[-12] [-c] [-d] [-D] [-e] [-f <1-6>] [-F] [-g] [-h] [-i] [-l] | |
[-m] [-n] [-p] [-r /path/to/file] [-t [integer]] [-v] [-w] | |
Long Form: | |
[--12hr] [--csv] [--dat] [--DXCC] [--errors] [--font <1-6>] [--FCC] [--grid] | |
[-help] [-info] [--logo] [--mono] [--nobig] [--poll] [--replay /path/to/file] | |
[--top [integer]] [--version] [--wrap] | |
" >&2 | |
return | |
} | |
#----------------------------------- | |
fnPARSE_CMDLINE() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# Loop through all the cmdline arguments. It works like this... | |
# | |
# In the loop, current value would be the option we're evaluating. | |
# If the option takes a parameter, we "look ahead" at the "next" | |
# indexed value, and see if it makes sense to the current option. | |
# An example would be the -f|--font option, and it's parameter, | |
# for font selection. Once the option, and it's parameter are | |
# evaluated, we increment the index enough to skip over that | |
# parameter, then loop to evaluate the next option in the array. | |
# | |
# If the "current" option does not expect a parmeter, we just | |
# evaluate as needed, before looping to the next item in the | |
# array of arguments and parameters.. | |
# | |
# I chose this method, because both the external GNU getopt | |
# command, and bash's builtin getops, have problems.... | |
# | |
# GNU's getopt does not handle empty flag arguments well. | |
# Bash's builtin getopts does. | |
# (Think of the "-t|--top" option to this script, which can | |
# be used either with or without specifying a history depth) | |
# | |
# Bash's getopts does't parse long option names. | |
# GNU's getopt does. | |
# (Think "--help" vs "-h") | |
# | |
# The method I use can do both - it handles empty arguments (such as | |
# the "-t|-top" option when no optional number of history lines is | |
# given), and can easily accommodate long-form options. It is also, | |
# frankly, much easier to understand, and more portable, than either | |
# GNU's getopt or bash's getopts. | |
for (( INDEX=0; INDEX<ARG_LIST_LENGTH; INDEX++ )) | |
do | |
OPTION="${ARG_LIST[${INDEX}]}" | |
case "${OPTION}" in | |
-12|--12hr) | |
MIL_TIME="0" | |
continue | |
;; | |
-c|--csv) | |
GET_CSV_NOW="1" | |
continue | |
;; | |
-d|--dat) | |
GET_DAT_NOW="1" | |
continue | |
;; | |
-D|--DXCC) | |
USE_DXCC="0" | |
continue | |
;; | |
-e|--errors) | |
SHOW_LOG_ERRORS="0" | |
continue | |
;; | |
-f|--font) | |
# If this option is given, see if the next value | |
# is a number from 1 to 6 | |
NEXT=$((INDEX + 1)) | |
PARAM="${ARG_LIST[${NEXT}]}" | |
if [[ "${PARAM}" == ?(-)+([1-6]) ]] | |
then | |
# It is, so use it to set the font. | |
MY_FONT="${PARAM}" | |
# Skip over the number we were passed, and look | |
# for the next cmdline option. | |
((INDEX++)) | |
else | |
# It is not, so show the usage text, and exit. | |
echo -n "${BELL}${FLASH}" | |
echo -e "\nERROR: The use of \"-f|--font\" requires an integer 1-6\n" >&2 | |
fnUSAGE | |
stty "${STTY}" | |
fnCLEANUP | |
exit 1 | |
fi | |
continue | |
;; | |
-F|--FCC) | |
FCC="1" | |
continue | |
;; | |
-g|--grid) | |
GRID="1" | |
continue | |
;; | |
-h|--help) | |
fnGET_HELP | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
;; | |
-i|--info) | |
SHOW_DEV_INFO="1" | |
continue | |
;; | |
-l|--logo) | |
LOGO="0" | |
continue | |
;; | |
-m|--mono) | |
# Forground colors | |
#BLA="" | |
RED="" | |
GRE="" | |
YEL="" | |
BLU="" | |
MAG="" | |
CYA="" | |
WHI="" | |
DEF="" | |
# Background colors | |
# Reserved for possible future use | |
# BG_BLA="" | |
# BG_RED="" | |
# BG_GRE="" | |
# BG_YEL="" | |
# BG_BLU="" | |
# BG_MAG="" | |
# BG_CYA="" | |
# BG_WHI="" | |
# BG_DEF="" | |
DMR_COLOR="${BOLD}" | |
YSF_COLOR="${BOLD}" | |
NXDN_COLOR="${BOLD}" | |
DSTAR_COLOR="${BOLD}" | |
P25_COLOR="${BOLD}" | |
M17_COLOR="${BOLD}" | |
OTHER_COLOR="${BOLD}" | |
continue | |
;; | |
-n|--nobig) | |
USE_BANNER="0" | |
continue | |
;; | |
-p|--poll) | |
BM_POLL="1" | |
continue | |
;; | |
-r|--replay) | |
# If this option is given, see if the next value | |
# is a file we can read | |
NEXT=$((INDEX + 1)) | |
PARAM="${ARG_LIST[${NEXT}]}" | |
if [[ ! -f "${PARAM}" ]] | |
then | |
echo "The file specified for replay does not exist." >&2 | |
echo "Aborting" >&2 | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
fi | |
if [[ ! -r "${PARAM}" ]] | |
then | |
echo "The file specified for replay lacks read permission." >&2 | |
echo "Aborting" >&2 | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
fi | |
REPLAY="1" | |
REPLAY_LOG="${PARAM}" | |
((INDEX++)) | |
continue | |
;; | |
-t|--top) | |
TOP="1" | |
# If this option is given, see if the next value | |
# is a number from 0 to 99 | |
NEXT=$((INDEX + 1)) | |
PARAM="${ARG_LIST[${NEXT}]}" | |
if [[ "${PARAM}" =~ ^-?[0-9]|[1-2][0-9]+$ ]] | |
then | |
# It is, so use it to set the history depth. | |
# By default, the -t|--top history is 5 lines deep. | |
# Can be overridden with optional numeric parameter passed to | |
# the "-t|--top" cmdline option. Passing a zero disables the | |
# history, but retains the display of all other -t|--top info. | |
HIST_MAX="${PARAM}" | |
# Skip over the number we were passed, and look | |
# for the next cmdline option. | |
((INDEX++)) || : | |
else | |
# It is not, so assume a default history of 5. | |
HIST_MAX="5" | |
fi | |
# When "-t|--top" is used, with or without a history, this defines | |
# the line number on which the scrolling region of the screen | |
# (the part showing the QSO details) begins. | |
BELOW_NOSCROLL=$((HIST_MAX + 8)) | |
continue | |
;; | |
-v|--version) | |
fnVERSION | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
;; | |
-w|--wrap) | |
NO_WRAP="1" | |
continue | |
;; | |
*) | |
echo -n "${BELL}${FLASH}" | |
echo -e "\nERROR: \"${OPTION}\" is not a valid option.\n" >&2 | |
fnUSAGE | |
echo -e "See \"pistar-lastqso --help\" for detailed descriptions of the above.\n" | |
stty "${STTY}" | |
fnCLEANUP | |
exit 1 | |
;; | |
esac | |
done | |
return | |
} | |
#----------------------------------- | |
fnGET_HELP() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
cat <<'EOF' | less -M | |
PISTAR-LASTQSO - HELP | |
(Cursor Up/Down keys to scroll, Page Up/Dn to page, Q to quit help.) | |
With no options, the script watches for DMR, YSF, D-Star, NXDN, P25, | |
and M17 traffic types. Log entries are parsed and presented as each | |
log line is read. Ctrl-C can be used to exit the program. | |
-12|--12hr | |
By default, timestamps are shown in 24 hour military format. | |
For example, 5:00 PM is displayed as "17:00:00". However, by | |
using this option, timestamps will be displayed in 12 hour | |
format, with am/pm indicated, as appropriate (ex: 05:00:00 pm). | |
-c|--csv | |
Download an updated user.csv from radioid.net now, rather than | |
waiting for the presently installed copy to age 7 days before it | |
updates automatically. | |
-d|--dat | |
Download an updated cty.dat from country-files.com now, rather | |
than waiting for the presently installed copy to age 30 days | |
before it updates automatically. | |
-D|--DXCC | |
When resolving QTH location for callsigns, pistar-lastqso first | |
gambles that the callsign might also be either a DMR or NXDN | |
user. In which case, a quick check of the DMR user.csv and/or | |
NXDN NXDN.csv files will find their QTH data there. | |
If the callsign can't be found in those files, pistar-lastqso | |
calls upon dxcc.pl to determine the country that issued the call | |
sign. This can potentially impact pistar-lastqso's ability to | |
"keep up" with log entries, as each call to dxcc.pl can take up | |
to 4 or 5 seconds to resolve. When traffic lasts more than a | |
few seconds, this is no problem, as the script normally just | |
sits there saying "QSO In Progress...". But when the bulk of | |
traffic is "[[kerchunks]]", which pistar-lastqso considers any | |
traffic lasting less than 2 seconds, the kerchunks can out-pace | |
the ability of dxcc.pl to keep up. A few kerchunks here and | |
there are generally not a problem. But a large series of them | |
in quick succession can cause delays. Effectively, you will be | |
listening to one callsign, but still seeing data on the screen | |
for traffic logged many seconds ago. | |
This "-D|--DXCC" cmdline option can be used to disable calls | |
to the dxcc.pl script, if such delays become an issue for you. | |
This option has no affect on DMR or NXDN traffic. DMR and NXDN | |
users go through a registration process that results in their | |
QTH data being readily available in the user.csv and NXDN.csv | |
files, thus calls to the dxcc.pl script aren't needed for DMR | |
or NXDN traffic. | |
-e|--errors | |
By default, pistar-lastqso will display any error messages found | |
in the MMDVM log, as they occur. It is NOT unusual to see an | |
occasional, or sporadic message, such as a queue overflow. But | |
if these or other errors are frequent or persist, you may need | |
to get help from the pi-star forums. In the meantime, you can use | |
the "-e" or "--error" option to suppress the onscreen reporting | |
of these errors. The errors will however still be counted for | |
the current session, and the count will still be reported on the | |
exit screen. It is your responsibility to investigate any cause. | |
Use of this option DOES NOT FIX THE ERRORS coming from pi-star. | |
It only stops telling you that they are happening. | |
NOTE: The script also displays and tallies any WARNINGS that may | |
show up in the log. Although the -e|--error option will supress | |
display of ERRORS, it will NOT supress display of WARNINGS. | |
-f|--font <1-6> | |
The [-f|--font] option forces use of the selected font, regardless | |
of screen-width. | |
Valid options are 1, 2, 3, 4, 5, or 6 | |
1 = "small" | |
2 = "standard" | |
3 = "big" | |
4 = "ansi_shadow" | |
5 = "emulator-dependent alternate charset (dbl-high/dbl-wide)" | |
6 = "emulator-dependent alternate charset (dbl-wide)" | |
If this option is omitted, the script will auto-select an | |
appropriately sized figlet font based on the following screen- | |
width thresholds: | |
< 80 chars wide: "small" font | |
80-120 chars wide: "standard" font | |
>120 chars wide: "big" font | |
The "ansi_shadow" and "emulator-dependent alternate character set" | |
fonts are never auto-selected. | |
Fonts # 5 and 6 may not work for everyone. Not all terminal | |
emulators or TERM-types support the vt100 ANSI control codes for | |
this feature. | |
-F|--FCC | |
For US callsigns only, enable amateur radio license level lookup | |
for callsigns, based on FCC records. The license can be one of | |
Novice, Technician, General, Advanced, or Extra. When enabled, | |
the license level is displayed on a line following the QTH data. | |
-g|--grid | |
For US callsigns only, enable Maidenhead Grid Square look-up for | |
callsigns, based on the US FCC mailing address of record, for a | |
callsign. The grid square appears at the end of the QTH data. | |
This works only for US callsigns. | |
-h|--help | |
Display this help screen, then exit. | |
-i|--info | |
Shows a little info in the top-right corner of the screen, when | |
the following THREE CONDITIONS apply... | |
A.) You MUST specify the "-i|--info" option. | |
B.) Your display MUST be at least 130 chars wide. | |
C.) You MUST also activate the non-scrolling information zone | |
at the top of the screen using the "-t|--top" option (the | |
number of lines of history, if any, is irrelevant). | |
If either B or C above is false, then A is useless. If all | |
three conditions are met, a small block of information, largely | |
helpful only to those looking to modify the script, is shown | |
in the top-right corner of the screen. An example looks like | |
this: | |
Options: -t 15 -f 5 -w -i -l | |
Debugging: 3>/dev/null Profiling: 4>/dev/null | |
xterm SIZE=44x168 TTY=pts0 PID=2110 BG_PID=2414 | |
Modes/X-Modes: YSF=1 DMR=1 NXDN=1 DSTAR=1 P25=1 M17=1 | |
YSF2DMR=0 YSF2NXDN=0 YSF2P25=0 DMR2YSR=0 DMR2NXDN=0 | |
Traffic Counts: DMR=0 YSF=0 NXDN=0 DSTAR=0 P25=0 M17=0 | |
DXCC=457 Grid=95105 Lic=95105 K=0 E=0 W=0 R=0 | |
The first line shows the options and parameters passed to the | |
script when launched. | |
The second line lists any targets specified for the debug or | |
profiling logs, as described in the ABOUT_DEBUGGING.md and | |
ABOUT_PROFILING.md files. | |
The third line shows TERM type, and size of the screen in rows & | |
columns (useful to those who want to modify the script, as there | |
is so much cursor management stuff going on). Also shown are the | |
TTY of the terminal/session the script is running on, the PID of | |
the script, and the PID of the background task that watches for | |
log activity (daily log rotations and pi-star nightly updates). | |
The fourth line shows which of the following modes are enabled | |
in pi-star: YSF, DMR, NXDN, DSTAR, P25, or M17. If enabled, the | |
mode will show a "1". Disabled will show a "0". | |
The fifth line shows which of the following cross-modes are | |
enabled: YSF2DMR, YSF2NXDN, YSF2P25, DMR2YSR, and DMR2NXDN. | |
If enabled, the mode will show a "1". Disabled will show a "0". | |
The sixth line shows a number of counters. They show the number | |
of DMR, YSF, NXDN, DSTAR, P25, and M17 messages seen since the | |
program was launched. | |
The last line shows the number of records in the DXCC, Grid, and | |
License caches, and the number of Kerchunks, Errors, Warnings, | |
and log Restarts observed since program launch. When the script | |
is monitoring live data, the -i|--info data is shown in pale | |
blue. When running in -r|--replay mode, the data is shown in | |
bright yellow, as a quick visual reminder. | |
-l|--logo | |
Disables the animated logo at startup. | |
-m|--mono | |
For monochrome displays, this option suppresses all color codes. | |
-n|--nobig | |
Disable the large font display of callsigns and ID #s. This | |
conserves vertical screen-space. | |
-p|--poll | |
For DMR users configured to use Brandmeister master servers, | |
pistar-lastqso will ordinarily query the BM servers ONCE at | |
startup, to see which talkgroups (both static AND dynamic) you | |
are linked to. Those TGs will be shown in the non-scrolling | |
region, when -t|--top is used. Adding or removing talkgroups, | |
through PTT, TG 4000, the pi-star admin screen, Brandmeister's | |
"My Devices", or by any other means, will not be reflected on | |
screen until pistar-lastqso is stopped and run again, from the | |
command line. | |
However, if the "-p|--poll" option is used, this causes the | |
program to "poll" Brandmeister between every communication | |
displayed, resulting in a screen that will effectively track | |
your currently linked TGs. This is especially helpful for | |
users who jump around a lot between dynamic TGs, as it will | |
track these TGs as they are linked or unlinked. | |
Note that because there is a network connection made to the | |
BM server, and a query made, every time the data is polled, | |
this *can* induce a slight delay, especially on single-core | |
Raspberry Pi Zero-based hotspots, or on slow networks. On | |
multi-core Pis (including the new Raspberry Pi Zero 2), with | |
a decent network connection, this delay isn't a problem. | |
-r|--replay /path/to/file | |
FOR DEVELOPMENT USE: This option allows the specified file to | |
be "replayed" from the start, as if it were the current log. | |
This aids testing of coding changes, against problematic log | |
entries, by replaying those log entries. Troubleshooting w/ | |
another user's log too, becomes possible. A fully qualified, | |
or relative path, to the filename, must be specified. | |
Examples: | |
-r /path/to/file | |
--replay /path/to/file | |
Not: | |
-r filename | |
--replay filename | |
This also allows adding support for modes for which the user | |
does not have appropriate radios, by soliciting sample logs | |
from contributors who do own radios of the appropriate type. | |
-t|--top [integer] | |
Adds an information zone to the top of the screen that shows | |
the version number of this script, date of the cty.dat and | |
user.csv files, name of the active log file, TX and RX freqs, | |
the hotspot's TCXO frequency and firmware version, CPU temp, | |
the Modem's device node (port), the computer platform/model, | |
the elapsed time since the script was launched, the system | |
load averages for the last 1, 5, and 15 minutes, the DMR | |
Master and (if Brandmeister) the first few Static & Dynamic | |
BrandMeister TalkGroups (dynamics are marked with an asterisk). | |
The last line shows the current reflectors in effect for the | |
other modes. | |
By default, the use of "-t|--top" also provides a history of | |
the last 5 QSOs observed. However, by specifying an integer | |
parameter, the number of QSOs displayed in the history can be | |
changed. A value of 0 (zero) disables the history (but retains | |
all the other information in the "-t|--top" section of the | |
screen). Other values determine the number of lines of history | |
that will be displayed. | |
Examples: | |
-t defaults to 5 lines of history | |
-t 0 disables the history | |
-t 7 displays a history 7 lines deep | |
-t 23 displays a history 23 lines deep | |
CAUTION: Specifying too many lines for your actual screen-size | |
will yield unpredictable (and certainly unsatisfactory) results. | |
I do NOT limit-check this parameter, because SSH sessions in | |
dynamically resizable windows could have a different number of | |
lines each time the screen is re-sized, rendering useless any | |
limit-check performed at startup. | |
As a point of reference, "-t 8 -n" will maximize the size of | |
the history, while still allowing full details for the QSO in | |
progress (without large fonts), on a typical 80x24 display. | |
Larger screens will support other combinations. Feel free to | |
experiment - YMMV. | |
-v|--version | |
Display this script's version number, then exit. | |
-w|--wrap | |
Ordinarily, if figlet decides it needs to wrap the text that it | |
displays, based on the size of the font selected, and the width | |
of the screen (in columns), it will do so. However, setting | |
this option tells figlet to ignore the screen width, and just | |
keep printing on the same line, even if that means the text will | |
disappear off the right edge of the screen. You sacrifice what- | |
ever information was off the edge of the screen, but without | |
wasting so much vertical screen space to line-wrap. | |
END OF HELP | |
(Cursor Up/Down keys to scroll, Page Up/Dn to page, Q to quit help.) | |
EOF | |
return | |
} | |
#----------------------------------- | |
fnVERSION() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
clear | |
echo "${BOLD}${WHI}$(basename "${0}") v${VERSION}${SGR0}" | |
cat <<'EOF' | |
Copyright © 2024 Ken Cormack, KE8DPF | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published | |
by the Free Software Foundation, either version 3 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program. If not, see <http://www.gnu.org/licenses/>. | |
EOF | |
return | |
} | |
#----------------------------------- | |
fnNO_LOGO() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
clear | |
echo "${BOLD}${WHI}PISTAR-LASTQSO v${VERSION}${SGR0}" | |
echo "Copyright © 2024 Ken Cormack - KE8DPF" | |
echo "${DIM}https://github.com/kencormack/pistar-lastqso${SGR0}" | |
echo | |
return | |
} | |
#----------------------------------- | |
fnLOGO() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
clear | |
# Initialize screen, disable line-wrap, cursor off, color to white | |
echo -n "${CUR_OFF}${SGR0}${WHI}" | |
#---------- | |
# This "pushes the logo down" from the top of the screen... | |
echo -n "${CUR_HOME}" | |
for LINE in {6..1} | |
do | |
echo -n "${DIM}${CYA}" | |
{ | |
cat <<'EOF' | |
██╗ █████╗ ███████╗████████╗ ██████╗ ███████╗ ██████╗ | |
██║ ██╔══██╗██╔════╝╚══██╔══╝ ██╔═══██╗██╔════╝██╔═══██╗ | |
██║ ███████║███████╗ ██║ ██║ ██║███████╗██║ ██║ | |
██║ ██╔══██║╚════██║ ██║ ██║▄▄ ██║╚════██║██║ ██║ | |
███████╗██║ ██║███████║ ██║ ╚██████╔╝███████║╚██████╔╝ | |
╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══▀▀═╝ ╚══════╝ ╚═════╝ | |
EOF | |
} | head -"${LINE}" | tail -1 | |
echo -n "${CUR_HOME}" | |
echo -n "${INS_LINE}" | |
done | |
for I in 1 2 | |
do | |
echo -n "${CUR_HOME}" | |
echo -n "${INS_LINE}" | |
done | |
#---------- | |
# This increases in "brightness", from dim, to normal, to bold. | |
for BRIGHTNESS in "${DIM}" "${SGR0}" "${BOLD}" | |
do | |
tput cup 3 0 | |
echo -n "${BRIGHTNESS}${WHI}" | |
cat <<'EOF' | |
██╗ █████╗ ███████╗████████╗ ██████╗ ███████╗ ██████╗ | |
██║ ██╔══██╗██╔════╝╚══██╔══╝ ██╔═══██╗██╔════╝██╔═══██╗ | |
██║ ███████║███████╗ ██║ ██║ ██║███████╗██║ ██║ | |
██║ ██╔══██║╚════██║ ██║ ██║▄▄ ██║╚════██║██║ ██║ | |
███████╗██║ ██║███████║ ██║ ╚██████╔╝███████║╚██████╔╝ | |
╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══▀▀═╝ ╚══════╝ ╚═════╝ | |
EOF | |
done | |
#---------- | |
# Draw the horizontal line under the logo | |
tput cup 9 9 | |
echo -en "${SGR0}${DIM}${WHI}" | |
for ((I=17; I<=76; I++)) | |
do | |
printf "${SBOX_HL}%0.s" | |
done | |
#---------- | |
# This goes in the center of the horizontal line | |
tput cup 9 28 | |
echo -n "${SGR0}${BOLD}${WHI} FOR PI-STAR HOTSPOTS ${SGR0}" | |
#---------- | |
# Usage: fnDRAW_BOX BOX_TOP BOX_LEFT BOX_HEIGHT BOX_WIDTH BOX_STYLE BOX_FILL BOX_SHADOW | |
# outer yellow box | |
echo -n "${SGR0}${YEL}" | |
fnDRAW_BOX 1 4 18 69 2 0 0 | |
# outer red box | |
echo -n "${SGR0}${RED}" | |
fnDRAW_BOX 0 2 20 73 2 0 0 | |
#---------- | |
# lower blue title/copyright box | |
echo -n "${SGR0}${BLU}" | |
fnDRAW_BOX 14 9 4 59 1 0 0 | |
#---------- | |
# title/copyright text | |
tput cup 15 29 | |
echo "${SGR0}${BOLD}${YEL}PISTAR-LASTQSO v${VERSION}${WHI}${SGR0}" | |
tput cuf 21 | |
echo "${SGR0}${BOLD}${BLU}Copyright © 2024 Ken Cormack/KE8DPF${WHI}${SGR0}" | |
tput cuf 17 | |
echo "${SGR0}${BOLD}${BLU}https://github.com/kencormack/pistar-lastqso${WHI}${SGR0}" | |
echo -n "${SGR0}${WHI}${CUR_ON}" | |
#---------- | |
# The 6 mode names in random sequence | |
local SEQUENCE | |
local NUM | |
# shellcheck disable=SC2207 | |
SEQUENCE=($(shuf -i 1-6 -n 6)) | |
for NUM in "${SEQUENCE[@]}" | |
do | |
case "${NUM}" in | |
1) tput cup 11 10 ; echo -n "${BOLD}${WHI}DMR${SGR0}" ;; | |
2) tput cup 11 20 ; echo -n "${BOLD}${WHI}NXDN${SGR0}" ;; | |
3) tput cup 11 31 ; echo -n "${BOLD}${WHI}P25${SGR0}" ;; | |
4) tput cup 11 41 ; echo -n "${BOLD}${WHI}DSTAR${SGR0}" ;; | |
5) tput cup 11 53 ; echo -n "${BOLD}${WHI}YSF${SGR0}" ;; | |
6) tput cup 11 63 ; echo -n "${BOLD}${WHI}M17${SGR0}" ;; | |
*) true ;; | |
esac | |
done | |
#---------- | |
# The 6 mode boxes in random sequence | |
echo -n "${SGR0}${BLU}" | |
# shellcheck disable=SC2207 | |
SEQUENCE=($(shuf -i 1-6 -n 6)) | |
for NUM in "${SEQUENCE[@]}" | |
do | |
case "${NUM}" in | |
1) fnDRAW_BOX 10 8 2 6 1 0 1 ;; | |
2) fnDRAW_BOX 10 18 2 7 1 0 1 ;; | |
3) fnDRAW_BOX 10 29 2 6 1 0 1 ;; | |
4) fnDRAW_BOX 10 39 2 8 1 0 1 ;; | |
5) fnDRAW_BOX 10 51 2 6 1 0 1 ;; | |
6) fnDRAW_BOX 10 61 2 6 1 0 1 ;; | |
*) true ;; | |
esac | |
done | |
return | |
} | |
#----------------------------------- | |
fnCHECK_MODES_ENABLED() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
if [[ ! "${DMR_ENABLED}${YSF_ENABLED}${NXDN_ENABLED}${DSTAR_ENABLED}${P25_ENABLED}${M17_ENABLED}" =~ "1" ]] | |
then | |
#---------- | |
echo -n "${SGR0}${RED}" | |
tput cup 11 18 | |
for ((I=18; I<=59; I++)) | |
do | |
printf "${DBOX_HL}%0.s" | |
done | |
fnDRAW_BOX 8 16 6 45 2 1 1 | |
#---------- | |
tput cup 10 24 | |
echo "${SGR0}${SMUL}${BOLD}${YEL}NO DIGITAL MODES FOUND ENABLED${SGR0}${WHI}" | |
#---------- | |
tput cuf 27 | |
echo "AT LEAST ONE IS REQUIRED" | |
tput cuf 20 | |
echo "PLEASE ENABLE, THEN RE-RUN THIS SCRIPT" | |
tput cup 22 0 | |
echo "${CLR_EL}${CUR_ON}Aborting" >&2 | |
#---------- | |
fnCLOSE_FDS | |
# If debugging and/or profiling was used, show the names of the logs, at exit | |
if [[ "${DEBUG_LOG}" != "/dev/null" ]] || [[ "${PROFILING_LOG}" != "/dev/null" ]] | |
then | |
if [[ "${MY_FONT}" = "5" ]] | |
then | |
DEBUG_PROFILING_LINE=$((LINES - 6)) | |
else | |
DEBUG_PROFILING_LINE=$((LINES - 4)) | |
fi | |
tput cup "${DEBUG_PROFILING_LINE}" 0 | |
fnLARGE_FONT "DEBUGGING: ${DEBUG_LOG}" | |
fnLARGE_FONT "PROFILING: ${PROFILING_LOG}" | |
echo | |
fi | |
#---------- | |
tput cup "${LINES}" 0 | |
echo -e "${TERM_RESET}" | |
stty "${STTY}" | |
reset | |
fnCLEANUP | |
exit | |
fi | |
return | |
} | |
#----------------------------------- | |
fnGOODBYE_BOX() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
echo -n "${SGR0}${BLU}" | |
fnDRAW_BOX 4 15 12 47 2 1 1 | |
tput cup 6 19 | |
echo "${BOLD}${WHI}THANK YOU FOR USING PISTAR-LASTQSO v${VERSION}${SGR0}${DEF}" | |
#---------- | |
tput cup 7 24 | |
ELAPSED="$(fnSECONDS2TIME)" | |
echo -n "${DIM}${CYA}Elapsed: ${ELAPSED}" | |
tput cup 8 31 | |
echo -n "${DIM}${CYA}Log Restarts: " | |
printf %02d "${LOG_RESTARTS}" | |
# Get present value of all the counters | |
. "${COUNT_FILE}" | |
#---------- | |
tput cup 10 23 | |
echo -n "${DIM}${WHI}QSOs Observed This Session: ${SGR0}${YEL}" | |
printf %05d "${QSO_COUNT}" | |
#---------- | |
tput cup 11 22 | |
echo -n "${DIM}${WHI}(DMR ${SGR0}${CYA}" | |
printf %05d "${DMR_COUNT}" | |
tput cup 11 32 | |
echo -n "${DIM}${WHI})${SGR0}" | |
tput cup 11 34 | |
echo -n "${DIM}${WHI}(YSF ${SGR0}${CYA}" | |
printf %05d "${YSF_COUNT}" | |
tput cup 11 44 | |
echo -n "${DIM}${WHI})${SGR0}" | |
tput cup 11 46 | |
echo -n "${DIM}${WHI}(P25 ${SGR0}${CYA}" | |
printf %05d "${P25_COUNT}" | |
tput cup 11 56 | |
echo -n "${DIM}${WHI})${SGR0}" | |
tput cup 12 21 | |
echo -n "${DIM}${WHI}(DSTAR ${SGR0}${CYA}" | |
printf %05d "${DSTAR_COUNT}" | |
tput cup 12 33 | |
echo -n "${DIM}${WHI})${SGR0}" | |
tput cup 12 35 | |
echo -n "${DIM}${WHI}(NXDN ${SGR0}${CYA}" | |
printf %05d "${NXDN_COUNT}" | |
tput cup 12 45 | |
echo -n "${DIM}${WHI})${SGR0}" | |
tput cup 12 47 | |
echo -n "${DIM}${WHI}(M17 ${SGR0}${CYA}" | |
printf %05d "${M17_COUNT}" | |
tput cup 12 57 | |
echo -n "${DIM}${WHI})${SGR0}" | |
#---------- | |
if [[ "${QSO_COUNT}" -gt 0 ]] | |
then | |
if [[ "${KERCHUNK_COUNT}" -gt "${QSO_COUNT}" ]] | |
then | |
KERCHUNK_COUNT="${QSO_COUNT}" | |
fi | |
PERCENTAGE="$(awk -v KERCHUNK_COUNT="${KERCHUNK_COUNT}" -v QSO_COUNT="${QSO_COUNT}" 'BEGIN {print (KERCHUNK_COUNT / QSO_COUNT * 100)}')" | |
fi | |
tput cup 13 24 | |
echo -n "${DIM}${WHI}Includes ${SGR0}${YEL}$(printf %04d "${KERCHUNK_COUNT}")${DIM}${WHI} Kerchunks (${SGR0}${YEL}$(printf %.2f "${PERCENTAGE}")%${DIM}${WHI})${SGR0}" | |
#---------- | |
tput cup 14 17 | |
echo -n "${CYA}Cache (DXCC $(printf %04d "$(grep -c . "${DXCC_CACHE_FILE}")"))" | |
tput cup 14 35 | |
echo -n "${CYA}(GRID $(printf %05d "$(grep -c . "${GRID_CACHE_FILE}")"))" | |
tput cup 14 49 | |
echo -n "${CYA}(FCC $(printf %05d "$(grep -c . "${LICENSE_CACHE_FILE}")"))" | |
#---------- | |
ERROR_COUNT_COLOR="" | |
WARNING_COUNT_COLOR="" | |
if [[ "${ERROR_COUNT}" -gt 0 ]] || [[ "${WARNING_COUNT}" -gt 0 ]] | |
then | |
if [[ "${ERROR_COUNT}" -gt 0 ]] | |
then | |
ERROR_COUNT_COLOR="${SGR0}${YEL}" | |
fi | |
if [[ "${WARNING_COUNT}" -gt 0 ]] | |
then | |
WARNING_COUNT_COLOR="${SGR0}${RED}" | |
fi | |
if [[ "${ERROR_COUNT}" -gt 0 ]] || [[ "${WARNING_COUNT}" -gt 0 ]] | |
then | |
echo -n "${SGR0}${RED}" | |
# Usage: fnDRAW_BOX BOX_TOP BOX_LEFT BOX_HEIGHT BOX_WIDTH BOX_STYLE BOX_FILL BOX_SHADOW | |
fnDRAW_BOX 15 24 3 30 2 1 1 | |
tput cup 16 27 | |
echo -n "${SGR0}${DIM}${WHI}${ERROR_COUNT_COLOR}MMDVM Log ERR MSGS: $(printf %05d "${ERROR_COUNT}")${SGR0}" | |
tput cup 17 27 | |
echo -n "${SGR0}${DIM}${WHI}${WARNING_COUNT_COLOR}MMDVM Log WARNINGS: $(printf %05d "${WARNING_COUNT}")${SGR0}" | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
# Watches the logfile for rotation, and appends "MMDVMHOST-STOP" | |
# to the working log (for the "while read" loop to catch.) | |
fnMMDVMLOG_FUSER() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# This function (exported and spawned later in the script) runs | |
# as a background task, in a seperate process. | |
# See if the MMDVMHost process has let go of the working log. | |
# If so, re-start log monitoring by writing "MMDVMHOST-STOP" to | |
# the log we've been watching. The main "while read" loop | |
# will then spot this and re-start monitoring, with the new log. | |
# If MMDVMHost still has the log open, do nothing, sleep a few | |
# seconds, then check again. | |
while true | |
do | |
# If the parent script goes away, exit this background task | |
if [[ "$(pgrep -f "${BASH} /usr/local/bin/pistar-lastqso")" = "" ]] | |
then | |
exit | |
fi | |
# If MMDVMHost has let go of the log, add a marker for the main script's | |
# "while read" loop to detect. | |
if [[ ! "$(sudo fuser -v "${WORKING_LOG}" 2>&1)" =~ "MMDVMHost" ]] | |
then | |
echo -e "MMDVMHOST-STOP\n" | sudo tee -a "${WORKING_LOG}" | |
sync;sync;sync | |
exit | |
fi | |
# If pistar-lastqso is still running, and MMDVMHost still | |
# has the log open, sleep before checking again. | |
sleep 2 | |
done | |
return | |
} | |
#----------------------------------- | |
fnSEPARATOR() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
echo -e "\r${CLR_EL}${DBL_LINE}${WHI}${SGR0}" | |
return | |
} | |
#----------------------------------- | |
fnLISTENING() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# If DMR is enabled... AND master is Brandmeister... | |
if [[ "$(grep "${GTAB}${DMR_MASTER}${GTAB}" "${DMRHOSTS}")" =~ "BM_" ]] && [[ "${DMR_ENABLED}" -eq 1 ]] | |
then | |
# ...AND the TG file doesn't exist... OR polling is enabled... | |
if [[ ! -f "${BM_TG_FILE}" ]] || [[ "${BM_POLL}" -eq 1 ]] | |
then | |
echo -en "\r${CLR_EL}" | |
echo -n " ${CYA}${BOLD}Polling BM Talkgroups... ${SGR0}${WHI}${CLR_EL}${CLR_ED}" | |
fi | |
fi | |
fnNOSCROLL_ZONE | |
echo -en "\r${CLR_EL}" | |
echo -n " ${GRE}${BOLD}Listening for Traffic... ${SGR0}${WHI}${CLR_EL}${CLR_ED}" | |
TA_DISPLAYED="0" | |
return | |
} | |
#----------------------------------- | |
fnQSO_IN_PROGRESS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
echo -en "\r${CLR_EL}" | |
echo -n " ${YEL}${BOLD}QSO In Progress... ${SGR0}${WHI}${CLR_EL}${CLR_ED}" | |
TA_DISPLAYED="0" | |
return | |
} | |
#----------------------------------- | |
# This takes the DMR user's callsign and looks up the | |
# city, state, and country fields | |
fnDMR_CSV_SEARCH() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# Fields in the file: | |
# RADIO_ID,CALLSIGN,FIRST_NAME,LAST_NAME,CITY,STATE,COUNTRY | |
# We want the last three. | |
CSV_RECORD="$(grep ",${CSV_CALLSIGN}," "${USERCSV}" | LC_ALL=C sort -u | head -1)" | |
# I've seen someone using the Malaysian Country Code of 5020386 as their | |
# DMR ID, so the log contained only that number (not a callsign), thus | |
# the name lookup based on the callsign failed, displaying the "From:" as | |
# simply "5020386 ()". This also resulted in City/State/Country lookup | |
# returning "n/a, Ontario, Canada" which was wierd. Anyway, this makes | |
# sure that the absence of a callsign doesn't return bad location data, | |
# by making a second pass through the user.csv file, this time looking | |
# for the DMR ID number, rather than the callsign. | |
if [[ "${CSV_RECORD}" = "" ]] | |
then | |
CSV_RECORD="$(grep "^${CSV_CALLSIGN}," "${USERCSV}" | LC_ALL=C sort -u | head -1)" | |
fi | |
if [[ "${CSV_RECORD}" != "" ]] | |
then | |
IFS=','; CUT_ARRAY=(${CSV_RECORD}); unset IFS; | |
CITY="${CUT_ARRAY[4]}" | |
STATE="${CUT_ARRAY[5]}" | |
COUNTRY="${CUT_ARRAY[6]}" | |
unset CUT_ARRAY | |
else | |
CITY="" | |
STATE="" | |
COUNTRY="" | |
fi | |
# If any fields came back empty, set a default of "n/a" (not available) | |
CITY="${CITY:-'n/a'}" | |
STATE="${STATE:-'n/a'}" | |
COUNTRY="${COUNTRY:-'n/a'}" | |
#---------- | |
if [[ "${COUNTRY}" = "United States" ]] && [[ "${GRID}" = "1" ]] | |
then | |
fnGET_US_GRIDSQUARE "${CSV_CALLSIGN}" | |
else | |
GRIDSQUARE="" | |
fi | |
return | |
} | |
#----------------------------------- | |
fnCONVERT_DATE_TIME() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# Convert the log entry UTC date/timestamps to localized, human-readable form | |
DATE="$(date -d "${LOGREC_DATE} ${LOGREC_TIME} UTC" +"%a %d %b %Y")" | |
if [[ "${MIL_TIME}" -eq 1 ]] | |
then | |
TIME="$(date -d "${LOGREC_DATE} ${LOGREC_TIME} UTC" +"%H:%M:%S %Z")" | |
AMPM=" " | |
else | |
TIME="$(date -d "${LOGREC_DATE} ${LOGREC_TIME} UTC" +"%I:%M:%S %p %Z")" | |
IFS=' '; CUT_ARRAY=(${TIME}); unset IFS; | |
AMPM="${CUT_ARRAY[1]}" | |
unset CUT_ARRAY | |
AMPM="${AMPM:0:1}" | |
fi | |
return | |
} | |
#----------------------------------- | |
fnDMR_GET_FROM() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# The MMDVMHost daemon internally knows the actual DMR ID# of the user, | |
# and does ID number to Callsign lookups from DMRIds.dat. However, in | |
# the logfile, you end up with the Callsign, and lose the ID number. | |
# Disabling this lookup gains you the ID numbers in the logfile, but | |
# loses the Callsigns. If I were to disable said lookups via this script, | |
# pi-star web-based Dashboard users wouldn't appreciate that. So, I work | |
# backwords, taking the Callsign that MMDVMHost writes to the logs, | |
# and looking up the Callsign in the DMRIds.dat file, to find the ID. | |
# The problem is that in the early days of DMR, some users were registering | |
# multiple IDs (one for their radio, one for each hotspot, etc.) That | |
# practice has been halted, with the prefered method of identifying | |
# hotspots by adding a two-digit suffix to the user's normal 7-digit ID. | |
# However, those earlier cases of multiple ID numbers mapped to the same | |
# Callsign still exist in the file. Each DMR ID links to only one Callsign, | |
# but not every Callsign links to just one ID. Some Callsigns "hit" more | |
# than once in the file. With no guarantee that I'm picking the correct ID | |
# linked to a given Callsign, I just pick the first number, on the | |
# assumption that that's the one that represents the user, not one of their | |
# hotspot. I wish MMDVMHost would log both Callsign and ID#. But it's one | |
# or the other, not both. For the vast majority of cases, my "lesser of | |
# two evils" approach will serve well enough. | |
# | |
# See if the sender is a callsign... | |
read -r FROM_ID FROM_NAME <<< "$(grep "${GTAB}${FROM}${GTAB}" "${DMRIDS}" | awk '{ print $1 " " $NF }' | head -1)" | |
if [[ "${FROM_NAME}" = "" ]] | |
then | |
# If not, then see if it is a talkgroup | |
FROM_NAME="$(grep "^${FROM};" "${TGLIST}")" | |
IFS=';'; CUT_ARRAY=(${FROM_NAME}); unset IFS; | |
FROM_NAME="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
# If that came back empty, see if it's in MY_LIST.txt | |
if [[ "${FROM_NAME}" = "" ]] && [[ -f "${MY_LIST}" ]] | |
then | |
FROM_NAME="$(grep "^${FROM};" "${MY_LIST}")" | |
IFS=';'; CUT_ARRAY=(${FROM_NAME}); unset IFS; | |
FROM_NAME="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
fi | |
else | |
if [[ -f "${USERCSV}" ]] | |
then | |
# If the recipient is a callsign, not a talkgroup, lookup | |
# further info about that callsign in the user.csv file | |
CSV_CALLSIGN="${FROM}" | |
fnDMR_CSV_SEARCH | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnDMR_GET_TO() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# See if the recipient is a callsign | |
read -r TO_ID TO_NAME <<< "$(grep "${GTAB}${TO}${GTAB}" "${DMRIDS}" | awk '{ print $1 " " $NF }' | head -1)" | |
# If not, see if it is a talkgroup | |
if [[ "${TO_NAME}" = "" ]] | |
then | |
TO_NAME="$(grep "^${TO};" "${TGLIST}")" | |
IFS=';'; CUT_ARRAY=(${TO_NAME}); unset IFS; | |
TO_NAME="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
# Some talkgroups do not have names given, in the TGList_BM.txt | |
# file that pi-star updates automatically. These next few lines | |
# allow the user to create a second lookup file in which they | |
# can list talkgroups they use, that are not named in pi-star's | |
# TGList_BM.txt file. | |
# | |
# This suplemental file is /usr/local/etc/MY_LIST.txt, and entries | |
# in that file must be in the same format as the TGList_BM.txt file. | |
# DO NOT INCLUDE SPACES. Since pi-star will not overwrite your | |
# MY_LIST.txt file, the info you list there will survive pi-star's | |
# automated updates. | |
# | |
# Exactly as with the TGList_BM.txt file, the fields in MY_LIST.txt | |
# include: | |
# Dest ID;Option;Name;Description | |
# Option: TG:0, REF:1, PC:2 | |
# Examples: | |
# 2627;0;BADEN-WUERTTEMBERG;TG2627 | |
# 2629;0;SACHSEN/THUERINGEN;TG2629 | |
# 26232;0;DREILAENDERECK-MITTE-DEUTSCHLAND;TG26232 | |
# 26274;0;BW-BOEBLINGEN;TG26274 | |
# 26283;0;REGION-MUENCHEN;TG26283 | |
# 26287;0;ALLGAEU-BODENSEE;TG26287 | |
# 26298;0;THUERINGEN;TG26298 | |
# | |
# You can also add certain 'private' IDs here that | |
# may not be listed. Example: | |
# 262993;0;GPS-WX;PC262993 | |
# 310999;0;APRS;PC310999 | |
# 9999995;0;PISTAR-HOSTFILES;PC9999995 | |
# 9999996;0;PISTAR-SHUTDOWN;PC9999996 | |
# 9999997;0;PISTAR-REBOOT;PC9999997 | |
# 9999998;0;PISTAR-SVC-RESTART;PC9999998 | |
# 9999999;0;PISTAR-SVC-KILL;PC9999999 | |
# | |
if [[ "${TO_NAME}" = "" ]] && [[ -f "${MY_LIST}" ]] | |
then | |
TO_NAME="$(grep "^${TO};" "${MY_LIST}")" | |
IFS=';'; CUT_ARRAY=(${TO_NAME}); unset IFS; | |
TO_NAME="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
fi | |
else | |
# If the recipient is a callsign, not a talkgroup, lookup | |
# further info about that callsign in the user.csv file | |
if [[ -f "${USERCSV}" ]] | |
then | |
CSV_CALLSIGN="${TO}" | |
fnDMR_CSV_SEARCH | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnBANNER() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# If -n|--nobig was used, get out of here. | |
if [[ "${USE_BANNER}" = "0" ]] | |
then | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_MODE}" = "D-Star" ]] | |
then | |
FROM_OR_SENDER="${SENDER}" | |
else | |
FROM_OR_SENDER="${FROM}" | |
fi | |
if [[ "${LOGREC_MODE}" = "M17" ]] && [[ "${M17_HOST_SUFFIX}" != "" ]] | |
then | |
FROM_OR_SENDER="${FROM}_${M17_HOST_SUFFIX}" | |
else | |
FROM_OR_SENDER="${FROM}" | |
fi | |
#---------- | |
# NO_WRAP is set with the "-w|--wrap" cmdline option. | |
if [[ "${NO_WRAP}" = "1" ]] | |
then | |
OUTPUT_WIDTH="1000" | |
else | |
OUTPUT_WIDTH="${COLUMNS}" | |
fi | |
#---------- | |
# This deals with the alternate charset font only... | |
if [[ "${MY_FONT}" = "5" ]] || [[ "${MY_FONT}" = "6" ]] | |
then | |
# Fonts #5 and 6 are not figlet fonts. | |
# Print what we need, then get out. | |
fnLARGE_FONT "${BOLD}${CONTACT_COLOR}${FROM_OR_SENDER} ... ${TO}" | |
return | |
fi | |
#---------- | |
# And this deals with figlet fonts... | |
# Disable the large font display callsign to save screen space, | |
# by passing the "--nobig" parameter to this script: | |
# pistar-lastqso -n|--nobig | |
case "${MY_FONT}" in | |
1) | |
FONT="small" | |
;; | |
2) | |
FONT="standard" | |
;; | |
3) | |
FONT="big" | |
;; | |
4) | |
FONT="ansi_shadow" | |
;; | |
*) | |
# A screen width of less than 80 columns will select figlet's | |
# "small" font. A screen width greater than 80 columns will | |
# use figlet's larger "standard" font. A screen width greater | |
# than 120 columns will use figlet's "big" font. | |
FONT="small" | |
if [[ "${COLUMNS}" -gt 80 ]] | |
then | |
FONT="standard" | |
fi | |
if [[ "${COLUMNS}" -gt 120 ]] | |
then | |
FONT="big" | |
fi | |
;; | |
esac | |
#---------- | |
BANNER="$(figlet -w "${OUTPUT_WIDTH}" -f "${FONT}" "${FROM_OR_SENDER} ... ${TO}")" | |
echo "${BOLD}${CONTACT_COLOR}${BANNER}${SGR0}" | |
#---------- | |
# Tighten up the leading with these two fonts. | |
# This moves the cursor up one line to recover | |
# an extra blank line that follows these fonts. | |
case "${FONT}" in | |
"big"|"ansi_shadow") | |
echo -n "${CUR_UP}" | |
;; | |
*) | |
: | |
;; | |
esac | |
echo -n "${CLR_ED}" | |
return | |
} | |
#----------------------------------- | |
fnYSF_END_NETWORK_TRAFFIC() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Fields to pass to the history... | |
BER="${BER%\%*}" | |
LOSS="${LOSS%\%*}" | |
fnCOLORIZE_BER | |
fnCOLORIZE_LOSS | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}%${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Loss:${SGR0} ${LOSS_COLOR}${BOLD}${LOSS}%${SGR0}${WHI}\r" | |
#---------- | |
if [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
} | |
#----------------------------------- | |
fnDSTAR_END_NETWORK_TRAFFIC() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
fnCOLORIZE_BER | |
fnCOLORIZE_LOSS | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}%${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Loss:${SGR0} ${LOSS_COLOR}${BOLD}${LOSS}%${SGR0}${WHI}\r" | |
#---------- | |
if [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
} | |
#----------------------------------- | |
fnM17_END_NETWORK_TRAFFIC() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Fields to pass to the history... | |
BER="n/a" | |
LOSS="n/a" | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
#---------- | |
if [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
} | |
#----------------------------------- | |
fnNXDN_END_NETWORK_TRAFFIC() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Fields to pass to the history... | |
BER="n/a" | |
LOSS="n/a" | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
#---------- | |
if [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
} | |
#----------------------------------- | |
fnP25_END_NETWORK_TRAFFIC() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Fields to pass to the history... | |
BER="n/a" | |
fnCOLORIZE_LOSS | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
tput cuf 32 | |
echo -en "${DIM}Loss:${SGR0} ${LOSS_COLOR}${BOLD}${LOSS}%${SGR0}${WHI}\r" | |
#---------- | |
if [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
} | |
#----------------------------------- | |
fnSHOW_CURRENT_CALL() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Show our human-readable date/time | |
echo -en "${SGR0}${DIM}${WHI}Date:${SGR0} ${BLU}${BOLD}${DATE}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Time:${SGR0} ${BLU}${BOLD}${TIME}${SGR0}${WHI}\r" | |
tput cuf 57 | |
echo -n "${DIM}QSOs Watched:${SGR0} ${BLU}${BOLD}" | |
printf %05d "${QSO_COUNT}" | |
echo "${SGR0}${WHI}${CLR_ED}" | |
#---------- | |
# For this, and the QTH lookups further down, we groom | |
# the incoming ${FROM}, to isolate just the callsign. | |
# For example, many YSF users will send things like | |
# "KE8DPF-KEN" or "KE8DPF/KEN". So we isolate just | |
# the "KE8DPF" callsign, to perform our lookups. | |
SEARCH_FROM="$(fnCHOP_CALL "${FROM}")" | |
# Search the CSV files for the "From" name | |
if ! [[ "${SEARCH_FROM}" =~ ^[0-9]+$ ]] | |
then | |
# the "From" contains letters (a callsign) | |
FROM_NAME="$(grep ",${SEARCH_FROM}," "${NXDNCSV}" "${USERCSV}" | tail -1)" | |
else | |
# the "From" is numeric only (not a callsign) | |
FROM_NAME="$(grep "^${SEARCH_FROM}," "${NXDNCSV}" "${USERCSV}" | tail -1)" | |
fi | |
IFS=','; CUT_ARRAY=(${FROM_NAME}); unset IFS; | |
FROM_NAME="${CUT_ARRAY[2]}" | |
unset CUT_ARRAY | |
#---------- | |
# Show the From (and From_Name, if found above) | |
if [[ "${LOGREC_MODE}" = "D-Star" ]] | |
then | |
FROM_OR_SENDER="${SENDER}" | |
else | |
FROM_OR_SENDER="${FROM}" | |
fi | |
if [[ -n "${FROM_NAME}" ]] | |
then | |
P_FROM_NAME="$(printf "%-15s" "(${FROM_NAME})")" | |
echo -en "${DIM}From:${SGR0} ${BOLD}${FROM_OR_SENDER} ${P_FROM_NAME}${SGR0}\r" | |
else | |
echo -en "${DIM}From:${SGR0} ${BOLD}${FROM_OR_SENDER}${SGR0}\r" | |
fi | |
#---------- | |
# Increment the counter | |
. "${COUNT_FILE}" | |
case "${LOGREC_MODE}" in | |
"DMR") | |
((DMR_COUNT++)) | |
;; | |
"YSF") | |
((YSF_COUNT++)) | |
;; | |
"NXDN") | |
((NXDN_COUNT++)) | |
;; | |
"D-Star") | |
((DSTAR_COUNT++)) | |
;; | |
"P25") | |
((P25_COUNT++)) | |
;; | |
"M17") | |
((M17_COUNT++)) | |
;; | |
*) | |
: | |
;; | |
esac | |
fnWRITE_COUNTER_FILE | |
#---------- | |
# Show the To | |
tput cuf 31 | |
if [[ -n "${TO}" ]] | |
then | |
echo -n "${DIM} To..:${SGR0} ${BOLD}${TO}" | |
fi | |
# If D-Star, add any reflector logged as "via" | |
if [[ "${LOGREC_MODE}" = "D-Star" ]] && [[ -n "${DSTAR_REFLECTOR}" ]] | |
then | |
echo -n " via ${DSTAR_REFLECTOR}" | |
DSTAR_REFLECTOR="" | |
fi | |
echo "${SGR0}${WHI}${CLR_ED}" | |
#---------- | |
# Show the Mode | |
echo -en "${DIM}Mode:${SGR0} ${BOLD}${CYA}${LOGREC_MODE}${SGR0}${WHI}\r" | |
# Show either NET or RF | |
tput cuf 32 | |
echo "${DIM}Src.:${SGR0} ${BOLD}${CYA}${SOURCE}${SGR0}${WHI}${CLR_ED}" | |
#---------- | |
CSV_RECORD="" | |
# Lookup the QTH data for the callsign. | |
# Try the DMR user.csv file first... | |
if [[ "${LOGREC_MODE}" != "NXDN" ]] | |
then | |
QTH_SRC="user.csv" | |
# Search the CSV files for the "From" name | |
if ! [[ "${SEARCH_FROM}" =~ ^[0-9]+$ ]] | |
then | |
# the "From" contains letters (a callsign) | |
CSV_RECORD="$(grep ",${SEARCH_FROM}," "${USERCSV}" | LC_ALL=C sort -u | head -1)" | |
else | |
# the "From" is numeric only (not a callsign) | |
CSV_RECORD="$(grep "^${SEARCH_FROM}," "${USERCSV}" | LC_ALL=C sort -u | head -1)" | |
fi | |
fi | |
if [[ "${LOGREC_MODE}" != "DMR" ]] | |
then | |
# If nothing found, try the NXDN.csv file... | |
if [[ "${CSV_RECORD}" = "" ]] | |
then | |
QTH_SRC="NXDN.csv" | |
# Search the CSV files for the "From" name | |
if ! [[ "${SEARCH_FROM}" =~ ^[0-9]+$ ]] | |
then | |
# the "From" contains letters (a callsign) | |
CSV_RECORD="$(grep ",${SEARCH_FROM}," "${NXDNCSV}" | LC_ALL=C sort -u | head -1)" | |
else | |
# the "From" is numeric only (not a callsign) | |
CSV_RECORD="$(grep "^${SEARCH_FROM}," "${NXDNCSV}" | LC_ALL=C sort -u | head -1)" | |
fi | |
fi | |
fi | |
#---------- | |
CITY="" | |
STATE="" | |
COUNTRY="" | |
# If we have a hit from either file, set the CITY, STATE, and COUNTRY | |
if [[ "${CSV_RECORD}" != "" ]] | |
then | |
# Callsign was found in one or the other csv file | |
IFS=','; CUT_ARRAY=(${CSV_RECORD}); unset IFS; | |
CITY="${CUT_ARRAY[4]}" | |
STATE="${CUT_ARRAY[5]}" | |
COUNTRY="${CUT_ARRAY[6]}" | |
unset CUT_ARRAY | |
else | |
# Callsign was not found in either csv file | |
# Last resort: Look up country based on callsign prefix | |
# But skip this lookup, if disabled on command line. | |
if [[ "${USE_DXCC}" = "1" ]] | |
then | |
# At least one numeric digit must appear in the first few characters | |
# of the callsign to have a chance of being valid. Yeah, I'm looking | |
# at users that come across as "AMERIC" (americalink). Likewise, | |
# we can't look up something that is digits-only. So look for BOTH | |
# alpha and digit chars in the first few chars of the From. Otherwise, | |
# don't waste resources looking it up (and knowing it will fail). | |
if [[ "${SEARCH_FROM:0:4}" == *[[:digit:]]* ]] && [[ "${SEARCH_FROM:0:4}" == *[[:alpha:]]* ]] | |
then | |
QTH_SRC="cty.dat" | |
# NOTE: The average time to resolve, calling dxcc.pl, | |
# is 4-5 seconds on a Raspberry Pi Zero. Calling | |
# the script creates a lot of overhead. | |
# | |
# Callsigns found in either user.csv or ndxn.csv | |
# can be resolved (and w/ greater detail) in fractions | |
# of a second, which is why dxcc.pl is my last resort | |
# to determine at least *some* QTH information. | |
# | |
# However, if I have to use dxcc.pl to resolve, I then | |
# cache the result so that next time that callsign | |
# appears, the country can be parsed from the cache | |
# to save time... A LOT of time! | |
# Search the cache. | |
DXCC_CACHE_RECORD="$(grep "^${SEARCH_FROM}:" "${DXCC_CACHE_FILE}")" | |
if [[ -n "${DXCC_CACHE_RECORD}" ]] | |
then | |
# The cache gave us the country. | |
COUNTRY="${DXCC_CACHE_RECORD#*:}" | |
COUNTRY="${COUNTRY/ of America/}" | |
DXCC_TIME="Cached" | |
else | |
# The cache search came up empty. Call on dxcc.pl. | |
# Capture both the "Country" resolved by dxcc.pl, AND the time it took to resolve. | |
# This technique captures both STDOUT and STDERR, saving them both in variables. | |
# With "TIMEFORMAT" defined earlier as "%R", the "time" command gives only the | |
# "real" time, in seconds. | |
{ | |
IFS=$'\n' read -r -d '' CAPTURED_STDERR; | |
IFS=$'\n' read -r -d '' CAPTURED_STDOUT; | |
} < <( (printf '\0%s\0' "$(time dxcc.pl "${SEARCH_FROM}" | grep "^Country Name:" | awk '{ print substr($0, index($0,$3)) }')" 1>&2) 2>&1) | |
# ^--^--- These spaces are important for proper operation, for | |
# linting/beautifying, and for syntax highlighting in "vim". | |
COUNTRY="${CAPTURED_STDOUT}" | |
COUNTRY="${COUNTRY/ of America/}" | |
DXCC_TIME="${CAPTURED_STDERR} secs" | |
if [[ -n "${COUNTRY}" ]] | |
then | |
# Store the from:country in the cache. | |
echo "${SEARCH_FROM}:${COUNTRY}" >> "${DXCC_CACHE_FILE}" | |
fi | |
fi | |
fi | |
fi | |
fi | |
# If any fields came back empty, set a default of "n/a" (not available) | |
CITY="${CITY:-'n/a'}" | |
STATE="${STATE:-'n/a'}" | |
COUNTRY="${COUNTRY:-'n/a'}" | |
#---------- | |
if [[ "${COUNTRY}" = "United States" ]] && [[ "${GRID}" = "1" ]] | |
then | |
fnGET_US_GRIDSQUARE "${SEARCH_FROM}" | |
else | |
GRIDSQUARE="" | |
fi | |
#---------- | |
# Show location details. | |
# If all the fields came back empty... | |
if [[ "${CITY}" = "n/a" ]] && [[ "${STATE}" = "n/a" ]] && [[ "${COUNTRY}" = "n/a" ]] | |
then | |
# If dxcc searches are enabled (default)... | |
if [[ "${USE_DXCC}" = "1" ]] | |
then | |
# Let the user know we came up empty, despite all the searches | |
echo -en "${DIM}File:${SGR0} ${MAG}${BOLD}none${SGR0}${WHI}\r" | |
tput cuf 18 | |
echo "${DIM}${WHI}QTH.: ${SGR0}${BOLD}${MAG}Unable to determine ${FROM}'s location${SGR0}${WHI}${CLR_ED}" | |
else | |
# Otherwise, remind the user that dxcc searches are disabled. | |
echo "${DIM}${WHI}QTH.: ${SGR0}${BOLD}${MAG}DXCC Lookups Disabled on Cmdline${SGR0}${WHI}${CLR_ED}" | |
fi | |
else | |
# If we got this far, we at least got some kind of results, | |
# either from the CSV searches, or from dxcc. | |
# Display those results for the user. | |
echo -en "${DIM}File:${SGR0} ${MAG}${BOLD}${QTH_SRC}${SGR0}${WHI}\r" | |
tput cuf 18 | |
if [[ "${QTH_SRC}" = "cty.dat" ]] | |
then | |
echo "${DIM}QTH:${SGR0} ${BOLD}${MAG}${COUNTRY}${GRIDSQUARE} (DXCC: ${DXCC_TIME})${SGR0}${WHI}${CLR_ED}" | |
else | |
echo "${DIM}QTH:${SGR0} ${BOLD}${MAG}${CITY}, ${STATE}, ${COUNTRY}${GRIDSQUARE}${SGR0}${WHI}${CLR_ED}" | |
fi | |
fi | |
#---------- | |
if [[ "${COUNTRY}" = "United States" ]] && [[ "${FCC}" = "1" ]] | |
then | |
fnGET_US_LICENSE "${SEARCH_FROM}" | |
if [[ -n "${LICENSE}" ]] | |
then | |
if [[ "${LICENSE}" = "CLUB" ]] | |
then | |
echo "${DIM}FCC :${SGR0} ${BOLD}${MAG}${LICENSE} callsign${SGR0}${WHI}${CLR_ED}" | |
else | |
echo "${DIM}FCC :${SGR0} ${BOLD}${MAG}${LICENSE} class license${SGR0}${WHI}${CLR_ED}" | |
fi | |
fi | |
else | |
LICENSE="" | |
fi | |
fnQSO_IN_PROGRESS | |
return | |
} | |
#----------------------------------- | |
fnDMR_SHOW_CURRENT_CALL() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# Show our human-readable date/time | |
echo -en "${SGR0}${DIM}${WHI}Date:${SGR0} ${BLU}${BOLD}${DATE}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Time:${SGR0} ${BLU}${BOLD}${TIME}${SGR0}${WHI}\r" | |
tput cuf 57 | |
echo -n "${DIM}QSOs Watched:${SGR0} ${BLU}${BOLD}${CLR_ED}" | |
printf %05d "${QSO_COUNT}" | |
echo "${SGR0}${WHI}${CLR_ED}" | |
#---------- | |
# Show formatted to and from names | |
P_FROM_NAME="$(printf "%-15s" "(${FROM_NAME})")" | |
P_TO_NAME="$(printf "%-15s" "(${TO_NAME})")" | |
echo -en "${DIM}From:${SGR0} ${BOLD}${FROM} ${P_FROM_NAME}${SGR0}\r" | |
. "${COUNT_FILE}" | |
((DMR_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 31 | |
echo "${DIM} To..:${SGR0} ${BOLD}${TO} ${P_TO_NAME}${SGR0}${WHI}${CLR_ED}" | |
#---------- | |
# Show DMR ID# (Best guess - see comment in function fnDMR_GET_FROM) | |
if [[ -n "${FROM_ID}" ]] | |
then | |
DMR_ID="${FROM_ID}" | |
else | |
DMR_ID="${TO_ID}" | |
fi | |
#---------- | |
# Ok, we found the Callsign in the DMRIds.dat file. Let's see | |
# if the Callsign is associated with multiple DMR ID #s. | |
if [[ "$(grep -c "${GTAB}${FROM}${GTAB}" "${DMRIDS}")" -gt 1 ]] | |
then | |
DMR_ID="${DMR_ID}(*)" | |
fi | |
echo -en "${DIM}ID #:${SGR0} ${MAG}${BOLD}${DMR_ID}${SGR0}${WHI}\r" | |
tput cuf 18 | |
#---------- | |
# Show location details from the user.csv file | |
if [[ "${CITY}" = "n/a" ]] && [[ "${STATE}" = "n/a" ]] && [[ "${COUNTRY}" = "n/a" ]] | |
then | |
echo "${MAG}${BOLD}No Location Data Found${SGR0}${WHI}${CLR_ED}" | |
else | |
if echo "${CITY}${STATE}${COUNTRY}" | grep -q "[a-zA-Z]" | |
then | |
echo "${DIM}QTH:${SGR0} ${MAG}${BOLD}${CITY}, ${STATE}, ${COUNTRY}${GRIDSQUARE}${SGR0}${WHI}${CLR_ED}" | |
fi | |
fi | |
#---------- | |
if [[ "${COUNTRY}" = "United States" ]] && [[ "${FCC}" = "1" ]] | |
then | |
fnGET_US_LICENSE "${FROM}" | |
if [[ -n "${LICENSE}" ]] | |
then | |
echo "${DIM}FCC :${SGR0} ${BOLD}${MAG}${LICENSE} class license${SGR0}${WHI}${CLR_ED}" | |
fi | |
else | |
LICENSE="" | |
fi | |
#---------- | |
# show the time slot, source (RF or network), and type (voice or data) | |
echo -en "${DIM}Mode:${SGR0} ${CYA}${BOLD}DMR TS${TIME_SLOT}${SGR0}${WHI}\r" | |
tput cuf 18 | |
echo -e "${DIM}Src:${SGR0} ${CYA}${BOLD}${SOURCE}${SGR0}${WHI}\t${DIM}Type:${SGR0} ${BOLD}${TYPE}${SGR0}${WHI}${CLR_ED}" | |
fnQSO_IN_PROGRESS | |
return | |
} | |
#----------------------------------- | |
fnCOLORIZE_BER() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# Default to Good/Green | |
BER_COLOR="${GRE}" | |
if [[ -n "$(echo "${BER}" | awk '$1 >=2.0{print}')" ]] && [[ -n "$(echo "${BER}" | awk '$1 <=4.9{print}')" ]] | |
then | |
# Fair | |
BER_COLOR="${YEL}" | |
else | |
if [[ -n "$(echo "${BER}" | awk '$1 >=5.0{print}')" ]] | |
then | |
# Poor | |
BER_COLOR="${RED}" | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnCOLORIZE_LOSS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# Default to Good/Green | |
LOSS_COLOR="${GRE}" | |
if [[ -n "$(echo "${LOSS}" | awk '$1 >1.0{print}')" ]] && [[ -n "$(echo "${LOSS}" | awk '$1 <3.0{print}')" ]] | |
then | |
# Fair | |
LOSS_COLOR="${YEL}" | |
else | |
if [[ -n "$(echo "${LOSS}" | awk '$1 >=3.0{print}')" ]] | |
then | |
# Poor | |
LOSS_COLOR="${RED}" | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnCOLORIZE_RSSI() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# What follows converts RSSI dBm to S-Meter values. | |
# We look for S9 through S1 signal strength... | |
# Very Strong | |
SMETER_COLOR="${GRE}" | |
if [[ "${DBM}" -lt 93 ]] | |
then | |
# If stronger than S9, calculate how much over | |
SMETER="S9+$((93-DBM))dB" | |
else | |
# Good | |
if [[ "${DBM}" -eq 93 ]] | |
then | |
SMETER="S9" | |
else | |
if [[ "${DBM}" -ge 99 ]] && [[ "${DBM}" -lt 93 ]] | |
then | |
SMETER="S8" | |
else | |
if [[ "${DBM}" -ge 85 ]] && [[ "${DBM}" -lt 105 ]] | |
then | |
SMETER="S7" | |
else | |
# Fair | |
SMETER_COLOR="${YEL}" | |
if [[ "${DBM}" -ge 105 ]] && [[ "${DBM}" -lt 111 ]] | |
then | |
SMETER="S6" | |
else | |
if [[ "${DBM}" -ge 111 ]] && [[ "${DBM}" -lt 117 ]] | |
then | |
SMETER="S5" | |
else | |
if [[ "${DBM}" -ge 117 ]] && [[ "${DBM}" -lt 123 ]] | |
then | |
SMETER="S4" | |
else | |
if [[ "${DBM}" -ge 123 ]] && [[ "${DBM}" -lt 129 ]] | |
then | |
SMETER="S3" | |
else | |
# Poor | |
SMETER_COLOR="${RED}" | |
if [[ "${DBM}" -ge 129 ]] && [[ "${DBM}" -lt 135 ]] | |
then | |
SMETER="S2" | |
else | |
if [[ "${DBM}" -ge 135 ]] && [[ "${DBM}" -lt 141 ]] | |
then | |
SMETER="S1" | |
else | |
# Things are REALLY bad, if you can't even reach S1 signal strength | |
if [[ "${DBM}" -ge 141 ]] | |
then | |
SMETER="S0" | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnDOWNLOAD_NXDNCSV() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# If the NXDN.csv file is missing, or is older than 7 days, | |
# then download a new one from the web. | |
if [[ ! -f "${NXDNCSV}" ]] || [[ -n "$(find "${NXDNCSV}" -mtime +7)" ]] || [[ "${GET_CSV_NOW}" -eq 1 ]] | |
then | |
echo -e "\nDownloading latest NXDN.csv file..." | |
sudo mount -o remount,rw / 2>/dev/null | |
if sudo wget -t 1 -q --show-progress --progress=bar:force:noscroll https://database.radioid.net/static/nxdn.csv -O "${NXDNCSV}.tmp" | |
then | |
# Check for a too-small (possibly corrupt) file | |
# Make sure it's at least ~100K in size | |
if [[ "$(sudo stat --format=%s "${NXDNCSV}.tmp")" -lt 100000 ]] | |
then | |
echo -e "\nThe download appears corrupt. Will continue without it.\n" >&2 | |
sleep 2 | |
else | |
echo "Applying the updated file..." | |
if sudo mv "${NXDNCSV}.tmp" "${NXDNCSV}" && sudo chown pi-star.pi-star "${NXDNCSV}" | |
then | |
echo "Update complete." | |
else | |
echo "There was a problem applying the update. Will continue without it." >&2 | |
sleep 2 | |
fi | |
fi | |
else | |
echo "Unable to retrieve latest NXDN.csv. Will continue without it." >&2 | |
sleep 2 | |
fi | |
sync;sync;sync | |
sudo mount -o remount,ro / 2>/dev/null | |
fi | |
return | |
} | |
#----------------------------------- | |
fnDOWNLOAD_USERCSV() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# If the user.csv file is missing, or is older than 7 days, | |
# then download a new one from the web. | |
if [[ ! -f "${USERCSV}" ]] || [[ -n "$(find "${USERCSV}" -mtime +7)" ]] || [[ "${GET_CSV_NOW}" -eq 1 ]] | |
then | |
echo -e "\nDownloading latest DMR user.csv file..." | |
sudo mount -o remount,rw / 2>/dev/null | |
if sudo wget -t 1 -q --show-progress --progress=bar:force:noscroll https://database.radioid.net/static/user.csv -O "${USERCSV}.tmp" | |
then | |
# Check for a too-small (possibly corrupt) file | |
# Make sure it's at least ~10M in size | |
if [[ "$(sudo stat --format=%s "${USERCSV}.tmp")" -lt 10000000 ]] | |
then | |
echo -e "\nThe download appears corrupt. Will continue without it.\n" >&2 | |
sleep 2 | |
else | |
echo "Applying the updated file..." | |
if sudo mv "${USERCSV}.tmp" "${USERCSV}" && sudo chown pi-star.pi-star "${USERCSV}" | |
then | |
echo "Update complete." | |
else | |
echo "There was a problem applying the update. Will continue without it." >&2 | |
sleep 2 | |
fi | |
fi | |
else | |
echo "Unable to retrieve latest user.csv. Will continue without it." >&2 | |
sleep 2 | |
fi | |
sync;sync;sync | |
sudo mount -o remount,ro / 2>/dev/null | |
fi | |
return | |
} | |
#----------------------------------- | |
fnDOWNLOAD_CTYDAT() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
# If the cty.dat file is missing, or is older than 30 days, | |
# then download a new one from the web. | |
if [[ ! -f "${CTYDAT}" ]] || [[ -n "$(find "${CTYDAT}" -mtime +30)" ]] || [[ "${GET_DAT_NOW}" -eq 1 ]] | |
then | |
echo -e "\nDownloading latest DXCC cty.dat file..." | |
sudo mount -o remount,rw / 2>/dev/null | |
BIGCTYZIP="$(curl -s --fail --max-time 2 "https://www.country-files.com/category/big-cty/" | grep "\[download\]" | LC_ALL=C sort -nr | head -1 | cut -f2 -d"\"")" | |
if [[ -n "${BIGCTYZIP}" ]] | |
then | |
mkdir /tmp/bigctydir 2>/dev/null | |
if sudo wget -t 1 -q --show-progress --progress=bar:force:noscroll "${BIGCTYZIP}" -O "/tmp/bigctydir/bigcty.zip" | |
then | |
# Check for a too-small (possibly corrupt) file | |
# Make sure it's at least ~10K in size | |
if [[ "$(sudo stat --format=%s "/tmp/bigctydir/bigcty.zip")" -lt 10000 ]] | |
then | |
echo -e "\nThe download appears corrupt. Will continue without it.\n" >&2 | |
sleep 2 | |
else | |
echo "Applying the updated file..." | |
sudo unzip /tmp/bigctydir/bigcty.zip -d /tmp/bigctydir > /dev/null 2>&1 | |
if sudo cp /tmp/bigctydir/cty.dat "${CTYDAT}" && sudo chown pi-star.pi-star "${CTYDAT}" | |
then | |
sudo rm -r /tmp/bigctydir 2>/dev/null | |
echo "Update complete." | |
else | |
echo "There was a problem applying the update. Will continue without it." >&2 | |
sleep 2 | |
fi | |
fi | |
else | |
echo "Unable to retrieve latest cty.dat. Will continue without it." >&2 | |
sleep 2 | |
fi | |
sync;sync;sync | |
sudo mount -o remount,ro / 2>/dev/null | |
else | |
echo "Unable to determine latest available cty.dat. Will continue without it." >&2 | |
sleep 2 | |
fi | |
fi | |
return | |
} | |
#----------------------------------- | |
fnBUILD_HISTORY() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
if [[ -n "${QSO_START_TIME}" ]] && [[ -n "${FROM}" ]] && [[ -n "${TO}" ]] | |
then | |
if [[ -n "${LOSS}" ]] || [[ -n "${BER}" ]] || [[ "${DATA}" -eq 1 ]] | |
then | |
#---------- | |
if [[ "${LOGREC_MODE}" = "D-Star" ]] | |
then | |
FROM_OR_SENDER="${SENDER}" | |
else | |
FROM_OR_SENDER="${FROM}" | |
fi | |
if [[ "${LOGREC_MODE}" = "M17" ]] && [[ "${M17_HOST_SUFFIX}" != "" ]] | |
then | |
FROM_OR_SENDER="${FROM}_${M17_HOST_SUFFIX}" | |
else | |
FROM_OR_SENDER="${FROM}" | |
M17_HOST_SUFFIX="" | |
fi | |
FROM_OR_SENDER="${FROM_OR_SENDER:0:11}" | |
#---------- | |
# Load the current history... | |
. "${HISTORY_FILE}" 2>/dev/null | |
#---------- | |
# PUSH CURRENT HISTORY DOWN. THIS DROPS THE OLDEST RECORD, | |
# AND LEAVES THE NEWEST SPOT EMPTY FOR THE RECORD WE ARE | |
# ABOUT TO ADD | |
rm "${HISTORY_FILE}" 2>/dev/null | |
HIST_COUNT="${HIST_MAX}" | |
until [[ "${HIST_COUNT}" -eq 0 ]] | |
do | |
HISTORY[HIST_COUNT]="${HISTORY[$((HIST_COUNT - 1))]}" | |
echo -e "HISTORY[${HIST_COUNT}]=\"${HISTORY[HIST_COUNT]}\"" >> "${HISTORY_FILE}" | |
((HIST_COUNT--)) | |
done | |
#---------- | |
HIST_TIME="$(printf %-8s "$(echo "${QSO_START_TIME}" | awk '{ print $1 }')")" | |
HIST_FROM="$(printf %-11s "${FROM_OR_SENDER}")" | |
HIST_TO="$(printf %-10s "${TO}")" | |
HIST_SECS="$(printf %5s "${SECS}")" | |
if [[ "${BER}" = "n/a" ]] | |
then | |
HIST_BER="-----" | |
else | |
HIST_BER="$(printf %4s "${BER}")%" | |
fi | |
if [[ "${LOSS}" = "n/a" ]] | |
then | |
HIST_LOSS="-----" | |
else | |
HIST_LOSS="$(printf %3s "${LOSS}")%" | |
fi | |
if [[ "${SOURCE}" = "NET" ]] | |
then | |
HIST_SRC="$(printf %-3s "Net")" | |
HIST_SMETER="" | |
else | |
HIST_SRC="$(printf %-3s "RF")" | |
if [[ "${RSSI}" = "n/a" ]] | |
then | |
HIST_SMETER="-----" | |
else | |
HIST_SMETER="${SMETER} (-${DBM} dBm)" | |
fi | |
fi | |
#---------- | |
if [[ "${DATA}" -eq 1 ]] | |
then | |
HIST_SECS="-----" | |
HIST_BER="-----" | |
fi | |
#---------- | |
# CONSTRUCT THE NEW RECORD WE'LL BE ADDING | |
HISTORY[1]="${BOLD}${BLU}${HIST_TIME}${DIM}${AMPM}${BOLD}${WHI}${SGR0}${SBOX_VL} ${BOLD}${CONTACT_COLOR}${HIST_FROM}${SGR0}${SBOX_VL} ${BOLD}${CONTACT_COLOR}${HIST_TO}${SGR0} ${SBOX_VL} ${BOLD}${BLU}${HIST_SECS}${WHI}${SGR0} ${SBOX_VL} ${BOLD}${BER_COLOR}${HIST_BER}${WHI}${SGR0} ${SBOX_VL} ${BOLD}${CYA}${HIST_SRC}${WHI}" | |
#---------- | |
if [[ "${DATA}" -eq 1 ]] | |
then | |
if [[ -n "${BLOCKS}" ]] | |
then | |
HISTORY[1]="${HISTORY[1]} ${SBOX_VL} ${BOLD}${YEL}Data (Blocks: ${BLOCKS})${WHI}${SGR0}" | |
else | |
HISTORY[1]="${HISTORY[1]} ${SBOX_VL} ${BOLD}${YEL}Data${WHI}${SGR0}" | |
fi | |
else | |
if [[ -n "${RSSI}" ]] | |
then | |
HISTORY[1]="${HISTORY[1]} ${SBOX_VL} ${BOLD}${SMETER_COLOR}${HIST_SMETER}${WHI}${SGR0}" | |
else | |
HISTORY[1]="${HISTORY[1]} ${SBOX_VL} ${BOLD}${LOSS_COLOR}${HIST_LOSS}${WHI}${SGR0}" | |
fi | |
fi | |
fi | |
fi | |
#---------- | |
# STORE THE NEW HISTORY, INCLUDING THE RECORD WE'VE JUST BUILT | |
# Save the history | |
HIST_COUNT="1" | |
until [[ "${HIST_COUNT}" -gt "${HIST_MAX}" ]] | |
do | |
echo -e "HISTORY[${HIST_COUNT}]=\"${HISTORY[HIST_COUNT]}\"" >> "${HISTORY_FILE}" | |
((HIST_COUNT++)) | |
done | |
#---------- | |
TIME="" | |
FROM="" | |
TO="" | |
SECS="" | |
BER="" | |
LOSS="" | |
SMETER="" | |
DBM="" | |
RSSI="" | |
DATA="" | |
return | |
} | |
#----------------------------------- | |
fnTALLY_ERRORS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
ERROR_MSG="$(echo "${RECORD}" | cut -f4- -d" ")" | |
. "${COUNT_FILE}" | |
((ERROR_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
# If we've masked the reporting of logged errors by | |
# using the "-e|--error" cmdline option, then | |
# skip displaying the error, but still tally it. | |
if [[ "${SHOW_LOG_ERRORS}" = "1" ]] | |
then | |
echo -n "${SGR0}${YEL}" | |
fnSEPARATOR | |
echo -en "${BOLD}${RED}ERROR: ${WHI}${ERROR_MSG}\r" | |
tput cuf 63 | |
echo "${BOLD}${RED} Count: $(printf %05d "${ERROR_COUNT}")" | |
echo -n "${SGR0}${YEL}" | |
fnSEPARATOR | |
fi | |
return | |
} | |
#----------------------------------- | |
fnTALLY_WARNINGS() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
WARNING_MSG="$(echo "${RECORD}" | cut -f4- -d" ")" | |
. "${COUNT_FILE}" | |
((WARNING_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
# Unlike errors, warnings will not be masked by | |
# the "-e|--error" cmdline option. They will | |
# always be displayed (as well as tallied.) | |
echo -n "${SGR0}${RED}" | |
fnSEPARATOR | |
echo -en "${BOLD}${RED}WARNING: ${WHI}${WARNING_MSG}\r" | |
tput cuf 63 | |
echo "${BOLD}${RED} Count: $(printf %05d "${WARNING_COUNT}")" | |
echo -n "${SGR0}${RED}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnDMR_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
#---------- | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_3="${CUT_ARRAY[2]}" | |
FIELD_5="${CUT_ARRAY[4]}" | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_9="${CUT_ARRAY[8]}" | |
FIELD_10="${CUT_ARRAY[9]}" | |
FIELD_11="${CUT_ARRAY[10]}" | |
FIELD_12="${CUT_ARRAY[11]}" | |
FIELD_13="${CUT_ARRAY[12]}" | |
FIELD_14="${CUT_ARRAY[13]}" | |
FIELD_15="${CUT_ARRAY[14]}" | |
FIELD_17="${CUT_ARRAY[16]}" | |
FIELD_18="${CUT_ARRAY[17]}" | |
FIELD_20="${CUT_ARRAY[19]}" | |
FIELD_21="${CUT_ARRAY[20]}" | |
unset CUT_ARRAY | |
#---------- | |
# Don't waste any time with these... | |
if [[ "${LOGREC_REMAINING_DATA}" =~ " CSBK " ]] || [[ "${LOGREC_REMAINING_DATA}" =~ " VSBK " ]] | |
then | |
return | |
fi | |
#---------- | |
# Compensate for shifting fields between "group" and "private" ID numbers | |
# "private" call log entries lack "TG" in the log line, which skews the | |
# data returned when parsing positional fields. These few lines inject a | |
# bogus "PC" (private contact) place-holder to substitute for the missing | |
# "TG", so as not to skew positional fields. A hack, but a useful one. | |
if [[ ! "${LOGREC_REMAINING_DATA}" =~ " TG " ]] | |
then | |
LOGREC_REMAINING_DATA="${LOGREC_REMAINING_DATA// to / to PC }" | |
fi | |
#---------- | |
# NEW TA HANDLER | |
# At some point, the format of DMR Talker Alias log entries | |
# changed. The data used to come in chunks, and I had to | |
# ensure the data was complete before displaying it. Now, | |
# it looks like mmdvmhost only logs the TA when it's complete, | |
# and I no longer have to check to ensure we have all of it. | |
if [[ "${LOGREC_REMAINING_DATA}" =~ ", Talker Alias" ]] | |
then | |
if [[ "${TA_DISPLAYED}" -eq 0 ]] | |
then | |
TA="$(echo "${LOGREC_REMAINING_DATA}" | grep "Talker Alias" | cut -f2 -d'"')" | |
if [[ -n "${TA}" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
echo "${DIM}TA..:${SGR0} ${MAG}${BOLD}${TA}${SGR0}${WHI}${CLR_ED}" | |
TA_DISPLAYED="1" | |
fnQSO_IN_PROGRESS | |
fi | |
fi | |
return | |
fi | |
# OLD TA HANDLER | |
# Up until recently, this was the format for DMR Talker Alias | |
# log entries. The data would come in chunks, and I'd need to | |
# ensure the data was complete in order to display it. | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "DMR Talker Alias" ]] | |
then | |
# The log entries show that TA arrives in chunks. Make sure we have | |
# the whole thing before we print it. Don't bother displaying, if | |
# we don't get the whole thing (Kerchunkers, for example, don't | |
# always connect long enough to transfer the whole TA.) | |
# Also, sometimes, Talker Alias is present, but contains no data. | |
# Don't bother showing if TA is empty or contains only partial data. | |
TA_BYTES_RECVD="${FIELD_8%/*}" | |
TA_BYTES_TOTAL="${FIELD_8#*/}" | |
if [[ "${TA_BYTES_RECVD}" -eq "${TA_BYTES_TOTAL}" ]] && [[ "${TA_DISPLAYED}" -eq 0 ]] | |
then | |
TA="$(echo "${LOGREC_REMAINING_DATA}" | grep "DMR Talker Alias" | cut -f2- -d":" | sed -u -e "s/^ //" -e "s/'//g")" | |
if [[ -n "${TA}" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
echo "${DIM}TA..:${SGR0} ${MAG}${BOLD}${TA}${SGR0}${WHI}${CLR_ED}" | |
TA_DISPLAYED="1" | |
fnQSO_IN_PROGRESS | |
fi | |
fi | |
return | |
fi | |
#---------- | |
fnCONVERT_DATE_TIME | |
# Determine DMR Time Slot, and RF or Network traffic | |
TIME_SLOT="${FIELD_3%,*}" | |
SOURCE="${FIELD_5//network/NET}" | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "late entry from" ]] | |
then | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
# A QSO has started... | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_9}" | |
fnDMR_GET_FROM | |
TO="${FIELD_12%,*}" | |
fnDMR_GET_TO | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
#---------- | |
TYPE="${YEL}Late Entry" | |
fnDMR_SHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "header from" ]] | |
then | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
# A QSO has started... | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_9}" | |
fnDMR_GET_FROM | |
TO="${FIELD_12%,*}" | |
fnDMR_GET_TO | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
#---------- | |
# Determine if voice or data | |
if [[ "${FIELD_6}" = "data" ]] | |
then | |
BLOCKS="${FIELD_13}" | |
TYPE="${YEL}Data Transfer" | |
else | |
TYPE="${CYA}Voice Call" | |
fi | |
fnDMR_SHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# The QSO has ended. | |
# If it was voice traffic... | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "end of voice" ]] | |
then | |
DATA="0" | |
# The number of seconds the contact lasted | |
SECS="${FIELD_15}" | |
#---------- | |
# If it was network traffic... | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "RSSI:" ]] | |
then | |
# For RF traffic... | |
# BER is in different positions, for network and RF log entries | |
# Remove any comma | |
BER="${FIELD_18%,*}" | |
# Remove the "%" symbol | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
#---------- | |
# Get the RSSI | |
RSSI="${FIELD_20}" | |
DBM="${RSSI%%/*}" | |
DBM="${DBM#*-}" | |
fnCOLORIZE_RSSI | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI}${DIM} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}%${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}RSSI:${SGR0} ${SMETER_COLOR}${BOLD}${SMETER} (-${DBM} dBm)${SGR0}${WHI}\r" | |
else | |
# For network traffic... | |
# BER is in different positions, for network and RF log entries | |
BER="${FIELD_21%,*}" | |
# Remove the "%" symbol | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
#---------- | |
# Get the packet loss | |
LOSS="${FIELD_17%\%*}" | |
fnCOLORIZE_LOSS | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}%${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Loss:${SGR0} ${LOSS_COLOR}${BOLD}${LOSS}%${SGR0}${WHI}\r" | |
fi | |
#---------- | |
# If call was less than 2 secs, | |
# and we know who it was from, | |
# then call it a kerchunk. | |
# Responses such as "disconnected", | |
# received with a 4000 disconnect | |
# are not counted. | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
SECS="${FIELD_8}" | |
#---------- | |
# Get the packet loss | |
LOSS="${LOSS%\%*}" | |
fnCOLORIZE_LOSS | |
#---------- | |
# Get the BER | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}%${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}Loss:${SGR0} ${LOSS_COLOR}${BOLD}${LOSS}%${SGR0}${WHI}\r" | |
#---------- | |
# If call was less than 2 secs, | |
# and we know who it was from, | |
# then call it a kerchunk. | |
# Responses such as "disconnected", | |
# received with a 4000 disconnect | |
# are not counted. | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# It was data traffic... | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "ended network data" ]] || [[ "${LOGREC_REMAINING_DATA}" =~ "ended RF data" ]] | |
then | |
DATA="1" | |
if [[ "${BLOCKS}" -gt 1 ]] | |
then | |
S="s" | |
else | |
S="" | |
fi | |
echo -e "\r${CLR_EL}${YEL}${BOLD}End of Data Transmission (${BLOCKS} block${S})${SGR0}${WHI}" | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network user has timed out" ]] | |
then | |
echo "Network user has timed out" | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed DMR log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_DMR | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnYSF_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_7="${CUT_ARRAY[6]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_9="${CUT_ARRAY[8]}" | |
FIELD_10="${CUT_ARRAY[9]}" | |
FIELD_11="${CUT_ARRAY[10]}" | |
FIELD_12="${CUT_ARRAY[11]}" | |
FIELD_14="${CUT_ARRAY[13]}" | |
FIELD_15="${CUT_ARRAY[14]}" | |
FIELD_17="${CUT_ARRAY[16]}" | |
FIELD_18="${CUT_ARRAY[17]}" | |
unset CUT_ARRAY | |
#---------- | |
# Don't waste any time with these... | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "invalid access attempt from" ]] | |
then | |
return | |
fi | |
#---------- | |
# Start of YSF network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network data from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
if [[ "${FIELD_7}" = "to" ]] | |
then | |
FROM="${FIELD_6}" | |
DG_ID="${FIELD_8}" | |
DG_ID_NUM="${FIELD_9}" | |
MASTER="${FIELD_11}" | |
else | |
# This drops space-separated values from the transmitted callsigns. | |
FROM="${FIELD_6}" | |
DG_ID="${FIELD_9}" | |
DG_ID_NUM="${FIELD_10}" | |
MASTER="${FIELD_12}" | |
fi | |
TO="${DG_ID} ${DG_ID_NUM}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="NET" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# Start of YSF RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF header from" ]] || [[ "${LOGREC_REMAINING_DATA}" =~ "received RF late entry from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF header from" ]] | |
then | |
# Parse Recvd RF header... | |
if [[ "${FIELD_7}" = "to" ]] | |
then | |
FROM="${FIELD_6}" | |
DG_ID="${FIELD_8}" | |
DG_ID_NUM="${FIELD_9}" | |
else | |
# This drops space-separated values from the transmitted callsigns. | |
FROM="${FIELD_6}" | |
DG_ID="${FIELD_8}" | |
DG_ID_NUM="${FIELD_10}" | |
fi | |
else | |
# Parse RF late entries... | |
if [[ "${FIELD_8}" = "to" ]] | |
then | |
FROM="${FIELD_7}" | |
DG_ID="${FIELD_9}" | |
DG_ID_NUM="${FIELD_10}" | |
else | |
# This drops space-separated values from the transmitted callsigns. | |
FROM="${FIELD_7}" | |
DG_ID="${FIELD_9}" | |
DG_ID_NUM="${FIELD_11}" | |
fi | |
fi | |
TO="${DG_ID} ${DG_ID_NUM}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="RF" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# YSF RF traffic dropped | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "transmission lost from" ]] | |
then | |
SECS="${FIELD_9}" | |
BER="${FIELD_12}" | |
RSSI="${FIELD_14}" | |
if [[ "${RSSI}" = "" ]] | |
then | |
RSSI="${BER}" | |
BER="0.0%" | |
fi | |
DG_ID_NUM="${DG_ID_NUM%,*}" | |
# Fields to pass to the history... | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
DBM="${RSSI%%/*}" | |
DBM="${DBM#*-}" | |
fnCOLORIZE_BER | |
fnCOLORIZE_RSSI | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -e "\r${BOLD}${YEL}TRANSMISSION LOST${SGR0}${WHI}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}RSSI:${SGR0} ${SMETER_COLOR}${BOLD}${SMETER} (-${DBM} dBm)${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
echo -n "${CLR_ED}" | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# End of YSF network traffic (Wires-X) | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network end of transmission from" ]] | |
then | |
SECS="${FIELD_12}" | |
LOSS="${FIELD_14}" | |
BER="${FIELD_18}" | |
fnYSF_END_NETWORK_TRAFFIC | |
return | |
fi | |
# End of YSF network traffic (Normal traffic) | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
SECS="${FIELD_6}" | |
LOSS="${FIELD_8}" | |
BER="${FIELD_12}" | |
fnYSF_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
# End of YSF RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF end of transmission from" ]] | |
then | |
SECS="${FIELD_12}" | |
BER="${FIELD_15}" | |
RSSI="${FIELD_17}" | |
if [[ "${RSSI}" = "" ]] | |
then | |
RSSI="${BER}" | |
BER="0.0%" | |
fi | |
DG_ID_NUM="${DG_ID_NUM%,*}" | |
# Fields to pass to the history... | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
DBM="${RSSI%%/*}" | |
DBM="${DBM#*-}" | |
fnCOLORIZE_BER | |
fnCOLORIZE_RSSI | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}RSSI:${SGR0} ${SMETER_COLOR}${BOLD}${SMETER} (-${DBM} dBm)${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed YSF log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_YSF | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnM17_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
# Revise field list below, as needed | |
FIELD_1="${CUT_ARRAY[0]}" | |
FIELD_2="${CUT_ARRAY[1]}" | |
FIELD_3="${CUT_ARRAY[2]}" | |
FIELD_4="${CUT_ARRAY[3]}" | |
FIELD_5="${CUT_ARRAY[4]}" | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_7="${CUT_ARRAY[6]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_9="${CUT_ARRAY[8]}" | |
FIELD_10="${CUT_ARRAY[9]}" | |
FIELD_11="${CUT_ARRAY[10]}" | |
FIELD_12="${CUT_ARRAY[11]}" | |
FIELD_13="${CUT_ARRAY[12]}" | |
FIELD_14="${CUT_ARRAY[13]}" | |
# Revise field list above, as needed | |
unset CUT_ARRAY | |
#---------- | |
# Start of M17 RF voice traffic | |
# M17, received RF voice transmission from KE8DPF to ALL | |
# 1 2 3 4 5 6 7 8 9 | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF voice transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_7}" | |
TO="${FIELD_9}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="RF" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# End of M17 RF traffic | |
# M17, received RF end of transmission from KE8DPF to ALL, 0.0 seconds, BER: 0.0% | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF end of transmission" ]] | |
then | |
SECS="${FIELD_11}" | |
BER="${FIELD_14}" | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
RSSI="n/a" | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# Transmission lost | |
# M17, transmission lost from KE8DPF to ALL, 0.1 seconds, BER: 6.2% | |
# 1 2 3 4 5 6 7 8 9 10 11 | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "transmission lost from" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
SECS="${FIELD_8}" | |
BER="${FIELD_11}" | |
RSSI="n/a" | |
# Fields to pass to the history... | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -e "\r${BOLD}${YEL}TRANSMISSION LOST${SGR0}${WHI}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
echo -n "${CLR_ED}" | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# Start of M17 network traffic | |
# M17, received network voice transmission from XXXXXX X to ALL | |
# M17, received network voice transmission from XXXXXX to ALL | |
# 1 2 3 4 5 6 7 8 9 10 | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network voice transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_7}" | |
if [[ "${FIELD_10}" = "ALL" ]] | |
then | |
TO="${FIELD_10}" | |
M17_HOST_SUFFIX="${FIELD_8}" | |
else | |
TO="${FIELD_9}" | |
M17_HOST_SUFFIX="" | |
fi | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="NET" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
# #---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "M17, text Data:" ]] | |
then | |
IFS=':'; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
M17_TEXT_DATA="${CUT_ARRAY[1]}" | |
unset CUT_ARRAY | |
# Ensure display of any "text Data" entry happens only once per message | |
if [[ "${FROM}" != "${LAST_FROM}" ]] | |
then | |
echo -e "\r${DIM}Text:${SGR0}${BOLD}${WHI}${M17_TEXT_DATA}${SGR0}" | |
LAST_FROM="${FROM}" | |
fi | |
return | |
fi | |
#---------- | |
# End of M17 network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network end of transmission from" ]] | |
then | |
FROM="${FIELD_8}" | |
if [[ "${FIELD_11}" = "ALL," ]] | |
then | |
TO="${FIELD_11%,*}" | |
M17_HOST_SUFFIX="${FIELD_9}" | |
SECS="${FIELD_12}" | |
else | |
TO="${FIELD_10%,*}" | |
M17_HOST_SUFFIX="" | |
SECS="${FIELD_11}" | |
fi | |
LOSS="n/a" | |
fnM17_END_NETWORK_TRAFFIC | |
# This next line resets ability to display "text Data" entries | |
LAST_FROM="${FROM}" | |
return | |
fi | |
# #---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
SECS="${FIELD_6}" | |
fnM17_END_NETWORK_TRAFFIC | |
# This next line resets ability to display "text Data" entries | |
LAST_FROM="${FROM}" | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed M17 log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_M17 | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnNXDN_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_7="${CUT_ARRAY[6]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_9="${CUT_ARRAY[8]}" | |
FIELD_10="${CUT_ARRAY[9]}" | |
FIELD_11="${CUT_ARRAY[10]}" | |
FIELD_12="${CUT_ARRAY[11]}" | |
unset CUT_ARRAY | |
#---------- | |
# Start of NXDN RF voice traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_6}" | |
TG_LABEL="${FIELD_8}" | |
TO="${FIELD_9}" | |
TO="${TG_LABEL} ${TO}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="RF" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# End of NXDN RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF end of transmission" ]] | |
then | |
SECS="${FIELD_7}" | |
BER="${FIELD_10}" | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
fnCOLORIZE_BER | |
RSSI="n/a" | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# Start of NXDN network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_6}" | |
TG_LABEL="${FIELD_8}" | |
TO="${FIELD_9}" | |
TO="${TG_LABEL} ${TO}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="NET" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# End of NXDN network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network end of transmission from" ]] | |
then | |
FROM="${FIELD_8}" | |
TG_LABEL="${FIELD_10}" | |
TO="${FIELD_11%,*}" | |
SECS="${FIELD_12}" | |
LOSS="n/a" | |
fnNXDN_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
SECS="${FIELD_6}" | |
fnNXDN_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed NXDN log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_NXDN | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnDSTAR_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
# IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
# FIELD_6="${CUT_ARRAY[5]}" | |
# unset CUT_ARRAY | |
#---------- | |
# Ignore these | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "invalid slow data header" ]] | |
then | |
return | |
fi | |
#---------- | |
# Start of DSTAR network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network header from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
SOURCE="NET" | |
# Take any entry like "KE8DPF / INFO" | |
SENDER="${LOGREC_REMAINING_DATA:37:13}" | |
# Remove all spaces ... "KE8DPF/INFO" | |
# Examples such as "KE8DPF / " become "KE8DPF/" | |
SENDER="${SENDER// /}" | |
# Preserve anything to the left of the slash | |
FROM="${SENDER%/*}" | |
# If the last character is just the slash | |
if [[ "${SENDER:0-1}" = "/" ]] | |
then | |
# then do without the slash | |
SENDER="${FROM}" | |
fi | |
TO="${LOGREC_REMAINING_DATA:54:8}" | |
TO="${TO// /}" | |
DSTAR_REFLECTOR="${LOGREC_REMAINING_DATA:67:8}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# Start of DSTAR RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF header from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
SOURCE="RF" | |
#---------- | |
# Take any entry like "KE8DPF / INFO" | |
SENDER="${LOGREC_REMAINING_DATA:32:13}" | |
# Remove all spaces ... "KE8DPF/INFO" | |
# Examples such as "KE8DPF / " become "KE8DPF/" | |
SENDER="${SENDER// /}" | |
# Preserve anything to the left of the slash | |
FROM="${SENDER%/*}" | |
# If the last character is just the slash | |
if [[ "${SENDER:0-1}" = "/" ]] | |
then | |
# then do without the slash | |
SENDER="${FROM}" | |
fi | |
#---------- | |
TO="${LOGREC_REMAINING_DATA:49:8}" | |
TO="${TO// /}" | |
#---------- | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# End of DSTAR RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF end of transmission" ]] | |
then | |
# Take any entry like "KE8DPF / INFO" | |
SENDER="${LOGREC_REMAINING_DATA:45:13}" | |
# Remove all spaces ... "KE8DPF/INFO" | |
# Examples such as "KE8DPF / " become "KE8DPF/" | |
SENDER="${SENDER// /}" | |
# Preserve anything to the left of the slash | |
FROM="${SENDER%/*}" | |
# If the last character is just the slash | |
if [[ "${SENDER:0-1}" = "/" ]] | |
then | |
# then do without the slash | |
SENDER="${FROM}" | |
fi | |
#---------- | |
TO="${LOGREC_REMAINING_DATA:62:8}" | |
TO="${TO// /}" | |
#---------- | |
IFS=','; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_3="${CUT_ARRAY[2]}" | |
FIELD_4="${CUT_ARRAY[3]}" | |
FIELD_5="${CUT_ARRAY[4]}" | |
unset CUT_ARRAY | |
#---------- | |
# seconds | |
IFS=' '; CUT_ARRAY=(${FIELD_3}); unset IFS; | |
SECS="${CUT_ARRAY[0]}" | |
unset CUT_ARRAY | |
#---------- | |
# BER | |
BER="${FIELD_4#*:}" | |
BER="${BER// /}" | |
BER="${BER%\%*}" | |
#---------- | |
# Get the RSSI | |
RSSI="${FIELD_5}" | |
DBM="${RSSI%%/*}" | |
DBM="${DBM#*-}" | |
fnCOLORIZE_RSSI | |
#---------- | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}RSSI:${SGR0} ${SMETER_COLOR}${BOLD}${SMETER} (-${DBM} dBm)${SGR0}${WHI}\r" | |
#---------- | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# End of DSTAR network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network end of transmission" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
# Take any entry like "KE8DPF / INFO" | |
SENDER="${LOGREC_REMAINING_DATA:50:13}" | |
# Remove all spaces ... "KE8DPF/INFO" | |
# Examples such as "KE8DPF / " become "KE8DPF/" | |
SENDER="${SENDER// /}" | |
# Preserve anything to the left of the slash | |
FROM="${SENDER%/*}" | |
# If the last character is just the slash | |
if [[ "${SENDER:0-1}" = "/" ]] | |
then | |
# then do without the slash | |
SENDER="${FROM}" | |
fi | |
TO="${LOGREC_REMAINING_DATA:67:8}" | |
TO="${TO// /}" | |
IFS=','; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_3="${CUT_ARRAY[2]}" | |
FIELD_4="${CUT_ARRAY[3]}" | |
FIELD_5="${CUT_ARRAY[4]}" | |
unset CUT_ARRAY | |
# seconds | |
IFS=' '; CUT_ARRAY=(${FIELD_3}); unset IFS; | |
SECS="${CUT_ARRAY[0]}" | |
unset CUT_ARRAY | |
# loss | |
LOSS="${FIELD_4%\%*}" | |
# BER | |
BER="${FIELD_5#*:}" | |
BER="${BER// /}" | |
BER="${BER%\%*}" | |
fnDSTAR_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
echo -en "\r${CLR_EL}" | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_12="${CUT_ARRAY[11]}" | |
unset CUT_ARRAY | |
# seconds | |
SECS="${FIELD_6}" | |
# loss | |
LOSS="${FIELD_8%\%*}" | |
# BER | |
BER="${FIELD_12#*:}" | |
BER="${BER// /}" | |
BER="${BER%\%*}" | |
fnDSTAR_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "link status" ]] | |
then | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed DSTAR log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_DSTAR | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnP25_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
if [[ "${LOGREC_REMAINING_DATA}" =~ " from to " ]] | |
then | |
return | |
fi | |
# We come into this function already knowing: | |
# The entire log line... | |
# RECORD | |
# The first 4 fields of the log line... | |
# LOGREC_TYPE LOGREC_DATE LOGREC_TIME LOGREC_MODE | |
# And "everything following" the first 3 fields... | |
# LOGREC_REMAINING_DATA | |
# | |
# The fields, between "RECORD" and "LOGREC_REMAINING_DATA" | |
# align with each other as follows: | |
# RECORD... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (awk fields) | |
# LOGREC_REMAINING_DATA... | |
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (array indexes) | |
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ("FIELD_x") | |
# | |
# Grab the parts that we need | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_6="${CUT_ARRAY[5]}" | |
FIELD_7="${CUT_ARRAY[6]}" | |
FIELD_8="${CUT_ARRAY[7]}" | |
FIELD_9="${CUT_ARRAY[8]}" | |
FIELD_10="${CUT_ARRAY[9]}" | |
FIELD_11="${CUT_ARRAY[10]}" | |
FIELD_13="${CUT_ARRAY[12]}" | |
FIELD_16="${CUT_ARRAY[15]}" | |
FIELD_18="${CUT_ARRAY[17]}" | |
unset CUT_ARRAY | |
#---------- | |
# What to do with these (if anything?)..." | |
# P25, received RF header | |
# P25, total frames: 36, bits: 44389, undecodable LC: 0, errors: 29, BER: 0.0653% | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF header" ]] || [[ "${LOGREC_REMAINING_DATA}" =~ "total frames" ]] | |
then | |
return | |
fi | |
#---------- | |
# Start of P25 RF voice traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF voice transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_7}" | |
TG_LABEL="${FIELD_9}" | |
TO="${FIELD_10}" | |
TO="${TG_LABEL} ${TO}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="RF" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# Start of P25 network traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received network transmission from" ]] | |
then | |
fnCONVERT_DATE_TIME | |
QSO_START_TIME="${TIME}" | |
FROM="${FIELD_6}" | |
TG_LABEL="${FIELD_8}" | |
TO="${FIELD_9}" | |
TO="${TG_LABEL} ${TO}" | |
echo -en "\r${CLR_EL}" | |
fnBANNER | |
SOURCE="NET" | |
. "${COUNT_FILE}" | |
((QSO_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
fnSHOW_CURRENT_CALL | |
return | |
fi | |
#---------- | |
# End of P25 RF traffic | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "received RF end of voice transmission" ]] | |
then | |
SECS="${FIELD_13}" | |
BER="${FIELD_16}" | |
RSSI="${FIELD_18}" | |
# Fields to pass to the history... | |
BER="${BER%,*}" | |
BER="${BER%\%*}" | |
DBM="${RSSI%%/*}" | |
DBM="${DBM#*-}" | |
fnCOLORIZE_BER | |
fnCOLORIZE_RSSI | |
echo -en "\r${CLR_EL}" | |
echo -en "\r${DIM}Dur.:${SGR0} ${BLU}${BOLD}${SECS}${SGR0}${WHI} sec\r" | |
tput cuf 18 | |
echo -en "${DIM}BER:${SGR0} ${BER_COLOR}${BOLD}${BER}${SGR0}${WHI}\r" | |
tput cuf 32 | |
echo -en "${DIM}RSSI:${SGR0} ${SMETER_COLOR}${BOLD}${SMETER} (-${DBM} dBm)${SGR0}${WHI}\r" | |
if [[ -n "${FROM}" ]] && [[ -n "$(echo "${SECS}" | awk '$1 <2.0{print}')" ]] | |
then | |
. "${COUNT_FILE}" | |
((KERCHUNK_COUNT++)) | |
fnWRITE_COUNTER_FILE | |
tput cuf 64 | |
echo "${YEL}[[kerchunk]]${SGR0}${WHI}" | |
else | |
echo | |
fi | |
#---------- | |
fnBUILD_HISTORY | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
fnLISTENING | |
return | |
fi | |
#---------- | |
# End of P25 network traffic (VARIANT A) | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network end of transmission from" ]] | |
then | |
SECS="${FIELD_11}" | |
LOSS="${FIELD_13%\%*}" | |
fnP25_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
# End of P25 network traffic (VARIANT B) | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network end of transmission," ]] | |
then | |
SECS="${FIELD_6}" | |
LOSS="${FIELD_8%\%*}" | |
fnP25_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
if [[ "${LOGREC_REMAINING_DATA}" =~ "network watchdog has expired" ]] | |
then | |
SECS="${FIELD_6}" | |
LOSS="${FIELD_8}" | |
fnP25_END_NETWORK_TRAFFIC | |
return | |
fi | |
#---------- | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
echo "Un-parsed P25 log entry..." >&2 | |
echo "${LOGREC_REMAINING_DATA}" >&2 | |
echo "${RECORD}" >>/tmp/unparsed_P25 | |
echo -n "${LINE_COLOR}" | |
fnSEPARATOR | |
return | |
} | |
#----------------------------------- | |
fnGPS_MAIN() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
trap fnWINCH WINCH | |
if [[ "${RECORD}" =~ "GPS position" ]] && [[ "${GPS_HIT}" = "0" ]] | |
then | |
# This gives us data that looks like... "-20.983585,-47.647781" | |
IFS=' '; CUT_ARRAY=(${LOGREC_REMAINING_DATA}); unset IFS; | |
FIELD_3="${CUT_ARRAY[2]}" | |
# This separates the latitude and longitude | |
IFS=','; CUT_ARRAY=(${FIELD_3}); unset IFS; | |
LATITUDE="${CUT_ARRAY[0]}" | |
LONGITUDE="${CUT_ARRAY[1]}" | |
# Indicate North or South, and East or West, based on | |
# whether the numbers are positive or negative. | |
NS="° N" | |
EW="° E" | |
if [[ "${LATITUDE}" =~ "-" ]] | |
then | |
NS="° S" | |
fi | |
if [[ "${LONGITUDE}" =~ "-" ]] | |
then | |
EW="° W" | |
fi | |
echo -e "\r${DIM}GPS :${SGR0} ${BOLD}${WHI}Lat ${LATITUDE}${NS}, Long ${LONGITUDE}${EW}${SGR0}" | |
fi | |
fnQSO_IN_PROGRESS | |
return | |
} | |
#----------------------------------- | |
fnMAIN_LOOP() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
#---------- | |
# For "live" logs, use tail. For replay, use cat. | |
if [[ "${REPLAY}" -eq 0 ]] | |
then | |
CMD="tail -f -n 1" | |
else | |
CMD="cat" | |
fi | |
#---------- | |
# Filter the log, looking for lines we'll want to parse. | |
eval "${CMD}" "${WORKING_LOG}" 2>/dev/null \ | |
| grep -E --line-buffered "^M: |^W: |^E: |^MMDVMHOST-STOP" \ | |
| grep -v -E --line-buffered "0000:|Opening|Closing|Embedded|Downlink|is running|link status|Logged into|slow data text" \ | |
| while read -r RECORD | |
do | |
# Square brackets cause trouble when parsing | |
# a record, so they are removed here. The only | |
# records of importance that I've seen them in | |
# (so far) are GPS position records. | |
RECORD="${RECORD//[\[\]]}" | |
#---------- | |
# See if MMDVMHost has shutdown or transitioned to a new log. | |
if [[ "${RECORD}" =~ "MMDVMHOST-STOP" ]] | |
then | |
if [[ "${REPLAY}" -eq 0 ]] | |
then | |
# If it did, it has triggered our exit from the loop. | |
# We appended an additional blank line into the log, so that when | |
# the "tail -f -n 1" next restarts against the log, the trigger | |
# will have receeded far enough back to not cause a false restart | |
# of this script. | |
# Once we've caught the trigger, kill the tail that drives the loop. | |
TAIL_PID="$(sudo fuser -v "${WORKING_LOG}" 2>&1 | grep "tail" | awk '{ print $2 }')" | |
kill "${TAIL_PID}" 2>/dev/null | |
return | |
fi | |
fi | |
#---------- | |
# The MMDVMHost daemon logs any errors or warnings it encounters. | |
# Errors are logged in records beginning with "E:". | |
# Warnings are logged in records beginning with "W:". | |
# FYI: Debug messages are logged in records beginning with "D:". | |
# FYI: Information messages are logged in records beginning with "I:". | |
# FYI: Modem Traffic messages are logged in records beginning with "M:". | |
# We filter out I: and D: records at the tail/cat, so no need to watch | |
# for them here. We're interested in the errors, warnings, and modem | |
# traffic (M:) records. | |
LOGREC_TYPE="${RECORD:0:2}" | |
case "${LOGREC_TYPE}" in | |
"E:") | |
fnTALLY_ERRORS | |
continue | |
;; | |
"W:") | |
fnTALLY_WARNINGS | |
continue | |
;; | |
*) | |
# Everything else is an M: record | |
: | |
;; | |
esac | |
#---------- | |
# Get the date, time, and mode | |
IFS=' '; CUT_ARRAY=(${RECORD}); unset IFS; | |
LOGREC_DATE="${CUT_ARRAY[1]}" | |
LOGREC_TIME="${CUT_ARRAY[2]}" | |
LOGREC_MODE="${CUT_ARRAY[3]}" | |
unset CUT_ARRAY | |
# Strip off the comma (ie: "YSF," becomes "YSF") | |
LOGREC_MODE="${LOGREC_MODE%,*}" | |
#---------- | |
# Grab everything that follows the date/timestamps... | |
LOGREC_REMAINING_DATA="${RECORD//${LOGREC_TYPE} ${LOGREC_DATE} ${LOGREC_TIME} /}" | |
#---------- | |
# Now parse it accordingly. | |
case "${LOGREC_MODE}" in | |
"GPS") | |
fnGPS_MAIN | |
# This flag prevents printing the record multiple | |
# times, when received multiple times, during a | |
# single transmission. | |
GPS_HIT="1" | |
;; | |
"DMR") | |
CONTACT_COLOR="${DMR_COLOR}" | |
LINE_COLOR="${BLU}" | |
fnDMR_MAIN | |
GPS_HIT="0" | |
;; | |
"YSF") | |
CONTACT_COLOR="${YSF_COLOR}" | |
LINE_COLOR="${MAG}" | |
fnYSF_MAIN | |
GPS_HIT="0" | |
;; | |
"NXDN") | |
CONTACT_COLOR="${NXDN_COLOR}" | |
LINE_COLOR="${DIM}${WHI}" | |
fnNXDN_MAIN | |
GPS_HIT="0" | |
;; | |
"D-Star") | |
CONTACT_COLOR="${DSTAR_COLOR}" | |
LINE_COLOR="${CYA}" | |
fnDSTAR_MAIN | |
GPS_HIT="0" | |
;; | |
"P25") | |
CONTACT_COLOR="${P25_COLOR}" | |
LINE_COLOR="${DIM}${YEL}" | |
fnP25_MAIN | |
GPS_HIT="0" | |
;; | |
"M17") | |
CONTACT_COLOR="${M17_COLOR}" | |
LINE_COLOR="${WHI}" | |
fnM17_MAIN | |
GPS_HIT="0" | |
;; | |
*) | |
CONTACT_COLOR="${OTHER_COLOR}" | |
LINE_COLOR="${DIM}${WHI}" | |
GPS_HIT="0" | |
;; | |
esac | |
done | |
return | |
} | |
#----------------------------------- | |
fnABORT() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
fnRESET_SCROLLING | |
#---------- | |
# Clear the bottom line of the screen | |
tput cup "${LINES}" 0 | |
echo -en "\r${CLR_EL}" | |
#---------- | |
# Re-Enable line-wrap, turn the cursor on, reset the terminal, | |
# and switch back to the primary screen buffer | |
echo -n "${WRAP_ON}${CUR_ON}${SGR0}${RMCUP}" | |
#---------- | |
if [[ "${HAS_BUFFER}" -eq 0 ]] | |
then | |
fnLOGO | |
fi | |
#---------- | |
fnGOODBYE_BOX | |
tput cup "${LINES}" 0 | |
#---------- | |
fnCLEANUP | |
fnCLOSE_FDS | |
#---------- | |
# If debugging and/or profiling was used, show the names of the logs, at exit | |
if [[ "${DEBUG_LOG}" != "/dev/null" ]] || [[ "${PROFILING_LOG}" != "/dev/null" ]] | |
then | |
if [[ "${MY_FONT}" = "5" ]] | |
then | |
DEBUG_PROFILING_LINE=$((LINES - 6)) | |
else | |
DEBUG_PROFILING_LINE=$((LINES - 4)) | |
fi | |
tput cup "${DEBUG_PROFILING_LINE}" 0 | |
fnLARGE_FONT "DEBUGGING: ${DEBUG_LOG}" | |
fnLARGE_FONT "PROFILING: ${PROFILING_LOG}" | |
echo | |
fi | |
#---------- | |
tput cup "${LINES}" 0 | |
echo -en "${TERM_RESET}" | |
stty "${STTY}" | |
reset | |
exit | |
} | |
#----------------------------------- | |
fnWINCH() | |
{ | |
echo -e "\n----------\nenter: ${FUNCNAME[0]} <- called by ${FUNCNAME[1]}" >&3 | |
#---------- | |
# Get the screensize. We'll be needing the number of lines. | |
read -r LINES COLUMNS <<< "$(python3 -c "${PY3_SCRIPT}")" 2>/dev/null | |
tput csr 0 $((LINES - 1)) | |
clear | |
#---------- | |
if [[ "${TOP}" -eq 1 ]] | |
then | |
tput cup "${BELOW_NOSCROLL}" 0 | |
fnNOSCROLL_ZONE | |
# Clear to end of display | |
echo -n "${CLR_ED}" | |
fi | |
return | |
} | |
########################################################### | |
# A few last items in the pre-flight checklist... | |
fnWRITE_COUNTER_FILE | |
# If user presses Ctrl-C, stop the program | |
trap fnABORT SIGINT SIGTERM | |
trap fnCLEANUP EXIT | |
#---------- | |
# Check for any options passed to the script. | |
if [[ -n "${ARG_LIST[0]}" ]] | |
then | |
fnPARSE_CMDLINE | |
fi | |
#---------- | |
# Make sure we have updated user.csv and NXDN.csv files | |
fnDOWNLOAD_USERCSV | |
fnDOWNLOAD_NXDNCSV | |
# Make sure we have an updated cty.dat file | |
fnDOWNLOAD_CTYDAT | |
#---------- | |
fnDEFINE_BOXCHARS | |
#---------- | |
if [[ "${LOGO}" -eq 1 ]] | |
then | |
fnLOGO | |
tput cup 22 2 | |
else | |
fnNO_LOGO | |
fi | |
#---------- | |
# If running normally (not in replay mode)... ensure that | |
# MMDVMHost is running, and that it is writing to the log. | |
if [[ "${REPLAY}" -eq 0 ]] | |
then | |
echo -en "\r${CLR_EL}${SGR0}${WHI}Checking MMDVMHost... " | |
if [[ "$(pgrep -f "/usr/local/bin/MMDVMHost /etc/mmdvmhost")" = "" ]] | |
then | |
echo -e "\n ${RED}MMDVMHost is NOT RUNNING.${SGR0}" >&2 | |
echo -e " Check status in the pi-star GUI and try again.\n" >&2 | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
else | |
if [[ ! "$(sudo fuser -v "${WORKING_LOG}" 2>&1)" =~ "MMDVMHost" ]] | |
then | |
echo -e "\n ${YEL}MMDVMHost is running, but is NOT writing to $(basename "${WORKING_LOG}").${SGR0}" >&2 | |
echo -e " Check status in the pi-star GUI and try again.\n" >&2 | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
fi | |
fi | |
sleep .5 | |
fi | |
#---------- | |
# Fetch the version number from what's posted on github. | |
# Compare that version number with what is currently running. | |
# If github has a later version, tell the user. | |
echo -en "\r${CLR_EL}${SGR0}${WHI}Performing Version Check... " | |
VER_CHECK_COLOR="${WHI}${BOLD}" | |
VER_CHECK="$(curl -s --fail --max-time 2 "https://raw.githubusercontent.com/kencormack/pistar-lastqso/main/pistar-lastqso" 2>/dev/null | grep -m 1 "^VERSION=")" | |
VER_CHECK="${VER_CHECK#*=}" | |
#---------- | |
# Indicator to me, when I'm working with test/development version. | |
IS_DEV="" | |
if fnCOMPARE_FLOAT_GT "${VERSION}" "${VER_CHECK}" | grep -q ">" | |
then | |
IS_DEV="*" | |
fi | |
#---------- | |
# See if the user is running a version older than the latest available | |
if fnCOMPARE_FLOAT_LT "${VERSION}" "${VER_CHECK}" | grep -q "<" | |
then | |
echo -e "\r${GRE}${BOLD}AN UPDATE TO THIS SCRIPT IS AVAILABLE AT:" | |
echo "${GRE}${DIM}https://github.com/kencormack/pistar-lastqso" | |
echo "See the CHANGE_LOG for details" | |
echo "${WHI}${DIM} Your Version: ${VERSION}${SGR0}" | |
echo -e "${WHI}${BOLD}Available Update: ${VER_CHECK}${SGR0}${WHI}\n" | |
VER_CHECK_COLOR="${RED}" | |
sleep 3 | |
fi | |
#---------- | |
echo -en "\r${CLR_EL}${SGR0}${WHI}Checking Active Modes... " | |
fnGET_MODES | |
fnCHECK_MODES_ENABLED | |
sleep .5 | |
#---------- | |
# Prime the cache files. | |
# Also performed in fnCLEANUP, to preserve | |
# copies of the caches in /home/pi-star. | |
echo -en "\r${CLR_EL}${SGR0}${WHI}Priming Caches... " | |
fnCOPY_CACHE | |
#---------- | |
echo -en "\r${CLR_EL}${SGR0}${WHI}Initializing... " | |
sleep .5 | |
#---------- | |
# CREATE A FRESH EMPTY HISTORY | |
# These will store the history to be displayed, if the | |
# no-scroll region is called upon. | |
if [[ "${TOP}" = 1 ]] | |
then | |
HIST_COUNT="1" | |
until [[ "${HIST_COUNT}" -gt "${HIST_MAX}" ]] | |
do | |
HISTORY[HIST_COUNT]=" ${SBOX_VL} ${SBOX_VL} ${SBOX_VL} ${SBOX_VL} ${SBOX_VL} ${SBOX_VL}${SGR0}${WHI}" | |
echo -e "HISTORY[${HIST_COUNT}]=\"${HISTORY[HIST_COUNT]}\"" >> "${HISTORY_FILE}" | |
((HIST_COUNT++)) | |
done | |
fi | |
#---------- | |
# collect the data for the no-scroll region | |
fnTOP_DATA | |
#---------- | |
echo -en "\r${CLR_EL}${SMCUP}" | |
echo -n "${CUR_HOME}" | |
if [[ "${TOP}" -eq 1 ]] | |
then | |
fnNOSCROLL_ZONE | |
tput cup "${BELOW_NOSCROLL}" 0 | |
echo -n "${CLR_ED}" | |
fi | |
#---------- | |
# Make the named function available for execution as a | |
# seperate job that can be run in the background. | |
export -f fnMMDVMLOG_FUSER | |
########################################################### | |
# Now for the meat and potatoes... | |
while true | |
do | |
#---------- | |
# Read in the counter values | |
. "${COUNT_FILE}" | |
#---------- | |
if [[ "${REPLAY}" -eq 0 ]] | |
then | |
# This forks fnMMDVMLOG_FUSER into the background as a child process. | |
# It runs "fuser" against the log. If "MMDVMHost" no longer shows | |
# in the output, then MMDVMHost is no longer writing to the log. The | |
# function then appends "MMDVMHOST-STOP" to the log file. The "read" in | |
# fnMAIN_LOOP will spot this, and act on it to re-start the main loop. | |
# | |
# If the background job is already running, don't spawn another copy. | |
if [[ "$(pgrep -f "${BASH} -c fnMMDVMLOG_FUSER")" = "" ]] | |
then | |
/bin/bash -c fnMMDVMLOG_FUSER > /dev/null 2>&1 & | |
BG_PID="$(pgrep -o -f "${BASH} -c fnMMDVMLOG_FUSER")" | |
fi | |
fi | |
#---------- | |
fnLISTENING | |
#---------- | |
# And here we go... | |
# set -x | |
fnMAIN_LOOP | |
# set +x | |
#---------- | |
fnCOPY_CACHE | |
#---------- | |
# See if we were replaying a log. If so, we've reached the end of the log. | |
if [[ "${REPLAY}" -eq 1 ]] | |
then | |
if [[ ! "$(sudo fuser -v "${WORKING_LOG}" 2>&1)" =~ "cat" ]] | |
then | |
echo -n "${BOLD}${WHI}" | |
fnSEPARATOR | |
fnSEPARATOR | |
echo -n "${SGR0}${GRE}" | |
figlet -w 80 -f ansi_shadow " COMPLETE" | |
tput cuu1 | |
# echo "COMPLETE" | |
echo -n "${BOLD}${WHI}" | |
fnSEPARATOR | |
fnSEPARATOR | |
echo -n "${SGR0}${DEF}" | |
stty "${STTY}" | |
fnCLEANUP | |
exit | |
fi | |
fi | |
#---------- | |
# If we've exited fnMAIN_LOOP, it's because MMDVMHost is no longer writing | |
# to the log that we've been watching. Either the service was stopped | |
# during pi-star's nightly update, it has rolled to a new day's log, or the | |
# user has reconfiged or performed some other task that required pi-star to | |
# cycle the services. | |
# | |
# Watch for new log activity, then resume watching QSOs. | |
echo -e "\n\n${RED}${DBL_LINE}${WHI}${SGR0}${CLR_EL}" | |
echo "${RED}$(basename "${WORKING_LOG}") LOG ACTIVITY HAS STOPPED.${WHI}${SGR0}" | |
#---------- | |
# If we weren't replaying, then watch for logging to restart. | |
echo -n "${YEL}WATCHING FOR NEW LOG ACTIVITY...${WHI}${SGR0} " | |
OLD_LOG="${WORKING_LOG}" | |
while [[ ! "$(sudo fuser -v "${WORKING_LOG}" 2>&1)" =~ "MMDVMHost" ]] | |
do | |
sleep 1 | |
# Make sure we're working with latest log. | |
CANDIDATE_FILES=("${LOGDIR}/${LOGROOT}"*.log) NEWEST=${CANDIDATE_FILES[0]} | |
for F in "${CANDIDATE_FILES[@]}" | |
do | |
if [[ "${F}" -nt "${NEWEST}" ]] | |
then | |
NEWEST="${F}" | |
fi | |
done | |
WORKING_LOG="${NEWEST}" | |
done | |
#---------- | |
# Set the appropriate message for the history display, indicating why logging cycled. | |
if [[ "${OLD_LOG}" != "${WORKING_LOG}" ]] | |
then | |
echo -e "\n${GRE}LOG ACTIVITY HAS RESUMED - NOW USING $(basename "${WORKING_LOG}")...${WHI}${SGR0}" | |
echo "${GRE}${DIM}Daily MMDVM log rotation detected.${WHI}${SGR0}" | |
BOUNCE_MSG="--LOG ROTATION--" | |
else | |
echo -e "\n${GRE}LOG ACTIVITY HAS RESUMED - STILL USING $(basename "${WORKING_LOG}")...${WHI}${SGR0}" | |
echo "${GRE}${DIM}Service bounce detected (pi-star update, user reconfig, or other restart.)${WHI}${SGR0}" | |
BOUNCE_MSG="-SVCS RESTARTED-" | |
fi | |
#---------- | |
# Load the current history... | |
. "${HISTORY_FILE}" | |
# Push all the entries one position later, in the list. | |
# This opens the first position for the restart indicator. | |
HIST_COUNT="${HIST_MAX}" | |
until [[ "${HIST_COUNT}" -eq 0 ]] | |
do | |
HISTORY[HIST_COUNT]="${HISTORY[$((HIST_COUNT - 1))]}" | |
((HIST_COUNT--)) | |
done | |
# Insert the restart marker as the most recent history entry. | |
HISTORY[1]="${CYA}$(date +%H:%M:%S) ${SGR0}${WHI}${SBOX_VL} ---------- ${SBOX_VL} ---------- ${SBOX_VL} ----- ${SBOX_VL} ----- ${SBOX_VL} --- ${SBOX_VL}${CYA} ${BOUNCE_MSG} ${SGR0}${WHI}" | |
# Save the updated history to a file, newest through oldest. | |
rm "${HISTORY_FILE}" 2>/dev/null | |
HIST_COUNT="1" | |
until [[ "${HIST_COUNT}" -gt "${HIST_MAX}" ]] | |
do | |
echo -e "HISTORY[${HIST_COUNT}]=\"${HISTORY[HIST_COUNT]}\"" >> "${HISTORY_FILE}" | |
((HIST_COUNT++)) | |
done | |
#---------- | |
# Reexamine the activated modes, in case services were bounced due to | |
# configuration changes made in the GUI's "Configuration" page. | |
fnGET_MODES | |
#---------- | |
# Reexamine other details (such as log name, Master Servers, etc.), | |
# in case any values shown in the no-scroll zone have changed. | |
fnTOP_DATA | |
echo -e "${GRE}${DBL_LINE}${WHI}${SGR0}${CLR_EL}\n" | |
((LOG_RESTARTS++)) | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment