tools: leds: add m4-led-perf-test.sh

Bash/sh script (BusyBox compatible) to stress-test the WAGO M4 RGB LED
strip driver by activating kernel LED triggers on all ten strip LEDs
(sys, run, io, em, u1-u6).

Three trigger modes selectable via --trigger=<mode>:

  pattern   (default)
    Phase 1 - FADE:       staggered smooth brightness ramp (200 ms
              offset per LED) using the pattern trigger
    Phase 2 - FAST FLASH: 50 ms on/off in distinct colours per LED
    Phase 3 - SLOW FLASH: 400 ms on/off with colour cycling

  timer
    Simple on/off blink with staggered delay_on/delay_off values
    (50 ms .. 500 ms) and per-LED colours.

  heartbeat
    Kernel heartbeat trigger, all LEDs white.

Additional options:
  --duration=<sec>   Total test duration in seconds (default: 30)
  --version          Print version string
  --help             Print usage

Cleanup handler (trap INT/TERM) restores trigger=none + brightness=0
on all LEDs when the test ends or is interrupted.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
This commit is contained in:
Heinrich Toews
2026-04-09 14:38:37 +02:00
parent 4f8924ebcd
commit b197308901
+384
View File
@@ -0,0 +1,384 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
#
# m4-led-perf-test.sh - Performance / stress test for WAGO M4 RGB LED strip
#
# Exercises all ten M4 strip LEDs (sys, run, io, em, u1-u6) by activating
# kernel LED triggers at various frequencies and patterns. Designed to run
# on a BusyBox-based target (ash/sh compatible, no bashisms).
#
# Triggers supported:
# pattern - (default) three phases: fade, flash-fast, flash-slow
# timer - simple on/off blink at staggered frequencies per LED
# heartbeat - kernel heartbeat trigger (frequency fixed by kernel)
#
# Usage:
# m4-led-perf-test.sh [--trigger=<pattern|timer|heartbeat>]
# [--duration=<seconds>]
# [--help] [--version]
#
# Author: WAGO GmbH & Co. KG
VERSION="1.0.0"
LED_SYSFS="/sys/class/leds"
# All M4 strip LED names (must match DT 'label' properties)
LEDS="sys run io em u1 u2 u3 u4 u5 u6"
# Default options
OPT_TRIGGER="pattern"
OPT_DURATION=30
# ---------------------------------------------------------------------------
# Logging helpers
# ---------------------------------------------------------------------------
log() { echo "[ m4-led-test ] $*"; }
info() { log "INFO $*"; }
warn() { log "WARN $*"; }
err() { log "ERROR $*" >&2; }
die() { err "$*"; cleanup; exit 1; }
# ---------------------------------------------------------------------------
# Usage / version
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Performance / stress test for the WAGO M4 RGB LED strip.
Activates kernel LED triggers on all ten strip LEDs at various
frequencies to stress the RPMsg / M4 communication path.
Options:
--trigger=<mode> Trigger mode to use (default: pattern)
pattern - three phases: fade in/out, fast flash,
slow flash with colour cycling
timer - simple on/off blink at staggered
frequencies (10 ms .. 500 ms)
heartbeat - kernel heartbeat trigger
--duration=<sec> How long to run the test in seconds (default: 30)
--version Print version and exit
--help Print this help and exit
Examples:
$(basename "$0")
$(basename "$0") --trigger=timer --duration=60
$(basename "$0") --trigger=heartbeat
EOF
}
# ---------------------------------------------------------------------------
# Argument parsing (BusyBox-compatible, no getopt)
# ---------------------------------------------------------------------------
for arg in "$@"; do
case "$arg" in
--help)
usage
exit 0
;;
--version)
echo "$(basename "$0") version $VERSION"
exit 0
;;
--trigger=*)
OPT_TRIGGER="${arg#--trigger=}"
;;
--duration=*)
OPT_DURATION="${arg#--duration=}"
;;
*)
err "Unknown option: $arg"
usage >&2
exit 1
;;
esac
done
# Validate trigger
case "$OPT_TRIGGER" in
pattern|timer|heartbeat) ;;
*) die "Invalid trigger '$OPT_TRIGGER'. Choose: pattern, timer, heartbeat" ;;
esac
# Validate duration (must be a positive integer)
case "$OPT_DURATION" in
''|*[!0-9]*) die "Invalid duration '$OPT_DURATION': must be a positive integer" ;;
esac
[ "$OPT_DURATION" -gt 0 ] || die "Duration must be > 0"
# ---------------------------------------------------------------------------
# LED sysfs helpers
# ---------------------------------------------------------------------------
# led_path <name> -> /sys/class/leds/<name>
led_path() { echo "${LED_SYSFS}/$1"; }
# led_write <name> <file> <value>
led_write() {
local path
path="$(led_path "$1")/$2"
if [ ! -w "$path" ]; then
warn "Not writable: $path — skipping"
return 1
fi
printf '%s' "$3" > "$path" 2>/dev/null || warn "Write failed: $path <- $3"
}
# led_set_trigger <name> <trigger>
led_set_trigger() { led_write "$1" trigger "$2"; }
# led_set_brightness <name> <value 0-255>
led_set_brightness() { led_write "$1" brightness "$2"; }
# led_set_multi_intensity <name> <R> <G> <B>
led_set_multi_intensity() { led_write "$1" multi_intensity "$2 $3 $4"; }
# led_set_pattern <name> <pattern-string>
led_set_pattern() { led_write "$1" pattern "$2"; }
# Check which LEDs are actually present in sysfs
check_leds() {
local found=0 missing=0 name
for name in $LEDS; do
if [ -d "$(led_path "$name")" ]; then
found=$((found + 1))
else
warn "LED '$name' not found in $LED_SYSFS — skipping"
missing=$((missing + 1))
fi
done
info "Found $found / $(echo $LEDS | wc -w) expected LEDs"
[ "$found" -gt 0 ] || die "No M4 LEDs found in $LED_SYSFS. Is the driver loaded?"
}
# Return only the LEDs that exist in sysfs
present_leds() {
local name
for name in $LEDS; do
[ -d "$(led_path "$name")" ] && printf '%s ' "$name"
done
}
# ---------------------------------------------------------------------------
# Cleanup: restore all LEDs to 'none' trigger and brightness=0
# ---------------------------------------------------------------------------
cleanup() {
info "Restoring all LEDs (trigger=none, brightness=0) ..."
local name
for name in $(present_leds); do
led_set_trigger "$name" "none"
led_set_brightness "$name" 0
done
info "Cleanup done."
}
# Trap SIGINT / SIGTERM so Ctrl-C always restores LEDs
trap 'info "Interrupted — cleaning up ..."; cleanup; exit 130' INT TERM
# ---------------------------------------------------------------------------
# Trigger implementations
# ---------------------------------------------------------------------------
# --- HEARTBEAT --------------------------------------------------------------
run_heartbeat() {
info "Setting trigger=heartbeat on all LEDs ..."
local name
for name in $(present_leds); do
# White at full brightness
led_set_multi_intensity "$name" 255 255 255
led_set_brightness "$name" 255
led_set_trigger "$name" heartbeat
info " $name -> heartbeat"
done
}
# --- TIMER ------------------------------------------------------------------
#
# Stagger delay_on / delay_off across LEDs to create a "running" effect.
# Pairs (on_ms, off_ms) cycle through a range from fast (50/50) to slow
# (500/500) and back.
run_timer() {
info "Setting trigger=timer on all LEDs with staggered frequencies ..."
# Pre-defined (on_ms off_ms R G B) tuples — one per LED
# Colours: red, green, blue, cyan, magenta, yellow, white, orange, lime, teal
set -- \
"50 50 255 0 0" \
"100 100 0 255 0" \
"200 200 0 0 255" \
"300 300 0 255 255" \
"400 400 255 0 255" \
"500 500 255 255 0" \
"150 350 255 255 255" \
"80 420 255 128 0" \
"250 250 128 255 0" \
"350 150 0 128 128"
local name idx on off r g b
idx=1
for name in $(present_leds); do
# Extract the idx-th tuple from positional params
eval "tuple=\$$idx"
on=$(echo "$tuple" | awk '{print $1}')
off=$(echo "$tuple" | awk '{print $2}')
r=$(echo "$tuple" | awk '{print $3}')
g=$(echo "$tuple" | awk '{print $4}')
b=$(echo "$tuple" | awk '{print $5}')
led_set_multi_intensity "$name" "$r" "$g" "$b"
led_set_brightness "$name" 255
led_set_trigger "$name" timer
led_write "$name" delay_on "$on"
led_write "$name" delay_off "$off"
info " $name -> timer on=${on}ms off=${off}ms RGB($r,$g,$b)"
idx=$((idx + 1))
done
}
# --- PATTERN ----------------------------------------------------------------
#
# Three phases, each lasting 1/3 of the total duration:
#
# Phase 1 — FADE: smooth brightness ramp up/down (pattern trigger)
# Phase 2 — FAST FLASH: rapid on/off (50 ms) in different colours per LED
# Phase 3 — SLOW FLASH: slow pulse (400 ms) with colour cycling
#
# Pattern format: "<brightness> <duration_ms> <brightness> <duration_ms> ..."
# brightness 0-255, duration in ms. The pattern trigger loops the sequence.
# Smooth fade: 0->255 in steps then 255->0, total ~2 s per cycle
FADE_PATTERN="0 100 32 100 64 100 96 100 128 100 160 100 192 100 224 100"
FADE_PATTERN+=" 255 100 224 100 192 100 160 100 128 100 96 100 64 100 32 100"
# Fast flash: full on 50 ms, off 50 ms
FLASH_FAST_PATTERN="255 50 0 50"
# Slow flash: full on 400 ms, off 400 ms
FLASH_SLOW_PATTERN="255 400 0 400"
run_pattern_phase() {
local phase_name="$1"
local pattern="$2"
shift 2
# Remaining args: "name R G B" tuples
info " Phase: $phase_name"
local name r g b
for name in $(present_leds); do
# Consume next R G B from positional args
r="$1"; g="$2"; b="$3"; shift 3 2>/dev/null || true
led_set_trigger "$name" none
led_set_multi_intensity "$name" "$r" "$g" "$b"
led_set_brightness "$name" 255
led_set_trigger "$name" pattern
led_set_pattern "$name" "$pattern"
info " $name -> pattern='$phase_name' RGB($r,$g,$b)"
done
}
run_pattern() {
local phase_dur total_leds phase_sleep
total_leds=$(present_leds | wc -w)
phase_dur=$(( OPT_DURATION / 3 ))
[ "$phase_dur" -lt 2 ] && phase_dur=2
info "Pattern test: 3 phases x ~${phase_dur}s (${total_leds} LEDs)"
# --- Phase 1: FADE (all white, staggered start via offset pattern) ------
# Give each LED a slightly rotated version of the fade pattern so they
# don't all pulse in sync — we pre-build per-LED patterns with an initial
# offset silence of (index * 200 ms).
info "--- Phase 1/3: FADE ---"
local idx=0 name offset_pattern prefix
for name in $(present_leds); do
offset_pattern=""
if [ "$idx" -gt 0 ]; then
# Insert a leading "0 <offset_ms>" step to stagger each LED
prefix=$((idx * 200))
offset_pattern="0 ${prefix} "
fi
led_set_trigger "$name" none
led_set_multi_intensity "$name" 255 255 255
led_set_brightness "$name" 255
led_set_trigger "$name" pattern
led_set_pattern "$name" "${offset_pattern}${FADE_PATTERN}"
info " $name -> fade (offset ${idx}x200ms)"
idx=$((idx + 1))
done
sleep "$phase_dur"
# --- Phase 2: FAST FLASH — different colour per LED --------------------
info "--- Phase 2/3: FAST FLASH ---"
run_pattern_phase "fast-flash" "$FLASH_FAST_PATTERN" \
255 0 0 \
0 255 0 \
0 0 255 \
255 255 0 \
0 255 255 \
255 0 255 \
255 128 0 \
128 0 255 \
0 255 128 \
255 255 255
sleep "$phase_dur"
# --- Phase 3: SLOW FLASH — colour cycling (shift colours between LEDs) --
info "--- Phase 3/3: SLOW FLASH ---"
run_pattern_phase "slow-flash" "$FLASH_SLOW_PATTERN" \
0 255 255 \
255 0 255 \
255 255 0 \
0 0 255 \
0 255 0 \
255 0 0 \
128 255 128 \
255 128 128 \
128 128 255 \
200 200 200
# Let the last phase run for the remainder of the duration
local elapsed=$(( phase_dur * 2 ))
local remaining=$(( OPT_DURATION - elapsed ))
[ "$remaining" -lt 1 ] && remaining=1
sleep "$remaining"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
info "============================================"
info " WAGO M4 LED Performance Test v${VERSION}"
info "============================================"
info "Trigger : $OPT_TRIGGER"
info "Duration : ${OPT_DURATION}s"
info "LED sysfs: $LED_SYSFS"
info "--------------------------------------------"
check_leds
case "$OPT_TRIGGER" in
pattern)
run_pattern
;;
timer)
run_timer
info "Running for ${OPT_DURATION}s ..."
sleep "$OPT_DURATION"
;;
heartbeat)
run_heartbeat
info "Running for ${OPT_DURATION}s ..."
sleep "$OPT_DURATION"
;;
esac
cleanup
info "Test complete."
exit 0