Skip to content
Merged

dev #12

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
20201dd
merge bash scripts and move to root, re add sh extension
timothywashburn Mar 21, 2026
a15efd0
refactor file and class names
timothywashburn Mar 21, 2026
3d750ed
remove unnecessary wrapper calls for globals
timothywashburn Mar 21, 2026
21dc827
remove legacy file
timothywashburn Mar 21, 2026
b232762
switch from global vars to a shared app context
timothywashburn Mar 21, 2026
351489a
separate controller logic
timothywashburn Mar 21, 2026
8d2b923
separate controller logic 2
timothywashburn Mar 21, 2026
8971b7d
traffic light api refactoring, remove dynamic imports
timothywashburn Mar 22, 2026
430f8b0
refactor method names
timothywashburn Mar 22, 2026
738e436
refactoring
timothywashburn Mar 22, 2026
c83a8f7
reorganize the ui
timothywashburn Mar 22, 2026
59aeb8c
remove old files
timothywashburn Mar 22, 2026
3b6971f
more refactoring
timothywashburn Mar 22, 2026
305faa4
refactor the api, re add barcode endpoint and simplify barcode accoun…
timothywashburn Mar 22, 2026
a8bea41
barcode scanner
timothywashburn Mar 31, 2026
adcc50a
replace tkinter with pyqt6
timothywashburn Mar 31, 2026
c3e7da8
fix dev image tagging
timothywashburn Mar 31, 2026
1adeb0f
switch to python full image
timothywashburn Mar 31, 2026
1d0099e
make check in image always pull updates
timothywashburn Mar 31, 2026
521a61f
re add dependency
timothywashburn Mar 31, 2026
616a1e6
add dependencies 2
timothywashburn Mar 31, 2026
426d8bc
add dependencies 3
timothywashburn Mar 31, 2026
4e1f6a9
soft depend barcode reader instead of hard depend
timothywashburn Mar 31, 2026
62e2abb
add dependencies 4
timothywashburn Mar 31, 2026
817ad49
add dependencies 5
timothywashburn Mar 31, 2026
7a4bc3b
add dependencies 6
timothywashburn Mar 31, 2026
c36463e
shrink name welcome text slightly
timothywashburn Mar 31, 2026
194ea57
add backoff and better error handling
timothywashburn Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ on:
push:
branches:
- main
- dev
workflow_dispatch:

concurrency:
group: production_environment
group: ${{ github.ref_name }}_environment
cancel-in-progress: true

jobs:
Expand All @@ -23,6 +24,17 @@ jobs:
id: lowercase
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

- name: Set image name
id: image
run: |
if [ "${{ github.ref_name }}" = "dev" ]; then
echo "name=check-in-dev" >> $GITHUB_OUTPUT
echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in-dev:latest" >> $GITHUB_OUTPUT
else
echo "name=check-in" >> $GITHUB_OUTPUT
echo "extra_tag=ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:latest" >> $GITHUB_OUTPUT
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -39,5 +51,5 @@ jobs:
context: .
push: true
tags: |
ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:${{ github.sha }}
ghcr.io/${{ steps.lowercase.outputs.owner }}/check-in:latest
ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.image.outputs.name }}:${{ github.sha }}
${{ steps.image.outputs.extra_tag }}
6 changes: 2 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
FROM python:3.13-slim
FROM python:3.13

RUN apt-get update && apt-get install -y \
python3-tk \
gcc \
python3-dev \
libqt6widgets6 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
services:
check-in:
image: ghcr.io/ucsd-makerspace/check-in:latest
pull_policy: always
privileged: true
network_mode: host
environment:
Expand Down
Binary file added fonts/Montserrat-VariableFont_wght.ttf
Binary file not shown.
8 changes: 0 additions & 8 deletions install.sh

This file was deleted.

3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ pyserial==3.5
requests==2.32.5
adafruit-circuitpython-pn532
RPi.GPIO; sys_platform == 'linux'
pyqt6==6.11.0
qtawesome
pyqt6-sip
20 changes: 20 additions & 0 deletions run_dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -a
source "$(dirname "$0")/.env"
set +a

for arg in "$@"; do
if [ "$arg" = "--dev" ] || [ "$arg" = "-d" ]; then
export DEV_MODE=1
break
fi
done

output_file="log.txt"

echo "" >> "$output_file"

date "+%Y-%m-%d %H:%M:%S" >> "$output_file"

python src/main.py "$@" 2>&1 | tee -a log.txt
Empty file added src/api/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions src/api/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging
import time

import requests


def _req(method, url, **kwargs):
start = time.time()
resp = requests.request(method, url, **kwargs)
ms = (time.time() - start) * 1000
logging.info(f"[CLIENT] {method.upper()} {url} -> {resp.status_code} ({ms:.0f}ms)")
return resp
64 changes: 40 additions & 24 deletions src/sheets.py → src/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@
import sys
import time

import requests

from config import API_BASE_URL
from api._client import _req


def _req(method, url, **kwargs):
start = time.time()
resp = requests.request(method, url, **kwargs)
ms = (time.time() - start) * 1000
logging.info(f"[CLIENT] {method.upper()} {url} -> {resp.status_code} ({ms:.0f}ms)")
return resp


def check_api_health(retries=3, delay=3):
logging.info(API_BASE_URL)
Expand All @@ -31,7 +23,7 @@ def check_api_health(retries=3, delay=3):
sys.exit(1)


class SheetManager:
class ApiClient:
def checkin_by_uuid(self, uuid):
try:
resp = _req("GET", f"{API_BASE_URL}/check-in/uuid/{uuid}", timeout=10)
Expand All @@ -58,26 +50,50 @@ def set_traffic_light(self, color):

def get_traffic_light(self):
try:
resp = requests.get(f"{API_BASE_URL}/traffic-light", timeout=5)
resp = _req("GET", f"{API_BASE_URL}/traffic-light", timeout=5)
return resp.json().get("color", "off")
except Exception as e:
logging.error(f"Error getting traffic light: {e}")
return "off"

def create_account(self, first_name, last_name, email, pid, rfid):
def lookup_by_pid(self, pid):
"""Returns dict with first_name/last_name/email/pid, or None if not found."""
try:
resp = _req("GET", f"{API_BASE_URL}/accounts/lookup/pid/{pid}", timeout=10)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
except Exception as e:
logging.error(f"Error looking up student by pid {pid}: {e}")
return None

def lookup_by_barcode(self, barcode):
"""Returns dict with first_name/last_name/email/pid, or None if not found."""
try:
resp = _req("GET", f"{API_BASE_URL}/accounts/lookup/barcode/{barcode}", timeout=10)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
except Exception as e:
logging.error(f"Error looking up student by barcode: {e}")
return None

def create_account(self, rfid, *, barcode=None, pid=None, first_name=None, last_name=None, email=None):
try:
resp = _req(
"POST",
f"{API_BASE_URL}/accounts",
json={
"first_name": first_name,
"last_name": last_name,
"email": email,
"pid": pid,
"rfid": rfid,
},
timeout=30,
)
payload = {"rfid": rfid}
if barcode:
payload["barcode"] = barcode
if pid:
payload["pid"] = pid
if first_name:
payload["first_name"] = first_name
if last_name:
payload["last_name"] = last_name
if email:
payload["email"] = email
resp = _req("POST", f"{API_BASE_URL}/accounts", json=payload, timeout=30)
resp.raise_for_status()
return resp.json()
except Exception as e:
Expand Down
44 changes: 44 additions & 0 deletions src/api/traffic_light_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import threading

from api.client import ApiClient
from hardware.traffic_light import TrafficLight


class TrafficLightApi:
def __init__(self, light: TrafficLight, sheets: ApiClient):
self._light = light
self._sheets = sheets

@property
def connected(self) -> bool:
return self._light.connected

def drive(self, color: str) -> None:
"""Directly set the physical traffic light without posting to the API."""
if color == "red":
self._light.set_red()
elif color == "green":
self._light.set_green()
elif color == "yellow":
self._light.set_yellow()
else:
self._light.set_off()

def _post(self, color: str) -> None:
threading.Thread(
target=self._sheets.set_traffic_light,
args=(color,),
daemon=True,
).start()

def request_red(self) -> None:
self._post("red")

def request_green(self) -> None:
self._post("green")

def request_yellow(self) -> None:
self._post("yellow")

def request_off(self) -> None:
self._post("off")
36 changes: 36 additions & 0 deletions src/app_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import threading

from api.client import ApiClient
from api.traffic_light_api import TrafficLightApi
from hardware.traffic_light import TrafficLight


class AppContext:
def __init__(self, sheets: ApiClient, traffic_light: TrafficLightApi):
self.sheets = sheets
self.traffic_light = traffic_light
self.window = None
self.nav = None
self.check_in = None
self.account = None
self.dispatcher = None
self.has_barcode_scanner = False
self._rfid_lock = threading.Lock()
self._rfid: str = ""

@property
def rfid(self) -> str:
with self._rfid_lock:
return self._rfid

@rfid.setter
def rfid(self, value: str) -> None:
with self._rfid_lock:
self._rfid = value

@classmethod
def create(cls, traffic_usb_id=None) -> "AppContext":
sheets = ApiClient()
light = TrafficLight(traffic_usb_id)
traffic = TrafficLightApi(light, sheets)
return cls(sheets, traffic)
Binary file removed src/assets/check_in_no_id_assets/image_2.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_4.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_5.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_6.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_7.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_8.png
Binary file not shown.
Binary file removed src/assets/manual_fill_assets/image_9.png
Binary file not shown.
Binary file removed src/assets/no_acc_no_waiver_swipe_assets/image_4.png
Binary file not shown.
Binary file removed src/assets/no_acc_no_waiver_swipe_assets/image_5.png
Binary file not shown.
Binary file removed src/assets/qr_codes_assets/image_3.png
Binary file not shown.
File renamed without changes
File renamed without changes
Binary file removed src/assets/waiver_no_acc_swipe_assets/button_1.png
Diff not rendered.
Binary file removed src/assets/waiver_no_acc_swipe_assets/image_2.png
Diff not rendered.
Binary file removed src/assets/waiver_no_acc_swipe_assets/image_3.png
Diff not rendered.
Binary file removed src/assets/waiver_no_acc_swipe_assets/image_4.png
Diff not rendered.
Binary file removed src/assets/waiver_no_acc_swipe_assets/image_5.png
Diff not rendered.
Empty file added src/controllers/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions src/controllers/account_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import logging

from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication


class AccountController:
def __init__(self, ctx):
self.ctx = ctx

def go_to_review_from_barcode(self, barcode):
self.ctx.nav.show_status("Looking up student...")
QApplication.processEvents()

student = self.ctx.sheets.lookup_by_barcode(barcode)
self.ctx.nav.hide_status()

if student is None:
self.ctx.nav.show_status("Student not found. Please enter your details manually.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return

self.ctx.nav.go_to_create_account_review(
pid=student["pid"],
first_name=student["first_name"],
last_name=student["last_name"],
email=student["email"],
)

def go_to_review_from_pid(self, pid):
self.ctx.nav.show_status("Looking up student...")
QApplication.processEvents()

student = self.ctx.sheets.lookup_by_pid(pid)
self.ctx.nav.hide_status()

if student is None:
self.ctx.nav.show_status("Student not found. Please check your PID.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return

self.ctx.nav.go_to_create_account_review(
pid=pid,
first_name=student["first_name"],
last_name=student["last_name"],
email=student["email"],
)

def create_account_from_review(self, *, first_name, last_name, email, pid):
if pid:
self._create(pid=pid)
else:
self._create(first_name=first_name, last_name=last_name, email=email)

def _create(self, *, barcode=None, pid=None, first_name=None, last_name=None, email=None):
self.ctx.nav.show_status("Account creation in progress!")
QApplication.processEvents()

result = self.ctx.sheets.create_account(
self.ctx.rfid,
barcode=barcode,
pid=pid,
first_name=first_name,
last_name=last_name,
email=email,
)
self.ctx.nav.hide_status()

if result is None:
self.ctx.nav.show_status("ERROR! Could not create account, please try manually.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return

logging.info("Account creation succeeded")
self.ctx.nav.pop()
Loading
Loading