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 a9b4de99ee
commit 84ebea31ae
+383
View File
@@ -0,0 +1,383 @@
#!/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 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