#include #include #include #include #include #include #include #include #include const char *FIRMWARE_VERSION = "MRWIZARD 0010 MRWIZARD"; 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/"); const String INFOLOG_URL = String("http://tools-auth.protospace.ca/infolog/"); const String UPDATE_URL = String("http://tools-auth.protospace.ca/update/"); Ticker ticker; #define CARD_BUFFER_LENGTH 14 char cardBuffer[CARD_BUFFER_LENGTH]; #define CARD_DATA_LENGTH 10 #define CARD_CHECK_LENGTH 2 #define CARD_HEAD_BYTE 0x2 #define CARD_TAIL_BYTE 0x3 struct __attribute__((packed)) cardData { char head; char data[CARD_DATA_LENGTH]; char checksum[CARD_CHECK_LENGTH]; char tail; }; #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 // times below are multiplied by DELAY_TIME, in ms #define LOCK_ARMED_TIMEOUT 1000 #define COMM_LOCK_IDLE_TIME 50 #define COMM_CARD_IDLE_TIME 1000 #define COMM_INFO_IDLE_TIME 3000 #define LED_ARMED_BLINK_TIME 50 #define LED_ERROR_BLINK_TIME 50 #define EEPROM_SIZE 4095 #define EEPROM_END_MARKER '\0' 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, // prevent arming while buttons held 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, COMM_INFO, } commState = COMM_INIT; #define SERIAL_LOGGING true #define LOG_SIZE 90 // 100 blew up the stack #define LOG_DATA_LENGTH CARD_DATA_LENGTH enum eventCodes { LOG_BOOT_UP, LOG_INIT_COMPLETE, LOG_WIFI_CONNECTED, LOG_WIFI_DISCONNECTED, LOG_COMM_LOCK_ARM, LOG_COMM_LOCK_DISARM, LOG_COMM_LOCK_FAIL, LOG_COMM_CARD_FAIL, LOG_COMM_INFO_FAIL, LOG_LOCK_OFF, LOG_LOCK_ARMED, LOG_LOCK_TIMEOUT, LOG_LOCK_ON, LOG_LOCK_DISARM, LOG_LOCK_ERROR, LOG_CARD_GOOD_READ, LOG_CARD_ACCEPTED, LOG_CARD_DENIED, LOG_UPDATE_FAILED, LOG_TEST, }; struct __attribute__((packed)) logData { uint32_t unixTime; uint8_t eventCode; char data[LOG_DATA_LENGTH]; }; struct logData eventLog[LOG_SIZE]; uint16_t logPosition = 0; void logEvent(uint8_t eventCode, const char *data = nullptr, size_t num = 0) { struct logData event; noInterrupts(); event.unixTime = time(nullptr); event.eventCode = eventCode; memset(event.data, 0, LOG_DATA_LENGTH); for (uint8_t i = 0; i < LOG_DATA_LENGTH; i++) { if (i >= num) break; event.data[i] = data[i]; } if (logPosition < LOG_SIZE) { eventLog[logPosition++] = event; } interrupts(); } void removeLogRecords(uint8_t num) { // shift records down by num because they've been sent if (num > logPosition) return; noInterrupts(); for (int i = 0; i < num; i++) { eventLog[i] = eventLog[i + num]; } logPosition -= num; interrupts(); } void setup() { Serial.begin(9600); if (SERIAL_LOGGING) Serial.println("[INFO] Serial started."); struct timeval tv = { .tv_sec = 0, .tv_usec = 0 }; struct timezone tz = { .tz_minuteswest = 0, .tz_dsttime = 0 }; settimeofday(&tv, &tz); if (SERIAL_LOGGING) Serial.println("[INFO] Set system time to 0."); logEvent(LOG_BOOT_UP, &FIRMWARE_VERSION[9], 4); if (SERIAL_LOGGING) Serial.print("[INFO] Booting firmware version: "); if (SERIAL_LOGGING) Serial.println(FIRMWARE_VERSION); 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 (SERIAL_LOGGING) Serial.print("[INFO] Wifi MAC Address: "); if (SERIAL_LOGGING) Serial.println(wifiMACAddr); ticker.attach_ms(DELAY_TIME, tickerLoop); logEvent(LOG_INIT_COMPLETE); } // 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 == CARD_HEAD_BYTE) { bufPos = 0; } if (bufPos >= CARD_BUFFER_LENGTH) { break; } cardBuffer[bufPos++] = readChar; if (readChar == CARD_TAIL_BYTE && 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(struct cardData *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() { struct cardData *cardData = (struct cardData *) cardBuffer; if (cardData->head == CARD_HEAD_BYTE && cardData->tail == CARD_TAIL_BYTE && 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 == EEPROM_END_MARKER) break; } if (SERIAL_LOGGING) Serial.println("[INFO] Good scan from card: " + cardStr); logEvent(LOG_CARD_GOOD_READ, cardStr.c_str(), cardStr.length()); if (authorizedCards.indexOf(cardStr) >= 0) { if (SERIAL_LOGGING) Serial.println("[INFO] Card is authorized on machine."); if (lockState == LOCK_OFF) { lockState = LOCK_PREARM; } logEvent(LOG_CARD_ACCEPTED, cardStr.c_str(), cardStr.length()); } else { if (SERIAL_LOGGING) Serial.println("[INFO] Card not authorized on machine."); LEDState = LED_ERROR; logEvent(LOG_CARD_DENIED, cardStr.c_str(), cardStr.length()); } } } void processWifiState() { switch(wifiState) { case WIFI_DISCONNECTED: commState = COMM_INIT; if (SERIAL_LOGGING) Serial.println("[INFO] Wifi 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 (SERIAL_LOGGING) Serial.println("[INFO] Wifi is connected."); logEvent(LOG_WIFI_CONNECTED); if (SERIAL_LOGGING) Serial.print("[INFO] Wifi IP Address: "); if (SERIAL_LOGGING) Serial.println(WiFi.localIP()); wifiState = WIFI_CONNECTED; } break; case WIFI_CONNECTED: if (WiFi.status() != WL_CONNECTED) { if (SERIAL_LOGGING) Serial.println("[INFO] Wifi disconnected."); logEvent(LOG_WIFI_DISCONNECTED); wifiState = WIFI_DISCONNECTED; } break; default: if (SERIAL_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 (SERIAL_LOGGING) Serial.println("[INFO] Arming interlock."); logEvent(LOG_LOCK_ARMED); lockState = LOCK_ARMED; } else { if (SERIAL_LOGGING) Serial.println("[ERROR] Buttons held, aborting."); logEvent(LOG_LOCK_ERROR); LEDState = LED_ERROR; lockState = LOCK_OFF; } break; case LOCK_ARMED: if (LEDState != LED_ERROR) LEDState = LED_ARMED; relayOff(); lockArmedTimeoutCount++; if (redButton()) { if (SERIAL_LOGGING) Serial.println("[INFO] Unarming interlock."); logEvent(LOG_LOCK_DISARM); lockState = LOCK_OFF; } else if (greenButton()) { if (SERIAL_LOGGING) Serial.println("[INFO] On button pressed."); lockState = LOCK_ON_PRESSED; } if (lockArmedTimeoutCount > LOCK_ARMED_TIMEOUT) { if (SERIAL_LOGGING) Serial.println("[INFO] Arming timed out, disarming."); logEvent(LOG_LOCK_TIMEOUT); lockState = LOCK_OFF; LEDState = LED_ERROR; } break; case LOCK_ON_PRESSED: if (redButton()) { if (SERIAL_LOGGING) Serial.println("[ERROR] Off button pressed, aborting."); logEvent(LOG_LOCK_ERROR); lockState = LOCK_OFF; } else if (!greenButton()) { if (SERIAL_LOGGING) Serial.println("[INFO] Turning machine on."); logEvent(LOG_LOCK_ON); lockState = LOCK_ON; } break; case LOCK_ON: if (LEDState != LED_ERROR) LEDState = LED_ON; relayOn(); if (redButton()) { if (SERIAL_LOGGING) Serial.println("[INFO] Off button pressed."); logEvent(LOG_LOCK_OFF); lockState = LOCK_OFF; } break; default: if (SERIAL_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 { LEDErrorBlinkCount = 0; LEDState = LED_OFF; } break; default: if (SERIAL_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; } String serializeLog() { size_t logLengthBytes = logPosition * sizeof(struct logData); return "{\"log\": \"" + base64::encode((uint8_t *) eventLog, logLengthBytes, false) + "\"}"; } void deserializeInfoJson(String input, uint8_t *processed, uint32_t *unixTime, String *version) { // Generated with: https://arduinojson.org/assistant/ const size_t bufferSize = JSON_OBJECT_SIZE(3) + 70; StaticJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(input); *processed = root["processed"]; *unixTime = root["unixTime"]; *version = root["version"].as(); } //TODO: abstract http functions void postState() { // Don't log more than one error at a time static bool logErrors = true; 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 (SERIAL_LOGGING) Serial.println("[INFO] Lock state HTTP begin."); if (SERIAL_LOGGING) Serial.print("[INFO] HTTP POST: "); String postData = serializeLockJson(lockState); if (SERIAL_LOGGING) Serial.println(postData); int16_t lockHTTPCode = lockHTTP.POST(postData); String lockHTTPCodeStr = String(lockHTTPCode); if (lockHTTPCode > 0) { if (SERIAL_LOGGING) Serial.printf("[INFO] POST success, code: %d\n", lockHTTPCode); if (lockHTTPCode == HTTP_CODE_OK) { logErrors = true; if (SERIAL_LOGGING) Serial.print("[INFO] Resource found, parsing response: "); String lockPayload = lockHTTP.getString(); if (SERIAL_LOGGING) Serial.println(lockPayload); String action = deserializeLockJson(lockPayload); if (action == "arm" && lockState == LOCK_OFF && LEDState == LED_OFF) { logEvent(LOG_COMM_LOCK_ARM); lockState = LOCK_PREARM; } else if (action == "disarm" && lockState != LOCK_ON) { logEvent(LOG_COMM_LOCK_DISARM); if (SERIAL_LOGGING) Serial.println("[INFO] Unarming interlock."); logEvent(LOG_LOCK_DISARM); lockState = LOCK_OFF; } if (SERIAL_LOGGING) Serial.println("[INFO] action: " + action); } else { if (SERIAL_LOGGING) Serial.println("[ERROR] Resource not found."); if (logErrors) logEvent(LOG_COMM_LOCK_FAIL, lockHTTPCodeStr.c_str(), lockHTTPCodeStr.length()); logErrors = false; } } else { if (SERIAL_LOGGING) Serial.printf("[ERROR] POST failed, error: %s\n", lockHTTP.errorToString(lockHTTPCode).c_str()); if (logErrors) logEvent(LOG_COMM_LOCK_FAIL, lockHTTPCodeStr.c_str(), lockHTTPCodeStr.length()); logErrors = false; } lockHTTP.end(); } void getCards() { // Don't log more than one error at a time static bool logErrors = true; 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 (SERIAL_LOGGING) Serial.println("[INFO] Card state HTTP begin."); if (SERIAL_LOGGING) Serial.println("[INFO] HTTP GET"); int16_t cardHTTPCode = cardHTTP.GET(); String cardHTTPCodeStr = String(cardHTTPCode); if (cardHTTPCode > 0) { if (SERIAL_LOGGING) Serial.printf("[INFO] GET success, code: %d\n", cardHTTPCode); if (cardHTTPCode == HTTP_CODE_OK) { logErrors = true; if (SERIAL_LOGGING) Serial.print("[INFO] Resource found, parsing response: "); String cardPayload = cardHTTP.getString(); cardPayload += String(EEPROM_END_MARKER); if (SERIAL_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 (SERIAL_LOGGING) Serial.println("[INFO] Finished getting card data."); } else { if (SERIAL_LOGGING) Serial.println("[ERROR] Resource not found."); if (logErrors) logEvent(LOG_COMM_CARD_FAIL, cardHTTPCodeStr.c_str(), cardHTTPCodeStr.length()); logErrors = false; } } else { if (SERIAL_LOGGING) Serial.printf("[ERROR] POST failed, error: %s\n", cardHTTP.errorToString(cardHTTPCode).c_str()); if (logErrors) logEvent(LOG_COMM_CARD_FAIL, cardHTTPCodeStr.c_str(), cardHTTPCodeStr.length()); logErrors = false; } cardHTTP.end(); } void postInfolog() { // Don't log more than one error at a time static bool logErrors = true; HTTPClient infoHTTP; //infoHTTP.begin("https://url", "7a 9c f4 db 40 d3 62 5a 6e 21 bc 5c cc 66 c8 3e a1 45 59 38"); //HTTPS infoHTTP.begin(INFOLOG_URL + wifiMACAddr + "/"); infoHTTP.addHeader("Content-Type", "application/json"); if (SERIAL_LOGGING) Serial.println("[INFO] Info state HTTP begin."); if (SERIAL_LOGGING) Serial.println("[INFO] HTTP POST."); String postData = serializeLog(); int16_t infoHTTPCode = infoHTTP.POST(postData); String infoHTTPCodeStr = String(infoHTTPCode); if (infoHTTPCode > 0) { if (SERIAL_LOGGING) Serial.printf("[INFO] POST success, code: %d\n", infoHTTPCode); if (infoHTTPCode == HTTP_CODE_OK) { logErrors = true; if (SERIAL_LOGGING) Serial.print("[INFO] Resource found, parsing response: "); String infoPayload = infoHTTP.getString(); if (SERIAL_LOGGING) Serial.println(infoPayload); uint8_t processed; uint32_t unixTime; String version = String(); deserializeInfoJson(infoPayload, &processed, &unixTime, &version); struct timeval tv = { .tv_sec = unixTime, .tv_usec = 0 }; struct timezone tz = { .tz_minuteswest = 0, .tz_dsttime = 0 }; settimeofday(&tv, &tz); removeLogRecords(processed); if (version != FIRMWARE_VERSION && lockState == LOCK_OFF) { noInterrupts(); if (SERIAL_LOGGING) Serial.println("[INFO] Firmware out of date. Updating..."); WiFiClient client; int16_t update_response = ESPhttpUpdate.update(client, UPDATE_URL + wifiMACAddr + "/"); interrupts(); if (SERIAL_LOGGING) printf("[ERROR] %s\n", ESPhttpUpdate.getLastErrorString().c_str()); String lastErrorNum = String(ESPhttpUpdate.getLastError()); logEvent(LOG_UPDATE_FAILED, lastErrorNum.c_str(), lastErrorNum.length()); } if (SERIAL_LOGGING) Serial.print("[INFO] Set system time to: "); if (SERIAL_LOGGING) Serial.println(unixTime); } else { if (SERIAL_LOGGING) Serial.println("[ERROR] Resource not found."); if (logErrors) logEvent(LOG_COMM_INFO_FAIL, infoHTTPCodeStr.c_str(), infoHTTPCodeStr.length()); logErrors = false; } } else { if (SERIAL_LOGGING) Serial.printf("[ERROR] POST failed, error: %s\n", infoHTTP.errorToString(infoHTTPCode).c_str()); if (logErrors) logEvent(LOG_COMM_INFO_FAIL, infoHTTPCodeStr.c_str(), infoHTTPCodeStr.length()); logErrors = false; } infoHTTP.end(); } void processCommState() { static uint16_t commLockIdleCount, commCardIdleCount, commInfoIdleCount; switch (commState) { case COMM_INIT: commLockIdleCount = 0; commCardIdleCount = 0; commInfoIdleCount = 0; commState = COMM_IDLE; break; case COMM_IDLE: commLockIdleCount++; commCardIdleCount++; commInfoIdleCount++; if (commLockIdleCount >= COMM_LOCK_IDLE_TIME) { commState = COMM_LOCK; } else if (commCardIdleCount >= COMM_CARD_IDLE_TIME) { commState = COMM_CARD; } else if (commInfoIdleCount >= COMM_INFO_IDLE_TIME) { commState = COMM_INFO; } break; case COMM_LOCK: { postState(); commLockIdleCount = 0; commState = COMM_IDLE; } break; case COMM_CARD: { getCards(); commCardIdleCount = 0; commState = COMM_IDLE; } break; case COMM_INFO: { postInfolog(); commInfoIdleCount = 0; commState = COMM_IDLE; } break; } }