// 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; } } }