This commit is contained in:
Tower committed 2026-10-21 19:13:16 +02:00
1 parent 3a473ec51e
commit 0ae74cd3fd
1 file changed
+838 -8
+838 -8
View File
@@ -43,12 +43,25 @@
# Windows voraus (Startmenue-Suche) und dass der Pico gerade am PC steckt,
# dessen Fokus auf dem Desktop liegt (kein Textfeld o.ae. offen), sonst
# landet der getippte Text an der falschen Stelle.
#
# Eigene Profile direkt am Geraet: letztes Profil "Neu" (per Encoder
# erreichbar) -> Taste1 = neues Profil anlegen (Name + 4 Tastenaktionen
# ueber Encoder/Taster zusammenklicken), Taste2 = eigenes Profil wieder
# loeschen. Landet persistent in CUSTOM_PROFILES_FILE im Flash-Speicher des
# Pico und uebersteht einen Neustart - kein PC/Thonny noetig. Siehe die
# Kommentare beim "Profil-Editor" weiter unten im Code fuer die genaue
# Bedienung.
import time
import random
from array import array
from machine import Pin, I2C, PWM
try:
import ujson as json # aeltere MicroPython-Firmware kennt nur "ujson"
except ImportError:
import json
import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode
from usb.device.hid import HIDInterface
@@ -128,7 +141,33 @@ SCREENSAVER_ENABLED = True
SCREENSAVER_TIMEOUT_MS = 30000 # 30s Inaktivitaet bis der Screensaver startet
SCREENSAVER_FRAME_MS = 80 # Bildwiederholrate der Animation
SCREENSAVER_ANIMATION = "stars" # Startwert - "stars"|"pacman"|"matrix"|"bounce"
PACMAN_SPEED_PX = 1 # Pac-Man Bewegung pro Bild in Pixel - kleiner = langsamer
PACMAN_SPEED_PX = 3 # Pac-Man Bewegung pro Bild in Pixel - kleiner = langsamer
# --- Mini-Spiele ("Games"-Profil, Taste 1-4 startet direkt ein Spiel) ---
# Verlassen jederzeit moeglich: Encoder-Taster druecken.
GAME_FRAME_MS = 60 # Bildrate fuer Dino/Flappy (ms zwischen Bildern)
GAME_DINO_GRAVITY = 1 # Fallbeschleunigung pro Bild
GAME_DINO_JUMP_VELOCITY = -7 # Sprungimpuls (negativ = nach oben)
GAME_DINO_SPEED_START = 3 # Anfangsgeschwindigkeit der Hindernisse (px/Bild)
GAME_FLAPPY_GRAVITY = 1 # Fallbeschleunigung pro Bild
GAME_FLAPPY_FLAP_VELOCITY = -3 # Aufwaerts-Impuls bei Tastendruck
GAME_FLAPPY_GAP_H = 13 # Luecke zwischen oberem/unterem Rohr (px)
GAME_FLAPPY_SPEED = 2 # Rohr-Geschwindigkeit (px/Bild)
GAME_TETRIS_COLS = 10 # Spielfeldbreite in Zellen
GAME_TETRIS_ROWS = 16 # Spielfeldhoehe in Zellen
GAME_TETRIS_CELL_PX = 2 # Zellengroesse in Pixel
GAME_TETRIS_DROP_MS_START = 500 # Anfangs-Falldauer pro Reihe (ms)
GAME_PACMAN_COLS = 32 # Labyrinth-Breite in Zellen
GAME_PACMAN_ROWS = 8 # Labyrinth-Hoehe in Zellen
GAME_PACMAN_CELL_PX = 4 # Zellengroesse in Pixel
GAME_PACMAN_MOVE_MS = 160 # Bewegungstakt Spieler (ms pro Feld)
GAME_PACMAN_GHOST_MOVE_EVERY = 2 # Geist bewegt sich nur bei jedem n-ten Takt
# --- Eigene Profile (direkt am Geraet anlegen, siehe "Neu"-Profil) ---
# Landen persistent (ueberleben einen Neustart) in dieser Datei im Flash-
# Speicher des Pico. Die festen Profile oben in PROFILES bleiben davon
# unberuehrt - geloescht werden kann nur, was man selbst angelegt hat.
CUSTOM_PROFILES_FILE = "/custom_profiles.json"
# ---------------------------------------------------------------------------
@@ -305,6 +344,12 @@ def do_action(action):
set_led_mode(action["mode"])
elif kind == "screensaver":
set_screensaver_animation(action["animation"])
elif kind == "game":
run_game(action["game"])
elif kind == "new_profile":
wizard_create_profile()
elif kind == "delete_profile":
wizard_delete_profile()
# "none" -> nichts tun
@@ -355,7 +400,7 @@ PROFILES = [
{"type": "open", "query": "Opera GX"},
{"type": "open", "query": "Visual Studio Code"},
{"type": "open", "query": "Thunderbird"},
{"type": "open", "query": "Bitwarden"},
{"type": "open", "query": "Vaultwarden"},
],
},
{
@@ -392,8 +437,72 @@ PROFILES = [
{"type": "screensaver", "animation": "bounce"},
],
},
{
# Mini-Spiele: Taste druecken startet direkt das jeweilige Spiel.
# Steuerung siehe Kommentar am Anfang des jeweiligen Spiels weiter
# unten im Code. Encoder-Taster druecken beendet ein Spiel jederzeit.
"name": "Games",
"labels": ["Dino", "Flpy", "Tetrs", "Pacmn"],
"actions": [
{"type": "game", "game": "dino"},
{"type": "game", "game": "flappy"},
{"type": "game", "game": "tetris"},
{"type": "game", "game": "pacman"},
],
},
]
# ---------------------------------------------------------------------------
# Eigene Profile: persistentes Laden von Flash + "Neu"-Menuepunkt anhaengen.
# BUILTIN_PROFILE_COUNT merkt sich, wo die festen Profile aufhoeren, damit
# der Loeschen-Dialog nur selbst angelegte Profile zur Auswahl anbietet.
# ---------------------------------------------------------------------------
BUILTIN_PROFILE_COUNT = len(PROFILES)
def _load_custom_profiles():
try:
with open(CUSTOM_PROFILES_FILE) as f:
data = json.load(f)
if isinstance(data, list):
return data
except Exception:
pass
return []
def _save_custom_profiles(profiles):
try:
with open(CUSTOM_PROFILES_FILE, "w") as f:
json.dump(profiles, f)
return True
except Exception:
return False
def _custom_profiles_list():
"""Aktuell selbst angelegte Profile (ohne feste Profile, ohne den
'Neu'-Menuepunkt) - das wird auf Flash gespeichert."""
return PROFILES[BUILTIN_PROFILE_COUNT:-1]
NEW_PROFILE_MENU_ENTRY = {
# Bleibt immer der letzte Eintrag in PROFILES (siehe wizard_create_profile,
# die neue Profile davor einfuegt).
"name": "Neu",
"labels": ["Erstl", "Losch", "-", "-"],
"actions": [
{"type": "new_profile"},
{"type": "delete_profile"},
NONE_ACTION,
NONE_ACTION,
],
}
for _cp in _load_custom_profiles():
PROFILES.append(_cp)
PROFILES.append(NEW_PROFILE_MENU_ENTRY)
# ---------------------------------------------------------------------------
# Hardware: Taster
# ---------------------------------------------------------------------------
@@ -550,10 +659,17 @@ _encoder_last_change_ms = 0
_encoder_step_accum = 0
profile_dirty = True # erzwingt initiales Display-Zeichnen
# Waehrend der Profil-Editor ("Neu"-Menue) laeuft, soll das Drehen des
# Encoders NICHT durch die Profile blaettern, sondern Listen im Editor
# scrollen. _encoder_mode steuert das um, _wiz_encoder_delta sammelt die
# Schritte fuer den Editor (siehe Mini-Spiele/Profil-Editor weiter unten).
_encoder_mode = "profile" # "profile" | "wizard"
_wiz_encoder_delta = 0
def _encoder_irq(pin):
global profile_index, _encoder_last_change_ms, profile_dirty, _encoder_step_accum
global _last_activity_ms, _screensaver_active
global _last_activity_ms, _screensaver_active, _wiz_encoder_delta
now = time.ticks_ms()
if time.ticks_diff(now, _encoder_last_change_ms) < ENCODER_DEBOUNCE_MS:
return
@@ -573,13 +689,19 @@ def _encoder_irq(pin):
_encoder_step_accum += 1 if forward else -1
if _encoder_step_accum >= ENCODER_STEPS_PER_CLICK:
profile_index = (profile_index + 1) % len(PROFILES)
_encoder_step_accum = 0
profile_dirty = True
if _encoder_mode == "wizard":
_wiz_encoder_delta += 1
else:
profile_index = (profile_index + 1) % len(PROFILES)
profile_dirty = True
elif _encoder_step_accum <= -ENCODER_STEPS_PER_CLICK:
profile_index = (profile_index - 1) % len(PROFILES)
_encoder_step_accum = 0
profile_dirty = True
if _encoder_mode == "wizard":
_wiz_encoder_delta -= 1
else:
profile_index = (profile_index - 1) % len(PROFILES)
profile_dirty = True
encoder_clk.irq(trigger=Pin.IRQ_FALLING, handler=_encoder_irq)
@@ -781,6 +903,714 @@ def _wake_screensaver():
draw_profile()
# ---------------------------------------------------------------------------
# Mini-Spiele (ueber das "Games"-Profil startbar)
# ---------------------------------------------------------------------------
# Alle vier Spiele teilen sich zwei Grundregeln:
# - Encoder-Taster druecken beendet das laufende Spiel sofort.
# - Nach "Game Over" (oder Sieg) einmal eine beliebige Taste druecken, um
# zurueck ins "Games"-Profil zu kommen.
# Waehrend ein Spiel laeuft, pausieren LED-Animation und Bildschirmschoner -
# die Hauptschleife wartet ja auf die Rueckkehr aus run_game().
# ---------------------------------------------------------------------------
def _game_exit_requested():
"""True, sobald der Encoder-Taster gedrueckt wird (jederzeit abbrechen)."""
return encoder_sw.value() == 0
def _game_any_button():
"""True, wenn irgendeine der 4 Tasten gerade gedrueckt ist."""
for btn in buttons:
if btn.value() == 0:
return True
return False
def _game_over_screen(title, score):
"""Zeigt Titel + Punktestand und wartet auf Tastendruck oder Encoder."""
oled.fill(0)
oled.text(title, 0, 4)
oled.text("Score: " + str(score), 0, 18)
oled.show()
# Erst warten, bis alles losgelassen ist (sonst fuehrt der Tastendruck,
# der zum Game Over gefuehrt hat, sofort wieder aus dem Bildschirm raus)
while _game_any_button() or _game_exit_requested():
time.sleep_ms(20)
while not (_game_any_button() or _game_exit_requested()):
time.sleep_ms(20)
# --- Spiel 1: Dino-Runner - Taste 1 = Springen ------------------------------
def _game_dino():
ground_y = 30
dino_x = 8
dino_w = 6
dino_h = 8
dino_y = ground_y - dino_h
vel_y = 0
on_ground = True
obst_x = 127
obst_w = 3
obst_h = random.randint(6, 10)
speed = GAME_DINO_SPEED_START
score = 0
while True:
if _game_exit_requested():
return
if buttons[0].value() == 0 and on_ground:
vel_y = GAME_DINO_JUMP_VELOCITY
on_ground = False
vel_y += GAME_DINO_GRAVITY
dino_y += vel_y
if dino_y >= ground_y - dino_h:
dino_y = ground_y - dino_h
vel_y = 0
on_ground = True
obst_x -= speed
if obst_x < -obst_w:
obst_x = 127
obst_h = random.randint(6, 10)
score += 1
if score % 5 == 0:
speed += 1
if dino_x < obst_x + obst_w and dino_x + dino_w > obst_x:
if dino_y + dino_h > ground_y - obst_h:
_game_over_screen("GAME OVER", score)
return
oled.fill(0)
oled.hline(0, ground_y, 128, 1)
oled.fill_rect(dino_x, dino_y, dino_w, dino_h, 1)
oled.fill_rect(obst_x, ground_y - obst_h, obst_w, obst_h, 1)
oled.text(str(score), 100, 0)
oled.show()
time.sleep_ms(GAME_FRAME_MS)
# --- Spiel 2: Floppy Fly - Taste 1 = Fluegelschlag --------------------------
def _game_flappy():
bird_x = 20
bird_y = 16
vel_y = 0
pipe_x = 127
pipe_w = 4
gap_h = GAME_FLAPPY_GAP_H
gap_y = random.randint(2, 32 - gap_h - 2)
score = 0
scored_this_pipe = False
while True:
if _game_exit_requested():
return
if buttons[0].value() == 0:
vel_y = GAME_FLAPPY_FLAP_VELOCITY
vel_y += GAME_FLAPPY_GRAVITY
bird_y += vel_y
pipe_x -= GAME_FLAPPY_SPEED
if pipe_x < -pipe_w:
pipe_x = 127
gap_y = random.randint(2, 32 - gap_h - 2)
scored_this_pipe = False
if not scored_this_pipe and pipe_x + pipe_w < bird_x:
score += 1
scored_this_pipe = True
crashed = bird_y < 0 or bird_y > 31
if pipe_x < bird_x + 2 and pipe_x + pipe_w > bird_x - 2:
if bird_y < gap_y or bird_y > gap_y + gap_h:
crashed = True
if crashed:
_game_over_screen("GAME OVER", score)
return
oled.fill(0)
oled.fill_rect(pipe_x, 0, pipe_w, gap_y, 1)
oled.fill_rect(pipe_x, gap_y + gap_h, pipe_w, 32 - (gap_y + gap_h), 1)
oled.fill_rect(bird_x - 2, int(bird_y) - 2, 4, 4, 1)
oled.text(str(score), 100, 0)
oled.show()
time.sleep_ms(GAME_FRAME_MS)
# --- Spiel 3: Mini-Tetris ----------------------------------------------------
# Taste1=Links, Taste2=Rechts, Taste3=Drehen, Taste4=Runter (Soft-Drop)
_TETRIS_SHAPES = {
"I": [[(0, 1), (1, 1), (2, 1), (3, 1)], [(2, 0), (2, 1), (2, 2), (2, 3)]],
"O": [[(1, 0), (2, 0), (1, 1), (2, 1)]],
"T": [
[(1, 0), (0, 1), (1, 1), (2, 1)],
[(1, 0), (1, 1), (2, 1), (1, 2)],
[(0, 1), (1, 1), (2, 1), (1, 2)],
[(1, 0), (0, 1), (1, 1), (1, 2)],
],
"S": [[(1, 0), (2, 0), (0, 1), (1, 1)], [(1, 0), (1, 1), (2, 1), (2, 2)]],
"Z": [[(0, 0), (1, 0), (1, 1), (2, 1)], [(2, 0), (1, 1), (2, 1), (1, 2)]],
"L": [
[(0, 0), (0, 1), (1, 1), (2, 1)],
[(1, 0), (2, 0), (1, 1), (1, 2)],
[(0, 1), (1, 1), (2, 1), (2, 2)],
[(1, 0), (1, 1), (0, 2), (1, 2)],
],
"J": [
[(2, 0), (0, 1), (1, 1), (2, 1)],
[(1, 0), (1, 1), (1, 2), (2, 2)],
[(0, 1), (1, 1), (2, 1), (0, 2)],
[(0, 0), (1, 0), (1, 1), (1, 2)],
],
}
_TETRIS_NAMES = list(_TETRIS_SHAPES.keys())
def _tetris_cells(shape, rot, px, py):
rots = _TETRIS_SHAPES[shape]
dxdy = rots[rot % len(rots)]
return [(px + dx, py + dy) for dx, dy in dxdy]
def _tetris_valid(cells, grid, cols, rows):
for cx, cy in cells:
if cx < 0 or cx >= cols or cy < 0 or cy >= rows:
return False
if grid[cy][cx]:
return False
return True
def _game_tetris():
cols = GAME_TETRIS_COLS
rows = GAME_TETRIS_ROWS
cell = GAME_TETRIS_CELL_PX
grid = [[0] * cols for _ in range(rows)]
def spawn():
s = random.choice(_TETRIS_NAMES)
return s, 0, cols // 2 - 2, 0
shape, rot, px, py = spawn()
score = 0
drop_ms = GAME_TETRIS_DROP_MS_START
last_drop = time.ticks_ms()
btn_last = [1, 1, 1, 1]
while True:
if _game_exit_requested():
return
now = time.ticks_ms()
for i in range(4):
state = buttons[i].value()
if state == 0 and btn_last[i] == 1:
if i == 0:
cells = _tetris_cells(shape, rot, px - 1, py)
if _tetris_valid(cells, grid, cols, rows):
px -= 1
elif i == 1:
cells = _tetris_cells(shape, rot, px + 1, py)
if _tetris_valid(cells, grid, cols, rows):
px += 1
elif i == 2:
new_rot = rot + 1
if _tetris_valid(_tetris_cells(shape, new_rot, px, py), grid, cols, rows):
rot = new_rot
elif _tetris_valid(_tetris_cells(shape, new_rot, px - 1, py), grid, cols, rows):
rot = new_rot
px -= 1
elif _tetris_valid(_tetris_cells(shape, new_rot, px + 1, py), grid, cols, rows):
rot = new_rot
px += 1
elif i == 3:
cells = _tetris_cells(shape, rot, px, py + 1)
if _tetris_valid(cells, grid, cols, rows):
py += 1
btn_last[i] = state
if time.ticks_diff(now, last_drop) >= drop_ms:
last_drop = now
cells = _tetris_cells(shape, rot, px, py + 1)
if _tetris_valid(cells, grid, cols, rows):
py += 1
else:
for cx, cy in _tetris_cells(shape, rot, px, py):
if 0 <= cy < rows and 0 <= cx < cols:
grid[cy][cx] = 1
cleared = 0
new_grid = []
for r in range(rows):
if all(grid[r]):
cleared += 1
else:
new_grid.append(grid[r])
for _ in range(cleared):
new_grid.insert(0, [0] * cols)
grid = new_grid
if cleared:
score += cleared * 10
drop_ms = max(120, drop_ms - cleared * 15)
shape, rot, px, py = spawn()
if not _tetris_valid(_tetris_cells(shape, rot, px, py), grid, cols, rows):
_game_over_screen("GAME OVER", score)
return
oled.fill(0)
for r in range(rows):
for c in range(cols):
if grid[r][c]:
oled.fill_rect(c * cell, r * cell, cell, cell, 1)
for cx, cy in _tetris_cells(shape, rot, px, py):
if 0 <= cy < rows and 0 <= cx < cols:
oled.fill_rect(cx * cell, cy * cell, cell, cell, 1)
oled.text("SCR", cols * cell + 4, 2)
oled.text(str(score), cols * cell + 4, 12)
oled.show()
time.sleep_ms(30)
# --- Spiel 4: Mini-Pacman -----------------------------------------------------
# Taste1=Hoch, Taste2=Runter, Taste3=Links, Taste4=Rechts. Ein Geist jagt dich,
# links/rechts gibt es einen Tunnel-Wrap wie im Original.
def _game_pacman_maze(cols, rows, tunnel_row):
grid = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
if r == 0 or r == rows - 1:
grid[r][c] = 1
elif c == 0 or c == cols - 1:
grid[r][c] = 0 if r == tunnel_row else 1
elif r in (2, rows - 3) and c % 4 in (1, 2):
grid[r][c] = 1
return grid
def _game_pacman():
cols = GAME_PACMAN_COLS
rows = GAME_PACMAN_ROWS
cell = GAME_PACMAN_CELL_PX
tunnel_row = rows // 2
walls = _game_pacman_maze(cols, rows, tunnel_row)
dots = set()
for r in range(rows):
for c in range(cols):
if not walls[r][c]:
dots.add((c, r))
px, py = 1, 1
dots.discard((px, py))
pdx, pdy = 1, 0
gx, gy = cols - 2, rows - 2
gdx, gdy = -1, 0
score = 0
tick = 0
last_move = time.ticks_ms()
btn_last = [1, 1, 1, 1]
def can_move(x, y, dx, dy):
ny = y + dy
if ny < 0 or ny >= rows:
return False
nx = (x + dx) % cols
return not walls[ny][nx]
while True:
if _game_exit_requested():
return
for i in range(4):
state = buttons[i].value()
if state == 0 and btn_last[i] == 1:
if i == 0:
pdx, pdy = 0, -1
elif i == 1:
pdx, pdy = 0, 1
elif i == 2:
pdx, pdy = -1, 0
elif i == 3:
pdx, pdy = 1, 0
btn_last[i] = state
now = time.ticks_ms()
if time.ticks_diff(now, last_move) >= GAME_PACMAN_MOVE_MS:
last_move = now
tick += 1
if can_move(px, py, pdx, pdy):
px = (px + pdx) % cols
py += pdy
if (px, py) in dots:
dots.discard((px, py))
score += 1
if tick % GAME_PACMAN_GHOST_MOVE_EVERY == 0:
best = None
best_dist = None
for ddx, ddy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
if can_move(gx, gy, ddx, ddy):
nx = (gx + ddx) % cols
ny = gy + ddy
dist = abs(nx - px) + abs(ny - py)
if best_dist is None or dist < best_dist:
best_dist = dist
best = (ddx, ddy)
if best is None:
best = (0, 0)
gdx, gdy = best
gx = (gx + gdx) % cols
gy += gdy
if (px, py) == (gx, gy):
_game_over_screen("GAME OVER", score)
return
if not dots:
_game_over_screen("CLEAR!", score)
return
oled.fill(0)
for (dx_, dy_) in dots:
oled.pixel(dx_ * cell + cell // 2, dy_ * cell + cell // 2, 1)
for r in range(rows):
for c in range(cols):
if walls[r][c]:
oled.fill_rect(c * cell, r * cell, cell, cell, 1)
oled.fill_rect(px * cell, py * cell, cell, cell, 1)
oled.rect(gx * cell, gy * cell, cell, cell, 1)
oled.show()
time.sleep_ms(20)
# ---------------------------------------------------------------------------
# Profil-Editor: eigene Profile direkt am Geraet anlegen und wieder loeschen
# ---------------------------------------------------------------------------
# Erreichbar ueber das feste "Neu"-Profil (letzter Eintrag, per Encoder
# erreichbar): Taste 1 = neues Profil erstellen, Taste 2 = eigenes Profil
# loeschen. Neue Profile landen in CUSTOM_PROFILES_FILE im Flash-Speicher
# des Pico und ueberleben damit einen Neustart; die festen Profile weiter
# oben im Code sind davon nicht betroffen.
#
# Bedienung beim Erstellen:
# 1) Name eingeben: Encoder dreht durch die Zeichen, Taste1 uebernimmt das
# angezeigte Zeichen, Taste2 loescht das letzte Zeichen, Taste3 = fertig
# (Name abschliessen), Taste4 fuegt direkt ein Leerzeichen ein.
# 2) Fuer jede der 4 Tasten des neuen Profils: erst Art waehlen (Tasten-
# Kombi / Medien-Taste / Keine), bei Tasten-Kombi zuerst die Modifier
# (Strg/Shift/Alt/Win) mit Taste 1-4 an/ausschalten und mit kurzem
# Encoder-Druck bestaetigen, danach die Basistaste aus einer Liste
# waehlen (Encoder dreht, Taste1 bestaetigt).
# In jeder Liste bricht ein Druck auf den Encoder-Taster den kompletten
# Vorgang ab (bei den Modifiern: erst ein LANGER Druck).
# ---------------------------------------------------------------------------
def _wiz_read_delta():
"""Liest die seit dem letzten Aufruf aufgelaufenen Encoder-Schritte im
Wizard-Modus und setzt den Zaehler zurueck."""
global _wiz_encoder_delta
d = _wiz_encoder_delta
_wiz_encoder_delta = 0
return d
def _wiz_wait_release():
"""Wartet, bis alle 4 Taster UND der Encoder-Taster losgelassen sind."""
while _game_any_button() or encoder_sw.value() == 0:
time.sleep_ms(20)
def _wiz_message(line1, line2="", wait_ms=900):
oled.fill(0)
oled.text(line1[:16], 0, 8)
if line2:
oled.text(line2[:16], 0, 18)
oled.show()
time.sleep_ms(wait_ms)
def _wiz_select(title, options, start=0):
"""Scrollbare Liste: Encoder dreht, Taste1 bestaetigt (gibt den Index
zurueck), Encoder-Taster druecken bricht ab (gibt None zurueck)."""
idx = start % len(options)
_wiz_read_delta()
last_draw = None
while True:
if idx != last_draw:
oled.fill(0)
oled.text(title[:16], 0, 0)
oled.text("> " + options[idx][:14], 0, 12)
nxt = options[(idx + 1) % len(options)]
oled.text(" " + nxt[:14], 0, 22)
oled.show()
last_draw = idx
if encoder_sw.value() == 0:
_wiz_wait_release()
return None
if buttons[0].value() == 0:
_wiz_wait_release()
return idx
d = _wiz_read_delta()
if d:
idx = (idx + d) % len(options)
time.sleep_ms(20)
_WIZ_NAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def _wiz_enter_name():
"""Zeichen-fuer-Zeichen-Namenseingabe (max. 10 Zeichen). Gibt den Namen
zurueck, oder None bei Abbruch (Encoder-Taster)."""
chars = _WIZ_NAME_CHARS
ci = 0
name = ""
_wiz_read_delta()
last_draw = None
while True:
state = (ci, name)
if state != last_draw:
oled.fill(0)
oled.text("Name:" + name, 0, 0)
oled.text("<" + chars[ci] + ">", 0, 12)
oled.text("1:OK 2:Del 3:Fin", 0, 22)
oled.show()
last_draw = state
if encoder_sw.value() == 0:
_wiz_wait_release()
return None
if buttons[0].value() == 0:
if len(name) < 10:
name += chars[ci]
_wiz_wait_release()
last_draw = None
elif buttons[1].value() == 0:
name = name[:-1]
_wiz_wait_release()
last_draw = None
elif buttons[2].value() == 0:
if name:
_wiz_wait_release()
return name
_wiz_wait_release()
elif buttons[3].value() == 0:
if len(name) < 10:
name += " "
_wiz_wait_release()
last_draw = None
d = _wiz_read_delta()
if d:
ci = (ci + d) % len(chars)
time.sleep_ms(20)
def _wiz_pick_modifiers(title):
"""Taste 1-4 schalten Strg/Shift/Alt/Win an/aus. Kurzer Encoder-Druck
uebernimmt die Auswahl, ein LANGER Encoder-Druck bricht ab."""
mods = [False, False, False, False]
names = ["C", "S", "A", "W"]
btn_last = [1, 1, 1, 1]
sw_last = 1
sw_down_start = None
last_draw = None
while True:
state_key = tuple(mods)
if state_key != last_draw:
line = " ".join(names[i] + ("*" if mods[i] else "_") for i in range(4))
oled.fill(0)
oled.text(title[:16], 0, 0)
oled.text(line[:16], 0, 12)
oled.text("1-4=Mod Enc=weit", 0, 22)
oled.show()
last_draw = state_key
for i in range(4):
s = buttons[i].value()
if s == 0 and btn_last[i] == 1:
mods[i] = not mods[i]
last_draw = None
btn_last[i] = s
sw = encoder_sw.value()
if sw == 0 and sw_last == 1:
sw_down_start = time.ticks_ms()
elif sw == 1 and sw_last == 0 and sw_down_start is not None:
held = time.ticks_diff(time.ticks_ms(), sw_down_start)
sw_down_start = None
if held >= ENCODER_SW_LONG_PRESS_MS:
return None
return mods
sw_last = sw
time.sleep_ms(20)
_WIZ_BASE_KEYS = (
[(c, c) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
+ [(str(d), "N" + str(d)) for d in range(10)]
+ [("F" + str(n), "F" + str(n)) for n in range(1, 13)]
+ [
("ENTER", "ENTER"),
("SPACE", "SPACE"),
("TAB", "TAB"),
("ESC", "ESCAPE"),
("BKSP", "BACKSPACE"),
("DEL", "DELETE"),
("UP", "UP"),
("DOWN", "DOWN"),
("LEFT", "LEFT"),
("RIGHT", "RIGHT"),
("HOME", "HOME"),
("END", "END"),
]
)
_WIZ_MEDIA_KEYS = [
("Vol+", "VOLUME_UP"),
("Vol-", "VOLUME_DOWN"),
("Mute", "MUTE"),
("Play", "PLAY_PAUSE"),
("Stop", "STOP"),
("Next", "NEXT_TRACK"),
("Prev", "PREV_TRACK"),
]
_WIZ_ACTION_CATEGORIES = ["Tasten-Kombi", "Medien-Taste", "Keine"]
_WIZ_MOD_ATTRS = ["LEFT_CTRL", "LEFT_SHIFT", "LEFT_ALT", "LEFT_UI"]
_WIZ_MOD_SYMS = ["^", "+", "~", "#"]
def _wiz_pick_keys_action(title):
mods = _wiz_pick_modifiers(title)
if mods is None:
return None, None
key_labels = [lbl for lbl, _ in _WIZ_BASE_KEYS]
ki = _wiz_select(title, key_labels)
if ki is None:
return None, None
key_label, key_attr = _WIZ_BASE_KEYS[ki]
keys = []
mod_label = ""
for i in range(4):
if mods[i]:
keys.append(getattr(K, _WIZ_MOD_ATTRS[i]))
mod_label += _WIZ_MOD_SYMS[i]
keys.append(getattr(K, key_attr))
label = (mod_label + key_label)[:5] or "-"
return label, {"type": "keys", "keys": keys}
def _wiz_pick_action(slot):
"""Fragt die Aktion fuer eine der 4 Tasten des neuen Profils ab. Gibt
(label, action) zurueck, oder (None, None) bei Abbruch."""
title = "Taste %d/4" % (slot + 1)
cat_idx = _wiz_select(title, _WIZ_ACTION_CATEGORIES)
if cat_idx is None:
return None, None
cat = _WIZ_ACTION_CATEGORIES[cat_idx]
if cat == "Keine":
return "-", {"type": "none"}
if cat == "Medien-Taste":
names = [n for n, _ in _WIZ_MEDIA_KEYS]
mi = _wiz_select(title, names)
if mi is None:
return None, None
label, attr = _WIZ_MEDIA_KEYS[mi]
return label[:5], {"type": "media", "code": getattr(M, attr)}
return _wiz_pick_keys_action(title)
def wizard_create_profile():
"""Legt ueber die Geraete-Bedienung ein neues Profil an und speichert es
persistent (siehe CUSTOM_PROFILES_FILE)."""
global PROFILES, profile_index, profile_dirty, _encoder_mode
_encoder_mode = "wizard"
_wiz_wait_release() # die Taste, die "Erstl" ausgeloest hat, nicht mitzaehlen
try:
name = _wiz_enter_name()
if name is None:
return
labels = []
actions = []
for slot in range(4):
label, action = _wiz_pick_action(slot)
if label is None:
_wiz_message("Abgebrochen")
return
labels.append(label)
actions.append(action)
new_profile = {"name": name, "labels": labels, "actions": actions}
insert_at = len(PROFILES) - 1 # vor dem "Neu"-Menuepunkt einfuegen
PROFILES.insert(insert_at, new_profile)
profile_index = insert_at
profile_dirty = True
if _save_custom_profiles(_custom_profiles_list()):
_wiz_message("Gespeichert!", name)
else:
_wiz_message("Nur temporaer!", "Speichern fehlg.")
finally:
_encoder_mode = "profile"
def wizard_delete_profile():
"""Loescht ein selbst erstelltes Profil wieder (feste Profile stehen
nicht zur Auswahl)."""
global PROFILES, profile_index, profile_dirty, _encoder_mode
_encoder_mode = "wizard"
_wiz_wait_release()
try:
customs = _custom_profiles_list()
if not customs:
_wiz_message("Keine eigenen", "Profile vorhanden")
return
names = [p["name"] for p in customs]
idx = _wiz_select("Loeschen?", names)
if idx is None:
return
target = customs[idx]
real_index = PROFILES.index(target)
del PROFILES[real_index]
if profile_index >= len(PROFILES):
profile_index = len(PROFILES) - 1
profile_dirty = True
if _save_custom_profiles(_custom_profiles_list()):
_wiz_message("Geloescht:", target["name"])
else:
_wiz_message("Geloescht,", "Speichern fehlg.")
finally:
_encoder_mode = "profile"
def run_game(name):
"""Startet eines der 4 Mini-Spiele (siehe "Games"-Profil)."""
oled.fill(0)
oled.text("Los geht's!", 10, 12)
oled.show()
time.sleep_ms(400)
# Sicherstellen, dass die Taste, die das Spiel gestartet hat, nicht noch
# gedrueckt ist - sonst wuerde z.B. Dino sofort beim Start hochspringen.
while _game_any_button():
time.sleep_ms(20)
if name == "dino":
_game_dino()
elif name == "flappy":
_game_flappy()
elif name == "tetris":
_game_tetris()
elif name == "pacman":
_game_pacman()
# ---------------------------------------------------------------------------
# Hauptschleife
# ---------------------------------------------------------------------------
@@ -847,4 +1677,4 @@ while True:
else:
_do_encoder_sw_action(ENCODER_SW_SHORT_ACTION)
time.sleep_ms(5)
time.sleep_ms(5)