From 84ebea31ae36bd3ff3d0d5f3253679bb9b8592d1 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 | 383 +++++++++++++++++++++++++++++++++ 1 file changed, 383 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..210f40a36544 --- /dev/null +++ b/tools/leds/m4-led-perf-test.sh @@ -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=] +# [--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 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