diff --git a/src/patterns.py b/src/patterns.py index 38588b5..2e4bbec 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -3,93 +3,450 @@ from neopixel import NeoPixel import utime import random +# 8 strips of 270 leds +strips = [(1, 270), (2, 277), + (3, 280), (4, 270), + (5, 270), (6, 270), + (7, 270), (10,270)] + + + + class Patterns: - def __init__(self): - self.pin_data = (21, 277) # Example: Pin 21, 277 LEDs - self.strip = NeoPixel(Pin(self.pin_data[0]), self.pin_data[1]) - self.run = False + def __init__(self,color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): + self.strips = [] + # Initialize all 8 strips + for pin, num_leds in strips: + strip = NeoPixel(Pin(pin, Pin.OUT), num_leds) + self.strips.append(strip) + self.pattern_step = 0 + self.last_update = utime.ticks_ms() + self.delay = delay + self.brightness = brightness + self.patterns = { + "off": self.off, + "on" : self.on, + "color_wipe": self.color_wipe_step, + "rainbow_cycle": self.rainbow_cycle_step, + "theater_chase": self.theater_chase_step, + "blink": self.blink_step, + "color_transition": self.color_transition_step, # Added new pattern + "flicker": self.flicker_step, + "scanner": self.scanner_step, # New: Single direction scanner + "bidirectional_scanner": self.bidirectional_scanner_step, # New: Bidirectional scanner + "strip_cycle": self.strip_cycle_step, # New: Cycle through strips + "external": None + } + self.selected = selected + # Ensure colors list always starts with at least two for robust transition handling + self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same + if not self.colors: # Ensure at least one color exists + self.colors = [(0, 0, 0)] - self.strip.fill((0,0,0)) - self.strip.write() - print(f"Initialized single strip on Pin {self.pin_data[0]} with {self.pin_data[1]} LEDs.") + self.transition_duration = delay * 50 # Default transition duration + self.hold_duration = delay * 10 # Default hold duration at each color + self.transition_step = 0 # Current step in the transition + self.current_color_idx = 0 # Index of the color currently being held/transitioned from + self.current_color = self.colors[self.current_color_idx] # The actual blended color + + self.hold_start_time = utime.ticks_ms() # Time when the current color hold started + + # New attributes for scanner patterns + self.scanner_direction = 1 # 1 for forward, -1 for backward + self.scanner_tail_length = 3 # Number of trailing pixels + + def sync(self): + self.pattern_step=0 + self.last_update = utime.ticks_ms() - self.delay + if self.selected == "color_transition": + self.transition_step = 0 + self.current_color_idx = 0 + self.current_color = self.colors[self.current_color_idx] + self.hold_start_time = utime.ticks_ms() # Reset hold time + # Reset scanner specific variables + self.scanner_direction = 1 + self.tick() + + def set_pattern_step(self, step): + self.pattern_step = step + + def tick(self): + if self.patterns[self.selected]: + self.patterns[self.selected]() + + def update_num_leds(self, pin, num_leds): + # Find and update the specific strip + for i, (strip_pin, _) in enumerate(strips): + if strip_pin == pin: + self.strips[i] = NeoPixel(Pin(pin, Pin.OUT), num_leds) + self.pattern_step = 0 + break + + def set_delay(self, delay): + self.delay = delay + # Update transition duration and hold duration when delay changes + self.transition_duration = self.delay * 50 + self.hold_duration = self.delay * 10 - def scan_single_led(self, color=(255, 255, 255), delay_ms=0): - """ - Scans a single LED along the length of the strip, turning it on and then off - as it moves. Optimized for speed by batching writes. + def set_brightness(self, brightness): + self.brightness = brightness - Args: - color (tuple): The (R, G, B) color of the scanning LED. - delay_ms (int): Optional extra delay in milliseconds between each LED position. - Set to 0 for fastest possible without *extra* delay. - """ - self.run = True - num_pixels = len(self.strip) - last_pixel_index = num_pixels - 1 - - # Turn off all pixels initially for a clean start if not already off - self.strip.fill((0, 0, 0)) - # No write here yet, as the first pixel will be set immediately - - while self.run: - # --- Scan Forward --- - for i in range(num_pixels): - if not self.run: - break - - # Turn on the current pixel - self.strip[i] = color - - # Turn off the previous pixel if not the first one - if i > 0: - self.strip[i - 1] = (0, 0, 0) - # If it's the first pixel, ensure the last one from previous cycle is off (if applicable) - elif i == 0 and num_pixels > 1: # Only relevant if scanning backwards too - self.strip[last_pixel_index] = (0,0,0) + def set_color1(self, color): + if len(self.colors) > 0: + self.colors[0] = color + if self.selected == "color_transition": + # If the first color is changed, potentially reset transition + # to start from this new color if we were about to transition from it + if self.current_color_idx == 0: + self.transition_step = 0 + self.current_color = self.colors[0] + self.hold_start_time = utime.ticks_ms() + else: + self.colors.append(color) - self.strip.write() # Write changes to the strip - if delay_ms > 0: - utime.sleep_ms(delay_ms) - - # Ensure the last pixel of the forward scan is turned off - if self.run and num_pixels > 0: - self.strip[last_pixel_index] = (0, 0, 0) - self.strip.write() # Write this final change + def set_color2(self, color): + if len(self.colors) > 1: + self.colors[1] = color + elif len(self.colors) == 1: + self.colors.append(color) + else: # List is empty + self.colors.append((0,0,0)) # Dummy color + self.colors.append(color) - # --- Scan Backward (optional, remove this loop if you only want forward) --- - for i in range(num_pixels - 1, -1, -1): # From last_pixel_index down to 0 - if not self.run: - break + def set_colors(self, colors): + if colors and len(colors) >= 2: + self.colors = colors + if self.selected == "color_transition": + self.sync() # Reset transition if new color list is provided + elif colors and len(colors) == 1: + self.colors = [colors[0], (255,255,255)] # Add a default second color + if self.selected == "color_transition": + print("Warning: 'color_transition' requires at least two colors. Adding a default second color.") + self.sync() + else: + print("Error: set_colors requires a list of at least one color.") + self.colors = [(0,0,0), (255,255,255)] # Fallback + if self.selected == "color_transition": + self.sync() - # Turn on the current pixel - self.strip[i] = color + def set_color(self, num, color): + # Changed: More robust index check + if 0 <= num < len(self.colors): + self.colors[num] = color + # If the changed color is part of the current or next transition, + # restart the transition for smoother updates + if self.selected == "color_transition": + current_from_idx = self.current_color_idx + current_to_idx = (self.current_color_idx + 1) % len(self.colors) + if num == current_from_idx or num == current_to_idx: + # If we change a color involved in the current transition, + # it's best to restart the transition state for smoothness. + self.transition_step = 0 + self.current_color_idx = current_from_idx # Stay at the current starting color + self.current_color = self.colors[self.current_color_idx] + self.hold_start_time = utime.ticks_ms() # Reset hold + return True + elif num == len(self.colors): # Allow setting a new color at the end + self.colors.append(color) + return True + return False - # Turn off the next pixel (which was the previous one in reverse scan) - if i < last_pixel_index: - self.strip[i + 1] = (0, 0, 0) - # If it's the last pixel of the reverse scan, ensure the first one from previous cycle is off (if applicable) - elif i == last_pixel_index and num_pixels > 1: # Only relevant if scanning forward too - self.strip[0] = (0,0,0) + def add_color(self, color): + self.colors.append(color) + if self.selected == "color_transition" and len(self.colors) == 2: + # If we just added the second color needed for transition + self.sync() - self.strip.write() # Write changes to the strip - if delay_ms > 0: - utime.sleep_ms(delay_ms) - # Ensure the first pixel of the backward scan is turned off - if self.run and num_pixels > 0: - self.strip[0] = (0, 0, 0) - self.strip.write() # Write this final change + def del_color(self, num): + # Changed: More robust index check and using del for lists + if 0 <= num < len(self.colors): + del self.colors[num] + # If the color being deleted was part of the current transition, + # re-evaluate the current_color_idx + if self.selected == "color_transition": + if len(self.colors) < 2: # Need at least two colors for transition + print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.") + self.select("on") # Or some other default + else: + # Adjust index if it's out of bounds after deletion or was the one transitioning from + self.current_color_idx %= len(self.colors) + self.transition_step = 0 + self.current_color = self.colors[self.current_color_idx] + self.hold_start_time = utime.ticks_ms() + return True + return False + def apply_brightness(self, color, brightness_override=None): + effective_brightness = brightness_override if brightness_override is not None else self.brightness + return tuple(int(c * effective_brightness / 255) for c in color) + + def select(self, pattern): + if pattern in self.patterns: + self.selected = pattern + self.sync() # Reset pattern state when selecting a new pattern + if pattern == "color_transition": + if len(self.colors) < 2: + print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.") + self.selected = "on" # Fallback if not enough colors + self.sync() # Re-sync for the new pattern + else: + self.transition_step = 0 + self.current_color_idx = 0 # Start from the first color in the list + self.current_color = self.colors[self.current_color_idx] + self.hold_start_time = utime.ticks_ms() # Reset hold timer + self.transition_duration = self.delay * 50 # Initialize transition duration + self.hold_duration = self.delay * 10 # Initialize hold duration + return True + return False + + def set(self, i, color): + # Find which strip contains LED i + current_pos = 0 + for strip in self.strips: + if i < current_pos + len(strip): + strip[i - current_pos] = color + return + current_pos += len(strip) + + def write(self): + for strip in self.strips: + strip.write() + + def fill(self, color=None): + fill_color = color if color is not None else self.colors[0] + for strip in self.strips: + for i in range(len(strip)): + strip[i] = fill_color + self.write() def off(self): - print("Turning off LEDs.") - self.run = False - self.strip.fill((0,0,0)) - self.strip.write() - utime.sleep_ms(50) + self.fill((0, 0, 0)) -# Example Usage (for MicroPython on actual hardware): -# (Same as before, just removed from the main block for brevity) + def on(self): + self.fill(self.apply_brightness(self.colors[0])) + + def color_wipe_step(self): + color = self.apply_brightness(self.colors[0]) + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + # Calculate total LEDs dynamically + total_leds = sum(len(strip) for strip in self.strips) + if self.pattern_step < total_leds: + # Clear all LEDs + self.fill((0, 0, 0)) + # Set the current LED + self.set(self.pattern_step, color) + self.write() + self.pattern_step += 1 + else: + self.pattern_step = 0 + self.last_update = current_time + + def rainbow_cycle_step(self): + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: + def wheel(pos): + if pos < 85: + return (pos * 3, 255 - pos * 3, 0) + elif pos < 170: + pos -= 85 + return (255 - pos * 3, 0, pos * 3) + else: + pos -= 170 + return (0, pos * 3, 255 - pos * 3) + + total_leds = sum(len(strip) for strip in self.strips) + for i in range(total_leds): + rc_index = (i * 256 // total_leds) + self.pattern_step + self.set(i, self.apply_brightness(wheel(rc_index & 255))) + self.write() + self.pattern_step = (self.pattern_step + 1) % 256 + self.last_update = current_time + + def theater_chase_step(self): + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + total_leds = sum(len(strip) for strip in self.strips) + for i in range(total_leds): + if (i + self.pattern_step) % 3 == 0: + self.set(i, self.apply_brightness(self.colors[0])) + else: + self.set(i, (0, 0, 0)) + self.write() + self.pattern_step = (self.pattern_step + 1) % 3 + self.last_update = current_time + + def blink_step(self): + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + if self.pattern_step % 2 == 0: + self.fill(self.apply_brightness(self.colors[0])) + else: + self.fill((0, 0, 0)) + self.pattern_step = (self.pattern_step + 1) % 2 + self.last_update = current_time + + def color_transition_step(self): + current_time = utime.ticks_ms() + + # Check for hold duration first + if utime.ticks_diff(current_time, self.hold_start_time) < self.hold_duration: + # Still in hold phase, just display the current solid color + self.fill(self.apply_brightness(self.current_color)) + self.last_update = current_time # Keep updating last_update to avoid skipping frames + return + + # If hold duration is over, proceed with transition + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + num_colors = len(self.colors) + if num_colors < 2: + # Should not happen if select handles it, but as a safeguard + self.select("on") + return + + from_color = self.colors[self.current_color_idx] + to_color_idx = (self.current_color_idx + 1) % num_colors + to_color = self.colors[to_color_idx] + + # Calculate interpolation factor (0.0 to 1.0) + # transition_step goes from 0 to transition_duration - 1 + if self.transition_duration > 0: + interp_factor = self.transition_step / self.transition_duration + else: + interp_factor = 1.0 # Immediately transition if duration is zero + + # Interpolate each color component + r = int(from_color[0] + (to_color[0] - from_color[0]) * interp_factor) + g = int(from_color[1] + (to_color[1] - from_color[1]) * interp_factor) + b = int(from_color[2] + (to_color[2] - from_color[2]) * interp_factor) + + self.current_color = (r, g, b) + self.fill(self.apply_brightness(self.current_color)) + + self.transition_step += self.delay # Advance the transition step by the delay + + if self.transition_step >= self.transition_duration: + # Transition complete, move to the next color and reset for hold phase + self.current_color_idx = to_color_idx + self.current_color = self.colors[self.current_color_idx] # Ensure current_color is the exact target color + self.transition_step = 0 # Reset transition progress + self.hold_start_time = current_time # Start hold phase for the new color + + self.last_update = current_time + + def flicker_step(self): + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: + base_color = self.colors[0] + # Increase the range for flicker_brightness_offset + # Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity) + flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5)) + flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset)) + + flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) + self.fill(flicker_color) + self.last_update = current_time + + def scanner_step(self): + """ + Mimics a 'Knight Rider' style scanner, moving in one direction. + """ + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + self.fill((0, 0, 0)) # Clear all LEDs + + # Calculate the head and tail position + head_pos = self.pattern_step + color = self.apply_brightness(self.colors[0]) + total_leds = sum(len(strip) for strip in self.strips) + + # Draw the head + if 0 <= head_pos < total_leds: + self.set(head_pos, color) + + # Draw the trailing pixels with decreasing brightness + for i in range(1, self.scanner_tail_length + 1): + tail_pos = head_pos - i + if 0 <= tail_pos < total_leds: + # Calculate fading color for tail + # Example: linear fade from full brightness to off + fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) + faded_color = tuple(int(c * fade_factor) for c in color) + self.set(tail_pos, faded_color) + + self.write() + + self.pattern_step += 1 + if self.pattern_step >= total_leds + self.scanner_tail_length: + self.pattern_step = 0 # Reset to start + + self.last_update = current_time + + def bidirectional_scanner_step(self): + """ + Mimics a 'Knight Rider' style scanner, moving back and forth. + """ + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay/100: + self.fill((0, 0, 0)) # Clear all LEDs + + color = self.apply_brightness(self.colors[0]) + total_leds = sum(len(strip) for strip in self.strips) + + # Calculate the head position based on direction + head_pos = self.pattern_step + + # Draw the head + if 0 <= head_pos < total_leds: + self.set(head_pos, color) + + # Draw the trailing pixels with decreasing brightness + for i in range(1, self.scanner_tail_length + 1): + tail_pos = head_pos - (i * self.scanner_direction) + if 0 <= tail_pos < total_leds: + fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) + faded_color = tuple(int(c * fade_factor) for c in color) + self.set(tail_pos, faded_color) + + self.write() + + self.pattern_step += self.scanner_direction + + # Change direction if boundaries are reached + if self.scanner_direction == 1 and self.pattern_step >= total_leds: + self.scanner_direction = -1 + self.pattern_step = total_leds - 1 # Start moving back from the last LED + elif self.scanner_direction == -1 and self.pattern_step < 0: + self.scanner_direction = 1 + self.pattern_step = 0 # Start moving forward from the first LED + + self.last_update = current_time + + def strip_cycle_step(self): + """ + Cycles through each strip, turning them on and off one by one. + """ + current_time = utime.ticks_ms() + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + # Turn off the previous strip + prev_strip = (self.pattern_step - 1) % len(self.strips) + for i in range(len(self.strips[prev_strip])): + self.strips[prev_strip][i] = (0, 0, 0) + + # Turn on the current strip + current_strip = self.pattern_step % len(self.strips) + color = self.apply_brightness(self.colors[0]) + + for i in range(len(self.strips[current_strip])): + self.strips[current_strip][i] = color + + self.write() + + # Move to next strip + self.pattern_step += 1 + + self.last_update = current_time diff --git a/src/web.py b/src/web.py index d4d2d4f..3b1278e 100644 --- a/src/web.py +++ b/src/web.py @@ -1,101 +1,43 @@ from microdot import Microdot, send_file, Response from microdot.utemplate import Template from microdot.websocket import with_websocket - -import json +import machine import wifi +import json -def web(settings, patterns, patterns2): +def web(settings, patterns): app = Microdot() Response.default_content_type = 'text/html' @app.route('/') - async def index(request): + async def index_hnadler(request): + mac = ''.join(['%02x' % b for b in wifi.get_mac()]) return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys()) @app.route("/static/") - def static(request, path): + def static_handler(request, path): if '..' in path: # Directory traversal is not allowed return 'Not found', 404 return send_file('static/' + path) - @app.post("/pattern") - def pattern(request): - try: - data = json.loads(request.body.decode('utf-8')) - pattern = data["pattern"] - if patterns.select(pattern): - patterns2.select(pattern) - settings["selected_pattern"] = pattern - settings.save() - return "OK", 200 - else: - return "Bad request", 400 - except (KeyError, json.JSONDecodeError): - return "Bad request", 400 + @app.post("/settings") + def settings_handler(request): + # Keep the POST handler for compatibility or alternative usage if needed + # For WebSocket updates, the /ws handler is now primary + return settings.set_settings(request.body.decode('utf-8'), patterns) - @app.post("/delay") - def delay(request): - try: - data = json.loads(request.body.decode('utf-8')) - delay = int(data["delay"]) - patterns.set_delay(delay) - patterns2.set_delay(delay) - settings["delay"] = delay - settings.save() - return "OK", 200 - except (ValueError, KeyError, json.JSONDecodeError): - return "Bad request", 400 - - @app.post("/brightness") - def brightness(request): - try: - data = json.loads(request.body.decode('utf-8')) - brightness = int(data["brightness"]) - patterns.set_brightness(brightness) - patterns2.set_brightness(brightness) - settings["brightness"] = brightness - settings.save() - return "OK", 200 - except (ValueError, KeyError, json.JSONDecodeError): - return "Bad request", 400 - - @app.post("/color") - def color(request): - try: - data = json.loads(request.body.decode('utf-8')) - color = data["color"] - patterns.set_color1(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB - patterns2.set_color1(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB - settings["color1"] = color - settings.save() - return "OK", 200 - except (KeyError, json.JSONDecodeError, ValueError): - return "Bad request", 400 - - @app.post("/color2") - def color2(request): - try: - data = json.loads(request.body.decode('utf-8')) - color = data["color2"] - patterns.set_color2(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB - patterns2.set_color2(tuple(int(color[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB - settings["color2"] = color - settings.save() - return "OK", 200 - except (KeyError, json.JSONDecodeError, ValueError): - return "Bad request", 400 - - @app.route("/external") + @app.route("/ws") @with_websocket async def ws(request, ws): - patterns.select("external") while True: data = await ws.receive() - print(data) - for i in range(min(patterns.num_leds, int(len(data)/3))): - patterns.set(i, (data[i*3], data[i*3+1], data[i*3+2])) - patterns.write() + if data: + + # Process the received data + _, status_code = settings.set_settings(json.loads(data), patterns, True) + #await ws.send(status_code) + else: + break return app