You asked an excellent question, not directly, but it made me think of a possible scenario: What if I turn on ALL the radios and increase the consumption? Well, at most you can use GPS and SDRs together — if you use everything at once, it can make the system unstable, and it might even crash or reboot depending on the kernel, drivers, and the fights between IOs, they clash from time to time, they’re kind of selfish! Well, in my setup, even turning on ALL the radios, even increasing the load, nothing changed in terms of temperature… it stays stable! And thank you for this question, I hadn’t thought about turning on all the radios at the same time!
#!/usr/bin/env bash
# uConsole CM5 HW Monitor v3 - btop-like layout
# No external UI framework. Uses ANSI cursor positioning and fixed panels.
INTERVAL=${INTERVAL:-1}
SMART_INTERVAL=${SMART_INTERVAL:-30}
AIOV2_INTERVAL=${AIOV2_INTERVAL:-3}
AIOV2_TIMEOUT=${AIOV2_TIMEOUT:-2}
BAR_W=${BAR_W:-18}
LOG_INTERVAL=${LOG_INTERVAL:-5}
LOG_FILE=${LOG_FILE:-memory_report.log}
LOG_ENABLED=${LOG_ENABLED:-1}
ESC=$'\033'
RESET="${ESC}[0m"
BOLD="${ESC}[1m"
DIM="${ESC}[2m"
GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"
RED="${ESC}[31m"
BLUE="${ESC}[36m"
MAG="${ESC}[35m"
WHITE="${ESC}[37m"
START_TS=$(date +%s)
LAST_TS=""
LAST_TEMP=""
FAN_ON_SEC=0
FAN_TOTAL_SEC=0
LAST_SMART_TS=0
SMART_CACHE="NVMES collecting..."
AIOV2_CACHE="N/A|N/A|N/A|N/A|N/A|not available"
LAST_AIOV2_TS=0
FAN_RESULT="N/A|not detected|--|--|off|0h00min|0"
PREV_CPU_TOTAL=""
PREV_CPU_IDLE=""
CPU_RESULT=0
GPU_TEMP="N/A"
GPU_TEMP_NOTE=""
TEMP_HISTORY_VALUES=()
TEMP_HISTORY_TIMES=()
THERMAL_TREND="baseline"
LAST_LOG_TS=0
cleanup() {
trap - EXIT INT TERM
printf "%b" "${ESC}[?25h${RESET}\n"
stty sane 2>/dev/null || true
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
printf "%b" "${ESC}[?25l"
term_rows() { tput lines 2>/dev/null || echo 24; }
term_cols() { tput cols 2>/dev/null || echo 80; }
mvcur() { printf "%b" "${ESC}[$1;$2H"; }
clreol() { printf "%b" "${ESC}[K"; }
line_at() {
local r="$1" c="$2" text="$3"
mvcur "$r" "$c"
printf "%b" "$text"
clreol
}
read_sys() { [[ -r "$1" ]] && cat "$1" 2>/dev/null || echo ""; }
csv_escape() {
local value="${1//$'\r'/ }"
value="${value//$'\n'/ }"
value="${value//\"/\"\"}"
printf '"%s"' "$value"
}
init_log_file() {
[[ "$LOG_ENABLED" == "1" ]] || return
if [[ ! -e "$LOG_FILE" || ! -s "$LOG_FILE" ]]; then
printf '%s\n' 'timestamp,epoch,uptime,cpu_usage_pct,load_1m,load_5m,load_15m,cpu_cores,cpu_freq_mhz,cpu_governor,ram_usage_pct,ram_used_gb,ram_total_gb,cpu_temp_c,gpu_temp_c,gpu_temp_source,nvme_temp_c,radio_temperatures,thermal_trend,throttle,gpu_clocks,battery_capacity_pct,battery_name,power_source,battery_status,battery_voltage_v,battery_current_a,battery_power_w,battery_energy_or_charge,battery_reported_health,battery_soc_status,fan_pct,fan_device,fan_type,fan_state_raw,fan_status,fan_on_time,fan_duty_pct,aiov2_gps,aiov2_lora,aiov2_sdr,aiov2_usb,aiov2_power,aiov2_raw,nvme_device,nvme_health,nvme_smart_temp_c,nvme_endurance_used_pct,nvme_spare,nvme_data_read,nvme_data_written,nvme_power_on_hours,nvme_unsafe_shutdowns,nvme_media_errors,nvme_log_entries,nvme_critical_warning,hwmon_sensors,pcie_links' >> "$LOG_FILE"
fi
}
log_snapshot() {
[[ "$LOG_ENABLED" == "1" ]] || return
local now_epoch="$1"
shift
(( now_epoch - LAST_LOG_TS >= LOG_INTERVAL )) || return
LAST_LOG_TS=$now_epoch
init_log_file
local first=1 value
{
for value in "$@"; do
if (( first )); then first=0; else printf ','; fi
csv_escape "$value"
done
printf '\n'
} >> "$LOG_FILE"
}
clamp_pct() {
local pct="${1%.*}"
[[ -z "$pct" || "$pct" == "N/A" ]] && pct=0
(( pct < 0 )) && pct=0
(( pct > 100 )) && pct=100
echo "$pct"
}
bar_plain() {
local pct color filled empty i
pct=$(clamp_pct "$1")
color="$2"
filled=$((pct * BAR_W / 100))
empty=$((BAR_W - filled))
printf "%b" "$color"
for ((i=0;i<filled;i++)); do printf "█"; done
printf "%b" "$DIM"
for ((i=0;i<empty;i++)); do printf "░"; done
printf "%b" "$RESET"
}
bar_high_bad() {
local pct color
pct=$(clamp_pct "$1")
if (( pct < 60 )); then color="$GREEN"; elif (( pct < 85 )); then color="$YELLOW"; else color="$RED"; fi
bar_plain "$pct" "$color"
}
bar_high_good() {
local pct color
pct=$(clamp_pct "$1")
if (( pct >= 60 )); then color="$GREEN"; elif (( pct >= 30 )); then color="$YELLOW"; else color="$RED"; fi
bar_plain "$pct" "$color"
}
status_temp_short() {
local t="$1"
[[ "$t" == "N/A" ]] && echo "N/A" && return
awk -v t="$t" 'BEGIN { if (t<40) print "OK cool"; else if (t<50) print "OK warm"; else if (t<60) print "ATT warm"; else if (t<70) print "BAD hot"; else print "CRIT" }'
}
status_color() {
case "$1" in
OK*) printf "%b%s%b" "$GREEN" "$1" "$RESET" ;;
ATT*|WARN*|CHECK*) printf "%b%s%b" "$YELLOW" "$1" "$RESET" ;;
BAD*|CRIT*) printf "%b%s%b" "$RED" "$1" "$RESET" ;;
*) printf "%b%s%b" "$BLUE" "$1" "$RESET" ;;
esac
}
fmt_seconds() { local s="$1"; printf "%dh%02dmin" $((s/3600)) $(((s%3600)/60)); }
fmt_time_h() {
awk -v h="$1" 'BEGIN { if(h<0)h=0; total=int(h*60+0.5); printf "%dh%02dmin", int(total/60), total%60 }'
}
find_battery() {
for p in /sys/class/power_supply/*; do
[[ -d "$p" ]] || continue
[[ "$(read_sys "$p/type")" == "Battery" ]] && echo "$p" && return
done
}
ac_online() {
for p in /sys/class/power_supply/*; do
[[ -d "$p" ]] || continue
case "$(read_sys "$p/type")" in
Mains|USB|USB_C|USB_PD|AC) [[ "$(read_sys "$p/online")" == "1" ]] && echo 1 && return ;;
esac
done
echo 0
}
thermal_by_type() {
local pattern="$1" z type v
for z in /sys/class/thermal/thermal_zone*; do
[[ -d "$z" ]] || continue
type=$(read_sys "$z/type")
[[ "$type" =~ $pattern ]] || continue
v=$(read_sys "$z/temp")
[[ "$v" =~ ^-?[0-9]+$ ]] || continue
awk -v v="$v" 'BEGIN {printf "%.1f", v/1000}'
return
done
echo "N/A"
}
temp_cpu() {
local t max="" z v c
t=$(thermal_by_type 'cpu|soc|package')
[[ "$t" != "N/A" ]] && { echo "$t"; return; }
for z in /sys/class/thermal/thermal_zone*/temp; do
[[ -r "$z" ]] || continue
v=$(read_sys "$z")
[[ "$v" =~ ^[0-9]+$ ]] || continue
(( v > 1000 )) || continue
c=$(awk -v v="$v" 'BEGIN {printf "%.1f", v/1000}')
if [[ -z "$max" ]] || awk -v c="$c" -v max="$max" 'BEGIN {exit !(c > max)}'; then max="$c"; fi
done
echo "${max:-N/A}"
}
temp_gpu() {
local t f name label v
GPU_TEMP="N/A"
GPU_TEMP_NOTE=""
t=$(thermal_by_type 'gpu|v3d')
if [[ "$t" != "N/A" ]]; then
GPU_TEMP="$t"
GPU_TEMP_NOTE="dedicated"
return
fi
for f in /sys/class/hwmon/hwmon*/temp*_input; do
[[ -r "$f" ]] || continue
name=$(read_sys "$(dirname "$f")/name")
label=$(read_sys "${f%_input}_label")
[[ "${name} ${label}" =~ [Gg][Pp][Uu]|[Vv]3[Dd] ]] || continue
v=$(read_sys "$f")
[[ "$v" =~ ^-?[0-9]+$ ]] || continue
GPU_TEMP=$(awk -v v="$v" 'BEGIN {printf "%.1f", v/1000}')
GPU_TEMP_NOTE="dedicated"
return
done
# On Raspberry Pi 5/CM5 the V3D GPU is part of the SoC and commonly has
# no separate thermal zone. In that case, use the SoC/CPU sensor explicitly.
t=$(temp_cpu)
if [[ "$t" != "N/A" ]]; then
GPU_TEMP="$t"
GPU_TEMP_NOTE="*"
fi
}
radio_temp_summary() {
local f name label v t found=0 out=""
for f in /sys/class/hwmon/hwmon*/temp*_input; do
[[ -r "$f" ]] || continue
name=$(read_sys "$(dirname "$f")/name")
label=$(read_sys "${f%_input}_label")
[[ "${name} ${label}" =~ [Rr][Tt][Ll]|[Ss][Dd][Rr]|[Ll][Oo][Rr][Aa]|[Gg][Pp][Ss]|[Rr][Aa][Dd][Ii][Oo]|[Ww][Ii][Ff][Ii]|[Ww][Ll][Aa][Nn]|mt7[0-9]+ ]] || continue
v=$(read_sys "$f")
[[ "$v" =~ ^-?[0-9]+$ ]] || continue
t=$(awk -v v="$v" 'BEGIN {printf "%.1f", v/1000}')
[[ -n "$out" ]] && out+=", "
out+="${label:-$name} ${t}C"
found=1
done
(( found )) && echo "$out" || echo "N/A (no radio thermal sensor)"
}
cpu_usage() {
local _ user nice system idle iowait irq softirq steal total idle_all diff_total diff_idle usage
read -r _ user nice system idle iowait irq softirq steal _ < /proc/stat
idle_all=$((idle + iowait))
total=$((user + nice + system + idle + iowait + irq + softirq + steal))
if [[ -n "$PREV_CPU_TOTAL" ]]; then
diff_total=$((total - PREV_CPU_TOTAL))
diff_idle=$((idle_all - PREV_CPU_IDLE))
if (( diff_total > 0 )); then
usage=$((100 * (diff_total - diff_idle) / diff_total))
else
usage=0
fi
else
usage=0
fi
PREV_CPU_TOTAL=$total
PREV_CPU_IDLE=$idle_all
CPU_RESULT=$usage
}
update_thermal_trend() {
local current="$1" now cutoff first_v first_t last_v last_t rate i
if [[ ! "$current" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
THERMAL_TREND="N/A"
return
fi
now=$(date +%s)
TEMP_HISTORY_VALUES+=("$current")
TEMP_HISTORY_TIMES+=("$now")
cutoff=$((now - 30))
while (( ${#TEMP_HISTORY_TIMES[@]} > 1 )) && (( TEMP_HISTORY_TIMES[0] < cutoff )); do
TEMP_HISTORY_TIMES=("${TEMP_HISTORY_TIMES[@]:1}")
TEMP_HISTORY_VALUES=("${TEMP_HISTORY_VALUES[@]:1}")
done
if (( ${#TEMP_HISTORY_TIMES[@]} < 2 )); then
THERMAL_TREND="baseline"
LAST_LOG_TS=0
return
fi
first_v=${TEMP_HISTORY_VALUES[0]}
first_t=${TEMP_HISTORY_TIMES[0]}
i=$((${#TEMP_HISTORY_TIMES[@]} - 1))
last_v=${TEMP_HISTORY_VALUES[$i]}
last_t=${TEMP_HISTORY_TIMES[$i]}
if (( last_t <= first_t )); then
THERMAL_TREND="stable"
return
fi
rate=$(awk -v a="$first_v" -v b="$last_v" -v dt="$((last_t-first_t))" 'BEGIN {printf "%.1f", ((b-a)/dt)*60}')
if awk -v r="$rate" 'BEGIN {exit !(r > -0.15 && r < 0.15)}'; then
THERMAL_TREND="stable"
else
THERMAL_TREND="${rate}C/min (30s)"
fi
}
temp_nvme() {
local f name v t
for f in /sys/class/hwmon/hwmon*/temp*_input; do
[[ -r "$f" ]] || continue
name=$(cat "$(dirname "$f")/name" 2>/dev/null)
case "$name" in
nvme|*nvme*)
v=$(cat "$f" 2>/dev/null)
[[ "$v" =~ ^[0-9]+$ ]] || continue
t=$(awk "BEGIN {printf \"%.1f\", $v/1000}")
echo "$t"; return ;;
esac
done
echo "N/A"
}
cpu_freq() { local f="/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"; [[ -r "$f" ]] && awk '{printf "%d", $1/1000}' "$f" || echo "N/A"; }
cpu_governor() { local f="/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"; [[ -r "$f" ]] && cat "$f" || echo "N/A"; }
ram_info() {
awk '
/MemTotal:/ {total=$2}
/MemAvailable:/ {avail=$2}
END {used=total-avail; pct=(used/total)*100; printf "%.0f %.1f %.1f", pct, used/1048576, total/1048576}' /proc/meminfo
}
throttle_info() {
if command -v vcgencmd >/dev/null 2>&1; then
local out code
out=$(vcgencmd get_throttled 2>/dev/null)
code=${out#*=}
[[ -z "$code" || "$code" == "$out" ]] && { echo "N/A CHECK"; return; }
if [[ "$code" == "0x0" ]]; then echo "0x0 OK"; else echo "$code CHECK"; fi
else
echo "N/A no-vcgencmd"
fi
}
gpu_info() {
local v3d="N/A" core="N/A" out
if command -v vcgencmd >/dev/null 2>&1; then
out=$(vcgencmd measure_clock v3d 2>/dev/null); [[ "$out" == frequency* ]] && v3d=$(awk -F= '{printf "%d", $2/1000000}' <<<"$out")
out=$(vcgencmd measure_clock core 2>/dev/null); [[ "$out" == frequency* ]] && core=$(awk -F= '{printf "%d", $2/1000000}' <<<"$out")
fi
echo "V3D ${v3d}MHz CORE ${core}MHz"
}
pcie_lines() {
local n=0 d speed width
for d in /sys/bus/pci/devices/*; do
[[ -d "$d" ]] || continue
speed=$(read_sys "$d/current_link_speed")
width=$(read_sys "$d/current_link_width")
[[ -z "$speed" && -z "$width" ]] && continue
n=$((n+1))
printf "%-12s %s x%s\n" "$(basename "$d")" "${speed:-?}" "${width:-?}"
(( n >= 4 )) && break
done
(( n == 0 )) && echo "N/A"
}
sensors_lines() {
local count=0 f name v t base line seen=""
for f in /sys/class/hwmon/hwmon*/temp*_input; do
[[ -r "$f" ]] || continue
base=$(dirname "$f")
name=$(cat "$base/name" 2>/dev/null)
[[ -z "$name" ]] && name="$(basename "$base")"
v=$(cat "$f" 2>/dev/null)
[[ "$v" =~ ^[0-9]+$ ]] || continue
t=$(awk "BEGIN {printf \"%.1f\", $v/1000}")
line="${name} ${t}"
[[ "$seen" == *"|$line|"* ]] && continue
seen+="|$line|"
count=$((count+1))
printf "%-14s %5s C %s\n" "$name" "$t" "$(status_temp_short "$t")"
(( count >= 7 )) && break
done
(( count == 0 )) && echo "N/A"
}
status_soc_short() {
local c="$1"
[[ -z "$c" || "$c" == "N/A" ]] && echo "N/A" && return
if (( c >= 80 )); then echo "OK high"; elif (( c >= 40 )); then echo "OK norm"; elif (( c >= 20 )); then echo "ATT charge"; elif (( c >= 10 )); then echo "BAD low"; else echo "CRIT"; fi
}
health_from_energy_short() {
local full="$1" design="$2"
[[ -z "$full" ]] && echo "N/A" && return
if [[ -n "$design" && "$design" -gt 0 ]]; then
awk -v f="$full" -v d="$design" 'BEGIN {h=(f/d)*100; if(h>100)h=100; if(h>=90)printf "OK %.0f%%",h; else if(h>=80)printf "OK wear %.0f%%",h; else if(h>=70)printf "ATT %.0f%%",h; else printf "BAD %.0f%%",h}'
else
awk -v f="$full" 'BEGIN {printf "%.1fWh", f/1000000}'
fi
}
battery_info() {
local BAT ONLINE CAP STAT VOLT_RAW CURR_RAW PWR_RAW ENOW EFULL EDESIGN VOLT_A CURR_A PWR_W NAME ENERGY_MODE
BAT=$(find_battery); ONLINE=$(ac_online)
if [[ -z "$BAT" ]]; then echo "BAT N/A"; return; fi
NAME=$(basename "$BAT")
CAP=$(read_sys "$BAT/capacity"); [[ -z "$CAP" ]] && CAP="N/A"
STAT=$(read_sys "$BAT/status"); [[ -z "$STAT" ]] && STAT="Unknown"
VOLT_RAW=$(read_sys "$BAT/voltage_now"); CURR_RAW=$(read_sys "$BAT/current_now"); PWR_RAW=$(read_sys "$BAT/power_now")
ENERGY_MODE="energy"
ENOW=$(read_sys "$BAT/energy_now"); EFULL=$(read_sys "$BAT/energy_full"); EDESIGN=$(read_sys "$BAT/energy_full_design")
if [[ -z "$ENOW" || -z "$EFULL" ]]; then
ENERGY_MODE="charge"
ENOW=$(read_sys "$BAT/charge_now")
EFULL=$(read_sys "$BAT/charge_full")
EDESIGN=$(read_sys "$BAT/charge_full_design")
fi
[[ -n "$VOLT_RAW" ]] && VOLT_A=$(awk "BEGIN {printf \"%.2f\", $VOLT_RAW/1000000}") || VOLT_A="N/A"
[[ -n "$CURR_RAW" ]] && CURR_A=$(awk "BEGIN {printf \"%.2f\", $CURR_RAW/1000000}") || CURR_A="N/A"
if [[ -n "$PWR_RAW" && "$PWR_RAW" =~ ^[0-9]+$ && "$PWR_RAW" -gt 0 ]]; then PWR_W=$(awk "BEGIN {printf \"%.2f\", $PWR_RAW/1000000}");
elif [[ "$VOLT_A" != "N/A" && "$CURR_A" != "N/A" ]]; then PWR_W=$(awk "BEGIN {printf \"%.2f\", $VOLT_A*$CURR_A}"); else PWR_W="N/A"; fi
local src="BAT"; [[ "$ONLINE" == 1 ]] && src="USB-C"
local eng="N/A" health="N/A"
if [[ -n "$ENOW" && -n "$EFULL" ]]; then
if [[ "$ENERGY_MODE" == "energy" ]]; then
eng=$(awk -v n="$ENOW" -v f="$EFULL" 'BEGIN {printf "%.2f/%.2fWh", n/1000000, f/1000000}')
else
eng=$(awk -v n="$ENOW" -v f="$EFULL" 'BEGIN {printf "%.2f/%.2fAh", n/1000000, f/1000000}')
fi
health=$(health_from_energy_short "$EFULL" "$EDESIGN")
fi
echo "$CAP|$NAME|$src|$STAT|$VOLT_A|$CURR_A|$PWR_W|$eng|$health|$(status_soc_short "$CAP")"
}
fan_info() {
local dev type cur max pct delta duty
for dev in /sys/class/thermal/cooling_device*; do
[[ -d "$dev" ]] || continue
type=$(read_sys "$dev/type")
[[ "$type" =~ fan|Fan ]] || continue
cur=$(read_sys "$dev/cur_state"); max=$(read_sys "$dev/max_state")
[[ -z "$cur" ]] && cur=0; [[ -z "$max" || "$max" == 0 ]] && max=255
pct=$(awk "BEGIN {printf \"%.0f\", ($cur/$max)*100}")
FAN_TOTAL_SEC=$((FAN_TOTAL_SEC + INTERVAL))
(( cur > 0 )) && FAN_ON_SEC=$((FAN_ON_SEC + INTERVAL))
duty=$(awk "BEGIN {printf \"%.0f\", ($FAN_ON_SEC/$FAN_TOTAL_SEC)*100}")
FAN_RESULT="$pct|$(basename "$dev")|$type|$cur/$max|$([[ $cur -gt 0 ]] && echo on || echo off)|$(fmt_seconds "$FAN_ON_SEC")|$duty"
return
done
FAN_RESULT="N/A|not detected|--|--|off|0h00min|0"
}
aiov2_update() {
local now out line gps lora sdr usb power
now=$(date +%s)
(( now - LAST_AIOV2_TS < AIOV2_INTERVAL )) && return
LAST_AIOV2_TS=$now
command -v aiov2_ctl >/dev/null 2>&1 || { AIOV2_CACHE="N/A|N/A|N/A|N/A|N/A|aiov2_ctl not installed"; return; }
out=$(timeout "${AIOV2_TIMEOUT}s" aiov2_ctl --watch 2>/dev/null | tr '\r' '\n' | tail -n 1)
[[ -z "$out" ]] && { AIOV2_CACHE="N/A|N/A|N/A|N/A|N/A|no watch output"; return; }
line=$(sed 's/\x1b\[[0-9;]*m//g' <<<"$out")
parse_state() {
local key="$1" val
val=$(grep -Eio "${key}[[:space:]:=_-]*(ON|OFF|UNKNOWN|HI|LO)" <<<"$line" | head -n1 | grep -Eio 'ON|OFF|UNKNOWN|HI|LO' | head -n1)
case "$val" in HI) val="ON";; LO) val="OFF";; esac
echo "${val:-N/A}"
}
gps=$(parse_state GPS); lora=$(parse_state LORA); sdr=$(parse_state SDR); usb=$(parse_state USB)
power=$(grep -Eo '[-+]?[0-9]+([.][0-9]+)?[[:space:]]*W' <<<"$line" | tail -n1 | tr -d ' ')
AIOV2_CACHE="$gps|$lora|$sdr|$usb|${power:-N/A}|$line"
}
smart_update() {
local now dev out health temp used spare thres read written hours unsafe media log crit
now=$(date +%s)
(( now - LAST_SMART_TS < SMART_INTERVAL )) && return
LAST_SMART_TS=$now
for dev in /dev/nvme0 /dev/nvme0n1; do
[[ -e "$dev" ]] || continue
out=$(sudo -n smartctl -a "$dev" 2>/dev/null || smartctl -a "$dev" 2>/dev/null)
[[ "$out" == *"SMART/Health Information"* ]] || continue
health=$(awk -F: '/SMART overall-health/{gsub(/^ +/,"",$2); print $2; exit}' <<<"$out")
temp=$(awk -F: '/^Temperature:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
used=$(awk -F: '/Percentage Used:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
spare=$(awk -F: '/Available Spare:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
thres=$(awk -F: '/Available Spare Threshold:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
read=$(awk -F'[][]' '/Data Units Read:/{print $2; exit}' <<<"$out")
written=$(awk -F'[][]' '/Data Units Written:/{print $2; exit}' <<<"$out")
hours=$(awk -F: '/Power On Hours:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
unsafe=$(awk -F: '/Unsafe Shutdowns:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
media=$(awk -F: '/Media and Data Integrity Errors:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
log=$(awk -F: '/Error Information Log Entries:/{gsub(/[^0-9]/,"",$2); print $2; exit}' <<<"$out")
crit=$(awk -F: '/Critical Warning:/{gsub(/^ +/,"",$2); print $2; exit}' <<<"$out")
SMART_CACHE="$dev|${health:-N/A}|${temp:-N/A}|${used:-N/A}|${spare:-N/A}/${thres:-N/A}|${read:-N/A}|${written:-N/A}|${hours:-N/A}|${unsafe:-N/A}|${media:-N/A}|${log:-N/A}|${crit:-N/A}"
return
done
SMART_CACHE="N/A|unable to read SMART|N/A|N/A|N/A|N/A|N/A|N/A|N/A|N/A|N/A|N/A"
}
box_hline() { local w="$1" i; printf "┌"; for ((i=0;i<w-2;i++)); do printf "─"; done; printf "┐"; }
bot_hline() { local w="$1" i; printf "└"; for ((i=0;i<w-2;i++)); do printf "─"; done; printf "┘"; }
sep_line() { local w="$1" i; printf "├"; for ((i=0;i<w-2;i++)); do printf "─"; done; printf "┤"; }
inner_rule() { local w="$1" i; printf ""; for ((i=0;i<w;i++)); do printf "─"; done; }
strip_ansi() { sed $'s/\033\\[[0-9;]*m//g' <<<"$1"; }
pad_line() {
local content="$1" w="$2" plain len
plain=$(strip_ansi "$content")
len=${#plain}
printf "│%b" "$content"
if (( len < w-2 )); then printf "%*s" $((w-2-len)) ""; fi
printf "│"
}
row2() {
local r="$1" l="$2" rr="$3" pw="$4" gap_col=$((pw+1))
mvcur "$r" 1; pad_line "$l" "$pw"
mvcur "$r" "$gap_col"; pad_line "$rr" "$pw"
clreol
}
fmt_uptime() {
local up; up=$(cut -d. -f1 /proc/uptime 2>/dev/null || echo 0)
printf "%02d:%02d:%02d" $((up/3600)) $(((up%3600)/60)) $((up%60))
}
kernel_short() { uname -r 2>/dev/null | cut -c1-20; }
arch_short() { uname -m 2>/dev/null; }
platform_short() {
if [[ -r /proc/device-tree/model ]]; then tr -d '\0' </proc/device-tree/model | sed 's/Raspberry Pi/RPi/;s/Compute Module/CM/' | cut -c1-24
else echo "CM5/Linux"; fi
}
# btop/neon-style fixed dashboard. It redraws the dashboard block in place, not the scrollback.
draw() {
local cols rows pw w now load cores load1 loadp cpu_pct freq temp gpu_temp radio_temp nvme gov ram throt gpu batt fan smart sensors pcie uptime trend
cols=$(term_cols); rows=$(term_rows)
if (( cols < 120 || rows < 32 )); then
printf "%b" "${ESC}[H${ESC}[2J"
printf "Terminal too small. Required: 120x32 | Current: %sx%s\n" "$cols" "$rows"
printf "Resize the terminal or reduce the font size. Press q to exit.\n"
return
fi
pw=$(((cols-3)/2)); (( pw > 70 )) && pw=70; (( pw < 58 )) && pw=58
w=$((pw*2))
now=$(date '+%Y-%m-%d %H:%M:%S')
load=$(cut -d' ' -f1-3 /proc/loadavg); cores=$(nproc); load1=$(awk '{print $1}' <<<"$load")
loadp=$(awk -v l="$load1" -v c="$cores" 'BEGIN {printf "%.0f", (l/c)*100}')
cpu_usage; cpu_pct="$CPU_RESULT"
freq=$(cpu_freq); temp=$(temp_cpu); temp_gpu; gpu_temp="$GPU_TEMP"; radio_temp=$(radio_temp_summary); nvme=$(temp_nvme); gov=$(cpu_governor); ram=($(ram_info)); throt=$(throttle_info); gpu=$(gpu_info)
batt=$(battery_info); fan_info; fan="$FAN_RESULT"; smart_update; aiov2_update
sensors=$(sensors_lines); pcie=$(pcie_lines); uptime=$(fmt_uptime)
update_thermal_trend "$temp"
trend="$THERMAL_TREND"
local bcap bname bsrc bstat bvolt bcurr bpwr beng bhealth bsoc
IFS='|' read -r bcap bname bsrc bstat bvolt bcurr bpwr beng bhealth bsoc <<<"$batt"
local fpct fdev ftype fpwm fstat ftime fduty
IFS='|' read -r fpct fdev ftype fpwm fstat ftime fduty <<<"$fan"
local sm_dev sm_health sm_temp sm_used sm_spare sm_read sm_wr sm_hours sm_unsafe sm_media sm_log sm_crit life_status unsafe_status
IFS='|' read -r sm_dev sm_health sm_temp sm_used sm_spare sm_read sm_wr sm_hours sm_unsafe sm_media sm_log sm_crit <<<"$SMART_CACHE"
if [[ "$sm_used" =~ ^[0-9]+$ ]]; then
if (( sm_used >= 100 )); then life_status="CRIT"; elif (( sm_used >= 90 )); then life_status="BAD"; elif (( sm_used >= 70 )); then life_status="ATT"; else life_status="OK"; fi
else life_status="N/A"; fi
[[ "$sm_unsafe" =~ ^[0-9]+$ ]] && (( sm_unsafe >= 50 )) && unsafe_status="ATT" || unsafe_status="OK"
local agps allora asdr ausb apower araw
IFS='|' read -r agps allora asdr ausb apower araw <<<"$AIOV2_CACHE"
local now_epoch load_1 load_5 load_15 sensors_csv pcie_csv
now_epoch=$(date +%s)
read -r load_1 load_5 load_15 <<<"$load"
sensors_csv=$(tr '\n' ';' <<<"$sensors" | sed 's/;*$//')
pcie_csv=$(tr '\n' ';' <<<"$pcie" | sed 's/;*$//')
log_snapshot "$now_epoch" \
"$now" "$now_epoch" "$uptime" "$cpu_pct" "$load_1" "$load_5" "$load_15" "$cores" "$freq" "$gov" \
"${ram[0]}" "${ram[1]}" "${ram[2]}" "$temp" "$gpu_temp" "$GPU_TEMP_NOTE" "$nvme" "$radio_temp" "$trend" "$throt" "$gpu" \
"$bcap" "$bname" "$bsrc" "$bstat" "$bvolt" "$bcurr" "$bpwr" "$beng" "$bhealth" "$bsoc" \
"$fpct" "$fdev" "$ftype" "$fpwm" "$fstat" "$ftime" "$fduty" \
"$agps" "$allora" "$asdr" "$ausb" "$apower" "$araw" \
"$sm_dev" "$sm_health" "$sm_temp" "$sm_used" "$sm_spare" "$sm_read" "$sm_wr" "$sm_hours" "$sm_unsafe" "$sm_media" "$sm_log" "$sm_crit" \
"$sensors_csv" "$pcie_csv"
printf "%b" "${ESC}[H"
line_at 1 1 "${GREEN}──────${RESET} ${BOLD}uConsole CM5 HW Monitor${RESET} ${now} | q/CTRL+C exit | log ${LOG_INTERVAL}s: ${LOG_FILE} | btop-like ${GREEN}──────${RESET}"
mvcur 2 1; printf "%b" "${GREEN}"; box_hline "$pw"; printf "%b" "$RESET"; mvcur 2 $((pw+1)); printf "%b" "${YELLOW}"; box_hline "$pw"; printf "%b" "$RESET"; clreol
row2 3 " ${GREEN}${BOLD}CPU / SYSTEM${RESET}" " ${YELLOW}${BOLD}BATTERY / POWER / FAN${RESET}" "$pw"
row2 4 " CPU Usage $(bar_high_bad "$cpu_pct") ${GREEN}${cpu_pct}%${RESET}" " Battery $(bar_high_good "$bcap") ${GREEN}${bcap}%${RESET}" "$pw"
row2 5 " Load Avg ${GREEN}${load}${RESET}" " Source ${GREEN}${bsrc}${RESET} State ${GREEN}${bstat}${RESET}" "$pw"
row2 6 " Freq ${GREEN}${freq} MHz${RESET}" " Voltage ${GREEN}${bvolt} V${RESET} Current ${GREEN}${bcurr} A${RESET}" "$pw"
row2 7 " Governor ${GREEN}${gov}${RESET}" " Power ${GREEN}${bpwr} W${RESET} Energy ${GREEN}${beng}${RESET}" "$pw"
row2 8 " RAM $(bar_high_bad "${ram[0]}") ${GREEN}${ram[0]}% ${ram[1]}/${ram[2]} GB${RESET}" " Health $(status_color "$bhealth")" "$pw"
row2 9 " Temp (CPU) $(bar_high_bad "${temp%.*}") ${GREEN}${temp} °C${RESET} $(status_color "$(status_temp_short "$temp")")" " Status $(status_color "$bsoc") Name ${GREEN}${bname}${RESET}" "$pw"
row2 10 " NVMe Temp $(bar_high_bad "${nvme%.*}") ${GREEN}${nvme} °C${RESET} $(status_color "$(status_temp_short "$nvme")")" " ---------------------------------------------------- " "$pw"
row2 11 " Throttle $(status_color "$throt")" " Fan $(bar_high_bad "$fpct") ${GREEN}${fpct}%${RESET}" "$pw"
row2 12 " GPU Temp $(bar_high_bad "${gpu_temp%.*}") ${GREEN}${gpu_temp} °C${RESET} (${GPU_TEMP_NOTE}) ${gpu}" " Device ${GREEN}${fdev}${RESET} PWM ${GREEN}${fpwm}${RESET}" "$pw"
row2 13 " Radio Temp ${BLUE}${radio_temp}${RESET}" " State ${GREEN}${fstat}${RESET} Duty ${GREEN}${fduty}%${RESET}" "$pw"
mvcur 14 1; printf "%b" "${GREEN}"; bot_hline "$pw"; printf "%b" "$RESET"; mvcur 14 $((pw+1)); printf "%b" "${YELLOW}"; bot_hline "$pw"; printf "%b" "$RESET"; clreol
mvcur 15 1; printf "%b" "${BLUE}"; box_hline "$pw"; printf "%b" "$RESET"; mvcur 15 $((pw+1)); printf "%b" "${MAG}"; box_hline "$pw"; printf "%b" "$RESET"; clreol
row2 16 " ${BLUE}${BOLD}SENSORS (HWMON)${RESET}" " ${MAG}${BOLD}NVMe SMART${RESET}" "$pw"
local i=0 sline name tv stat qual
while IFS= read -r sline; do
((i++)); ((i>7)) && break
name=$(awk '{print $1}' <<<"$sline"); tv=$(awk '{print $2}' <<<"$sline"); stat=$(awk '{print $4}' <<<"$sline"); qual=$(awk '{print $5}' <<<"$sline")
local leftline=" ${name}"
leftline+="$(printf '%*s' $((18-${#name})) '')"
leftline+="${BLUE}${tv} °C${RESET} $(status_color "$stat") ${qual}"
case $i in
1) row2 $((16+i)) "$leftline" " Device ${sm_dev} Health $(status_color "$sm_health")" "$pw" ;;
2) row2 $((16+i)) "$leftline" " Temp ${sm_temp} °C Critical ${sm_crit}" "$pw" ;;
3) row2 $((16+i)) "$leftline" " Endurance Used ${sm_used}% Spare ${sm_spare} $(status_color "$life_status")" "$pw" ;;
4) row2 $((16+i)) "$leftline" " Read ${sm_read} Written ${sm_wr}" "$pw" ;;
5) row2 $((16+i)) "$leftline" " Hours ${sm_hours} h Unsafe Shutdowns ${sm_unsafe} $(status_color "$unsafe_status")" "$pw" ;;
6) row2 $((16+i)) "$leftline" " Media Errors ${sm_media} Log Entries ${sm_log}" "$pw" ;;
7) row2 $((16+i)) "$leftline" " Warning Composite Temperature Time N/A" "$pw" ;;
esac
done <<<"$sensors"
while (( i < 7 )); do
((i++));
case $i in
1) row2 $((16+i)) " -- -- -- --" " Device ${sm_dev} Health $(status_color "$sm_health")" "$pw" ;;
2) row2 $((16+i)) " -- -- -- --" " Temp ${sm_temp} °C Critical ${sm_crit}" "$pw" ;;
3) row2 $((16+i)) " -- -- -- --" " Endurance Used ${sm_used}% Spare ${sm_spare} $(status_color "$life_status")" "$pw" ;;
4) row2 $((16+i)) " -- -- -- --" " Read ${sm_read} Written ${sm_wr}" "$pw" ;;
5) row2 $((16+i)) " -- -- -- --" " Hours ${sm_hours} h Unsafe Shutdowns ${sm_unsafe} $(status_color "$unsafe_status")" "$pw" ;;
6) row2 $((16+i)) " -- -- -- --" " Media Errors ${sm_media} Log Entries ${sm_log}" "$pw" ;;
7) row2 $((16+i)) " -- -- -- --" " SMART refresh every ${SMART_INTERVAL}s" "$pw" ;;
esac
done
mvcur 24 1; printf "%b" "${BLUE}"; bot_hline "$pw"; printf "%b" "$RESET"; mvcur 24 $((pw+1)); printf "%b" "${MAG}"; bot_hline "$pw"; printf "%b" "$RESET"; clreol
mvcur 25 1; printf "%b" "$WHITE"; box_hline "$w"; printf "%b" "$RESET"; clreol
line_at 26 1 "│ ${WHITE}${BOLD} PCIe /DIAGNOSTICS ${RESET}$(printf '%*s' $((w-22)) '')│"
local pl row=27 diag idx
mapfile -t pciel < <(pcie_lines)
for idx in 0 1 2 3; do
pl="${pciel[$idx]:---}"
case $idx in
0) diag="Architecture $(arch_short) | Thermal Trend ${trend}" ;;
1) diag="Kernel $(kernel_short) | Throttling ${throt}" ;;
2) diag="Platform $(platform_short) | AIO GPS ${agps} LORA ${allora}" ;;
3) diag="Uptime ${uptime} | AIO SDR ${asdr} USB ${ausb} Power ${apower}" ;;
esac
line_at "$row" 1 "│ PCIE_${pl} | ${diag}"
mvcur "$row" "$w"; printf "│"; clreol
((row++))
done
mvcur 31 1; printf "%b" "$WHITE"; bot_hline "$w"; printf "%b" "$RESET"; clreol
line_at 32 1 "${BLUE}Tip:${RESET} terminal font size is controlled by terminal/SSH client, not Bash. ${BLUE}CSV log: ${LOG_FILE} every ${LOG_INTERVAL}s. SMART may need sudo NOPASSWD.${RESET}"
}
init_log_file
printf "%b" "${ESC}[2J${ESC}[H"
cpu_usage
sleep 0.2
while true; do
draw
if read -rsn1 -t "$INTERVAL" key; then
case "$key" in q|Q) break ;; esac
fi
done
It will create one new entry every 5 second (Or you can define the capture interval), recording all monitored values (58 values) into the memory_report.log file. The script is fully parameterized, so the logging interval, directories, and all other configuration settings can be easily adjusted to suit your needs.
I also included the GPU Temp and Radios Power. Since the temperature comes from a shared SoC sensor, it took a bit more work to measure it accurately.