aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-18 21:08:57 +0200
committerDanilo M. <danix@danix.xyz>2026-10-18 21:08:57 +0200
commit64270272c4de5e3c2563e2f30ef35e7efaa384ae (patch)
tree67108581dca90fde1202f1df12a650dafd0e4491 /bin
parente00d8be5e8aff7a6588bd3f932a6706574a07ee6 (diff)
downloadunified-desktop-theme-64270272c4de5e3c2563e2f30ef35e7efaa384ae.tar.gz
unified-desktop-theme-64270272c4de5e3c2563e2f30ef35e7efaa384ae.zip
feat(ledstrip): breathe the accent while suspended
`udt-ledstrip sleep` plays the accent alone as the strip's breathing scene, desaturated to 60 and at half brightness. Its DP 51 layout, mode 0x03 with flags 0, was read off the strip's status push while the Tuya app set a breathing scene; the selftest checks it against that capture. AGENTS.md also records why poweroff never reached the strip: rc.6 stops networking before it signals Hyprland, so the hyprland.shutdown handler fires with no route. The power menu now switches the strip off before loginctl instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bin')
-rwxr-xr-xbin/udt-ledstrip32
1 files changed, 25 insertions, 7 deletions
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"]: