From 692c1493b7ab1e0435176c130eab623b749026d0 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Mon, 6 Apr 2026 16:36:19 +1000 Subject: [PATCH] feat: Add software_cycle effect for revolving pad lights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New software_cycle() method creates rotating light chase across all pads - Pattern: CENTER → LEFT → RIGHT → CENTER (customizable) - Speed parameter controls rotation speed - apply_effect() now supports "cycle" effect type - Updates module to v1.5.0 --- lego_dimensions_reader.py | 118 +++++++++++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/lego_dimensions_reader.py b/lego_dimensions_reader.py index 5b95dfb..cb1a842 100644 --- a/lego_dimensions_reader.py +++ b/lego_dimensions_reader.py @@ -6,6 +6,11 @@ For Moonlight Drive-In Video Player System Communicates with LEGO Dimensions USB portal (PS3/PS4/Wii U versions) to detect character and vehicle disc placement/removal events. +v1.5.0 Changes: +- Added software_cycle() method for revolving light chase effect +- Cycle effect rotates colors through CENTER → LEFT → RIGHT pads +- apply_effect() now supports "cycle" effect type + v1.4.0 Changes: - Added nfc_card_id field: decimal ID matching standard NFC readers - Calculated from bytes 3-6 of UID as big-endian uint32 @@ -86,7 +91,7 @@ VENDOR_ID = 0x0e6f PRODUCT_ID = 0x0241 # Module version -__version__ = "1.4.0" +__version__ = "1.5.0" def get_usb_backend(): @@ -184,6 +189,7 @@ class LegoDimensionsReader: Provides event-driven tag detection with callbacks for integration with the Moonlight Drive-In video player system. + v1.5.0: Added software_cycle() for revolving light chase effect. v1.4.0: TagInfo now includes nfc_card_id matching standard NFC readers. v1.3.0: No default LED behavior on tag insert/remove. Callbacks are fully responsible for LED control. @@ -216,6 +222,7 @@ class LegoDimensionsReader: self._active_tags: Dict[int, TagInfo] = {} # Effect control - track running effects per pad + # For cycle effect, we use pad value 99 as a special "all pads" key self._effect_threads: Dict[int, threading.Thread] = {} self._effect_stop_flags: Dict[int, threading.Event] = {} @@ -504,8 +511,23 @@ class LegoDimensionsReader: def stop_all_effects(self): """Stop all running software effects.""" + # Stop individual pad effects for pad in [Pad.CENTER, Pad.LEFT, Pad.RIGHT]: self.stop_effect(pad) + # Stop cycle effect (uses special key 99) + self.stop_cycle_effect() + + def stop_cycle_effect(self): + """Stop the cycle effect specifically.""" + cycle_key = 99 # Special key for cycle effect + if cycle_key in self._effect_stop_flags: + self._effect_stop_flags[cycle_key].set() + if cycle_key in self._effect_threads: + thread = self._effect_threads[cycle_key] + if thread.is_alive(): + thread.join(timeout=0.5) + del self._effect_threads[cycle_key] + del self._effect_stop_flags[cycle_key] def _set_color_raw(self, pad_value: int, r: int, g: int, b: int): """Set color without stopping effects (internal use).""" @@ -596,20 +618,98 @@ class LegoDimensionsReader: self._effect_threads[pad.value] = thread thread.start() - def apply_effect(self, pad: Pad, color: List[int], effect: str = "solid"): + def software_cycle(self, color: List[int], speed: float = 0.3, + pattern: Optional[List[Pad]] = None, trail: bool = True, + count: int = 0): + """ + Cycle/revolve lights through pads in sequence. + Creates a rotating light chase effect across all pads. + + Args: + color: [R, G, B] values for the cycling light + speed: Seconds each pad stays lit before moving to next + pattern: List of pads in order (default: CENTER, LEFT, RIGHT) + trail: If True, leaves a dim trail. If False, only one pad lit at a time. + count: Number of full cycles (0 = infinite until stopped) + """ + # Stop any existing cycle and individual pad effects + self.stop_cycle_effect() + for pad in [Pad.CENTER, Pad.LEFT, Pad.RIGHT]: + self.stop_effect(pad) + + # Default pattern: CENTER -> LEFT -> RIGHT (clockwise-ish) + if pattern is None: + pattern = [Pad.CENTER, Pad.LEFT, Pad.RIGHT] + + cycle_key = 99 # Special key for cycle effect + stop_flag = threading.Event() + self._effect_stop_flags[cycle_key] = stop_flag + + def _cycle_loop(): + cycle_count = 0 + current_index = 0 + + # Calculate dim trail color (25% brightness) + trail_color = [int(c * 0.25) for c in color] + + while not stop_flag.is_set(): + # Set all pads + for i, pad in enumerate(pattern): + if i == current_index: + # Current pad: full brightness + self._set_color_raw(pad.value, color[0], color[1], color[2]) + elif trail: + # Trail: dim color + self._set_color_raw(pad.value, trail_color[0], trail_color[1], trail_color[2]) + else: + # No trail: off + self._set_color_raw(pad.value, 0, 0, 0) + + # Wait before moving to next pad + if stop_flag.wait(speed): + break + + # Move to next pad + current_index = (current_index + 1) % len(pattern) + + # Check if completed a full cycle + if current_index == 0: + cycle_count += 1 + if count > 0 and cycle_count >= count: + # End on first pad lit + self._set_color_raw(pattern[0].value, color[0], color[1], color[2]) + for pad in pattern[1:]: + self._set_color_raw(pad.value, 0, 0, 0) + break + + thread = threading.Thread(target=_cycle_loop, daemon=True) + self._effect_threads[cycle_key] = thread + thread.start() + + def apply_effect(self, pad: Pad, color: List[int], effect: str = "solid", + speed: float = 1.0): """ Apply a named effect to a pad. Args: - pad: Which pad + pad: Which pad (ignored for "cycle" effect which uses all pads) color: [R, G, B] values - effect: "solid", "flash", or "pulse" + effect: "solid", "flash", "pulse", or "cycle" + speed: Speed multiplier (lower = faster for flash/pulse, + seconds per step for cycle) """ effect = effect.lower() - if effect == "flash": - self.software_flash(pad, color) + + if effect == "cycle": + # Cycle uses all pads, speed is seconds per pad + cycle_speed = 0.3 * speed # Default 0.3s, adjustable + self.software_cycle(color, speed=cycle_speed) + elif effect == "flash": + on_time = 0.3 * speed + off_time = 0.3 * speed + self.software_flash(pad, color, on_time=on_time, off_time=off_time) elif effect == "pulse": - self.software_pulse(pad, color) + self.software_pulse(pad, color, speed=speed) else: self.set_pad_color(pad, color) @@ -834,8 +934,8 @@ if __name__ == "__main__": print(f"\nLEGO Dimensions Portal Reader v{__version__}") print(f"Platform: {platform.system()}") print("="*50) - print("v1.4.0: Now shows NFC Card ID (decimal) for compatibility") - print(" with standard NFC readers and mapping files") + print("v1.5.0: Added 'cycle' effect for revolving lights!") + print(" Set effect: 'cycle' in tag_colors.json") print("="*50) reader = LegoDimensionsReader()