218 lines
7.2 KiB
C++
218 lines
7.2 KiB
C++
#include "controller.h"
|
|
#include "crypto.h"
|
|
#include "config.h"
|
|
|
|
#include <WiFi.h>
|
|
|
|
// 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;
|
|
} |