Project Architecture & Technical CS Role
HU UniversityFor our 1st semester HBO-ICT capstone project at Hogeschool Utrecht (HU), our team was challenged to rebuild the Steam Gaming Platform. Taking on the Technical Computer Science (TI) role, my core responsibility was engineering the seamless bridge between physical hardware components and software logic.
The system architecture was split across two computing layers: a standard Raspberry Pi executed the main Steam application backend, while a dedicated Raspberry Pi Pico microcontroller ran custom MicroPython scripts to handle real-time sensor polling, screen rendering, RFID keycard scans, and physical actuator triggers.
Physical Hardware Components
Microcontroller IOTo make the Steam application interactive and tactile, we integrated an array of physical sensors, displays, and actuators directly onto the Raspberry Pi Pico header pins:
RFID Keycard Reader
MFRC522 scanner on SPI pins allowing users to log into their Steam account by scanning a physical card.
ST7735 TFT Screen
128x160 full-color screen displaying login prompts, live Steam Wallet balance updates, and promotional ad banners.
LDR Light Sensor
ADC light intensity sensor dynamically switching the UI between Light Mode and Dark Mode based on room lighting.
SG90 Servo Actuator
PWM servo motor producing physical haptic sweeps upon successful card authentication and mode switches.
Raspberry Pi Pico MicroPython Code
MicroPythonThe exact MicroPython script running on the Raspberry Pi Pico, managing non-blocking serial communication, MFRC522 RFID authentication, ST7735 TFT display rendering, and LDR light sensing:
import sys
import select
import time
import network
import micropython
from mfrc522 import MFRC522
from ST7735 import ST7735
from machine import Pin, SPI, ADC, PWM
micropython.kbd_intr(-1)
THRESHOLD_LIGHT = 35000
THRESHOLD_DARK = 25000
LDR_INTERVAL_MS = 500
# TFT display dimensions
TFT_W = 128
TFT_H = 160
IMG_TIMEOUT_MS = 5000
# Hardware setup SPI & TFT
spi = SPI(1, baudrate=16000000, polarity=0, phase=0, sck=Pin(10), mosi=Pin(11))
tft = ST7735(spi, rst=5, ce=7, dc=0)
# RFID scanner
rfid = MFRC522(sck=18, mosi=19, miso=16, rst=20, cs=17)
# Light sensor
ldr = ADC(Pin(26))
# Actuator Servo
sg90 = PWM(Pin(3))
sg90.freq(50)
# TFT Screen startup
tft.begin()
tft.fill_screen(0x0000)
tft.p_string(10, 20, "Steam")
tft.p_string(10, 40, "Scan RFID to login")
poll_obj = select.poll()
poll_obj.register(sys.stdin, select.POLLIN)
current_mode = "UNKNOWN"
last_ldr_time = 0
logged_in = False
serial_buf = ""
img_mode = False
img_expected = 0
img_buf = bytearray()
img_start_time = 0
def spin():
sg90.duty_u16(2000)
time.sleep(0.5)
sg90.duty_u16(7500)
time.sleep(0.5)
sg90.duty_u16(2000)
time.sleep(0.3)
def tft_show(line1="", line2=""):
tft.fill_screen(0x0000)
tft.p_string(10, 20, line1)
tft.p_string(10, 40, line2)
def reset_img_mode():
global img_mode, img_expected, img_buf
img_mode = False
img_expected = 0
img_buf = bytearray()
def check_ldr():
global current_mode
val = ldr.read_u16()
new_mode = current_mode
if val > THRESHOLD_LIGHT:
new_mode = "LIGHT"
elif val < THRESHOLD_DARK:
new_mode = "DARK"
if new_mode != current_mode:
current_mode = new_mode
spin()
print("LDR:" + str(current_mode) + ":" + str(val))
def check_rfid():
global logged_in
if logged_in:
return
stat, _ = rfid.request(rfid.REQIDL)
if stat == rfid.OK:
stat, uid = rfid.anticoll(rfid.PICC_ANTICOLL1)
if stat == rfid.OK:
print("RFID:" + "".join(f"{b:02X}" for b in uid))
logged_in = True
spin()
def check_serial():
global serial_buf, logged_in, img_start_time, img_mode
if img_mode and time.ticks_diff(time.ticks_ms(), img_start_time) > IMG_TIMEOUT_MS:
print("IMG:TIMEOUT")
reset_img_mode()
return
if not poll_obj.poll(0):
return
if img_mode:
_read_image_chunk()
else:
_read_text_char()
def _read_image_chunk():
global img_buf, img_expected
needed = img_expected - len(img_buf)
chunk = sys.stdin.buffer.read(min(needed, 1024))
if chunk:
img_buf.extend(chunk)
if len(img_buf) >= img_expected:
try:
tft.draw_bmp(0, 0, TFT_W, TFT_H, bytes(img_buf))
print("IMG:OK")
except Exception as e:
print("IMG:ERR:" + str(e))
reset_img_mode()
def _read_text_char():
global serial_buf
while poll_obj.poll(0):
ch = sys.stdin.read(1)
if ch == "\n":
msg = serial_buf.strip()
serial_buf = ""
if msg:
_handle_message(msg)
break
else:
serial_buf += ch
def _handle_message(msg: str):
global logged_in, img_mode, img_expected, img_buf, img_start_time
if msg.startswith("IMG:"):
try:
img_expected = int(msg.split(":")[1])
img_buf = bytearray()
img_mode = True
img_start_time = time.ticks_ms()
sys.stdout.write("IMG:READY\n")
except (ValueError, IndexError):
print("IMG:ERR:INVALID_SIZE")
elif msg.startswith("BAL:"):
balance = msg.split(":")[1]
bal_text = "EUR " + balance
x_pos = 128 - (len(bal_text) * 6) - 2
tft.fill_screen(0x0000)
tft.p_string(10, 20, "Steam Wallet")
tft.p_string(x_pos, 5, bal_text)
elif msg == "LOGOUT":
logged_in = False
reset_img_mode()
tft_show("Steam", "Scan RFID...")
spin()
# Test initial LDR state
check_ldr()
print(f"Initial LDR Mode: {current_mode}")
while True:
try:
now = time.ticks_ms()
if time.ticks_diff(now, last_ldr_time) >= LDR_INTERVAL_MS:
check_ldr()
last_ldr_time = now
if not logged_in:
check_rfid()
check_serial()
time.sleep(0.02)
except Exception as e:
print("CRASH:" + str(e))
time.sleep(2)