From 4b06aa08419f2ec5866fe8a5a631073d90b2e9fd Mon Sep 17 00:00:00 2001 From: Jimmy Date: Wed, 10 Sep 2025 16:41:41 +1200 Subject: [PATCH] change to hoop --- src/boot.py | 12 +- src/dma.py | 117 +++++++++++++ src/main.py | 80 ++++----- src/patterns.py | 361 +++++++++------------------------------ src/templates/index.html | 30 ---- src/web.py | 46 +---- src/ws2812.py | 68 ++++++++ 7 files changed, 309 insertions(+), 405 deletions(-) create mode 100644 src/dma.py create mode 100644 src/ws2812.py diff --git a/src/boot.py b/src/boot.py index bd892c1..bc0859a 100644 --- a/src/boot.py +++ b/src/boot.py @@ -11,9 +11,9 @@ password = settings.get('wifi', {}).get('password', None) ip = settings.get('wifi', {}).get('ip', None) gateway = settings.get('wifi', {}).get('gateway', None) -for i in range(10): - config = wifi.connect(ssid, password, ip, gateway) - if config: - print(config) - break - time.sleep(0.1) \ No newline at end of file +# for i in range(10): +# config = wifi.connect(ssid, password, ip, gateway) +# if config: +# print(config) +# break +# time.sleep(0.1) \ No newline at end of file diff --git a/src/dma.py b/src/dma.py new file mode 100644 index 0000000..04abead --- /dev/null +++ b/src/dma.py @@ -0,0 +1,117 @@ +from machine import Pin +from rp2 import PIO, StateMachine, asm_pio +from time import sleep +import array +import uctypes +from uctypes import BF_POS, BF_LEN, UINT32, BFUINT32, struct + +PIO0_BASE = 0x50200000 +PIO1_BASE = 0x50300000 +DMA_BASE = 0x50000000 +DMA_CHAN_WIDTH = 0x40 +DMA_CHAN_COUNT = 12 + +DMA_SIZE_BYTE = 0x0 +DMA_SIZE_HALFWORD = 0x1 +DMA_SIZE_WORD = 0x2 + +# DMA: RP2040 datasheet 2.5.7 +DMA_CTRL_TRIG_FIELDS = { + "AHB_ERROR": 31<= 0 and sm_num < 4): + self.dma_chan.WRITE_ADDR_REG = PIO0_BASE + 0x10 + sm_num *4 + self.dma_chan.CTRL_TRIG.TREQ_SEL = sm_num + elif (sm_num < 8): + self.dma_chan.WRITE_ADDR_REG = PIO1_BASE + 0x10 + (sm_num-4) *4 + self.dma_chan.CTRL_TRIG.TREQ_SEL = sm_num + 4 + + if (block_size == 8): + self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_BYTE + if (block_size == 16): + self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_HALFWORD + if (block_size == 32): + self.dma_chan.CTRL_TRIG.DATA_SIZE = DMA_SIZE_WORD + + self.dma_chan.TRANS_COUNT_REG = transfer_count + + #Do I just always want these? + self.dma_chan.CTRL_TRIG.INCR_WRITE = 0 + self.dma_chan.CTRL_TRIG.INCR_READ = 1 + + def start_transfer(self, buffer): + self.dma_chan.READ_ADDR_REG = uctypes.addressof(buffer) + self.dma_chan.CTRL_TRIG.EN = 1 + + def transfer_count(self): + return self.dma_chan.TRANS_COUNT_REG + + def busy(self): + if self.dma_chan.CTRL_TRIG.DATA_SIZE == 1: + return True + else: + return False + + + + + + + + + + diff --git a/src/main.py b/src/main.py index 1386884..a425974 100644 --- a/src/main.py +++ b/src/main.py @@ -1,54 +1,34 @@ -import asyncio -from settings import Settings -from web import web + +from time import sleep +from neopixel import NeoPixel +from machine import UART, Pin, PWM, ADC +import _thread +import network +import espnow + from patterns import Patterns -import gc -import utime -import machine -import ntptime -import time -import wifi + +adc = ADC(2, atten=ADC.ATTN_11DB) +sta = network.WLAN(network.WLAN.IF_STA) # Or network.WLAN.IF_AP +sta.active(True) + +e = espnow.ESPNow() +e.active(True) + +#e.add_peer(broadcast) + +p = Patterns() + +_thread.start_new_thread(p.scan_single_led, ((255,0,0),0)) + +while True: + value = adc.read_uv()*2 + if value < 3_500_000: + p.run = False + p.off() + print(f"Voltage {value}") + sleep(1) -async def main(): - - - - settings = Settings() - - patterns = Patterns(4, settings["num_leds"], selected=settings["selected_pattern"]) - patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in (1, 5, 3))) - patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in (1, 5, 3))) - patterns.set_brightness(int(settings["brightness"])) - patterns.set_delay(int(settings["delay"])) - - w = web(settings, patterns) - print(settings) - # start the server in a bacakground task - print("Starting") - server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80)) - wdt = machine.WDT(timeout=10000) - - async def tick(): - while True: - patterns.tick() - await asyncio.sleep_ms(1) - - asyncio.create_task(tick()) - - first = True - - while True: - #print(time.localtime()) - - # gc.collect() - for i in range(60): - wdt.feed() - await asyncio.sleep_ms(500) - - # cleanup before ending the application - await server - -asyncio.run(main()) - +i = 0 diff --git a/src/patterns.py b/src/patterns.py index 8e5fd5d..38588b5 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -4,289 +4,92 @@ import utime import random class Patterns: - def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): - self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) - self.num_leds = num_leds - 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, - "random_color_wipe": self.random_color_wipe_step, - "random_rainbow_cycle": self.random_rainbow_cycle_step, - "random_theater_chase": self.random_theater_chase_step, - "random_blink": self.random_blink_step, - "color_transition": self.color_transition_step, - "2 step forward 1 step back": self.two_steps_forward_one_step_back_step, - "external": None - } - self.selected = selected - self.color1 = color1 - self.color2 = color2 - self.transition_duration = 50 # Duration of color transition in milliseconds - self.transition_step = 0 - - def sync(self): - self.pattern_step=0 - self.last_update = utime.ticks_ms() + 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 tick(self): - if self.patterns[self.selected]: - self.patterns[self.selected]() - - def update_num_leds(self, pin, num_leds): - self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) - self.num_leds = num_leds - self.pattern_step = 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.") - def set_delay(self, delay): - self.delay = delay - def set_brightness(self, brightness): - self.brightness = brightness - - def set_color1(self, color): - print(color) - self.color1 = self.apply_brightness(color) - - def set_color2(self, color): - self.color2 = self.apply_brightness(color) - - def apply_brightness(self, color): - return tuple(int(c * self.brightness / 255) for c in color) - - def select(self, pattern): - if pattern in self.patterns: - self.selected = pattern - return True - return False - - def set(self, i, color): - self.n[i] = color + 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. + + 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) + + + 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 + + + # --- 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 + + # Turn on the current pixel + self.strip[i] = color + + # 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) + + 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 write(self): - self.n.write() - - def fill(self): - for i in range(self.num_leds): - self.n[i] = self.color1 - self.n.write() def off(self): - color = self.color1 - self.color1 = (0,0,0) - self.fill() - self.color1 = color - - def on(self): - color = self.color1 - self.color1 = self.apply_brightness(self.color1) - self.fill() - self.color1 = color - - - def color_wipe_step(self): - color = self.apply_brightness(self.color1) - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - if self.pattern_step < self.num_leds: - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - self.n[self.pattern_step] = self.apply_brightness(color) - self.n.write() - self.pattern_step += 1 - else: - self.pattern_step = 0 - self.last_update = current_time + print("Turning off LEDs.") + self.run = False + self.strip.fill((0,0,0)) + self.strip.write() + utime.sleep_ms(50) - 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) - - for i in range(self.num_leds): - rc_index = (i * 256 // self.num_leds) + self.pattern_step - self.n[i] = self.apply_brightness(wheel(rc_index & 255)) - self.n.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: - for i in range(self.num_leds): - if (i + self.pattern_step) % 3 == 0: - self.n[i] = self.apply_brightness(self.color1) - else: - self.n[i] = (0, 0, 0) - self.n.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: - for i in range(self.num_leds): - self.n[i] = self.apply_brightness(self.color1) - else: - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - self.n.write() - self.pattern_step = (self.pattern_step + 1) % 2 - self.last_update = current_time - - def random_color_wipe_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) - if self.pattern_step < self.num_leds: - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - self.n[self.pattern_step] = self.apply_brightness(color) - self.n.write() - self.pattern_step += 1 - else: - self.pattern_step = 0 - self.last_update = current_time - - def random_rainbow_cycle_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - 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) - - random_offset = random.randint(0, 255) - for i in range(self.num_leds): - rc_index = (i * 256 // self.num_leds) + self.pattern_step + random_offset - self.n[i] = self.apply_brightness(wheel(rc_index & 255)) - self.n.write() - self.pattern_step = (self.pattern_step + 1) % 256 - self.last_update = current_time - - def random_theater_chase_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) - for i in range(self.num_leds): - if (i + self.pattern_step) % 3 == 0: - self.n[i] = self.apply_brightness(color) - else: - self.n[i] = (0, 0, 0) - self.n.write() - self.pattern_step = (self.pattern_step + 1) % 3 - self.last_update = current_time - - def random_blink_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) - if self.pattern_step % 2 == 0: - for i in range(self.num_leds): - self.n[i] = self.apply_brightness(color) - else: - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - self.n.write() - self.pattern_step = (self.pattern_step + 1) % 2 - self.last_update = current_time - - def color_transition_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - # Calculate transition factor based on elapsed time - transition_factor = (self.pattern_step * 100) / self.transition_duration - if transition_factor > 100: - transition_factor = 100 - color = self.interpolate_color(self.color1, self.color2, transition_factor / 100) - - # Apply the interpolated color to all LEDs - for i in range(self.num_leds): - self.n[i] = self.apply_brightness(color) - self.n.write() - - self.pattern_step += self.delay - if self.pattern_step > self.transition_duration: - self.pattern_step = 0 - - self.last_update = current_time - - def interpolate_color(self, color1, color2, factor): - return ( - int(color1[0] + (color2[0] - color1[0]) * factor), - int(color1[1] + (color2[1] - color1[1]) * factor), - int(color1[2] + (color2[2] - color1[2]) * factor) - ) - - def two_steps_forward_one_step_back_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - # Move forward 2 steps and backward 1 step - if self.direction == 1: # Moving forward - if self.scanner_position < self.num_leds - 2: - self.scanner_position += 2 # Move forward 2 steps - else: - self.direction = -1 # Change direction to backward - else: # Moving backward - if self.scanner_position > 0: - self.scanner_position -= 1 # Move backward 1 step - else: - self.direction = 1 # Change direction to forward - - # Set all LEDs to off - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - - # Set the current position to the color - self.n[self.scanner_position] = self.apply_brightness(self.color1) - - # Apply the color transition - transition_factor = (self.pattern_step * 100) / self.transition_duration - if transition_factor > 100: - transition_factor = 100 - color = self.interpolate_color(self.color1, self.color2, transition_factor / 100) - self.n[self.scanner_position] = self.apply_brightness(color) - - self.n.write() - self.pattern_step += self.delay - if self.pattern_step > self.transition_duration: - self.pattern_step = 0 - - self.last_update = current_time - -if __name__ == "__main__": - p = Patterns(4, 180) - p.set_color1((255,0,0)) - p.set_color2((0,255,0)) - #p.set_delay(10) - try: - while True: - for key in p.patterns: - print(key) - p.select(key) - for _ in range(2000): - p.tick() - utime.sleep_ms(1) - except KeyboardInterrupt: - p.fill((0, 0, 0)) +# Example Usage (for MicroPython on actual hardware): +# (Same as before, just removed from the main block for brevity) diff --git a/src/templates/index.html b/src/templates/index.html index 57b9eac..6063d30 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -37,35 +37,5 @@ - - - - diff --git a/src/web.py b/src/web.py index 048e60d..d4d2d4f 100644 --- a/src/web.py +++ b/src/web.py @@ -5,7 +5,7 @@ from microdot.websocket import with_websocket import json import wifi -def web(settings, patterns): +def web(settings, patterns, patterns2): app = Microdot() Response.default_content_type = 'text/html' @@ -20,24 +20,13 @@ def web(settings, patterns): return 'Not found', 404 return send_file('static/' + path) - @app.post("/num_leds") - def num_leds(request): - try: - data = json.loads(request.body.decode('utf-8')) - num_leds = int(data["num_leds"]) - patterns.update_num_leds(4, num_leds) - settings["num_leds"] = num_leds - settings.save() - return "OK", 200 - except (ValueError, KeyError, json.JSONDecodeError): - return "Bad request", 400 - @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 @@ -52,6 +41,7 @@ def web(settings, patterns): 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 @@ -64,6 +54,7 @@ def web(settings, patterns): 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 @@ -76,6 +67,7 @@ def web(settings, patterns): 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 @@ -88,39 +80,13 @@ def web(settings, patterns): 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.post("/wifi_settings") - def wifi_settings(request): - try: - data = json.loads(request.body.decode('utf-8')) - print(data) - ssid = settings['wifi']['ssid'] = data['ssid'] - password = settings['wifi']['password'] = data.get('password', settings['wifi']['password']) - ip = settings['wifi']['ip'] = data.get('ip', None) - gateway = settings['wifi']['gateway'] = data.get('gateway', None) - print(settings) - - if config := wifi.connect(ssid, password, ip, gateway): - print(config) - settings.save() - - return "OK", 200 - except Exception as e: - print(f"Wifi {e}") - return "Bad request", 400 - - - @app.post("/sync") - def sync(request): - patterns.sync() - return "OK", 200 - @app.route("/external") @with_websocket async def ws(request, ws): diff --git a/src/ws2812.py b/src/ws2812.py new file mode 100644 index 0000000..c9d9c2f --- /dev/null +++ b/src/ws2812.py @@ -0,0 +1,68 @@ + +import array, time +from machine import Pin +import rp2 +from time import sleep +import dma + +@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8) +def ws2812(): + T1 = 2 + T2 = 5 + T3 = 3 + wrap_target() + label("bitloop") + out(x, 1) .side(0) [T3 - 1] + jmp(not_x, "do_zero") .side(1) [T1 - 1] + jmp("bitloop") .side(1) [T2 - 1] + label("do_zero") + nop() .side(0) [T2 - 1] + wrap() + +class WS2812B: + def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False): + self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin)) + self.sm.active(1) + self.ar = bytearray(num_leds*3) + self.num_leds = num_leds + self.brightness = brightness + self.invert = invert + self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3) + + def show(self): + self.pio_dma.start_transfer(self.ar) + + def set(self, i, color): + self.ar[i*3] = int(color[1]*self.brightness) + self.ar[i*3+1] = int(color[0]*self.brightness) + self.ar[i*3+2] = int(color[2]*self.brightness) + + def fill(self, color): + for i in range(self.num_leds): + self.set(i, color) + + def busy(self): + return self.pio_dma.busy() + + BLACK = (0, 0, 0) + RED = (255, 0, 0) + YELLOW = (255, 150, 0) + GREEN = (0, 255, 0) + CYAN = (0, 255, 255) + BLUE = (0, 0, 255) + PURPLE = (180, 0, 255) + WHITE = (255, 255, 255) + COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE) + +if __name__ == "__main__": + num_leds, pin, sm, brightness = 10, 0, 0, 1 + ws0 = WS2812B(num_leds, pin, sm, brightness) + while True: + for color in ws0.COLORS: + ws0.fill(color) + ws0.show() + time.sleep(1) + + + +