#include "controller.h" #include "crypto.h" #include "config.h" #include // The url parameter to send the message to the local server constexpr char messageUrlParameter[] = "m"; SesameController::SesameController(uint16_t localWebServerPort, uint8_t remoteDeviceCount) : storage(remoteDeviceCount), localWebServer(localWebServerPort) { // Set up response buffer responseStatus = (SesameEvent*) responseBuffer; responseMessage = (AuthenticatedMessage*) (responseBuffer + 1); } void SesameController::configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, TimeConfiguration timeConfig, WifiConfiguration wifiConfig, KeyConfiguration keyConfig) { this->wifiConfig = wifiConfig; this->keyConfig = keyConfig; // Prepare EEPROM for reading and writing storage.configure(); Serial.println("[INFO] Storage configured"); servo.configure(servoConfig); Serial.println("[INFO] Servo configured"); // Direct messages and errors over the websocket to the controller server.configure(serverConfig, this); Serial.println("[INFO] Server connection configured"); timeCheck.configure(timeConfig); // Direct messages from the local web server to the controller localWebServer.on("/message", HTTP_POST, [this] (AsyncWebServerRequest *request) { this->handleLocalMessage(request); this->sendPreparedLocalResponse(request); }); Serial.println("[INFO] Local web server configured"); //storage.resetMessageCounters(); storage.printMessageCounters(); } void SesameController::loop(uint32_t millis) { server.loop(); servo.loop(millis); ensureWiFiConnection(millis); ensureWebSocketConnection(); } // MARK: Local void SesameController::handleLocalMessage(AsyncWebServerRequest *request) { if (!request->hasParam(messageUrlParameter)) { Serial.println("Missing url parameter"); prepareResponseBuffer(SesameEvent::InvalidUrlParameter); return; } String encoded = request->getParam(messageUrlParameter)->value(); if (!convertHexMessageToBinary(encoded.c_str())) { Serial.println("Invalid hex encoding"); prepareResponseBuffer(SesameEvent::InvalidMessageSize); return; } processMessage((AuthenticatedMessage*) receivedMessageBuffer); } void SesameController::sendPreparedLocalResponse(AsyncWebServerRequest *request) { request->send_P(200, "application/octet-stream", responseBuffer, responseSize); Serial.printf("[INFO] Local response %u (%u bytes)\n", responseBuffer[0], responseSize); } // MARK: Server void SesameController::sendServerError(SesameEvent event) { prepareResponseBuffer(event); sendPreparedServerResponse(); } void SesameController::handleServerMessage(uint8_t* payload, size_t length) { if (length != AUTHENTICATED_MESSAGE_SIZE) { prepareResponseBuffer(SesameEvent::InvalidMessageSize); return; } processMessage((AuthenticatedMessage*) payload); sendPreparedServerResponse(); } void SesameController::sendPreparedServerResponse() { server.sendResponse(responseBuffer, responseSize); Serial.printf("[INFO] Server response %u (%u bytes)\n", responseBuffer[0], responseSize); } // MARK: Message handling void SesameController::processMessage(AuthenticatedMessage* message) { SesameEvent event = verifyAndProcessReceivedMessage(message); prepareResponseBuffer(event, message->message.device); } /** * Process a received message. * * Checks whether the received data is a valid, * and then signals that the motor should move. * * @param message The message received from the remote * @return The response to signal to the server. */ SesameEvent SesameController::verifyAndProcessReceivedMessage(AuthenticatedMessage* message) { if (!isAuthenticMessage(message, keyConfig.remoteKey, keySize)) { return SesameEvent::MessageAuthenticationFailed; } if (!storage.isDeviceIdValid(message->message.device)) { return SesameEvent::MessageDeviceInvalid; } if (!storage.isMessageCounterValid(message->message.id, message->message.device)) { return SesameEvent::MessageCounterInvalid; } if (!timeCheck.isMessageTimeAcceptable(message->message.time)) { return SesameEvent::MessageTimeMismatch; } storage.didUseMessageCounter(message->message.id, message->message.device); // Move servo servo.pressButton(); Serial.printf("[Info] Accepted message %d\n", message->message.id); return SesameEvent::MessageAccepted; } bool allowMessageResponse(SesameEvent event) { switch (event) { case SesameEvent::MessageTimeMismatch: case SesameEvent::MessageCounterInvalid: case SesameEvent::MessageAccepted: case SesameEvent::MessageDeviceInvalid: return true; default: return false; } } uint16_t SesameController::prepareResponseBuffer(SesameEvent event, uint8_t deviceId) { *responseStatus = event; if (!allowMessageResponse(event)) { return 1; } responseMessage->message.time = timeCheck.getEpochTime(); responseMessage->message.id = storage.getNextMessageCounter(deviceId); responseMessage->message.device = deviceId; if (!authenticateMessage(responseMessage, keyConfig.localKey, keySize)) { *responseStatus = SesameEvent::InvalidResponseAuthentication; return 1; } return 1 + AUTHENTICATED_MESSAGE_SIZE; } // MARK: Reconnecting void SesameController::ensureWiFiConnection(uint32_t millis) { static uint32_t nextWifiReconnect = 0; // Reconnect to WiFi if(millis > nextWifiReconnect && WiFi.status() != WL_CONNECTED) { Serial.println("[INFO] Reconnecting WiFi..."); WiFi.setHostname(wifiConfig.networkName); WiFi.begin(wifiConfig.ssid, wifiConfig.password); isReconnecting = true; nextWifiReconnect = millis + wifiConfig.reconnectInterval; } } void SesameController::ensureWebSocketConnection() { if (isReconnecting && WiFi.status() == WL_CONNECTED) { isReconnecting = false; Serial.print("WiFi IP address: "); Serial.println(WiFi.localIP()); server.connect(); timeCheck.startNTP(); timeCheck.printLocalTime(); localWebServer.begin(); } } // MARK: Helper // Based on https://stackoverflow.com/a/23898449/266720 bool SesameController::convertHexMessageToBinary(const char* str) { // TODO: Fail if invalid hex values are used uint8_t idx0, idx1; // mapping of ASCII characters to hex values const uint8_t hashmap[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>? 0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO }; size_t len = strlen(str); if (len != AUTHENTICATED_MESSAGE_SIZE * 2) { // Require exact message size return false; } for (size_t pos = 0; pos < len; pos += 2) { idx0 = ((uint8_t)str[pos+0] & 0x1F) ^ 0x10; idx1 = ((uint8_t)str[pos+1] & 0x1F) ^ 0x10; receivedMessageBuffer[pos/2] = (uint8_t)(hashmap[idx0] << 4) | hashmap[idx1]; }; return true; }