From 7a2c6a5c2e7d56fd32a72dad4d4aebd6a9942d9a Mon Sep 17 00:00:00 2001 From: DanjelRicci <5889576+DanjelRicci@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:28:02 +0200 Subject: [PATCH 1/2] Added `rgb_react.sh` RGB daemon --- script/device/rgb_react.sh | 343 +++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100755 script/device/rgb_react.sh diff --git a/script/device/rgb_react.sh b/script/device/rgb_react.sh new file mode 100755 index 000000000..e5baa540c --- /dev/null +++ b/script/device/rgb_react.sh @@ -0,0 +1,343 @@ +#!/bin/sh +# rgb_react.sh - Screen reactive RGB lighting +# +# Samples the framebuffer and drives the RGB sticks to match the on-screen +# colour. Runs while the RGB mode is set to Screen React and exits otherwise, +# so it is (re)started by "murgb restore" and left alone by every other mode. +# +# Testing: set FORCE_ON=1, set the RGB mode to Off, then run this over SSH to +# always react regardless of the saved mode. + +. /opt/muos/script/var/func.sh + +# Behaviour +FORCE_ON=0 # 1: ignore the saved mode and always react (testing only) +SCREEN_REACT_MODE=10 # settings/rgb/mode value that enables Screen React + +# Sampling +FB_DEVICE="/dev/fb0" +MARGIN_PERCENT=20 # ignored screen border per side (0-50) +GRID_LONG_SIDE=5 # sample points along the long edge (4-6) +SATURATION_BOOST=250 # dull colour boost (100: none, 200: 2x) +SAMPLE_INTERVAL_MS=333 +FRAME_INTERVAL_MS=33 + +# LED hardware +LED_SYSFS="/sys/class/led_anim" +LED_JOYPAD="/sys/devices/platform/singleadc-joypad" +SERIAL_DEVICE="/dev/ttyS5" +MCU_PWR="/sys/class/power_supply/axp2202-battery/mcu_pwr" + +BACKEND="" +SERIAL_READY=0 + +DETECT_BACKEND() { + case "$(GET_VAR "config" "settings/rgb/backend")" in + 1) BACKEND="SYSFS" && return ;; + 2) BACKEND="SERIAL" && return ;; + 3) BACKEND="JOYPAD" && return ;; + esac + + if [ -d "$LED_JOYPAD" ] && [ -w "$LED_JOYPAD/led_set" ]; then + BACKEND="JOYPAD" + elif [ -d "$LED_SYSFS" ]; then + BACKEND="SYSFS" + elif [ -c "$SERIAL_DEVICE" ]; then + BACKEND="SERIAL" + fi +} + +SYSFS_WRITE() { + [ -w "$LED_SYSFS/$1" ] && printf "%s\n" "$2" >"$LED_SYSFS/$1" +} + +UPDATE_SYSFS() { + # $1 brightness 0-255, $2-4 left RGB, $5-7 right RGB + HEX_L=$(printf "%02X%02X%02X " "$2" "$3" "$4") + HEX_R=$(printf "%02X%02X%02X " "$5" "$6" "$7") + + SYSFS_WRITE "max_scale" "$(($1 * 60 / 255))" + + if [ "$HEX_L" = "$HEX_R" ] && [ -w "$LED_SYSFS/effect_rgb_hex_lr" ]; then + SYSFS_WRITE "effect_rgb_hex_lr" "$HEX_L" + else + SYSFS_WRITE "effect_rgb_hex_l" "$HEX_L" + SYSFS_WRITE "effect_rgb_hex_r" "$HEX_R" + fi + + if [ -w "$LED_SYSFS/effect_lr" ]; then + SYSFS_WRITE "effect_lr" "4" + else + SYSFS_WRITE "effect_l" "4" + SYSFS_WRITE "effect_r" "4" + fi + + SYSFS_WRITE "effect_enable" "1" +} + +SERIAL_PREPARE() { + [ -w "$MCU_PWR" ] && printf "1\n" >"$MCU_PWR" + stty -F "$SERIAL_DEVICE" 115200 cs8 -parenb -cstopb -opost -isig -icanon -echo 2>/dev/null + SERIAL_READY=1 +} + +UPDATE_SERIAL() { + # $1 brightness 0-255, $2-4 left RGB, $5-7 right RGB + [ "$SERIAL_READY" -eq 1 ] || SERIAL_PREPARE + + BRI=$1 + LR=$2 LG=$3 LB=$4 + RR=$5 RG=$6 RB=$7 + + set -- 1 "$BRI" + I=0 && while [ "$I" -lt 8 ]; do set -- "$@" "$RR" "$RG" "$RB" && I=$((I + 1)); done + I=0 && while [ "$I" -lt 8 ]; do set -- "$@" "$LR" "$LG" "$LB" && I=$((I + 1)); done + + SUM=0 && for B in "$@"; do SUM=$(((SUM + B) & 255)); done + + printf "%b" "$(printf '\\x%02X' "$@" "$SUM")" >"$SERIAL_DEVICE" +} + +UPDATE_JOYPAD() { + # TODO: joypad backend (rg-vita-pro) via $LED_JOYPAD - stubbed for now. + : +} + +UPDATE_LEDS() { + case "$BACKEND" in + SYSFS) UPDATE_SYSFS "$@" ;; + SERIAL) UPDATE_SERIAL "$@" ;; + JOYPAD) UPDATE_JOYPAD "$@" ;; + esac +} + +BOOST_COLOUR() { + # $1-3 weighted RGB sums, $4 total weight -> BR BG BB (boosted, clamped) + if [ "$4" -le 0 ]; then + BR=0 BG=0 BB=0 + return + fi + + AR=$(($1 / $4)) AG=$(($2 / $4)) AB=$(($3 / $4)) + + MAX=$AR && [ "$AG" -gt "$MAX" ] && MAX=$AG + [ "$AB" -gt "$MAX" ] && MAX=$AB + MIN=$AR && [ "$AG" -lt "$MIN" ] && MIN=$AG + [ "$AB" -lt "$MIN" ] && MIN=$AB + + if [ "$MAX" -gt 0 ]; then + SAT=$(((MAX - MIN) * 100 / MAX)) + else + SAT=0 + fi + + # Only boost dull colours, leave already vivid ones alone + BOOST=$((100 + (100 - SAT) * (SATURATION_BOOST - 100) / 100)) + GRAY=$(((AR + AG + AB) / 3)) + + BR=$((GRAY + (AR - GRAY) * BOOST / 100)) + BG=$((GRAY + (AG - GRAY) * BOOST / 100)) + BB=$((GRAY + (AB - GRAY) * BOOST / 100)) + + [ "$BR" -lt 0 ] && BR=0 + [ "$BR" -gt 255 ] && BR=255 + [ "$BG" -lt 0 ] && BG=0 + [ "$BG" -gt 255 ] && BG=255 + [ "$BB" -lt 0 ] && BB=0 + [ "$BB" -gt 255 ] && BB=255 +} + +SAMPLE() { + # Read a staggered grid of pixels and set the TARGET colours. Vivid pixels + # carry more weight; sides are split for dual stick devices. + COL_SPACING=$((SAMPLE_W / GRID_W)) + HALF_SPACING=$((COL_SPACING / 2)) + QUARTER_SPACING=$((HALF_SPACING / 2)) + + WR=0 WG=0 WB=0 WT=0 + WR_L=0 WG_L=0 WB_L=0 WT_L=0 + WR_R=0 WG_R=0 WB_R=0 WT_R=0 + + ROW=0 + while [ "$ROW" -lt "$GRID_H" ]; do + PY=$((MARGIN_H + ROW * SAMPLE_H / GRID_H + SAMPLE_H / (GRID_H * 2))) + + COL=0 + while [ "$COL" -lt "$GRID_W" ]; do + PX=$((MARGIN_W + COL * COL_SPACING + HALF_SPACING)) + if [ "$((ROW % 2))" -eq 1 ]; then + PX=$((PX - QUARTER_SPACING)) + else + PX=$((PX + QUARTER_SPACING)) + fi + + # Framebuffer is BGRA (4 bytes per pixel) + OFFSET=$(((PY * FB_WIDTH + PX) * 4)) + set -- $(dd if="$FB_DEVICE" bs=1 skip="$OFFSET" count=4 2>/dev/null | od -An -tu1) + B=$1 G=$2 R=$3 + + MAX=$R && [ "$G" -gt "$MAX" ] && MAX=$G + [ "$B" -gt "$MAX" ] && MAX=$B + MIN=$R && [ "$G" -lt "$MIN" ] && MIN=$G + [ "$B" -lt "$MIN" ] && MIN=$B + + if [ "$MAX" -gt 0 ]; then + W=$(((MAX - MIN) * 100 / MAX)) + [ "$W" -lt 1 ] && W=1 + else + W=1 + fi + + if [ "$STICK_COUNT" -eq 1 ]; then + WR=$((WR + R * W)) WG=$((WG + G * W)) WB=$((WB + B * W)) WT=$((WT + W)) + elif [ "$PX" -lt "$HALF_WIDTH" ]; then + WR_L=$((WR_L + R * W)) WG_L=$((WG_L + G * W)) WB_L=$((WB_L + B * W)) WT_L=$((WT_L + W)) + else + WR_R=$((WR_R + R * W)) WG_R=$((WG_R + G * W)) WB_R=$((WB_R + B * W)) WT_R=$((WT_R + W)) + fi + + COL=$((COL + 1)) + done + ROW=$((ROW + 1)) + done + + if [ "$STICK_COUNT" -eq 1 ]; then + BOOST_COLOUR "$WR" "$WG" "$WB" "$WT" + TARGET_RL=$BR TARGET_GL=$BG TARGET_BL=$BB + TARGET_RR=$BR TARGET_GR=$BG TARGET_BR=$BB + else + BOOST_COLOUR "$WR_L" "$WG_L" "$WB_L" "$WT_L" + TARGET_RL=$BR TARGET_GL=$BG TARGET_BL=$BB + BOOST_COLOUR "$WR_R" "$WG_R" "$WB_R" "$WT_R" + TARGET_RR=$BR TARGET_GR=$BG TARGET_BR=$BB + fi +} + +SMOOTH() { + # $1 current, $2 target -> SM (moves current one step toward target) + SM_DIFF=$(($2 - $1)) + SM_STEP=$((SM_DIFF * SMOOTHING / 100)) + [ "$SM_DIFF" -gt 0 ] && [ "$SM_STEP" -eq 0 ] && SM_STEP=1 + [ "$SM_DIFF" -lt 0 ] && [ "$SM_STEP" -eq 0 ] && SM_STEP=-1 + SM=$(($1 + SM_STEP)) +} + +NOW_MS() { + read -r UP _ /dev/null) +[ -n "$OLD_PID" ] && [ "$OLD_PID" != "$$" ] && kill -0 "$OLD_PID" 2>/dev/null && exit 0 +echo "$$" >"$PID_FILE" +trap 'rm -f "$PID_FILE"' EXIT INT TERM + +# Nothing to do without RGB sticks +[ "$(GET_VAR "device" "led/rgb")" -eq 1 ] || exit 0 +[ "$FORCE_ON" -eq 1 ] || [ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 + +STICK_COUNT=$(GET_VAR "device" "board/stick") +[ -z "$STICK_COUNT" ] && STICK_COUNT=2 +[ "$STICK_COUNT" -eq 0 ] && exit 0 + +DETECT_BACKEND +[ -z "$BACKEND" ] && exit 0 +[ "$BACKEND" = "JOYPAD" ] && exit 0 + +set -- $(fbset | awk '/geometry/ { print $2, $3 }') +FB_WIDTH=$1 FB_HEIGHT=$2 +[ -z "$FB_WIDTH" ] && exit 0 + +# Sampling grid keeps the aspect ratio, minimum 3 per side +if [ "$FB_WIDTH" -ge "$FB_HEIGHT" ]; then + GRID_W=$GRID_LONG_SIDE + GRID_H=$((FB_HEIGHT * GRID_LONG_SIDE / FB_WIDTH)) + [ "$GRID_H" -lt 3 ] && GRID_H=3 +else + GRID_H=$GRID_LONG_SIDE + GRID_W=$((FB_WIDTH * GRID_LONG_SIDE / FB_HEIGHT)) + [ "$GRID_W" -lt 3 ] && GRID_W=3 +fi + +MARGIN_W=$((FB_WIDTH * MARGIN_PERCENT / 100)) +MARGIN_H=$((FB_HEIGHT * MARGIN_PERCENT / 100)) +SAMPLE_W=$((FB_WIDTH - MARGIN_W * 2)) +SAMPLE_H=$((FB_HEIGHT - MARGIN_H * 2)) +HALF_WIDTH=$((FB_WIDTH / 2)) + +# Reach the target colour by the next sample for smooth transitions +FRAMES=$((SAMPLE_INTERVAL_MS / FRAME_INTERVAL_MS)) +if [ "$FRAMES" -gt 1 ]; then + SMOOTHING=$((370 / FRAMES)) + [ "$SMOOTHING" -lt 10 ] && SMOOTHING=10 + [ "$SMOOTHING" -gt 50 ] && SMOOTHING=50 +else + SMOOTHING=50 +fi + +CURR_RL=0 CURR_GL=0 CURR_BL=0 +CURR_RR=0 CURR_GR=0 CURR_BR=0 +TARGET_RL=0 TARGET_GL=0 TARGET_BL=0 +TARGET_RR=0 TARGET_GR=0 TARGET_BR=0 +BRIGHT=153 +NEXT_SAMPLE=0 +PAUSED=0 + +while :; do + NOW_MS + LOOP_START=$NOW + + if [ "$FORCE_ON" -ne 1 ]; then + [ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 + [ "$(GET_VAR "config" "settings/rgb/mode")" = "$SCREEN_REACT_MODE" ] || exit 0 + fi + + # Stand down only when muOS blanks the LEDs on idle/dim (it does the same + # to every RGB mode). IDLE_STATE, not the stale IS_IDLE, is cleared again + # on resume. Low battery is left alone: like every other mode we keep + # reacting, and muOS's dedicated power LED is the warning. + IDLE=0 + [ -r "$IDLE_STATE" ] && read -r IDLE <"$IDLE_STATE" + if [ "$IDLE" = "1" ]; then + [ "$PAUSED" -eq 1 ] || { UPDATE_LEDS 0 0 0 0 0 0 0; PAUSED=1; } + sleep 1 + continue + fi + + # Resuming after a pause: muOS briefly wakes then re-idles on the first + # input after dimming, so confirm the wake is sustained before reacting + # again - otherwise the sticks would flash for a single frame. + if [ "$PAUSED" -eq 1 ]; then + sleep 0.2 + IDLE=0 + [ -r "$IDLE_STATE" ] && read -r IDLE <"$IDLE_STATE" + [ "$IDLE" = "1" ] && continue + PAUSED=0 + fi + + [ -r "$FB_DEVICE" ] || { sleep 1 && continue; } + + if [ "$NOW" -ge "$NEXT_SAMPLE" ]; then + BRIGHT=$(GET_VAR "config" "settings/rgb/bright") + [ -z "$BRIGHT" ] && BRIGHT=153 + SAMPLE + NEXT_SAMPLE=$((NOW + SAMPLE_INTERVAL_MS)) + fi + + SMOOTH "$CURR_RL" "$TARGET_RL" && CURR_RL=$SM + SMOOTH "$CURR_GL" "$TARGET_GL" && CURR_GL=$SM + SMOOTH "$CURR_BL" "$TARGET_BL" && CURR_BL=$SM + SMOOTH "$CURR_RR" "$TARGET_RR" && CURR_RR=$SM + SMOOTH "$CURR_GR" "$TARGET_GR" && CURR_GR=$SM + SMOOTH "$CURR_BR" "$TARGET_BR" && CURR_BR=$SM + + UPDATE_LEDS "$BRIGHT" "$CURR_RL" "$CURR_GL" "$CURR_BL" "$CURR_RR" "$CURR_GR" "$CURR_BR" + + NOW_MS + SLEEP_MS=$((FRAME_INTERVAL_MS - (NOW - LOOP_START))) + [ "$SLEEP_MS" -lt 10 ] && SLEEP_MS=10 + sleep "$((SLEEP_MS / 1000)).$(printf "%03d" "$((SLEEP_MS % 1000))")" +done From 9df464961da397f0c049862974844e912335f0a6 Mon Sep 17 00:00:00 2001 From: DanjelRicci <5889576+DanjelRicci@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:07:13 +0200 Subject: [PATCH 2/2] Removed FORCE_ON testing flag from rgb_react.sh --- script/device/rgb_react.sh | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/script/device/rgb_react.sh b/script/device/rgb_react.sh index e5baa540c..d8d061c70 100755 --- a/script/device/rgb_react.sh +++ b/script/device/rgb_react.sh @@ -4,14 +4,10 @@ # Samples the framebuffer and drives the RGB sticks to match the on-screen # colour. Runs while the RGB mode is set to Screen React and exits otherwise, # so it is (re)started by "murgb restore" and left alone by every other mode. -# -# Testing: set FORCE_ON=1, set the RGB mode to Off, then run this over SSH to -# always react regardless of the saved mode. . /opt/muos/script/var/func.sh # Behaviour -FORCE_ON=0 # 1: ignore the saved mode and always react (testing only) SCREEN_REACT_MODE=10 # settings/rgb/mode value that enables Screen React # Sampling @@ -237,7 +233,7 @@ trap 'rm -f "$PID_FILE"' EXIT INT TERM # Nothing to do without RGB sticks [ "$(GET_VAR "device" "led/rgb")" -eq 1 ] || exit 0 -[ "$FORCE_ON" -eq 1 ] || [ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 +[ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 STICK_COUNT=$(GET_VAR "device" "board/stick") [ -z "$STICK_COUNT" ] && STICK_COUNT=2 @@ -290,10 +286,8 @@ while :; do NOW_MS LOOP_START=$NOW - if [ "$FORCE_ON" -ne 1 ]; then - [ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 - [ "$(GET_VAR "config" "settings/rgb/mode")" = "$SCREEN_REACT_MODE" ] || exit 0 - fi + [ "$(GET_VAR "config" "settings/general/rgb")" -eq 1 ] || exit 0 + [ "$(GET_VAR "config" "settings/rgb/mode")" = "$SCREEN_REACT_MODE" ] || exit 0 # Stand down only when muOS blanks the LEDs on idle/dim (it does the same # to every RGB mode). IDLE_STATE, not the stale IS_IDLE, is cleared again