diff options
| -rw-r--r-- | AGENTS.md | 14 | ||||
| -rwxr-xr-x | bin/udt-ledstrip | 32 |
2 files changed, 37 insertions, 9 deletions
@@ -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"]: |
