From 5ead815ef23262e0aab76738f31dca41f61b7983 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sat, 10 Jan 2026 19:00:22 -0700 Subject: [PATCH] Commit WIP code --- .gitignore | 1 + demos/button/button.ino | 30 ++ demos/wifi-setup/wifi-setup.ino | 0 firmware/firmware.ino | 555 ++++++++++++++++++++++++++++++++ 4 files changed, 586 insertions(+) create mode 100644 demos/button/button.ino create mode 100644 demos/wifi-setup/wifi-setup.ino create mode 100644 firmware/firmware.ino diff --git a/.gitignore b/.gitignore index 4d1146e..2c9abeb 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,4 @@ out.* *.csv *.txt *.json +.aider* diff --git a/demos/button/button.ino b/demos/button/button.ino new file mode 100644 index 0000000..c42b967 --- /dev/null +++ b/demos/button/button.ino @@ -0,0 +1,30 @@ +// Button Input pullup demo +// Connect to D19 +// Note: no debounce logic + +const int BUTTON_PIN = 19; // GIOP19 pin connected to button + +// Variables will change: +int lastState = LOW; // the previous state from the input pin +int currentState; // the current reading from the input pin + +void setup() { + // initialize serial communication at 9600 bits per second: + Serial.begin(115200); + // initialize the pushbutton pin input + // the pull-up input pin will be HIGH when the switch is open and LOW when the switch is closed. + pinMode(BUTTON_PIN, INPUT_PULLUP); +} + +void loop() { + // read the state of the switch/button: + currentState = digitalRead(BUTTON_PIN); + + if(lastState == HIGH && currentState == LOW) + Serial.println("The button is pressed"); + else if(lastState == LOW && currentState == HIGH) + Serial.println("The button is released"); + + // save the the last state + lastState = currentState; +} \ No newline at end of file diff --git a/demos/wifi-setup/wifi-setup.ino b/demos/wifi-setup/wifi-setup.ino new file mode 100644 index 0000000..e69de29 diff --git a/firmware/firmware.ino b/firmware/firmware.ino new file mode 100644 index 0000000..2460013 --- /dev/null +++ b/firmware/firmware.ino @@ -0,0 +1,555 @@ +// Tool Lockout +// Controls access to machines based on card scans +// +// Arduino IDE 2.3.4 +// Board: ESP32 +// - select "ESP32 Dev Module" + + +#include +#include +#include +#include +#include +#include +#include // version 1.15.2 +//#include +//#include // v6.19.4 +//#include // v2.2.9 +//#include // v1.3.0 + +#include "secrets.h" + +//String portalAPI = "https://api.my.protospace.ca"; +String portalAPI = "https://api.spaceport.dns.t0.vc"; + +WiFiClientSecure wc; + +String authorizedCards = ""; +String scannedCard = ""; + +#define DEBUG 1 + +#define LED_PIN 18 +#define NUMPIXELS 1 +Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); + +#define BUTTON1SW D5 +#define NUM_BUTTONS 1 + +#define BUTTON_HOLD_TIME 10000 +#define BUTTON_PRESS_TIME 20 + +#define CONTROLLER_IDLE_DELAY_MS 4500 +#define CONTROLLER_UI_DELAY_MS 1000 +#define CONTROLLER_OFFER_DELAY_MS 90000 +#define CONNECT_TIMEOUT_MS 30000 +#define ELLIPSIS_ANIMATION_DELAY_MS 1000 +#define SCROLL_ANIMATION_DELAY_MS 250 + +enum lockoutStates { + LOCKOUT_BEGIN, + LOCKOUT_DISENGAGED, + LOCKOUT_ENGAGED, + NUM_LOCKOUTSTATES +}; +enum lockoutStates lockoutState = LOCKOUT_BEGIN; + +enum buttonStates { + BUTTON_OPEN, + BUTTON_CLOSED, + BUTTON_CHECK_PRESSED, + BUTTON_PRESSED, + BUTTON_HELD, + NUM_BUTTONSTATES, +}; +enum buttonStates button1State = BUTTON_OPEN; + +enum controllerStates { + CONTROLLER_BEGIN, + CONTROLLER_WIFI_CONNECT, + CONTROLLER_GET_TIME, + CONTROLLER_IDLE, + CONTROLLER_PING, + CONTROLLER_OFFER_HOST, + CONTROLLER_SEND_HOURS, + CONTROLLER_IDLE_DELAY, + CONTROLLER_IDLE_WAIT, + CONTROLLER_UI_DELAY, + CONTROLLER_UI_WAIT, +}; +enum controllerStates controllerState = CONTROLLER_BEGIN; + +enum LEDStates { + LED_BEGIN, + LED_INIT, + LED_IDLE, +}; +enum LEDStates LEDState = LED_BEGIN; + +void rebootArduino() { + delay(1000); + ESP.restart(); +} + +void processLockoutState() { + static unsigned long timer = millis(); + + switch (lockoutState) { + case LOCKOUT_BEGIN: + Serial.println("[LOCK] Begin"); + + lockoutState = LOCKOUT_DISENGAGED; + break; + + case LOCKOUT_DISENGAGED: + break; + + case LOCKOUT_CHECK_CARD: + if (scannedCard.length() == 0) { + lockoutState = LOCKOUT_UNAUTHORIZED_BEGIN; + break; + } + + if (authorizedCards.indexOf(scannedCard) == -1) { + lockoutState = LOCKOUT_UNAUTHORIZED_BEGIN; + break; + } + + lockoutState = LOCKOUT_AUTHORIZED; + + break; + + case LOCKOUT_UNAUTHORIZED_BEGIN: + timer = millis(); + LEDState = LED_DENIED; + lockoutState = LOCKOUT_UNAUTHORIZED_DELAY; + break; + + case LOCKOUT_UNAUTHORIZED_DELAY: + if (millis() - timer > 4000) { // overflow safe + LEDState = LED_IDLE; + lockoutState = LOCKOUT_DISENGAGED; + break; + } + break; + + case LOCKOUT_AUTHORIZED: + LEDState = LED_ENGAGED; + lockoutState = LOCKOUT_ENGAGED; + break; + + case LOCKOUT_ENGAGED: + if (button1State == BUTTON_PRESSED) { + LEDState = LED_IDLE; + lockoutState = LOCKOUT_DISENGAGED; + break; + } + break; + + + } + + return; +} + +void processControllerState() { + static unsigned long timer = millis(); + static unsigned long prev_scan_time = 0; + static enum controllerStates nextControllerState; + static int statusCode; + static int retryCount; + + String response; + String postData; + + bool failed; + time_t now; + struct tm timeinfo; + int i; + int result; + HTTPClient https; + + switch (controllerState) { + case CONTROLLER_BEGIN: + + Serial.println("[WIFI] Connecting..."); + retryCount = 0; + + timer = millis(); + controllerState = CONTROLLER_WIFI_CONNECT; + break; + + case CONTROLLER_WIFI_CONNECT: + LEDState = LED_INIT; + + if (WiFi.status() == WL_CONNECTED) { + Serial.print("[WIFI] Connected. IP address: "); + Serial.println(WiFi.localIP()); + + Serial.println("[TIME] Setting time using NTP."); + configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + nextControllerState = CONTROLLER_GET_TIME; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + if (millis() - timer > CONNECT_TIMEOUT_MS) { // overflow safe + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); + + delay(5000); + + rebootArduino(); + } + timer = millis(); + + break; + + case CONTROLLER_GET_TIME: + + time(&now); + if (now > 8 * 3600 * 2) { + gmtime_r(&now, &timeinfo); + Serial.print("[TIME] Current time in UTC: "); + Serial.print(asctime(&timeinfo)); + + + Serial.println("Moving to idle state..."); + controllerState = CONTROLLER_IDLE; + break; + } + + if (millis() - timer > CONNECT_TIMEOUT_MS) { // overflow safe + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); + + nextControllerState = CONTROLLER_BEGIN; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + break; + + case CONTROLLER_IDLE: + + LEDState = LED_IDLE; + + break; + + case CONTROLLER_PING: + + LEDState = LEDS_OFF; + + result = https.begin(wc, portalAPI + "/stats/"); + + if (!result) { + Serial.println("[WIFI] https.begin failed."); + + retryCount++; + if (retryCount > 5) { + nextControllerState = CONTROLLER_BEGIN; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + controllerState = CONTROLLER_IDLE_DELAY; + break; + } + + result = https.GET(); + + Serial.printf("[WIFI] Http code: %d\n", result); + + if (result != HTTP_CODE_OK) { + Serial.printf("[WIFI] Portal GET failed, error:\n%s\n", https.errorToString(result).c_str()); + + retryCount++; + if (retryCount > 5) { + // TODO: display this each time with a retry count below + nextControllerState = CONTROLLER_BEGIN; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + controllerState = CONTROLLER_IDLE_DELAY; + break; + } + retryCount = 0; + + response = https.getString(); + + if (prev_scan_time != 0 && prev_scan_time != scan_time) { + Serial.println("[SCAN] New scan, offering to host."); + + + controllerState = CONTROLLER_OFFER_HOST; + timer = millis(); + prev_scan_time = scan_time; // delete? + break; + } + prev_scan_time = scan_time; + + time(&now); + + if (closing_time > (unsigned long) now) { + Serial.print("[HOST] Protospace is open, showing closing time: "); + Serial.print(closing_time_str); + Serial.print(" host: "); + Serial.println(host_name); + + } else { + } + + controllerState = CONTROLLER_IDLE_DELAY; + + break; + + case CONTROLLER_OFFER_HOST: + LEDState = LEDS_SCROLL; + + if (button1State == BUTTON_PRESSED) { + host_hours = 2; + LEDState = LEDS_BUTTON1; + controllerState = CONTROLLER_SEND_HOURS; + break; + } else if (button2State == BUTTON_PRESSED) { + host_hours = 4; + LEDState = LEDS_BUTTON2; + controllerState = CONTROLLER_SEND_HOURS; + break; + } else if (button3State == BUTTON_PRESSED) { + host_hours = 6; + LEDState = LEDS_BUTTON3; + controllerState = CONTROLLER_SEND_HOURS; + break; + } else if (millis() - timer > CONTROLLER_OFFER_DELAY_MS) { // overflow safe + Serial.println("[HOST] Offer timed out, returning to idle state."); + controllerState = CONTROLLER_IDLE; + LEDState = LEDS_OFF; + break; + } + + break; + + case CONTROLLER_SEND_HOURS: + Serial.println("[HOST] Sending hosting hours to portal."); + + result = https.begin(wc, portalAPI + "/hosting/offer/"); + + if (!result) { + Serial.println("[HOST] https.begin failed."); + nextControllerState = CONTROLLER_BEGIN; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + postData = "member_id=" + + String(member_id) + + "&hours=" + + host_hours; + + Serial.println("[HOST] POST data:"); + Serial.println(postData); + + https.addHeader("Content-Type", "application/x-www-form-urlencoded"); + https.addHeader("Content-Length", String(postData.length())); + https.addHeader("Authorization", VANGUARD_API_TOKEN); + result = https.POST(postData); + + Serial.printf("[HOST] Http code: %d\n", result); + + if (result != HTTP_CODE_OK) { + Serial.printf("[HOST] Bad send, error:\n%s\n", https.errorToString(result).c_str()); + nextControllerState = CONTROLLER_BEGIN; + controllerState = CONTROLLER_UI_DELAY; + break; + } + + + controllerState = CONTROLLER_UI_DELAY; + nextControllerState = CONTROLLER_IDLE; + break; + + + case CONTROLLER_IDLE_DELAY: + timer = millis(); + controllerState = CONTROLLER_IDLE_WAIT; + break; + + case CONTROLLER_IDLE_WAIT: + if (millis() - timer > CONTROLLER_IDLE_DELAY_MS) { // overflow safe + controllerState = CONTROLLER_IDLE; + } + break; + + case CONTROLLER_UI_DELAY: + timer = millis(); + controllerState = CONTROLLER_UI_WAIT; + break; + + case CONTROLLER_UI_WAIT: + if (millis() - timer > CONTROLLER_UI_DELAY_MS) { // overflow safe + controllerState = nextControllerState; + } + break; + } + + return; +} + + +void setLEDOff() { + pixels.setPixelColor(0, pixels.Color(0, 0, 0)); + pixels.show(); +} + +void setLEDRed() { + pixels.setPixelColor(0, pixels.Color(150, 0, 0)); + pixels.show(); +} + +void setLEDGreen() { + pixels.setPixelColor(0, pixels.Color(0, 150, 0)); + pixels.show(); +} + +void setLEDBlue() { + pixels.setPixelColor(0, pixels.Color(0, 0, 150)); + pixels.show(); +} + +void setLEDOrange() { + pixels.setPixelColor(0, pixels.Color(150, 75, 0)); + pixels.show(); +} + + +void processLEDState() { + switch(LEDState) { + case LED_BEGIN: + setLEDOff(); + break; + + case LED_INIT: + setLEDOrange(); + break; + + case LED_IDLE: + setLEDRed(); + + // TODO: blink orange if there's an error + + break; + + } + + return; +} + +void pollButtons() { + static unsigned long button1Time = 0; + + processButtonState(BUTTON1SW, button1State, button1Time); + + if (button1State == BUTTON_PRESSED) { + Serial.println("Button 1 pressed"); + } else if (button1State == BUTTON_HELD) { + Serial.println("Button 1 held"); + } +} + +void processButtonState(int buttonPin, buttonStates &buttonState, unsigned long &buttonTime) { + bool pinState = !digitalRead(buttonPin); + + switch(buttonState) { + case BUTTON_OPEN: + if (pinState) { + buttonState = BUTTON_CLOSED; + buttonTime = millis(); + } + break; + case BUTTON_CLOSED: + if (millis() >= buttonTime + BUTTON_HOLD_TIME) { + buttonState = BUTTON_HELD; + } + if (pinState) { + ; + } else { + buttonState = BUTTON_CHECK_PRESSED; + } + break; + case BUTTON_CHECK_PRESSED: + if (millis() >= buttonTime + BUTTON_PRESS_TIME) { + buttonState = BUTTON_PRESSED; + } else { + buttonState = BUTTON_OPEN; + } + break; + case BUTTON_PRESSED: + buttonState = BUTTON_OPEN; + break; + case BUTTON_HELD: + if (!pinState) { + buttonState = BUTTON_OPEN; + } + break; + default: + break; + } +} + +void setup() +{ + Serial.begin(115200); + + Serial.println(""); + Serial.println("======= BOOT UP ======="); + + pinMode(BUTTON1LED, OUTPUT); + + pinMode(BUTTON1SW, INPUT_PULLUP); + + delay(1000); + + pixels.begin(); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + + //X509List cert(lets_encrypt_ca); + //wc.setTrustAnchors(&cert); + wc.setInsecure(); // disables all SSL checks. don't use in production + + Serial.println("Setup complete."); + delay(500); +} + +void loop() { + pollButtons(); + processLockoutState(); + processControllerState(); + processLEDState(); + + if (Serial2.available() > 0) { + String data = Serial2.readString(); + + Serial.print("RFID scan: "); + Serial.print(data); + Serial.print(", len: "); + Serial.println(data.length()); + + if (data.substring(1, 11) == "0700B5612A") { + rebootArduino(); + } + + if (controllerState == CONTROLLER_IDLE && lockoutState == LOCKOUT_DISENGAGED) { + scannedCard = data.substring(1, 11); + + Serial.print("Card: "); + Serial.println(scannedCard); + + lockoutState = LOCKOUT_CHECK_CARD; + } + } +}