Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
01874a6
Preserve deployed kiosk production state
ScriptsSandbox Aug 13, 2026
ec43ef2
Support Triton Student Numbers
ScriptsSandbox Aug 13, 2026
8351163
Preserve deployed registration kiosk behavior
ScriptsSandbox Aug 13, 2026
242abcf
Add safe PID TSN alias resolution
ScriptsSandbox Aug 13, 2026
1d2ffb3
Preserve deployed normalized registration form
ScriptsSandbox Aug 13, 2026
839370c
Preserve deployed staff app source
ScriptsSandbox Aug 13, 2026
e81d73c
Add institutional recovery package
ScriptsSandbox Aug 13, 2026
edc4907
Add kiosk revision display
robbrau1992 Aug 14, 2026
4cc7b3b
Allow multiple cards per kiosk account
ScriptsSandbox Aug 14, 2026
daccca0
Replace prior cards during kiosk linking
ScriptsSandbox Aug 14, 2026
1aa0f37
Support legacy card sheet during replacement
ScriptsSandbox Aug 14, 2026
7872b28
Improve waiver and closing UX
ScriptsSandbox Aug 14, 2026
93568dc
Fix inactivity countdown behavior
ScriptsSandbox Aug 14, 2026
b9fdaf5
Improve closing and profile correction flows
ScriptsSandbox Aug 14, 2026
61fcbed
Delay inactivity warning until final 15 seconds
ScriptsSandbox Aug 14, 2026
fb48857
Simplify profile role selection
ScriptsSandbox Aug 14, 2026
ac093ec
Add staff profile editing and split graduate roles
ScriptsSandbox Aug 14, 2026
8c6485b
Prevent stale staff dashboard refreshes
ScriptsSandbox Aug 14, 2026
c070dca
Clarify graduate program analytics
ScriptsSandbox Aug 14, 2026
2370997
Add Staff Desk group card onboarding
ScriptsSandbox Aug 15, 2026
5da601d
Accept production card sheet layout
ScriptsSandbox Aug 17, 2026
9a7c433
Show card connection blockers to staff
ScriptsSandbox Aug 17, 2026
9492673
Accept legacy or Scripps DocuSign waivers
ScriptsSandbox Aug 17, 2026
339610f
Record dual-waiver kiosk release
ScriptsSandbox Aug 17, 2026
52cabdc
Move waiver status to Google Sheets
ScriptsSandbox Aug 17, 2026
f36a2dd
Accept legacy waivers in Staff Desk
ScriptsSandbox Aug 17, 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
115 changes: 84 additions & 31 deletions ESP-32/src/scanner.ino
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const int daylightOffset_sec = 0;
#define GRAY 0x2104
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);



// Exact kiosk palette, converted from CSS hex to RGB565.
#define KIOSK_CREAM 0xF77C // #F2EEE3
#define KIOSK_ORANGE 0xF483 // #F7931E
#define KIOSK_GRAY 0x73AF // #747678
#define KIOSK_CYAN 0x1DD9 // #18B9C8
#define KIOSK_NAVY 0x1127 // #13243C
// PN532 params

#define PN532_SS (21)
Expand Down Expand Up @@ -96,28 +104,80 @@ void setup() {
////////////////////// DISPLAY /////////////////////
////////////////////////////////////////////////////////

void drawDownArrow(int cx, int top, int width, int height, uint16_t color) {
int shaftWidth = width / 3;
int shoulder = top + height / 2;
tft.fillRect(cx - shaftWidth / 2, top, shaftWidth, height / 2 + 1, color);
tft.fillTriangle(cx - width / 2, shoulder, cx + width / 2, shoulder,
cx, top + height, color);
}

void drawUpArrow(int cx, int top, int width, int height, uint16_t color) {
int shaftWidth = width / 3;
int shoulder = top + height / 2;
tft.fillTriangle(cx, top, cx - width / 2, shoulder,
cx + width / 2, shoulder, color);
tft.fillRect(cx - shaftWidth / 2, shoulder, shaftWidth, height / 2 + 1, color);
}

void homeScreen() {
tft.setFont(&FreeSerif24pt7b);
tft.fillScreen(ST77XX_BLACK);
tft.setCursor(20, 40);
tft.setTextColor(ST77XX_YELLOW);
tft.setTextSize(1);
tft.println("UC San Diego");
tft.setCursor(20, 80);
tft.println("Makerspace");
tft.fillRect(20, 45, tft.width()-95,5, ST77XX_BLUE);
tft.fillRect(tft.width()-52, 45, 20,5, ST77XX_BLUE);
//tft.drawLine(0, 35, tft.width()-100,30, ST77XX_BLUE);
//tft.drawLine(tft.width()-70, 35, tft.width()-30,30, ST77XX_BLUE);

tft.setTextSize(1);
//tft.setCursor(0, 60);
tft.setCursor(20, 160);

tft.print("Please Scan");
tft.setCursor(20, 200);

tft.print("Your ID card");
tft.setFont(NULL);
tft.fillScreen(KIOSK_NAVY);
tft.setTextWrap(false);
tft.setTextColor(KIOSK_CREAM);
tft.setTextSize(4);
tft.setCursor(18, 18);
tft.print("TAP ID");
drawDownArrow(160, 72, 124, 104, KIOSK_ORANGE);
tft.setTextSize(2);
tft.setCursor(82, 214);
tft.print("ON BLUE HAND");
}

void animateToLookUp() {
// Only redraw the text and arrow regions; the background never flashes.
tft.fillRect(0, 8, 320, 48, KIOSK_NAVY);
tft.fillRect(0, 205, 320, 35, KIOSK_NAVY);

for (int frame = 0; frame < 5; frame++) {
int height = 104 - (frame * 23);
int width = 124 - (frame * 5);
tft.fillRect(85, 66, 150, 122, KIOSK_NAVY);
if (height > 12) {
drawDownArrow(160, 72 + (104 - height) / 2, width, height, KIOSK_ORANGE);
} else {
tft.fillRect(108, 126, 104, 10, KIOSK_ORANGE);
}
delay(45);
}

for (int frame = 1; frame <= 5; frame++) {
int height = 10 + (frame * 19);
int width = 104 + (frame * 4);
tft.fillRect(85, 66, 150, 122, KIOSK_NAVY);
drawUpArrow(160, 126 - height / 2, width, height, KIOSK_CREAM);
delay(45);
}

// Static hold: professional, attention-directing, and flicker-free.
tft.fillScreen(KIOSK_NAVY);
tft.fillTriangle(0, 0, 112, 0, 86, 34, KIOSK_ORANGE);
tft.setFont(NULL);
tft.setTextWrap(false);
tft.setTextColor(KIOSK_CREAM);
tft.setTextSize(2);
tft.setCursor(15, 9);
tft.print("NEXT");
drawUpArrow(160, 42, 112, 94, KIOSK_CREAM);
tft.setTextColor(KIOSK_CREAM);
tft.setTextSize(4);
tft.setCursor(76, 146);
tft.print("LOOK UP");
tft.fillRect(105, 188, 110, 4, KIOSK_ORANGE);
tft.setTextColor(KIOSK_ORANGE);
tft.setTextSize(2);
tft.setCursor(70, 211);
tft.print("FINISH CHECK-IN");
}


Expand All @@ -140,14 +200,6 @@ void loop() {
success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength,100);

if (success) {
tft.fillScreen(ST77XX_BLACK);
tft.setCursor(0, 30);

tft.setFont(&FreeSerif24pt7b);

tft.setTextSize(1);
tft.println("Found Card!\n");

uint32_t szPos;

for (szPos = 0; szPos < uidLength; szPos++)
Expand All @@ -156,8 +208,9 @@ void loop() {
//gmtime_r(&nowSecs, &timeinfo);
Serial.println(String(uidStr));

tft.setTextSize(1);
delay(500);
// The reader confirms only the scan; the kiosk confirms check-in.
animateToLookUp();
delay(5000);
do {
versiondata = nfc.getFirmwareVersion();
nfc.SAMConfig();
Expand Down
6 changes: 6 additions & 0 deletions Kiosk-v2/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ CARD_UID_HMAC_SECRET=
# Public URL of the deployed Apps Script registration web app. The kiosk hides
# the registration QR and link until this is set at build time.
NEXT_PUBLIC_REGISTRATION_URL=

# Public DocuSign PowerForm URL displayed on the waiver-required screen.
NEXT_PUBLIC_WAIVER_URL=

# Normally omitted because localhost uses the loopback scanner bridge.
NEXT_PUBLIC_SCANNER_WS_URL=
2 changes: 2 additions & 0 deletions Kiosk-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Interactive kiosk prototype and production technical spike for the Scripps Sandb
- `app/` contains the 1920×1080 kiosk interface and interactive failure/exception flows.
- `bridge/` contains the Raspberry Pi serial-to-browser reader bridge.
- `docs/production-spec.md` captures the agreed workflows, permissions, data model, failure behavior, migration, and milestones.
- `docs/recovery-manifest-v1.md` identifies the institutional recovery point.
- `docs/disaster-recovery.md` is the fresh-Pi, ESP32, Apps Script, and data restore runbook.

## Prerequisites

Expand Down
10 changes: 5 additions & 5 deletions Kiosk-v2/app/api/registrations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
id,
makeDisplayName,
normalizeEmail,
normalizeIdentifier,
normalizeRegistrationIdentifier,
REGISTRATION_CONSENT_VERSION,
WAIVER_POWERFORM_URL,
} from "@/lib/registration";
Expand All @@ -31,7 +31,7 @@ type RegistrationPayload = {
};

const allowedUserTypes = new Set(["student", "staff", "faculty", "postdoc", "visitor", "other"]);
const allowedIdentifierTypes = new Set(["pid", "employee_id", "other"]);
const allowedIdentifierTypes = new Set(["pid", "tsn", "employee_id", "other"]);

function validEmail(value: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
Expand Down Expand Up @@ -66,7 +66,7 @@ export async function POST(request: Request) {
: null;
const identifierType = payload.identifierType?.trim().toLowerCase() ?? "";
const identifierValue = payload.identifierValue?.trim() ?? "";
const normalizedIdentifier = normalizeIdentifier(identifierValue);
const normalizedIdentifier = normalizeRegistrationIdentifier(identifierValue, identifierType);

if (!firstName || !lastName || !affiliation) {
return Response.json({ error: "Name and affiliation are required." }, { status: 400 });
Expand All @@ -77,8 +77,8 @@ export async function POST(request: Request) {
if (!validEmail(primaryEmail) || (secondaryEmail && !validEmail(secondaryEmail))) {
return Response.json({ error: "Enter a valid email address." }, { status: 400 });
}
if (normalizedIdentifier.length < 4 || normalizedIdentifier.length > 32) {
return Response.json({ error: "Enter a valid UC San Diego ID number." }, { status: 400 });
if (!normalizedIdentifier) {
return Response.json({ error: "Enter a valid PID, TSN, or employee ID." }, { status: 400 });
}
if (!payload.consent) {
return Response.json({ error: "Consent is required to create an account." }, { status: 400 });
Expand Down
Loading