From b197308901b2922c67f2fe7afb7c904b0bdb0146 Mon Sep 17 00:00:00 2001 From: Heinrich Toews Date: Thu, 9 Apr 2026 14:38:37 +0200 Subject: [PATCH] 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=: 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= 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 --- tools/leds/m4-led-perf-test.sh | 384 +++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100755 tools/leds/m4-led-perf-test.sh diff --git a/tools/leds/m4-led-perf-test.sh b/tools/leds/m4-led-perf-test.sh new file mode 100755 index 000000000000..66648284abfa --- /dev/null +++ b/tools/leds/m4-led-perf-test.sh @@ -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=] +# [--duration=] +# [--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 < 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= 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 -> /sys/class/leds/ +led_path() { echo "${LED_SYSFS}/$1"; } + +# led_write +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 +led_set_trigger() { led_write "$1" trigger "$2"; } + +# led_set_brightness +led_set_brightness() { led_write "$1" brightness "$2"; } + +# led_set_multi_intensity +led_set_multi_intensity() { led_write "$1" multi_intensity "$2 $3 $4"; } + +# led_set_pattern +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 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 " 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