aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md14
-rwxr-xr-xbin/udt-ledstrip32
2 files changed, 37 insertions, 9 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 7547181..4c3ccae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -227,10 +227,20 @@ by its UDP 6667 broadcast rather than a stored address. The effect is DP 51,
omits: it was read off the strip's status pushes while the app set scenes. LEDs
render pastel accents as white, so saturation is floored at full.
The strip also follows the session: hypr-theme's autostart runs `udt-ledstrip
-on` at login and `off` on Hyprland's `hyprland.shutdown` event, which logout
-and poweroff both raise. `off` cannot wait for a broadcast as the session
+on` at login and `off` on Hyprland's `hyprland.shutdown` event, which covers
+logout. Poweroff and reboot do not reach it in time: rc.6 stops networking
+before it signals Hyprland, so the event fires with no route to the strip.
+rofi-scripts' `udt-powermenu.sh` therefore runs `off` itself before
+`loginctl`. `off` cannot wait for a broadcast as the session
dies, so the last address is cached in `~/.cache/udt/ledstrip.json` and
discovery is only the fallback.
+Suspend does not switch it off: `udt-ledstrip sleep` breathes the accent alone,
+desaturated and at half brightness (DP 51 mode `0x03`, read off the strip the
+same way), and hypridle's `after_sleep_cmd` runs `on` to bring the flow back.
+`sleep` is sent before `loginctl suspend`, by the power menu and by
+hypridle's idle timeout, for the same reason as `off`: NetworkManager drops the
+network on the same sleep signal hypridle's `before_sleep_cmd` answers. That
+`before_sleep_cmd` sends it too, as a best effort for any other suspend path.
**Sublime's, GRUB's and the homepage's accents are replaced, not substituted
into a placeholder.** GRUB and the homepage are generated by udt-palette with
diff --git a/bin/udt-ledstrip b/bin/udt-ledstrip
index ca39063..039fe35 100755
--- a/bin/udt-ledstrip
+++ b/bin/udt-ledstrip
@@ -7,13 +7,16 @@
Usage: udt-ledstrip play the gradient, leaving the strip on or off
udt-ledstrip on|off switch it, the gradient with it when on
+ udt-ledstrip sleep breathe the accent, soft and dim, while asleep
udt-ledstrip --selftest
The colours are the three udt-accent wrote for the window border, the accent
between its two hue neighbours, played as the strip's "flow" scene: a gradient
moving along the strip. udt-accent starts this detached on every run, like
udt-keyboard. hypr-theme's autostart runs `on` at login and `off` on
-Hyprland's shutdown event, so the strip follows the session.
+Hyprland's shutdown event, so the strip follows the session. `sleep` is sent
+before a suspend and `on` after it, so a sleeping desktop breathes instead of
+going dark.
The device id and local key come from ~/.config/udt/tuya/devices.json, written
once by `python -m tinytuya wizard` against a Tuya IoT cloud project; after
@@ -54,6 +57,10 @@ DISCOVERY_TIMEOUT = 20
SATURATION_FLOOR = 100
BRIGHTNESS = 100
SPEED = 50
+# Sleep: the accent alone, desaturated and at half brightness, breathing slowly.
+SLEEP_SATURATION = 60
+SLEEP_BRIGHTNESS = 50
+SLEEP_SPEED = 20
SESS_START, SESS_FINISH, CONTROL = 3, 5, 13
@@ -61,17 +68,20 @@ SESS_START, SESS_FINISH, CONTROL = 3, 5, 13
# status pushes; Tuya does not document it): version 1, scene id (0 for a
# custom one), mode, speed twice, flags, two zero bytes, brightness, then
# per colour a two byte hue and a saturation byte. Mode 0x0a with flags
-# 0xe0 is the app's moving gradient.
+# 0xe0 is the app's moving gradient; mode 0x03 with flags 0 is its breathing.
FLOW, FLOW_FLAGS = 0x0A, 0xE0
+BREATH, BREATH_FLAGS = 0x03, 0x00
-def scene(colours):
- """DP 51 bytes playing #rrggbb colours as a flow, base64 for the wire."""
- body = bytearray([1, 0, FLOW, SPEED, SPEED, FLOW_FLAGS, 0, 0, BRIGHTNESS])
+def scene(colours, mode=FLOW, flags=FLOW_FLAGS, speed=SPEED,
+ brightness=BRIGHTNESS, saturation=None):
+ """DP 51 bytes playing #rrggbb colours, base64 for the wire. A fixed
+ saturation replaces each colour's own, floored one."""
+ body = bytearray([1, 0, mode, speed, speed, flags, 0, 0, brightness])
for c in colours:
h, s, _ = colorsys.rgb_to_hsv(*(int(c[i:i + 2], 16) / 255 for i in (1, 3, 5)))
body += (round(h * 360) % 360).to_bytes(2, "big")
- body.append(max(round(s * 100), SATURATION_FLOOR))
+ body.append(saturation or max(round(s * 100), SATURATION_FLOOR))
return base64.b64encode(body).decode()
@@ -149,6 +159,9 @@ def selftest():
raw = base64.b64decode(scene(["#ff0000", "#0000ff"]))
assert raw[:9] == bytes([1, 0, FLOW, SPEED, SPEED, FLOW_FLAGS, 0, 0, BRIGHTNESS])
assert raw[9:] == bytes([0, 0, 100, 0, 240, 100])
+ # As the app wrote a breathing scene: 01 00 03 32 32 00 00 00 32, hue, sat.
+ raw = base64.b64decode(scene(["#00ff00"], BREATH, BREATH_FLAGS, 50, 50, 60))
+ assert raw == bytes([1, 0, 3, 50, 50, 0, 0, 0, 50, 0, 120, 60])
key = b"0123456789abcdef"
assert unpack(key, pack(key, 7, CONTROL, b"payload")) == (CONTROL, b"payload")
print("ok")
@@ -157,7 +170,7 @@ def selftest():
def main():
if sys.argv[1:] == ["--selftest"]:
return selftest()
- if sys.argv[1:] not in ([], ["on"], ["off"]):
+ if sys.argv[1:] not in ([], ["on"], ["off"], ["sleep"]):
sys.exit(__doc__.split("\n\n")[1])
try:
devices = json.loads(DEVICES.read_text())
@@ -173,6 +186,11 @@ def main():
return
if sys.argv[1:] == ["off"]:
dps = {"20": False}
+ elif sys.argv[1:] == ["sleep"] and colours:
+ # The border's middle colour is the accent itself.
+ dps = {"20": True, "21": "scene",
+ "51": scene([colours[len(colours) // 2]], BREATH, BREATH_FLAGS,
+ SLEEP_SPEED, SLEEP_BRIGHTNESS, SLEEP_SATURATION)}
elif colours:
dps = {"21": "scene", "51": scene(colours)}
if sys.argv[1:] == ["on"]: