192 lines
5.4 KiB
Python
192 lines
5.4 KiB
Python
# base_station_main.py -- Trivia buzzer JUDGE (Raspberry Pi Pico W)
|
|
#
|
|
# Modes: "quizbowl" (live on reset) or "jeopardy" (stand-by until activated).
|
|
# Control button GP15 -> GND:
|
|
# quizbowl : press = reset / re-arm.
|
|
# jeopardy : press on STAND BY = ACTIVATE; press when ARMED/LOCKED = reset.
|
|
#
|
|
# Color LEDs: the base lights the winning team's color (blue or green),
|
|
# matching the buzzer NAMEs. Onboard LED: solid = someone buzzed.
|
|
|
|
import network, socket, select, time
|
|
from machine import Pin
|
|
|
|
SSID = "TriviaBuzzers"
|
|
PASSWORD = "buzzin123"
|
|
PORT = 5000
|
|
|
|
MODE = "jeopardy" # "jeopardy" or "quizbowl"
|
|
LOCK_TIMEOUT_MS = 10000
|
|
BATT_LOW_V = 3.60
|
|
|
|
BUTTON_PIN = 15 # control button GP15 -> GND
|
|
BLUE_LED_PIN = 16 # base's blue LED -> resistor -> GND
|
|
GREEN_LED_PIN = 17 # base's green LED -> resistor -> GND
|
|
|
|
led = Pin("LED", Pin.OUT) # onboard: solid = buzzed
|
|
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)
|
|
|
|
# winner NAME -> indicator LED on the base
|
|
TEAM_LEDS = {
|
|
"blue": Pin(BLUE_LED_PIN, Pin.OUT),
|
|
"green": Pin(GREEN_LED_PIN, Pin.OUT),
|
|
}
|
|
|
|
def set_team_led(name):
|
|
for k, p in TEAM_LEDS.items():
|
|
p.value(1 if k == name else 0)
|
|
|
|
def team_leds_off():
|
|
for p in TEAM_LEDS.values():
|
|
p.value(0)
|
|
|
|
# --- Wi-Fi access point ---
|
|
ap = network.WLAN(network.AP_IF)
|
|
ap.config(essid=SSID, password=PASSWORD)
|
|
ap.active(True)
|
|
while not ap.active():
|
|
time.sleep(0.1)
|
|
print("AP ready:", ap.ifconfig())
|
|
|
|
# --- TCP server ---
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(("0.0.0.0", PORT))
|
|
srv.listen(8)
|
|
srv.setblocking(False)
|
|
|
|
poller = select.poll()
|
|
poller.register(srv, select.POLLIN)
|
|
|
|
clients = {}
|
|
phase = "standby"
|
|
winner = None
|
|
lock_time = 0
|
|
|
|
def send(sock, msg):
|
|
try:
|
|
sock.send(msg)
|
|
except OSError:
|
|
pass
|
|
|
|
def state_msg():
|
|
if phase == "locked":
|
|
return b"LOCKED " + winner.encode() + b"\n"
|
|
if phase == "standby":
|
|
return b"STANDBY\n"
|
|
return b"ARMED\n"
|
|
|
|
def broadcast(msg, exclude=None):
|
|
for c in list(clients.values()):
|
|
if c["sock"] is not exclude:
|
|
send(c["sock"], msg)
|
|
|
|
def battery_summary():
|
|
if not clients:
|
|
return
|
|
parts = []
|
|
for c in clients.values():
|
|
v = c.get("batt")
|
|
if v is None:
|
|
parts.append("%s ?" % c["name"])
|
|
else:
|
|
parts.append("%s %.2f%s" % (c["name"], v, "!" if v < BATT_LOW_V else ""))
|
|
print("Batteries:", " ".join(parts))
|
|
|
|
def go_armed():
|
|
global phase, winner
|
|
phase, winner = "armed", None
|
|
led.off()
|
|
team_leds_off()
|
|
broadcast(b"ARMED\n")
|
|
print("--- ARMED (buzz now) ---")
|
|
|
|
def go_standby():
|
|
global phase, winner
|
|
phase, winner = "standby", None
|
|
led.off()
|
|
team_leds_off()
|
|
broadcast(b"STANDBY\n")
|
|
print("--- STAND BY ---")
|
|
|
|
def start_round():
|
|
battery_summary()
|
|
if MODE == "jeopardy":
|
|
go_standby()
|
|
else:
|
|
go_armed()
|
|
|
|
def handle_line(fd, line):
|
|
global phase, winner, lock_time
|
|
c = clients.get(fd)
|
|
if not c:
|
|
return
|
|
if line.startswith(b"HELLO"):
|
|
parts = line.split()
|
|
if len(parts) > 1:
|
|
c["name"] = parts[1].decode()
|
|
send(c["sock"], state_msg())
|
|
elif line.startswith(b"BATT"):
|
|
parts = line.split()
|
|
if len(parts) > 1:
|
|
try:
|
|
v = float(parts[1])
|
|
except ValueError:
|
|
return
|
|
was = c.get("batt")
|
|
c["batt"] = v
|
|
if v < BATT_LOW_V and (was is None or was >= BATT_LOW_V):
|
|
print("[BATTERY LOW] %s = %.2f V" % (c["name"], v))
|
|
elif line.startswith(b"PRESS"):
|
|
if phase == "armed":
|
|
phase, winner = "locked", c["name"]
|
|
lock_time = time.ticks_ms()
|
|
led.on()
|
|
set_team_led(winner)
|
|
print("WINNER:", winner)
|
|
send(c["sock"], b"WIN\n")
|
|
broadcast(b"LOCKED " + winner.encode() + b"\n", exclude=c["sock"])
|
|
else:
|
|
send(c["sock"], state_msg())
|
|
|
|
start_round()
|
|
last_btn = 0
|
|
|
|
while True:
|
|
for sock, ev in poller.poll(20):
|
|
if sock is srv:
|
|
conn, addr = srv.accept()
|
|
conn.setblocking(False)
|
|
poller.register(conn, select.POLLIN)
|
|
clients[conn.fileno()] = {"sock": conn, "name": str(addr[0]),
|
|
"buf": b"", "batt": None}
|
|
send(conn, state_msg())
|
|
else:
|
|
fd = sock.fileno()
|
|
try:
|
|
data = sock.recv(64)
|
|
except OSError:
|
|
continue
|
|
if not data:
|
|
poller.unregister(sock)
|
|
clients.pop(fd, None)
|
|
sock.close()
|
|
continue
|
|
c = clients.get(fd)
|
|
if not c:
|
|
continue
|
|
c["buf"] += data
|
|
while b"\n" in c["buf"]:
|
|
line, c["buf"] = c["buf"].split(b"\n", 1)
|
|
handle_line(fd, line.strip())
|
|
|
|
if button.value() == 0 and time.ticks_diff(time.ticks_ms(), last_btn) > 300:
|
|
last_btn = time.ticks_ms()
|
|
if MODE == "jeopardy" and phase == "standby":
|
|
go_armed()
|
|
else:
|
|
start_round()
|
|
|
|
if phase == "locked" and LOCK_TIMEOUT_MS and time.ticks_diff(time.ticks_ms(), lock_time) > LOCK_TIMEOUT_MS:
|
|
print("auto-reset after timeout")
|
|
start_round() |