feat: Add software_cycle effect for revolving pad lights
- 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
This commit is contained in:
+109
-9
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user