#include #include #include #include #include #include const char *WIFI_SSID PROGMEM = "Protospace"; const char *WIFI_PASS PROGMEM = "yycmakers"; char wifiMACAddr[20] = ""; const String SOCKET_URL = String("http://tools-socket.protospace.ca/api/lockout/"); const String CARD_URL = String("http://tools-auth.protospace.ca/cards/"); Ticker ticker; #define CARD_BUFFER_LENGTH 14 char cardBuffer[CARD_BUFFER_LENGTH]; #define CARD_DATA_LENGTH 10 #define CARD_CHECK_LENGTH 2 typedef struct __attribute__((packed)) cardData { char head; char data[CARD_DATA_LENGTH]; char checksum[CARD_CHECK_LENGTH]; char tail; } cardData_t; #define LOGGING true #define RELAY_PIN D1 #define GREEN_BUTTON_PIN D3 #define RED_BUTTON_PIN D4 #define GREEN_LED_PIN D6 #define RED_LED_PIN D7 #define RELAY_CLOSED HIGH #define RELAY_OPEN !RELAY_CLOSED #define BUTTON_CLOSED LOW #define BUTTON_OPEN !BUTTON_CLOSED #define LED_PIN_ON HIGH #define LED_PIN_OFF !LED_PIN_ON #define DELAY_TIME 10 #define LOCK_ARMED_TIMEOUT 1000 #define COMM_LOCK_IDLE_TIME 50 #define COMM_CARD_IDLE_TIME 1000 #define LED_ARMED_BLINK_TIME 50 #define LED_ERROR_BLINK_TIME 50 #define EEPROM_SIZE 4095 enum wifiStates { WIFI_DISCONNECTED, WIFI_CONNECTING, WIFI_CONNECTED, } wifiState = WIFI_DISCONNECTED; enum LEDStates { LED_OFF, LED_ARMED, LED_ON, LED_ERROR, } LEDState = LED_OFF; enum lockStates { LOCK_OFF, LOCK_PREARM, LOCK_ARMED, LOCK_ON_PRESSED, // to wait until button is released LOCK_ON, } lockState = LOCK_OFF; enum commStates { COMM_INIT, COMM_IDLE, COMM_LOCK, COMM_CARD, } commState = COMM_INIT; void setup() { Serial.begin(9600); if (LOGGING) Serial.println("[INFO] Serial started."); pinMode(RELAY_PIN, OUTPUT); pinMode(GREEN_BUTTON_PIN, INPUT_PULLUP); pinMode(RED_BUTTON_PIN, INPUT_PULLUP); pinMode(GREEN_LED_PIN, OUTPUT); pinMode(RED_LED_PIN, OUTPUT); EEPROM.begin(EEPROM_SIZE); byte ar[6]; WiFi.macAddress(ar); sprintf(wifiMACAddr, "%02X%02X%02X%02X%02X%02X", ar[0], ar[1], ar[2], ar[3], ar[4], ar[5]); if (LOGGING) Serial.print("[INFO] Wifi MAC Address: "); if (LOGGING) Serial.println(wifiMACAddr); ticker.attach_ms(DELAY_TIME, tickerLoop); } // The stuff in this loop must not be blocked by network delay void tickerLoop() { processLockState(); processLEDState(); if (Serial.available() >= CARD_BUFFER_LENGTH) { uint8_t bufPos = 0; while (true) { char readChar = Serial.read(); if (readChar == -1) { break; } else if (readChar == 0x2) { bufPos = 0; } if (bufPos >= CARD_BUFFER_LENGTH) { break; } cardBuffer[bufPos++] = readChar; if (readChar == 0x3 && bufPos == CARD_BUFFER_LENGTH) { if (lockState == LOCK_OFF && LEDState == LED_OFF) checkCard(); break; } } } } void loop() { processWifiState(); processCommState(); delay(DELAY_TIME); } int8_t charToNum(char input) { return String("0123456789ABCDEF").indexOf(input); } bool checksum(cardData_t *cardData) { // checksum is each hex data byte xord'd together. // each char is a hex nibble, so we have to work in pairs. int8_t even = 0, odd = 0; for (int8_t i = 0; i < CARD_DATA_LENGTH; i++) { int8_t num = charToNum(cardData->data[i]); if (num == -1) return false; if (i % 2 == 0) even ^= num; if (i % 2 == 1) odd ^= num; } int8_t checksum_even = charToNum(cardData->checksum[0]); int8_t checksum_odd = charToNum(cardData->checksum[1]); if (even == checksum_even && odd == checksum_odd) { return true; } else { return false; } } void checkCard() { cardData_t *cardData = (cardData_t *) cardBuffer; if (cardData->head == 0x2 && cardData->tail == 0x3 && checksum(cardData)) { String cardStr = String(); String authorizedCards = String(); for (uint8_t i = 0; i < CARD_DATA_LENGTH; i++) { cardStr += cardData->data[i]; } for (uint16_t i = 0; i < EEPROM_SIZE; i++) { char tmp = EEPROM.read(i); authorizedCards += tmp; if (tmp == '$') break; } if (LOGGING) Serial.println("[INFO] Good scan from card: " + cardStr); if (authorizedCards.indexOf(cardStr) >= 0) { if (LOGGING) Serial.println("[INFO] Card is authorized on machine."); if (lockState == LOCK_OFF) { lockState = LOCK_PREARM; } } else { if (LOGGING) Serial.println("[INFO] Card not authorized on machine."); LEDState = LED_ERROR; } } } void processWifiState() { switch(wifiState) { case WIFI_DISCONNECTED: commState = COMM_INIT; if (LOGGING) Serial.println("[INFO] Wifi is disconnected. Attempting to connect..."); WiFi.begin(WIFI_SSID, WIFI_PASS); wifiState = WIFI_CONNECTING; break; case WIFI_CONNECTING: commState = COMM_INIT; if (WiFi.status() == WL_CONNECTED) { if (LOGGING) Serial.println("[INFO] Wifi is connected."); if (LOGGING) Serial.print("[INFO] Wifi IP Address: "); if (LOGGING) Serial.println(WiFi.localIP()); wifiState = WIFI_CONNECTED; } break; case WIFI_CONNECTED: if (WiFi.status() != WL_CONNECTED) { wifiState = WIFI_DISCONNECTED; } break; default: if (LOGGING) Serial.println("[ERROR] Invalid wifi state."); wifiState = WIFI_DISCONNECTED; break; } } bool greenButton() { return digitalRead(GREEN_BUTTON_PIN) == BUTTON_CLOSED; } bool redButton() { return digitalRead(RED_BUTTON_PIN) == BUTTON_CLOSED; } void relayOn() { digitalWrite(RELAY_PIN, RELAY_CLOSED); } void relayOff() { digitalWrite(RELAY_PIN, RELAY_OPEN); } void processLockState() { static uint16_t lockArmedTimeoutCount; if (lockState != LOCK_ARMED) lockArmedTimeoutCount = 0; switch (lockState) { case LOCK_OFF: if (LEDState != LED_ERROR) LEDState = LED_OFF; relayOff(); break; case LOCK_PREARM: if (!greenButton() && !redButton()) { if (LOGGING) Serial.println("[INFO] Arming interlock."); lockState = LOCK_ARMED; } else { lockState = LOCK_OFF; LEDState = LED_ERROR; } break; case LOCK_ARMED: if (LEDState != LED_ERROR) LEDState = LED_ARMED; relayOff(); lockArmedTimeoutCount++; if (redButton()) { if (LOGGING) Serial.println("[INFO] Unarming interlock."); lockState = LOCK_OFF; } else if (greenButton()) { if (LOGGING) Serial.println("[INFO] On button pressed."); lockState = LOCK_ON_PRESSED; } if (lockArmedTimeoutCount > LOCK_ARMED_TIMEOUT) { if (LOGGING) Serial.println("[INFO] Arming timed out, disarming."); lockState = LOCK_OFF; LEDState = LED_ERROR; } break; case LOCK_ON_PRESSED: if (redButton()) { if (LOGGING) Serial.println("[ERROR] Off button pressed, aborting."); lockState = LOCK_OFF; } else if (!greenButton()) { if (LOGGING) Serial.println("[INFO] Turning machine on."); lockState = LOCK_ON; } break; case LOCK_ON: if (LEDState != LED_ERROR) LEDState = LED_ON; relayOn(); if (redButton()) { if (LOGGING) Serial.println("[INFO] Off button pressed."); lockState = LOCK_OFF; } break; default: if (LOGGING) Serial.println("[ERROR] Invalid lock state."); lockState = LOCK_OFF; break; } } void greenLEDOn() { digitalWrite(GREEN_LED_PIN, LED_PIN_ON); } void greenLEDOff() { digitalWrite(GREEN_LED_PIN, LED_PIN_OFF); } void redLEDOn() { digitalWrite(RED_LED_PIN, LED_PIN_ON); } void redLEDOff() { digitalWrite(RED_LED_PIN, LED_PIN_OFF); } void processLEDState() { static uint16_t LEDArmedBlinkCount, LEDErrorBlinkCount; if (LEDState != LED_ARMED) LEDArmedBlinkCount = 0; if (LEDState != LED_ERROR) LEDErrorBlinkCount = 0; switch (LEDState) { case LED_OFF: greenLEDOff(); redLEDOn(); break; case LED_ARMED: LEDArmedBlinkCount++; if (LEDArmedBlinkCount < LED_ARMED_BLINK_TIME) { greenLEDOn(); redLEDOn(); } else if (LEDArmedBlinkCount < LED_ARMED_BLINK_TIME * 2) { greenLEDOff(); redLEDOn(); } else { LEDArmedBlinkCount = 0; } break; case LED_ON: greenLEDOn(); redLEDOn(); break; case LED_ERROR: LEDErrorBlinkCount++; if (LEDErrorBlinkCount < LED_ERROR_BLINK_TIME) { greenLEDOff(); redLEDOff(); } else if (LEDErrorBlinkCount < LED_ERROR_BLINK_TIME * 2) { greenLEDOff(); redLEDOn(); } else { LEDState = LED_OFF; } break; default: if (LOGGING) Serial.println("[ERROR] Invalid LED state."); LEDState = LED_OFF; break; } } // JSON functions to prevent memory leaking see: // https://arduinojson.org/v5/faq/i-found-a-memory-leak-in-the-library/ String serializeLockJson(uint8_t lockState) { // Generated with: https://arduinojson.org/assistant/ const size_t bufferSize = JSON_OBJECT_SIZE(1) + 50; StaticJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.createObject(); root["lockState"] = (uint8_t) lockState; String postData = String(); root.printTo(postData); return postData; } String deserializeLockJson(String input) { // Generated with: https://arduinojson.org/assistant/ const size_t bufferSize = JSON_OBJECT_SIZE(1) + 50; StaticJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(input); String action = root["action"]; return action; } void postState() { HTTPClient lockHTTP; //lockHTTP.begin("https://url", "7a 9c f4 db 40 d3 62 5a 6e 21 bc 5c cc 66 c8 3e a1 45 59 38"); //HTTPS lockHTTP.begin(SOCKET_URL + wifiMACAddr); lockHTTP.addHeader("Content-Type", "application/json"); if (LOGGING) Serial.println("[INFO] Lock state HTTP begin."); if (LOGGING) Serial.print("[INFO] HTTP POST: "); String postData = serializeLockJson(lockState); if (LOGGING) Serial.println(postData); int16_t lockHTTPCode = lockHTTP.POST(postData); if (lockHTTPCode > 0) { if (LOGGING) Serial.printf("[INFO] POST success, code: %d\n", lockHTTPCode); if (lockHTTPCode == HTTP_CODE_OK) { if (LOGGING) Serial.print("[INFO] Resource found, parsing response: "); String lockPayload = lockHTTP.getString(); if (LOGGING) Serial.println(lockPayload); String action = deserializeLockJson(lockPayload); if (action == "arm" && lockState == LOCK_OFF && LEDState == LED_OFF) { lockState = LOCK_PREARM; } else if (action == "disarm") { lockState = LOCK_OFF; } if (LOGGING) Serial.println("[INFO] action: " + action); } else { if (LOGGING) Serial.println("[ERROR] Resource not found."); } } else { if (LOGGING) Serial.printf("[ERROR] POST failed, error: %s\n", lockHTTP.errorToString(lockHTTPCode).c_str()); } lockHTTP.end(); } void getCards() { HTTPClient cardHTTP; //cardHTTP.begin("https://url", "7a 9c f4 db 40 d3 62 5a 6e 21 bc 5c cc 66 c8 3e a1 45 59 38"); //HTTPS cardHTTP.begin(CARD_URL + wifiMACAddr + "/"); cardHTTP.addHeader("Content-Type", "application/json"); if (LOGGING) Serial.println("[INFO] Card state HTTP begin."); if (LOGGING) Serial.println("[INFO] HTTP GET"); int16_t cardHTTPCode = cardHTTP.GET(); if (cardHTTPCode > 0) { if (LOGGING) Serial.printf("[INFO] GET success, code: %d\n", cardHTTPCode); if (cardHTTPCode == HTTP_CODE_OK) { if (LOGGING) Serial.print("[INFO] Resource found, parsing response: "); String cardPayload = cardHTTP.getString(); cardPayload += "$"; // Mark the end if (LOGGING) Serial.println(cardPayload); noInterrupts(); // commit() disables interrupts, but we want an atomic EEPROM buffer write for (int i = 0; i < cardPayload.length(); i++) { if (i >= EEPROM_SIZE) break; EEPROM.write(i, cardPayload.charAt(i)); } EEPROM.commit(); interrupts(); if (LOGGING) Serial.println("[INFO] Finished getting card data."); } else { if (LOGGING) Serial.println("[ERROR] Resource not found."); } } else { if (LOGGING) Serial.printf("[ERROR] POST failed, error: %s\n", cardHTTP.errorToString(cardHTTPCode).c_str()); } cardHTTP.end(); } void processCommState() { static uint16_t commLockIdleCount, commCardIdleCount; switch (commState) { case COMM_INIT: commLockIdleCount = 0; commCardIdleCount = 0; commState = COMM_IDLE; break; case COMM_IDLE: commLockIdleCount++; commCardIdleCount++; if (commLockIdleCount >= COMM_LOCK_IDLE_TIME) { commState = COMM_LOCK; } else if (commCardIdleCount >= COMM_CARD_IDLE_TIME) { commState = COMM_CARD; } break; case COMM_LOCK: { postState(); commLockIdleCount = 0; commState = COMM_IDLE; } break; case COMM_CARD: { getCards(); commCardIdleCount = 0; commState = COMM_IDLE; } break; } }