Guide & kod

Du har ett ESP32-C3 Supermini med en 8×8 NeoPixel-matris kopplad till GPIO 4 — färdigflashat med MicroPython och Glitched-effekten igång. Här är allt du behöver för att ändra eller skriva egna effekter.

Vad finns på kortet redan

När du sätter i USB startar kortet → kör main.py automatiskt.

Anslut till kortet

Sätt i USB-C till din dator. macOS / Linux / Windows hittar den utan drivrutiner (USB-Serial/JTAG är inbyggt).

# macOS / Linux: hitta porten
ls /dev/cu.usbmodem*           # macOS
ls /dev/ttyACM* /dev/ttyUSB*   # Linux

Windows: kolla Enhetshanteraren → Portar (COM & LPT) — ofta COM3, COM5, osv.

Två sätt att jobba med kortet — välj det du gillar:

Thonny — fönster-läget

  1. Hämta gratis från thonny.org.
  2. Öppna Thonny → Tools → Options → Interpreter.
  3. Välj "MicroPython (ESP32)", port = din USB-port (t.ex. /dev/cu.usbmodem*).
  4. Klicka OK. Nu syns kortets filsystem nere till vänster — du ska se main.py och boot.py.
  5. För att stoppa Glitched-effekten: tryck Thonnys röda Stop-knapp. Nu har du REPL-promten >>>.
  6. Öppna main.py, ändra som du vill, spara (Cmd/Ctrl+S sparar tillbaka på kortet).
  7. Tryck den gröna Run-knappen eller skriv exec(open('main.py').read()) i REPL för att testa.
Tip: behåll en kopia av main.py på datorn också (kopiera ur Thonny) — då kan du alltid återställa.

mpremote — kommandoraden

pip3 install --user mpremote

# Lista filer på kortet
mpremote connect /dev/cu.usbmodem* ls

# Hämta main.py till datorn för säkerhetskopia
mpremote connect /dev/cu.usbmodem* cp :main.py main.py.bak

# Ladda upp en ny fil som main.py
mpremote connect /dev/cu.usbmodem* cp 02_bounce.py :main.py

# Starta om så main.py kör direkt
mpremote connect /dev/cu.usbmodem* reset

# Öppna REPL (Ctrl-] för att gå ur)
mpremote connect /dev/cu.usbmodem* repl
⚠️ "could not enter raw repl" — kortet är upptaget eller fast i bootloader. Dra ur USB, vänta 2 sek, sätt i igen.

Pixel-layout — verifiera först

Olika 8×8-WS2812-paneler är wirade olika:

Snabbtest — klistra in i REPL och se var färgerna hamnar:

import machine, neopixel
np = neopixel.NeoPixel(machine.Pin(4), 64)
np[0]  = (8, 0, 0)   # RÖD — var landar den?
np[7]  = (0, 8, 0)   # GRÖN
np[8]  = (0, 0, 8)   # BLÅ
np[63] = (8, 8, 0)   # GUL
np.write()

Glitched-koden och alla exempel antar rad-major rak med pixel 0 i övre vänstra hörnet (index = y * 8 + x):

 0  1  2  3  4  5  6  7
 8  9 10 11 12 13 14 15
16 17 18 19 20 21 22 23
24 25 26 27 28 29 30 31
32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47
48 49 50 51 52 53 54 55
56 57 58 59 60 61 62 63

Om panelen visar text upp-och-ner eller spegelvänd — byt ut xy(x, y) i scriptet:

# Standard (pixel 0 uppe vänster):
def xy(x, y): return y * 8 + x

# Pixel 0 nere vänster (vänd lodrätt):
def xy(x, y): return (7 - y) * 8 + x

# Serpentin, pixel 0 uppe vänster:
def xy(x, y):
    return y * 8 + (x if y % 2 == 0 else 7 - x)

Kodexempel — testa nästa effekt

Tio färdiga skript att stoppa in som main.py på kortet. Klicka för att se koden, kopiera till klippbord, eller ladda ner direkt.

01 Alla pixlar tända Sanity-check: tänd alla 64 pixlar svagt
Ladda ner 11 rader
"""Tänd alla 64 pixlar svagt vitt — verifierar att kortet är wirat rätt."""
import machine, neopixel

NUM = 64
PIN = 4

np = neopixel.NeoPixel(machine.Pin(PIN), NUM)
for i in range(NUM):
    np[i] = (2, 2, 2)
np.write()
print("alla", NUM, "pixlar tända på pin", PIN)
02 Studsande boll 2×2-boll med subpixel-rörelse
Ladda ner 53 rader
"""Studsande 2x2-boll på 8x8 NeoPixel-matris (rad-major rak layout)."""
import machine, neopixel, time

PIN = 4
W, H = 8, 8
NUM = W * H

np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

# Rak rad-layout: alla rader går vänster -> höger.
# rad 0: 0..7, rad 1: 8..15 (pixel 8 rakt under pixel 0), osv.
def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def draw_ball(cx, cy, color):
    for ox in range(BALL_SIZE):
        for oy in range(BALL_SIZE):
            x, y = cx + ox, cy + oy
            if 0 <= x < W and 0 <= y < H:
                np[xy(x, y)] = color

BALL_SIZE = 2
BALL = (8, 0, 0)

x, y = 0.0, 0.0
dx, dy = 0.35, 0.27
MAX_X = W - BALL_SIZE
MAX_Y = H - BALL_SIZE

try:
    while True:
        x += dx
        y += dy
        if x <= 0:
            x = 0; dx = -dx
        elif x >= MAX_X:
            x = MAX_X; dx = -dx
        if y <= 0:
            y = 0; dy = -dy
        elif y >= MAX_Y:
            y = MAX_Y; dy = -dy

        clear()
        draw_ball(int(round(x)), int(round(y)), BALL)
        np.write()
        time.sleep_ms(40)
except KeyboardInterrupt:
    clear()
    np.write()
03 Effektrullning Cyklar mellan plasma, rainbow, sparkles...
Ladda ner 207 rader
"""Cyklande scenarier:
  plasma -> vortex (spiral) -> tunnel (sug-in) -> glitter -> text '6446.se' med skimmer
"""
import machine, neopixel, time, random, math

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

DIM = 24       # tak för "ljusa" effekter
DIM_BG = 4     # tak för bakgrundsskimmer bakom text
CX, CY = 3.5, 3.5

def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def wheel(pos, peak=DIM):
    """0-255 -> RGB med tak `peak` per kanal."""
    pos &= 255
    third = peak * 3
    if pos < 85:
        return (pos * third // 765, (255 - pos * 3) * peak // 765, 0)
    elif pos < 170:
        pos -= 85
        return ((255 - pos * 3) * peak // 765, 0, pos * third // 765)
    else:
        pos -= 170
        return (0, pos * third // 765, (255 - pos * 3) * peak // 765)

def scale(c, factor):
    return (int(c[0] * factor), int(c[1] * factor), int(c[2] * factor))

def bg_color(hue, peak=6):
    """Som wheel men minst 1 per kanal — bakgrund blir alltid fylld, aldrig svart."""
    pos = hue & 255
    span = peak - 1
    if pos < 85:
        r = 1 + pos * span // 85
        g = 1 + (85 - pos) * span // 85
        b = 1
    elif pos < 170:
        pos -= 85
        r = 1 + (85 - pos) * span // 85
        g = 1
        b = 1 + pos * span // 85
    else:
        pos -= 170
        r = 1
        g = 1 + pos * span // 85
        b = 1 + (85 - pos) * span // 85
    return (r, g, b)

# =================================================================
# Effekt 1: plasma (klassisk demoscene — flytande färgmoln)
# =================================================================
def plasma(duration_ms=9000):
    t0 = time.ticks_ms()
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        for y in range(H):
            for x in range(W):
                v = (math.sin(x * 0.7 + frame * 0.10)
                     + math.sin(y * 0.9 + frame * 0.13)
                     + math.sin((x + y) * 0.5 + frame * 0.08)
                     + math.sin(math.sqrt((x - CX) ** 2 + (y - CY) ** 2) + frame * 0.15))
                hue = int(v * 40 + frame * 2) & 255
                np[xy(x, y)] = wheel(hue)
        np.write()
        frame += 1
        time.sleep_ms(35)

# =================================================================
# Effekt 2: vortex — roterande spiralarmar
# =================================================================
def vortex(duration_ms=9000):
    t0 = time.ticks_ms()
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        for y in range(H):
            for x in range(W):
                dx, dy = x - CX, y - CY
                r = math.sqrt(dx * dx + dy * dy)
                a = math.atan2(dy, dx)
                # hue följer vinkeln + spiraltvist beroende på radie + rotation över tid
                hue = int(a * 40 + r * 28 - frame * 6) & 255
                # ljuspuls inåt så det känns som armar
                bright = 0.35 + 0.65 * (0.5 + 0.5 * math.sin(r * 1.6 - frame * 0.28))
                np[xy(x, y)] = scale(wheel(hue), bright)
        np.write()
        frame += 1
        time.sleep_ms(35)

# =================================================================
# Effekt 3: tunnel — koncentriska ringar suger inåt
# =================================================================
def tunnel(duration_ms=9000):
    t0 = time.ticks_ms()
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        for y in range(H):
            for x in range(W):
                dx, dy = x - CX, y - CY
                r = math.sqrt(dx * dx + dy * dy)
                a = math.atan2(dy, dx)
                # ringar flyter inåt (negativ riktning), liten vridning för djupkänsla
                phase = frame * 0.45 - r * 1.7 + a * 0.4
                bright = (0.5 + 0.5 * math.sin(phase)) ** 2
                hue = int(r * 50 + frame * 3) & 255
                np[xy(x, y)] = scale(wheel(hue), bright)
        np.write()
        frame += 1
        time.sleep_ms(35)

# =================================================================
# Effekt 4: glitter (random sparkles)
# =================================================================
def sparkle(duration_ms=7000):
    state = [[0, 0.0] for _ in range(NUM)]
    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        for _ in range(2):
            i = random.randint(0, NUM - 1)
            state[i][0] = random.randint(0, 255)
            state[i][1] = 1.0
        for i in range(NUM):
            hue, inten = state[i]
            if inten > 0:
                np[i] = scale(wheel(hue), inten)
                state[i][1] = max(0.0, inten - 0.08)
            else:
                np[i] = (0, 0, 0)
        np.write()
        time.sleep_ms(40)

# =================================================================
# Effekt 5: scrollande text "6446.se" med skimmer-bakgrund
# =================================================================
FONT = {
    '6': ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
    '4': ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    '.': ["00000", "00000", "00000", "00000", "00000", "00000", "01100"],
    's': ["00000", "00000", "01110", "10000", "01110", "00001", "11110"],
    'e': ["00000", "00000", "01110", "10001", "11111", "10000", "01110"],
    ' ': ["00000"] * 7,
}

def text_columns(text):
    cols = []
    for ch in text:
        glyph = FONT.get(ch, FONT[' '])
        char_w = len(glyph[0])
        for cx in range(char_w):
            col = 0
            for ry in range(7):
                if glyph[ry][cx] == '1':
                    col |= (1 << ry)
            cols.append(col)
        cols.append(0)  # mellanrum mellan tecken
    return cols

def scroll_text_shimmer(text, text_color=(DIM, DIM, DIM), duration_ms=15000):
    cols = text_columns(text)
    t0 = time.ticks_ms()
    offset = -W
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        # bakgrund: fyllt plasmaskimmer, varje pixel alltid färgad
        for y in range(H):
            for x in range(W):
                v = (math.sin(x * 0.55 + frame * 0.06)
                     + math.sin(y * 0.7 + frame * 0.08)
                     + math.sin((x + y) * 0.4 + frame * 0.05))
                hue = int(v * 50 + frame * 2) & 255
                np[xy(x, y)] = bg_color(hue, peak=6)
        # text ovanpå
        for x in range(W):
            ci = offset + x
            if 0 <= ci < len(cols):
                col = cols[ci]
                for y in range(H):
                    if col & (1 << y):
                        np[xy(x, y)] = text_color
        np.write()
        offset += 1
        frame += 1
        if offset > len(cols):
            offset = -W
        time.sleep_ms(80)

# =================================================================
# Cykla allt
# =================================================================
try:
    while True:
        plasma(9000)
        vortex(9000)
        tunnel(9000)
        sparkle(6000)
        scroll_text_shimmer("6446.se", duration_ms=15000)
except KeyboardInterrupt:
    clear()
    np.write()
04 Eld Klassisk demoscene-eld (Doom/Amiga-stil)
Ladda ner 73 rader
"""Klassisk demoscene-eld (Doom/Amiga-stil).

Algoritm:
  1) Botten-raden eldas slumpmässigt het.
  2) Varje pixel ovanför ärver värme från en pixel under sig (med slumpmässig
     horisontell offset = vind) och kyls av lite slumpmässigt.
  3) Värmevärdet mappas via en 16-stegs eldpalett: svart -> röd -> orange -> gul -> vit-het.
"""
import machine, neopixel, time, random

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

def xy(x, y):
    return y * W + x

# 16-stegs eldpalett (svart -> röd -> orange -> gul -> vit-het). Max ~50 per kanal.
FIRE_PALETTE = [
    (0, 0, 0),
    (4, 0, 0),
    (9, 0, 0),
    (14, 0, 0),
    (20, 0, 0),
    (26, 1, 0),
    (32, 3, 0),
    (38, 6, 0),
    (44, 10, 0),
    (48, 16, 0),
    (50, 22, 0),
    (52, 28, 0),
    (52, 34, 2),
    (54, 40, 8),
    (56, 46, 16),
    (60, 52, 28),
]

heat = bytearray(NUM)

def step():
    # Stoka botten-raderna
    for x in range(W):
        heat[xy(x, H - 1)] = random.randint(200, 255)
        heat[xy(x, H - 2)] = random.randint(140, 230)

    # Propagera uppåt med kylning + horisontell vind
    for y in range(H - 3, -1, -1):
        for x in range(W):
            src_x = x + random.randint(-1, 1)
            if src_x < 0:
                src_x = 0
            elif src_x >= W:
                src_x = W - 1
            src = heat[xy(src_x, y + 1)]
            cooling = random.randint(0, 38)
            v = src - cooling
            heat[xy(x, y)] = v if v > 0 else 0

def render():
    for i in range(NUM):
        np[i] = FIRE_PALETTE[heat[i] >> 4]
    np.write()

try:
    while True:
        step()
        render()
        time.sleep_ms(130)
except KeyboardInterrupt:
    for i in range(NUM):
        np[i] = (0, 0, 0)
    np.write()
05 Mario-sprites Svamp → 1UP → stjärna → eldblomma → mynt
Ladda ner 208 rader
"""Mario-sprite-galleri: svamp -> 1UP -> stjärna -> eldblomma -> snurrande mynt.
Slide-övergångar mellan varje (sidescroll-stil)."""
import machine, neopixel, time, random

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

def xy(x, y):
    return y * W + x

# Färgpalett (låg ljusstyrka)
PAL = {
    '.': (0, 0, 0),
    'R': (24, 0, 0),     # Mario-röd
    'W': (18, 18, 18),   # vit
    'G': (0, 22, 0),     # 1UP-grön
    'Y': (24, 20, 0),    # gul
    'O': (26, 8, 0),     # orange
    'B': (4, 4, 4),      # mörkbrun/svart-kontur
}

MUSHROOM = [
    "..RRRR..",
    ".RWWWWR.",
    "RWWRWRWR",
    "RRRRRRRR",
    "RRWRRWRR",
    ".RRRRRR.",
    "..WWWW..",
    "..WWWW..",
]

ONE_UP = [
    "..GGGG..",
    ".GWWWWG.",
    "GWWGWGWG",
    "GGGGGGGG",
    "GGWGGWGG",
    ".GGGGGG.",
    "..WWWW..",
    "..WWWW..",
]

STAR = [
    "...YY...",
    "..YYYY..",
    "YYYYYYYY",
    ".YYYYYY.",
    ".YYYYYY.",
    ".YY..YY.",
    ".Y....Y.",
    "Y......Y",
]

FLOWER = [
    "..ORRO..",
    ".OYYYYO.",
    "OYYWWYYO",
    ".OYYYYO.",
    "...OO...",
    "...GG...",
    "..GGGG..",
    "..G..G..",
]

COIN_FRONT = [
    "..YYYY..",
    ".YYYYYY.",
    "YYOYYOYY",
    "YYOYYOYY",
    "YYYYYYYY",
    "YYOOOOYY",
    ".YYYYYY.",
    "..YYYY..",
]

COIN_NARROW = [
    "...YY...",
    "...YY...",
    "...OY...",
    "...OY...",
    "...YY...",
    "...OY...",
    "...YY...",
    "...YY...",
]

COIN_EDGE = [
    "....Y...",
    "....Y...",
    "....Y...",
    "....Y...",
    "....Y...",
    "....Y...",
    "....Y...",
    "....Y...",
]

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def draw(sprite, sx=0, tint=1.0):
    """Rita sprite med vänster kant vid kolumn sx. tint skalar färgen."""
    for ry in range(8):
        row = sprite[ry]
        for rx in range(8):
            x = rx + sx
            if 0 <= x < W:
                c = PAL[row[rx]]
                if c != (0, 0, 0):
                    np[xy(x, ry)] = (int(c[0] * tint), int(c[1] * tint), int(c[2] * tint))

def show_static(sprite, dur_ms=2200):
    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        clear()
        draw(sprite)
        np.write()
        time.sleep_ms(80)

def show_star(dur_ms=2500):
    """Stjärna med gnistor runt om."""
    t0 = time.ticks_ms()
    sparkle_positions = []  # (x, y, life)
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        clear()
        draw(STAR)
        # gnistor: lägg till nya
        for _ in range(2):
            sparkle_positions.append([random.randint(0, W - 1),
                                      random.randint(0, H - 1), 4])
        # rita gnistor, minska livstid
        new_sparkles = []
        for sp in sparkle_positions:
            x, y, life = sp
            if life > 0:
                # tona ned vit gnista
                v = 6 * life
                # endast lägg till om pixeln är svart (inte ovanpå stjärnan)
                idx = xy(x, y)
                if np[idx] == (0, 0, 0):
                    np[idx] = (v, v, v // 2)
                sp[2] = life - 1
                new_sparkles.append(sp)
        sparkle_positions = new_sparkles
        np.write()
        time.sleep_ms(80)

def show_flower(dur_ms=2500):
    """Eldblomma som pulserar mellan röd-orange och gul-orange."""
    t0 = time.ticks_ms()
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        clear()
        # pulsera mellan 0.5 och 1.0 ljusstyrka via sin
        import math
        tint = 0.55 + 0.45 * (0.5 + 0.5 * math.sin(frame * 0.25))
        draw(FLOWER, tint=tint)
        np.write()
        frame += 1
        time.sleep_ms(60)

def show_coin(dur_ms=2500):
    """Snurrande mynt: front -> narrow -> edge -> narrow -> front -> ..."""
    frames = [COIN_FRONT, COIN_FRONT, COIN_NARROW, COIN_EDGE,
              COIN_NARROW, COIN_FRONT, COIN_FRONT, COIN_FRONT]
    t0 = time.ticks_ms()
    i = 0
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        clear()
        draw(frames[i % len(frames)])
        np.write()
        i += 1
        time.sleep_ms(110)

def transition(s_out, s_in, dur_ms=700):
    """Slide-övergång: s_out åker ut åt vänster, s_in glider in från höger."""
    steps = 9
    for k in range(steps):
        clear()
        draw(s_out, sx=-k)
        draw(s_in, sx=8 - k)
        np.write()
        time.sleep_ms(dur_ms // steps)

# Sekvens: (sprite, visa-funktion, övergångs-sprite-för-slide)
SHOWS = [
    (MUSHROOM,    lambda: show_static(MUSHROOM)),
    (ONE_UP,      lambda: show_static(ONE_UP)),
    (STAR,        show_star),
    (FLOWER,      show_flower),
    (COIN_FRONT,  show_coin),
]

try:
    i = 0
    while True:
        sprite, show_fn = SHOWS[i]
        show_fn()
        next_sprite = SHOWS[(i + 1) % len(SHOWS)][0]
        transition(sprite, next_sprite)
        i = (i + 1) % len(SHOWS)
except KeyboardInterrupt:
    clear()
    np.write()
06 Scrollande text '6446.se' med plasma-bakgrund
Ladda ner 93 rader
"""Bara scrollande '6446.se' med fyllt plasma-skimmer som bakgrund. Loopar för evigt."""
import machine, neopixel, time, math

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

DIM = 24  # textens ljusstyrka

def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def bg_color(hue, peak=6):
    """Alltid >=1 per kanal — bakgrunden är aldrig svart."""
    pos = hue & 255
    span = peak - 1
    if pos < 85:
        r = 1 + pos * span // 85
        g = 1 + (85 - pos) * span // 85
        b = 1
    elif pos < 170:
        pos -= 85
        r = 1 + (85 - pos) * span // 85
        g = 1
        b = 1 + pos * span // 85
    else:
        pos -= 170
        r = 1
        g = 1 + pos * span // 85
        b = 1 + (85 - pos) * span // 85
    return (r, g, b)

# 5x7-font för "6446.se"
FONT = {
    '6': ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
    '4': ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    '.': ["00000", "00000", "00000", "00000", "00000", "00000", "01100"],
    's': ["00000", "00000", "01110", "10000", "01110", "00001", "11110"],
    'e': ["00000", "00000", "01110", "10001", "11111", "10000", "01110"],
    ' ': ["00000"] * 7,
}

def text_columns(text):
    cols = []
    for ch in text:
        glyph = FONT.get(ch, FONT[' '])
        char_w = len(glyph[0])
        for cx in range(char_w):
            col = 0
            for ry in range(7):
                if glyph[ry][cx] == '1':
                    col |= (1 << ry)
            cols.append(col)
        cols.append(0)  # 1 col mellanrum
    return cols

TEXT_COLOR = (DIM, DIM, DIM)
cols = text_columns("6446.se")
offset = -W
frame = 0

try:
    while True:
        # bakgrund: fyllt plasma-skimmer
        for y in range(H):
            for x in range(W):
                v = (math.sin(x * 0.55 + frame * 0.06)
                     + math.sin(y * 0.7 + frame * 0.08)
                     + math.sin((x + y) * 0.4 + frame * 0.05))
                hue = int(v * 50 + frame * 2) & 255
                np[xy(x, y)] = bg_color(hue, peak=6)
        # text ovanpå
        for x in range(W):
            ci = offset + x
            if 0 <= ci < len(cols):
                col = cols[ci]
                for y in range(H):
                    if col & (1 << y):
                        np[xy(x, y)] = TEXT_COLOR
        np.write()
        offset += 1
        frame += 1
        if offset > len(cols):
            offset = -W
        time.sleep_ms(80)
except KeyboardInterrupt:
    clear()
    np.write()
07 Trafiksignal-gubbe Röd → gul → grön cykel
Ladda ner 150 rader
"""Trafiksignal-gubbe: röd står -> SAM -> gul springer -> grön går -> SAM -> gul springer -> ..."""
import machine, neopixel, time, random

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

RED = (24, 0, 0)
GREEN = (0, 24, 0)
YELLOW = (26, 22, 0)
TEXT = (24, 24, 12)

# --- sprites ---
STAND = [
    "...##...",
    "...##...",
    "..####..",
    ".######.",
    "..####..",
    "..####..",
    "..#..#..",
    ".##..##.",
]

# Gångcykel (4 frames)
WALK = [
    [   "...##...", "...##...", "..####..", ".#####..",
        "..####..", "...####.", "..##.#..", ".##...##" ],
    [   "...##...", "...##...", "..####..", "..####..",
        "..####..", "..####..", "..####..", "..#..#.." ],
    [   "...##...", "...##...", "..####..", "..#####.",
        "..####..", ".####...", "..#.##..", "##...##." ],
    [   "...##...", "...##...", "..####..", "..####..",
        "..####..", "..####..", "..####..", "..#..#.." ],
]

# Springcykel (2 frames för snabb alternering)
RUN = [
    [   "....##..", "....##..", "..####..", ".####...",
        "..####..", "...####.", ".##..#..", "##....##" ],
    [   "..##....", "..##....", "..####..", "...####.",
        "..####..", ".####...", "..#..##.", "##....##" ],
]

def draw(sprite, color):
    clear()
    for y in range(8):
        for x in range(8):
            if sprite[y][x] == '#':
                np[xy(x, y)] = color
    np.write()

def show_static(sprite, color, dur_ms):
    draw(sprite, color)
    time.sleep_ms(dur_ms)

def animate(frames, color, dur_ms, frame_ms):
    t0 = time.ticks_ms()
    i = 0
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        draw(frames[i % len(frames)], color)
        time.sleep_ms(frame_ms)
        i += 1

# --- font (5x7) ---
FONT = {
    'S': [".###.", "#...#", "#....", ".###.", "....#", "#...#", ".###."],
    'A': [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
    'M': ["#...#", "##.##", "#.#.#", "#...#", "#...#", "#...#", "#...#"],
}

def wheel(pos, peak=10):
    """0-255 -> RGB regnbåge med max `peak` per kanal."""
    pos &= 255
    if pos < 85:
        return (pos * peak // 85, (85 - pos) * peak // 85, 0)
    elif pos < 170:
        pos -= 85
        return ((85 - pos) * peak // 85, 0, pos * peak // 85)
    else:
        pos -= 170
        return (0, pos * peak // 85, (85 - pos) * peak // 85)

# Persistent sparkle-state: varje pixel har [hue, livstid 0..1]
_sparkle = [[0, 0.0] for _ in range(NUM)]

def show_letter_sparkle(ch, color, dur_ms=600, frame_ms=70):
    """Visa bokstav med regnbågs-glitter i bakgrunden (svagt lysande)."""
    glyph = FONT[ch]
    glyph_w = len(glyph[0])
    x0 = (W - glyph_w) // 2
    # Vilka pixlar tillhör bokstaven (ska inte få sparkles ovanpå)
    letter_pixels = set()
    for ry in range(7):
        for cx in range(glyph_w):
            if glyph[ry][cx] == '#':
                letter_pixels.add(xy(x0 + cx, ry))

    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < dur_ms:
        # Spawna 2-3 nya gnistor på slumpvisa bakgrundspixlar
        for _ in range(3):
            idx = random.randint(0, NUM - 1)
            if idx not in letter_pixels:
                _sparkle[idx][0] = random.randint(0, 255)
                _sparkle[idx][1] = 1.0
        # Rendera bakgrund (svagt lysande, fadar)
        for i in range(NUM):
            if i in letter_pixels:
                continue
            hue, life = _sparkle[i]
            if life > 0:
                c = wheel(hue, peak=10)
                np[i] = (int(c[0] * life), int(c[1] * life), int(c[2] * life))
                _sparkle[i][1] = max(0.0, life - 0.10)
            else:
                np[i] = (0, 0, 0)
        # Bokstav ovanpå
        for idx in letter_pixels:
            np[idx] = color
        np.write()
        time.sleep_ms(frame_ms)

def show_text(text, color, letter_ms=600, gap_ms=120):
    for ch in text:
        show_letter_sparkle(ch, color, letter_ms)
        clear()
        np.write()
        time.sleep_ms(gap_ms)

# --- main loop ---
try:
    while True:
        show_static(STAND, RED, 5000)
        show_text("SAM", TEXT)
        animate(RUN, YELLOW, 1500, 90)
        animate(WALK, GREEN, 5000, 180)
        show_text("SAM", TEXT)
        animate(RUN, YELLOW, 1500, 90)
except KeyboardInterrupt:
    clear()
    np.write()
08 Joystick-diagnos Skriver ut vilken pin som är intryckt
Ladda ner 39 rader
"""Joystick-diagnostik: skriver ut vilken pin som är intryckt.
Antar pull-up (intern), så tryck = LOW.
Visar också en pixel per pin på matrisen så vi ser direkt."""
import machine, neopixel, time

PIN_LED = 4
PINS = [6, 7, 8, 9, 10]
COLORS = [(20, 0, 0), (0, 20, 0), (0, 0, 20), (20, 20, 0), (20, 0, 20)]

np = neopixel.NeoPixel(machine.Pin(PIN_LED), 64)

inputs = []
for p in PINS:
    inputs.append(machine.Pin(p, machine.Pin.IN, machine.Pin.PULL_UP))

print("Tryck på joysticken — pin som är LOW visas. Ctrl-C för att stoppa.")
last_state = [1] * len(PINS)
while True:
    # läs alla
    state = [pin.value() for pin in inputs]
    # rensa matrisen
    for i in range(64):
        np[i] = (0, 0, 0)
    # för varje pin: tänd en kolumn-pixel om LOW
    for i, v in enumerate(state):
        if v == 0:
            # lyser tre pixlar i en rad för synlighet
            for r in range(3):
                np[r * 8 + i] = COLORS[i]
    np.write()
    # logga på change
    for i, v in enumerate(state):
        if v != last_state[i]:
            if v == 0:
                print("  GPIO", PINS[i], "TRYCKT")
            else:
                print("  GPIO", PINS[i], "släppt")
    last_state = state
    time.sleep_ms(50)
09 Endless runner 3 lanes, hoppa över hinder
Ladda ner 174 rader
"""Endless runner med 3 lanes, oändligt spel (man dör aldrig).
- UP/DOWN-klick byter lane ETT steg
- Röd hinder = -1 poäng (min 0), regnbågs-collectible = +1
- Topp-raden visar score 0-7. Vid 8 -> liten regnbågs-show -> reset
- Difficulty rampar långsamt upp över tid
- Håll KLICK i 2 sek -> reset hela spelet (score + difficulty)
"""
import machine, neopixel, time, random

LED_PIN = 4
JOY_UP, JOY_DOWN, JOY_LEFT, JOY_RIGHT, JOY_CLICK = 10, 9, 7, 8, 6

W = H = 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(LED_PIN), NUM)

up_btn    = machine.Pin(JOY_UP,    machine.Pin.IN, machine.Pin.PULL_UP)
down_btn  = machine.Pin(JOY_DOWN,  machine.Pin.IN, machine.Pin.PULL_UP)
click_btn = machine.Pin(JOY_CLICK, machine.Pin.IN, machine.Pin.PULL_UP)

def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def wheel(pos, peak=28):
    pos &= 255
    if pos < 85:
        return (pos * peak // 85, (85 - pos) * peak // 85, 0)
    elif pos < 170:
        pos -= 85
        return ((85 - pos) * peak // 85, 0, pos * peak // 85)
    else:
        pos -= 170
        return (0, pos * peak // 85, (85 - pos) * peak // 85)

PLAYER_X = 1
LANES = [2, 4, 6]
COLOR_PLAYER    = (16, 16, 24)
COLOR_LANE_HINT = (1, 1, 1)
COLOR_SCORE     = (0, 28, 4)
COLOR_RESET     = (16, 0, 16)

def play_show():
    """Regnbågs-show ~1,3 sek vid 8 poäng."""
    clear()
    for i in range(8):
        np[xy(i, 0)] = (0, 32, 0)
    np.write()
    time.sleep_ms(250)
    t0 = time.ticks_ms()
    f = 0
    while time.ticks_diff(time.ticks_ms(), t0) < 1300:
        for y in range(H):
            for x in range(W):
                hue = (f * 10 + x * 24 + y * 18) & 255
                np[xy(x, y)] = wheel(hue, peak=32)
        for _ in range(5):
            i = random.randint(0, NUM - 1)
            np[i] = (40, 40, 40)
        np.write()
        f += 1
        time.sleep_ms(45)

def reset_anim():
    """Lila puls + släck när klick hållits i 2 sek."""
    for _ in range(2):
        for i in range(NUM):
            np[i] = COLOR_RESET
        np.write()
        time.sleep_ms(150)
        clear()
        np.write()
        time.sleep_ms(120)

def spawn_entity():
    lane = random.randint(0, 2)
    kind = 0 if random.random() < 0.55 else 1
    return [7, lane, kind]

# --- outer loop: kör spelet, restartar vid hold-click ---
while True:
    player_lane = 1
    entities = []
    score = 0
    tick = 0
    up_prev = 1
    down_prev = 1
    click_pressed_ms = 0

    while True:
        tick += 1
        up_now = up_btn.value()
        down_now = down_btn.value()
        click_now = click_btn.value()
        up_just = (up_prev == 1 and up_now == 0)
        down_just = (down_prev == 1 and down_now == 0)
        up_prev = up_now
        down_prev = down_now

        if up_just and player_lane > 0:
            player_lane -= 1
        if down_just and player_lane < 2:
            player_lane += 1
        player_y = LANES[player_lane]

        # Klick-hold: 2 sek = restart
        now_ms = time.ticks_ms()
        if click_now == 0:  # pressed
            if click_pressed_ms == 0:
                click_pressed_ms = now_ms
            elif time.ticks_diff(now_ms, click_pressed_ms) >= 2000:
                reset_anim()
                break  # restart outer loop -> initierar allt
        else:
            click_pressed_ms = 0

        # Score-baserad difficulty:
        # 0 poäng = långsamt, fler poäng = snabbare. Tappar man röd sänks farten igen.
        # score 0 -> speed 10 (~600ms/pixel), score 7 -> speed 3 (~180ms/pixel)
        speed = max(3, 10 - score)
        spawn_chance = 0.18 + score * 0.035   # 0.18 -> 0.43

        if tick % speed == 0:
            kept = []
            for e in entities:
                e[0] -= 1
                if e[0] >= 0:
                    kept.append(e)
            entities = kept
            if random.random() < spawn_chance:
                ent = spawn_entity()
                if not any(e[0] == 7 for e in entities):
                    entities.append(ent)

        # Kollisioner
        survivors = []
        for e in entities:
            x, lane, k = e
            if x == PLAYER_X and lane == player_lane:
                if k == 0:
                    score = max(0, score - 1)
                else:
                    score += 1
                continue
            survivors.append(e)
        entities = survivors

        # Render
        clear()
        for i in range(min(score, 8)):
            np[xy(i, 0)] = COLOR_SCORE
        for y in LANES:
            np[xy(W - 1, y)] = COLOR_LANE_HINT
        for e in entities:
            x, lane, k = e
            if 0 <= x < W:
                y = LANES[lane]
                if k == 0:
                    np[xy(x, y)] = (48, 0, 0)
                else:
                    hue = (tick * 10 + x * 32 + y * 16) & 255
                    np[xy(x, y)] = wheel(hue, peak=30)
        np[xy(PLAYER_X, player_y)] = COLOR_PLAYER
        np.write()

        if score >= 8:
            play_show()
            score = 0
            entities = []

        time.sleep_ms(60)
10 Glitched-bundle Allt-i-ett: meny + flera scener
Ladda ner 178 rader
"""Glitched-bundle för 8x8 NeoPixel på GPIO 4 / ESP32-C3.
Cyklar: vortex -> '46elks' scrollar med glitter -> Glitched G med glitter.
Lägg som main.py på nyflashat kort så går det igång automatiskt."""
import machine, neopixel, time, math, random

PIN = 4
W, H = 8, 8
NUM = W * H
np = neopixel.NeoPixel(machine.Pin(PIN), NUM)

DIM = 24
CX, CY = 3.5, 3.5

def xy(x, y):
    return y * W + x

def clear():
    for i in range(NUM):
        np[i] = (0, 0, 0)

def wheel(pos, peak=DIM):
    pos &= 255
    if pos < 85:
        return (pos * peak // 85, (85 - pos) * peak // 85, 0)
    elif pos < 170:
        pos -= 85
        return ((85 - pos) * peak // 85, 0, pos * peak // 85)
    else:
        pos -= 170
        return (0, pos * peak // 85, (85 - pos) * peak // 85)

def scale(c, f):
    return (int(c[0] * f), int(c[1] * f), int(c[2] * f))

# =================================================================
# Vortex: roterande spiralarmar
# =================================================================
def vortex(duration_ms=9000):
    t0 = time.ticks_ms()
    frame = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        for y in range(H):
            for x in range(W):
                dx, dy = x - CX, y - CY
                r = math.sqrt(dx * dx + dy * dy)
                a = math.atan2(dy, dx)
                hue = int(a * 40 + r * 28 - frame * 6) & 255
                bright = 0.35 + 0.65 * (0.5 + 0.5 * math.sin(r * 1.6 - frame * 0.28))
                np[xy(x, y)] = scale(wheel(hue), bright)
        np.write()
        frame += 1
        time.sleep_ms(35)

# =================================================================
# Glitter-bakgrund (persistent state med fade)
# =================================================================
_sparkle = [[0, 0.0] for _ in range(NUM)]

def update_sparkle(spawn_per_frame=3, fade_rate=0.08):
    for _ in range(spawn_per_frame):
        i = random.randint(0, NUM - 1)
        _sparkle[i][0] = random.randint(0, 255)
        _sparkle[i][1] = 1.0
    for i in range(NUM):
        if _sparkle[i][1] > 0:
            _sparkle[i][1] = max(0.0, _sparkle[i][1] - fade_rate)

def render_sparkle(skip_indices=None, peak=12):
    """Rita glitter; pixlar i skip_indices lämnas svarta (för att texten/loggan kan ritas över)."""
    if skip_indices is None:
        skip_indices = set()
    for i in range(NUM):
        if i in skip_indices:
            continue
        hue, life = _sparkle[i]
        if life > 0:
            c = wheel(hue, peak=peak)
            np[i] = (int(c[0] * life), int(c[1] * life), int(c[2] * life))
        else:
            np[i] = (0, 0, 0)

# =================================================================
# Glitched G (grövre — 2-pixel-tjocka kanter)
# =================================================================
GLITCHED_G = [
    ".######.",
    "########",
    "##......",
    "##.####.",
    "##.####.",
    "##......",
    "########",
    ".######.",
]

def show_g_with_glitter(duration_ms=6500):
    g_pixels = set()
    for y in range(8):
        for x in range(8):
            if GLITCHED_G[y][x] == '#':
                g_pixels.add(xy(x, y))
    t0 = time.ticks_ms()
    f = 0
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        update_sparkle(spawn_per_frame=2, fade_rate=0.08)
        render_sparkle(skip_indices=g_pixels, peak=12)
        # G:t cyklar i regnbåge ovanpå
        g_color = wheel(f * 4, peak=DIM)
        for idx in g_pixels:
            np[idx] = g_color
        np.write()
        f += 1
        time.sleep_ms(55)

# =================================================================
# Scrollande '46elks' med glitter bakom
# =================================================================
FONT = {
    '4': ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    '6': ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
    'e': ["00000", "00000", "01110", "10001", "11111", "10000", "01110"],
    'l': ["01100", "00100", "00100", "00100", "00100", "00100", "01110"],
    'k': ["10000", "10000", "10010", "10100", "11000", "10100", "10010"],
    's': ["00000", "00000", "01110", "10000", "01110", "00001", "11110"],
    ' ': ["00000"] * 7,
}

def text_columns(text):
    cols = []
    for ch in text:
        glyph = FONT[ch]
        w = len(glyph[0])
        for cx in range(w):
            col = 0
            for ry in range(7):
                if glyph[ry][cx] == '1':
                    col |= (1 << ry)
            cols.append(col)
        cols.append(0)
    return cols

def scroll_46elks_with_glitter(duration_ms=7000, step_ms=100):
    cols = text_columns("46elks")
    offset = -W
    text_color = (28, 28, 28)
    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < duration_ms:
        # ta reda på vilka pixlar texten täcker just nu (för att hoppa över glitter där)
        text_pixels = set()
        for x in range(W):
            ci = offset + x
            if 0 <= ci < len(cols):
                col = cols[ci]
                for y in range(H):
                    if col & (1 << y):
                        text_pixels.add(xy(x, y))
        update_sparkle(spawn_per_frame=3, fade_rate=0.08)
        render_sparkle(skip_indices=text_pixels, peak=12)
        # text ovanpå
        for idx in text_pixels:
            np[idx] = text_color
        np.write()
        offset += 1
        if offset > len(cols):
            offset = -W
        time.sleep_ms(step_ms)

# =================================================================
# Huvudcykel
# =================================================================
try:
    while True:
        vortex(9000)
        scroll_46elks_with_glitter(8000)
        show_g_with_glitter(6500)
except KeyboardInterrupt:
    clear()
    np.write()
Så ersätter du Glitched-effekten: ladda ner ett exempel, döp om till main.py, spara på kortet (via Thonny eller mpremote cp 02_bounce.py :main.py), och tryck reset.

Claude Code — låt en AI skriva åt dig

Claude Code är ett CLI-verktyg från Anthropic som läser och skriver i din kodbas. Riktigt bra för att iterera på pixel-effekter.

Installation

# macOS / Linux (kräver Node 18+)
npm install -g @anthropic-ai/claude-code

# Starta i din projektmapp
mkdir min-neopixel && cd min-neopixel
curl -O https://6446.se/static/examples/02_bounce.py   # startkod
claude

Workshop-prompter att kopiera

Jag har en 8x8 WS2812 NeoPixel-matris på GPIO 4 på en
ESP32-C3 Supermini som kör MicroPython. Pixel 0 är uppe
vänster och xy(x, y) = y * 8 + x. Här är min main.py:
@02_bounce.py

Lägg till en regnbåge i bakgrunden bakom den studsande bollen.

# Andra prompter att prova:
"Gör om text-scrolln så den skriver mitt namn"
"Skriv en effekt där en sinusvåg böljer i regnbågsfärger"
"Bygg en pong-spel med joystick på GPIO 0, 1 och 2"

Pusha ändringen till kortet

mpremote connect /dev/cu.usbmodem* cp 02_bounce.py :main.py + reset
Tips: Tala alltid om hårdvaru-kontexten (8×8, GPIO 4, ESP32-C3, MicroPython, xy-mappning). Då skriver Claude rätt API från första försöket.

Andra AI-alternativ

Reflasha (om något skiter sig)

Om kortet hänger sig eller du tappat main.py — flasha om MicroPython från scratch:

pip3 install --user --break-system-packages esptool

curl -LO https://micropython.org/resources/firmware/ESP32_GENERIC_C3-20260406-v1.28.0.bin

esptool --chip esp32c3 --port /dev/cu.usbmodem* erase-flash
esptool --chip esp32c3 --port /dev/cu.usbmodem* --baud 460800 \
        write-flash -z 0x0 ESP32_GENERIC_C3-20260406-v1.28.0.bin
Efter "Hard resetting via RTS pin..." händer ingenting fysiskt — RTS är inte kopplad. Dra ur USB och sätt i igen.

Sen, för att få tillbaka Glitched-effekten: ladda ner 10_glitched.py och pusha som main.py.

Felsökning

SymptomTrolig orsak
Kortet syns inte i Thonny / mpremoteUSB-kabeln saknar datapar — testa en annan
"could not enter raw repl"Kortet är fast i bootloader → dra ur/sätt i USB
Inget händer / pixlar tänds inteDatalinan har lossnat från GPIO 4
Bara första pixeln tändsTrasig datalina mellan pixel 1 och 2
Slumpfärger / flickerStrömproblem — sänk ljusstyrkan (RGB-värden) eller mata in extern 5V
Text är spegelvänd eller upp-och-nerPanelen är annorlunda wirad — se Pixel-layout
Editorn säger "Permission denied"En annan process håller porten — stäng Thonny eller andra mpremote-sessioner