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)
|
Communicates with LEGO Dimensions USB portal (PS3/PS4/Wii U versions)
|
||||||
to detect character and vehicle disc placement/removal events.
|
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:
|
v1.4.0 Changes:
|
||||||
- Added nfc_card_id field: decimal ID matching standard NFC readers
|
- Added nfc_card_id field: decimal ID matching standard NFC readers
|
||||||
- Calculated from bytes 3-6 of UID as big-endian uint32
|
- Calculated from bytes 3-6 of UID as big-endian uint32
|
||||||
@@ -86,7 +91,7 @@ VENDOR_ID = 0x0e6f
|
|||||||
PRODUCT_ID = 0x0241
|
PRODUCT_ID = 0x0241
|
||||||
|
|
||||||
# Module version
|
# Module version
|
||||||
__version__ = "1.4.0"
|
__version__ = "1.5.0"
|
||||||
|
|
||||||
|
|
||||||
def get_usb_backend():
|
def get_usb_backend():
|
||||||
@@ -184,6 +189,7 @@ class LegoDimensionsReader:
|
|||||||
Provides event-driven tag detection with callbacks for integration
|
Provides event-driven tag detection with callbacks for integration
|
||||||
with the Moonlight Drive-In video player system.
|
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.4.0: TagInfo now includes nfc_card_id matching standard NFC readers.
|
||||||
v1.3.0: No default LED behavior on tag insert/remove.
|
v1.3.0: No default LED behavior on tag insert/remove.
|
||||||
Callbacks are fully responsible for LED control.
|
Callbacks are fully responsible for LED control.
|
||||||
@@ -216,6 +222,7 @@ class LegoDimensionsReader:
|
|||||||
self._active_tags: Dict[int, TagInfo] = {}
|
self._active_tags: Dict[int, TagInfo] = {}
|
||||||
|
|
||||||
# Effect control - track running effects per pad
|
# 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_threads: Dict[int, threading.Thread] = {}
|
||||||
self._effect_stop_flags: Dict[int, threading.Event] = {}
|
self._effect_stop_flags: Dict[int, threading.Event] = {}
|
||||||
|
|
||||||
@@ -504,8 +511,23 @@ class LegoDimensionsReader:
|
|||||||
|
|
||||||
def stop_all_effects(self):
|
def stop_all_effects(self):
|
||||||
"""Stop all running software effects."""
|
"""Stop all running software effects."""
|
||||||
|
# Stop individual pad effects
|
||||||
for pad in [Pad.CENTER, Pad.LEFT, Pad.RIGHT]:
|
for pad in [Pad.CENTER, Pad.LEFT, Pad.RIGHT]:
|
||||||
self.stop_effect(pad)
|
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):
|
def _set_color_raw(self, pad_value: int, r: int, g: int, b: int):
|
||||||
"""Set color without stopping effects (internal use)."""
|
"""Set color without stopping effects (internal use)."""
|
||||||
@@ -596,20 +618,98 @@ class LegoDimensionsReader:
|
|||||||
self._effect_threads[pad.value] = thread
|
self._effect_threads[pad.value] = thread
|
||||||
thread.start()
|
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.
|
Apply a named effect to a pad.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pad: Which pad
|
pad: Which pad (ignored for "cycle" effect which uses all pads)
|
||||||
color: [R, G, B] values
|
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()
|
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":
|
elif effect == "pulse":
|
||||||
self.software_pulse(pad, color)
|
self.software_pulse(pad, color, speed=speed)
|
||||||
else:
|
else:
|
||||||
self.set_pad_color(pad, color)
|
self.set_pad_color(pad, color)
|
||||||
|
|
||||||
@@ -834,8 +934,8 @@ if __name__ == "__main__":
|
|||||||
print(f"\nLEGO Dimensions Portal Reader v{__version__}")
|
print(f"\nLEGO Dimensions Portal Reader v{__version__}")
|
||||||
print(f"Platform: {platform.system()}")
|
print(f"Platform: {platform.system()}")
|
||||||
print("="*50)
|
print("="*50)
|
||||||
print("v1.4.0: Now shows NFC Card ID (decimal) for compatibility")
|
print("v1.5.0: Added 'cycle' effect for revolving lights!")
|
||||||
print(" with standard NFC readers and mapping files")
|
print(" Set effect: 'cycle' in tag_colors.json")
|
||||||
print("="*50)
|
print("="*50)
|
||||||
|
|
||||||
reader = LegoDimensionsReader()
|
reader = LegoDimensionsReader()
|
||||||
|
|||||||
Reference in New Issue
Block a user