diff --git a/README.md b/README.md index 7854233..a9f21d9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Sesame -Mechanism and code for phone-based door opening \ No newline at end of file +Mechanism and code for phone-based door opening + +## Requirements + +The following Arduino libraries are required: +- [ESP32 Arduino Core](https://github.com/espressif/arduino-esp32) +- [ESP32Servo](https://github.com/madhephaestus/ESP32Servo) diff --git a/Sesame-Device.ino b/Sesame-Device.ino new file mode 100644 index 0000000..1da37cc --- /dev/null +++ b/Sesame-Device.ino @@ -0,0 +1,382 @@ +/** + * Sesame-Device + * Christoph Hagen, 2022. + * + * The code for a simple door unlock mechanism where a servo pushes on an existing + * physical button. + */ +#include +#include +#include +#include +#include +#include // To mark used keys as expired +#include // To control the servo + + +/* Settings */ + +// The WiFi network to connect to +constexpr const char* WIFI_SSID = "MyNetwork"; +constexpr const char* WIFI_PWD = "MyPassword"; + +// The remote server to connect to +constexpr const char* SERVER_URL = "christophhagen.de"; +constexpr const int SERVER_PORT = 443; +constexpr const char* SERVER_PATH = "/sesame/listen"; +constexpr const char* SERVER_PSK = "access token"; +#define USE_SSL // Use SSL for the Websocket connection to the server + +// The interval to attempt to reconnect the socket to the server +constexpr unsigned long SOCKET_RECONNECT_TIME = 5000; + +// Crypto setting for the security of keys +constexpr uint8_t KEY_STRENGTH = 128; +constexpr uint8_t KEY_BYTE_COUNT = KEY_STRENGTH / 8; + +// The keys defined to allow opening +// Once a key is used, the corresponding byte in EEPROM is set to 1, +// to prevent it from being used again. +const uint8_t keys[][KEY_BYTE_COUNT] = { + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, +}; +const uint16_t KEY_COUNT = sizeof keys / KEY_BYTE_COUNT; + + +/* Servo */ + +// Servo is Emax ES08MA II + +// The time (in ms) to keep the door button pressed +constexpr uint32_t OPENING_DURATION = 2000; + +// The timer to use to control the servo +constexpr int pwmTimer = 0; + +// Pin wired up to the servo data line +constexpr int servoPin = 14; + +// The Emax is a standard 50 Hz servo +constexpr int servoFrequency = 50; + +// The microseconds to set the servo to the pressed state +constexpr int PRESS_STATE_INTERVAL = 1550; + +// The microseconds to set the servo to the released state +constexpr int RELEASE_STATE_INTERVAL = 1700; + + +/* Global variables */ + +// Servo controller +Servo servo; + +// PWM Module needed for the servo +ESP32PWM pwm; + +// WiFi module to connect to the network +WiFiMulti WiFiMulti; + +// WebSocket to connect to the control server +WebSocketsClient webSocket; + +// Indicator that the socket is connected. +bool socketIsConnected = false; + +// The index of the next valid key, which is the next key after the highest used key. +// If the index is larger or equal to the total key count, then no usable keys exist. +uint16_t nextKeyIndex = KEY_COUNT; + +// Flag to signal that the door button should be pressed +bool shouldStartOpening = false; + +// Indicator that the door button is pushed +bool buttonIsPressed = false; + +// The time (in ms since start) when the door opening should end +uint32_t openingEndTime = 0; + + +/* Events */ + +/** + * An event occuring due to a server request + */ +enum class SesameEvent { + TextReceived = 1, + UnexpectedSocketEvent = 2, + InvalidPayloadSize = 3, + InvalidKeyIndex = 4, + InvalidKey = 5, + KeyAlreadyUsed = 6, + KeyWasSkipped = 7, + KeyAccepted = 8, +}; + + +/* Key management */ + +/** + * Get the index of the next key which wasn't used yet + * + * A key is unused, if the EEPROM corresponding to the key is zero. + * If all keys have been used, returns KEY_COUNT + */ +uint16_t indexOfNextUsableKey(); + +/** + * Checks if a key was already marked as used. + * + * A key was used, if the EEPROM byte corresponding to the key is non-zero. + * @param keyIndex The index of the key in the 'keys' array. + * @return 0, if the key is unused, else non-zero. + */ +uint8_t keyWasAlreadyUsed(uint16_t keyIndex); + +/** + * Marks a key as used + * + * Sets the corresponding EEPROM byte to non-zero. + * @param keyIndex The index of the key in the 'keys' array. + */ +void markKeyUsed(uint16_t keyIndex); + +/** + * Marks all keys unused. + * + * Sets the EEPROM data for each key index to zero. + * + * WARNING: Only to be used when a new set of keys has been set. + * Otherwise replay attacks are possible with observed old keys. + */ +void markAllKeysUnused(); + +/** + * Compares two keys in constant time + * @return 1, if the keys are equal, else 0 + */ +uint8_t keysAreEqual(const uint8_t* key1, const uint8_t* key2); + + +/* Servo management */ + +/** + * Push the door opener button down by moving the servo arm. + */ +void pressButton(); + +/** + * Release the door opener button by moving the servo arm. + */ +void releaseButton(); + + +/* Logic */ + +/** + * Send a response event to the server. + * + * Sends the event type as a single byte representing the raw event type. + * @param event The event type + */ +void sendResponse(SesameEvent event) { + uint8_t response[1]; + response[0] = static_cast(event); + webSocket.sendBIN(response, 1); + //webSocket.sendTXT(text); + Serial.printf("[INFO] Event %d\n", response[0]); +} + +/** + * Send the pre-shared key to the server to complete the socket connection. + */ +void authenticateDevice() { + webSocket.sendTXT(SERVER_PSK); +} + +/** + * Process received binary data. + * + * Checks whether the received data is a valid and unused key, + * and then signals that the motor should move. + * + * @param payload The pointer to the received data. + * @param length The number of bytes received. + * @return The event to signal to the server. + */ +SesameEvent handleReceivedRequest(const uint8_t* payload, size_t length) { + if (length != KEY_BYTE_COUNT + 2) { + return SesameEvent::InvalidPayloadSize; + } + uint16_t keyIndex = ((uint16_t) payload[0] << 8) + payload[1]; + if (keyIndex >= KEY_COUNT) { + return SesameEvent::InvalidKeyIndex; + } + if (!keysAreEqual(payload + 2, keys[keyIndex])) { + return SesameEvent::InvalidKeyIndex; + } + if (keyWasAlreadyUsed(keyIndex)) { + return SesameEvent::KeyAlreadyUsed; + } + if (nextKeyIndex > keyIndex) { + return SesameEvent::KeyWasSkipped; + } + markKeyUsed(keyIndex); + nextKeyIndex = keyIndex + 1; + // Move servo + shouldStartOpening = true; + return SesameEvent::KeyAccepted; +} + +/** + * Process received binary data. + * + * Checks whether the received data is a valid and unused key, + * and then signals that the motor should move. + * Sends the event id to the server as a response to the request. + * + * If the key is valid, then `shouldStartOpening` is set to true. + * + * @param payload The pointer to the received data. + * @param length The number of bytes received. + */ +void processReceivedBytes(uint8_t * payload, size_t length) { + SesameEvent event = handleReceivedRequest(payload, length); + sendResponse(event); +} + +/** + * Callback for WebSocket events. + * + * Updates the connection state and processes received keys. + * + * @param payload The pointer to received data + * @param length The number of bytes received + */ +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + socketIsConnected = false; + Serial.println("[INFO] Socket disconnected."); + break; + case WStype_CONNECTED: + socketIsConnected = true; + authenticateDevice(); + Serial.printf("[INFO] Socket connected to url: %s\n", payload); + break; + case WStype_TEXT: + sendResponse(SesameEvent::TextReceived); + break; + case WStype_BIN: + handleReceivedRequest(payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + sendResponse(SesameEvent::UnexpectedSocketEvent); + break; + } +} + +void setup() { + Serial.begin(115200); + Serial.setDebugOutput(true); + Serial.println("[INFO] Device started"); + + markAllKeysUnused(); + Serial.println("[WARN] All keys reset"); + + ESP32PWM::allocateTimer(pwmTimer); + servo.setPeriodHertz(servoFrequency); + servo.attach(servoPin); + Serial.println("[INFO] Servo configured"); + + Serial.printf("[INFO] Key security: %d bit (%d byte keys)\n", KEY_STRENGTH, KEY_BYTE_COUNT); + nextKeyIndex = indexOfNextUsableKey(); + uint8_t percentage = (KEY_COUNT - nextKeyIndex) * 100 / KEY_COUNT; + Serial.printf("[INFO] %d of %d keys remaining (%d %%)\n", KEY_COUNT - nextKeyIndex, KEY_COUNT, percentage); + Serial.printf("[INFO] Connecting to WiFi '%s'\n", WIFI_SSID); + WiFiMulti.addAP(WIFI_SSID, WIFI_PWD); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + Serial.println("[INFO] WiFi connected"); + Serial.printf("[INFO] Opening socket %s%s on port %d\n", SERVER_URL, SERVER_PATH, SERVER_PORT); + + #ifdef USE_SSL + webSocket.beginSSL(SERVER_URL, SERVER_PORT, SERVER_PATH); + #else + webSocket.begin(SERVER_URL, SERVER_PORT, SERVER_PATH); + #endif + webSocket.onEvent(webSocketEvent); + + // try again every 5000 ms if connection has failed + webSocket.setReconnectInterval(SOCKET_RECONNECT_TIME); +} + +void loop() { + webSocket.loop(); + if (shouldStartOpening) { + shouldStartOpening = false; + openingEndTime = millis() + OPENING_DURATION; + pressButton(); + } + if (buttonIsPressed && millis() > openingEndTime) { + releaseButton(); + } +} + +/* Key management */ + +uint16_t indexOfNextUsableKey() { + // Find the highest key which was previously used + for (uint16_t keyIndex = 0; keyIndex < KEY_COUNT; keyIndex += 1) { + if (EEPROM.read(KEY_COUNT - keyIndex)) { + // The following key is the next unused + return keyIndex + 1; + } + } + // No key previously used + return 0; +} + +uint8_t keyWasAlreadyUsed(uint16_t keyIndex) { + return EEPROM.read(keyIndex); +} + +void markKeyUsed(uint16_t keyIndex) { + EEPROM.write(keyIndex, 1); +} + +void markAllKeysUnused() { + for (uint16_t keyIndex = 0; keyIndex < KEY_COUNT; keyIndex += 1) { + EEPROM.write(keyIndex, 0); + } +} + +uint8_t keysAreEqual(const uint8_t* key1, const uint8_t* key2) { + uint8_t result = 0; + for (uint8_t i = 0; i < KEY_BYTE_COUNT; i += 1) { + result |= key1[i] ^ key2[i]; + } + if (result) { + return 0; + } + return 1; +} + +/* Servo management */ + +void pressButton() { + servo.write(PRESS_STATE_INTERVAL); + buttonIsPressed = true; +} + +void releaseButton() { + servo.write(RELEASE_STATE_INTERVAL); + buttonIsPressed = false; +} diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index 6da656e..0000000 --- a/platformio.ini +++ /dev/null @@ -1,16 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -[env:esp32doit-devkit-v1] -platform = espressif32 -board = esp32doit-devkit-v1 -framework = arduino -lib_deps = madhephaestus/ESP32Servo@^0.11.0 -upload_port = /dev/cu.usbserial-0001 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index b9f42da..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include - -const int serialBaud = 115200; - -ESP32PWM pwm; -const int pwmTimer = 0; -Servo servo; -const int servoPin = 14; // Pin wired up to the servo data line -const int servoFrequency = 50; - -// Published values for Emax ES08MA II -const int minUs = 1500; -const int maxUs = 1900; - -void setup() { - Serial.begin(serialBaud); - - ESP32PWM::allocateTimer(pwmTimer); - - servo.setPeriodHertz(servoFrequency); - servo.attach(servoPin, minUs, maxUs); -} - -void loop() { - for (uint8_t i = 0; i < 10; i += 1) { - servo.write(i*10); - delay(1000); - } -} - -void end() { - servo.detach(); - pwm.detachPin(servoPin); -}