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.
All kod finns också på GitHub: github.com/B2elks/6446 — klona, forka, skicka PR.
Vad finns på kortet redan
- MicroPython 1.28 som operativsystem. Stödjer
machine,neopixel,time,math,randomdirekt. main.py= "Glitched-bundle": cyklar mellan en spiralvirvel, scrollande "46elks"-text, och en glittrande Glitched-G. Hela koden finns som 10_glitched.py.- Pixel-data går på GPIO 4. Antal: 64 (8×8).
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 med filhanterare + editor + REPL (lätt nybörjarval)
- mpremote — kommandoraden, snabbt och scriptbart
Thonny — fönster-läget
- Hämta gratis från thonny.org.
- Öppna Thonny → Tools → Options → Interpreter.
- Välj "MicroPython (ESP32)", port = din USB-port (t.ex.
/dev/cu.usbmodem*). - Klicka OK. Nu syns kortets filsystem nere till vänster — du ska se
main.pyochboot.py. - För att stoppa Glitched-effekten: tryck Thonnys röda Stop-knapp. Nu har du REPL-promten
>>>. - Öppna
main.py, ändra som du vill, spara (Cmd/Ctrl+S sparar tillbaka på kortet). - Tryck den gröna Run-knappen eller skriv
exec(open('main.py').read())i REPL för att testa.
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
Pixel-layout — verifiera först
Olika 8×8-WS2812-paneler är wirade olika:
- Rad-major rak — alla rader vänster→höger
- Serpentin — varannan rad i zigzag
- Roterad — monterad 90° eller 180° fel
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
"""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
"""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...
"""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)
"""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
"""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
"""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
"""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
"""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
"""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
"""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()
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
Andra AI-alternativ
- ChatGPT / Gemini-webbgränssnitt — bra för enstaka skripts, klistra in koden för hand.
- Cursor / Windsurf / Continue — AI inbakat i editorn.
- GitHub Copilot — autokomplettering medan du skriver.
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
Sen, för att få tillbaka Glitched-effekten: ladda ner 10_glitched.py och pusha som main.py.
Felsökning
| Symptom | Trolig orsak |
|---|---|
Kortet syns inte i Thonny / mpremote | USB-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 inte | Datalinan har lossnat från GPIO 4 |
| Bara första pixeln tänds | Trasig datalina mellan pixel 1 och 2 |
| Slumpfärger / flicker | Strömproblem — sänk ljusstyrkan (RGB-värden) eller mata in extern 5V |
| Text är spegelvänd eller upp-och-ner | Panelen är annorlunda wirad — se Pixel-layout |
| Editorn säger "Permission denied" | En annan process håller porten — stäng Thonny eller andra mpremote-sessioner |