From e73224aa527fb5464daee2ee46a06cf760514f1c Mon Sep 17 00:00:00 2001 From: harrysof <73562755+harrysof@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:57:46 +0100 Subject: [PATCH] Add LED protocol v2 fade tracking Update the LED mirror to emit the v2 GET_LED snapshot expected by the companion app. This adds a protocol version byte and 12-byte per-region payload with from_r/g/b so fade commands can cross-fade from the pad's current colour instead of a stale value. The LED state machine now stores the pre-fade colour, the listener sends a 40-byte reply, and the README documents the wire format and compatibility requirements. --- README.md | 10 ++++++---- rpcs3/Emu/Io/Dimensions.cpp | 10 ++++++++++ rpcs3/Emu/Io/Dimensions.h | 9 ++++++--- rpcs3/Emu/Io/DimensionsListener.cpp | 27 ++++++++++++++++++++------- rpcs3/Emu/Io/DimensionsListener.h | 8 ++++++-- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d881ac7..dfb26f9 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,14 @@ LOAD deliberately overwrites an occupied slot (remove first, then load), matchin ### LED mirror (`GET_LED`) -LOAD/REMOVE/MOVE are fire-and-forget; `GET_LED` is the one command that answers. It returns a fixed 30-byte snapshot of what the game has the three LED regions doing: +LOAD/REMOVE/MOVE are fire-and-forget; `GET_LED` is the one command that answers. It returns a fixed 40-byte snapshot (protocol version 2) of what the game has the three LED regions doing: ``` [0] 0x4C 'L' magic [1] serial increments whenever any region actually changes -[2] 0x03 region count -[3..] 3 x 9 bytes: pad, mode, r, g, b, on_ms, off_ms, count, speed_ms +[2] 0x02 protocol version +[3] 0x03 region count +[4..] 3 x 12 bytes: pad, mode, r, g, b, from_r, from_g, from_b, on_ms, off_ms, count, speed_ms ``` | Field | Meaning | @@ -95,6 +96,7 @@ LOAD/REMOVE/MOVE are fire-and-forget; `GET_LED` is the one command that answers. | `pad` | LED region — `1` centre, `2` left, `3` right | | `mode` | `0` off, `1` solid, `2` flash, `3` fade | | `r`, `g`, `b` | The colour the game set, verbatim | +| `from_r`, `from_g`, `from_b` | For a fade, the colour the pad was already showing when the command landed; the real toypad cross-fades from it to `r/g/b` rather than ramping one colour's brightness | | `on_ms` / `off_ms` | Flash on and off durations, in Toypad ticks (~40ms each) | | `count` | Cycle count; `0` means "until the next command" (`0xFF` on the wire is normalized to `0`) | | `speed_ms` | Fade tick time | @@ -103,7 +105,7 @@ The emulated Toypad builds this snapshot in `handle_led_command`, parsing the ga The **serial** only advances when a command actually changes a region's state — re-sending an identical command leaves it alone. A polling client can therefore skip snapshots it has already applied and never restart a flash mid-cycle. -This is byte-for-byte the same snapshot the Cemu fork returns, so LegoToypad's LED mirror works against either emulator with no changes. +This is the protocol version 2 snapshot the Cemu and shadPS4 listeners also return, so LegoToypad's LED mirror works against this emulator with no changes. (Version 1 was a 30-byte reply with no version byte and no `from_r/g/b`; the app now rejects it, which is why RPCS3 needed this update.) ## Code map diff --git a/rpcs3/Emu/Io/Dimensions.cpp b/rpcs3/Emu/Io/Dimensions.cpp index 538c86c..eff8a66 100644 --- a/rpcs3/Emu/Io/Dimensions.cpp +++ b/rpcs3/Emu/Io/Dimensions.cpp @@ -630,6 +630,10 @@ void dimensions_toypad::set_led_state(u8 pad, u8 mode, u8 r, u8 g, u8 b, u8 on_m auto apply = [&](u8 target_pad) { led_pad_state& state = m_led_state[led_pad_index(target_pad)]; + // A fade's "from" colour is whatever the pad was already showing (or + // already fading towards) the moment this command lands, so a fade + // issued mid-fade still anchors to something on-screen. + const u8 from_r = state.r, from_g = state.g, from_b = state.b; if (state.mode == mode && state.r == r && state.g == g && state.b == b && state.on_ms == on_ms && state.off_ms == off_ms && state.count == count && state.speed_ms == speed_ms) return; // unchanged - don't bump the poll serial @@ -638,6 +642,12 @@ void dimensions_toypad::set_led_state(u8 pad, u8 mode, u8 r, u8 g, u8 b, u8 on_m state.r = r; state.g = g; state.b = b; + if (mode == 3) // Fade: remember the pre-command colour to cross-fade from + { + state.from_r = from_r; + state.from_g = from_g; + state.from_b = from_b; + } state.on_ms = on_ms; state.off_ms = off_ms; state.count = count; diff --git a/rpcs3/Emu/Io/Dimensions.h b/rpcs3/Emu/Io/Dimensions.h index f4ab7e1..c11bd26 100644 --- a/rpcs3/Emu/Io/Dimensions.h +++ b/rpcs3/Emu/Io/Dimensions.h @@ -31,14 +31,17 @@ class dimensions_toypad std::optional> pop_added_removed_response(); // Mirror a pad region's LED state ("keystone puzzle" glow). Same encoding - // and wire snapshot as the Cemu fork, so the LegoToypad controller app can - // render the pads glowing for RPCS3 too. mode: 0 off, 1 solid, 2 flash, - // 3 fade; pad: 1=center, 2=left, 3=right. + // and wire snapshot as the Cemu/RPCS3 protocol version 2, so the LegoToypad + // controller app can render the pads glowing for RPCS3 too. mode: 0 off, + // 1 solid, 2 flash, 3 fade; pad: 1=center, 2=left, 3=right. from_r/g/b is + // the colour the pad was already showing when a fade command landed - the + // real toypad cross-fades between that and r/g/b, not one colour's brightness. struct led_pad_state { u8 pad = 0; u8 mode = 0; u8 r = 0, g = 0, b = 0; + u8 from_r = 0, from_g = 0, from_b = 0; u8 on_ms = 0, off_ms = 0, count = 0, speed_ms = 0; }; void handle_led_command(const u8* buf, u32 buf_size); diff --git a/rpcs3/Emu/Io/DimensionsListener.cpp b/rpcs3/Emu/Io/DimensionsListener.cpp index 178ecf7..d60ca29 100644 --- a/rpcs3/Emu/Io/DimensionsListener.cpp +++ b/rpcs3/Emu/Io/DimensionsListener.cpp @@ -41,6 +41,11 @@ namespace std::atomic g_listener_running{false}; std::atomic g_listen_sock{invalid_sock}; constexpr auto move_pickup_delay = std::chrono::milliseconds(500); + // LED snapshot protocol version. v2 is a 40-byte reply that adds the + // pre-fade colour (from_r/g/b) alongside the target colour, so a client can + // render the real toypad's two-colour cross-fade. v1 was 30 bytes with no + // version byte; the companion app now requires v2. + constexpr u8 led_protocol_version = 2; #ifdef _WIN32 std::thread g_picker_thread; @@ -231,22 +236,30 @@ namespace const auto states = g_dimensionstoypad.get_led_states(); const u8 serial = g_dimensionstoypad.get_led_serial(); - u8 response[3 + 3 * 9] = {}; + // Wire format v2: { 'L', serial, protocol version, region count, + // then 3 regions x 12 bytes: pad, mode, r, g, b, from_r, from_g, + // from_b, on_ms, off_ms, count, speed_ms }. from_r/g/b is the + // pre-fade colour; see dim_listener_log and Dimensions.h. + u8 response[4 + 3 * 12] = {}; response[0] = 0x4C; // 'L' magic response[1] = serial; - response[2] = 0x03; // region count + response[2] = led_protocol_version; + response[3] = 0x03; // region count for (usz i = 0; i < 3; ++i) { - const usz off = 3 + i * 9; + const usz off = 4 + i * 12; response[off + 0] = states[i].pad; response[off + 1] = states[i].mode; response[off + 2] = states[i].r; response[off + 3] = states[i].g; response[off + 4] = states[i].b; - response[off + 5] = states[i].on_ms; - response[off + 6] = states[i].off_ms; - response[off + 7] = states[i].count; - response[off + 8] = states[i].speed_ms; + response[off + 5] = states[i].from_r; + response[off + 6] = states[i].from_g; + response[off + 7] = states[i].from_b; + response[off + 8] = states[i].on_ms; + response[off + 9] = states[i].off_ms; + response[off + 10] = states[i].count; + response[off + 11] = states[i].speed_ms; } if (!send_all(client, response, sizeof(response))) dim_listener_log.error("Failed to send GET_LED response"); diff --git a/rpcs3/Emu/Io/DimensionsListener.h b/rpcs3/Emu/Io/DimensionsListener.h index 5d306e4..955dfb7 100644 --- a/rpcs3/Emu/Io/DimensionsListener.h +++ b/rpcs3/Emu/Io/DimensionsListener.h @@ -1,12 +1,16 @@ #pragma once // TCP listener that lets external companion apps (e.g. LegoToypad) drive the -// emulated LEGO Dimensions Toypad, wire-compatible with harrysof's -// Cemu-2.6-Remote-Toypad-Build listener: +// emulated LEGO Dimensions Toypad, wire-compatible with the Cemu and shadPS4 +// listeners (LED protocol version 2): // // LOAD: 0x01 pad index 0x00 0x00 + 180 tag bytes + u16le path length + path // REMOVE: 0x02 pad index 0x00 0x00 // MOVE: 0x03 destPad destIndex srcPad srcIndex +// GET_LED:0x04 0x00 0x00 0x00 0x00 -> 40-byte LED snapshot: +// { 'L', serial, version(2), region count(3), then 3 regions x 12 +// bytes: pad, mode, r, g, b, from_r, from_g, from_b, on_ms, off_ms, +// count, speed_ms } // // pad: 1 = center, 2 = left, 3 = right; index: 0..6. // Listens on 127.0.0.1:9191 (override with env RPCS3_TOYPAD_PORT).