Commit WIP code

This commit is contained in:
2026-01-10 19:00:22 -07:00
parent 27300d472f
commit 5ead815ef2
4 changed files with 586 additions and 0 deletions

1
.gitignore vendored
View File

@@ -150,3 +150,4 @@ out.*
*.csv *.csv
*.txt *.txt
*.json *.json
.aider*

30
demos/button/button.ino Normal file
View File

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

View File

555
firmware/firmware.ino Normal file
View File

@@ -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 <Arduino.h>
#include <HardwareSerial.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Adafruit_NeoPixel.h> // version 1.15.2
//#include <WebServer.h>
//#include <ArduinoJson.h> // v6.19.4
//#include <ElegantOTA.h> // v2.2.9
//#include <WebSerial.h> // 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;
}
}
}