New state machine, local UDP route

This commit is contained in:
Christoph Hagen 2024-04-22 12:58:18 +02:00
parent 3afe8b16a9
commit da22f8a37c
7 changed files with 289 additions and 145 deletions

View File

@ -6,17 +6,46 @@
#include "configurations/EthernetConfiguration.h"
#include "configurations/KeyConfiguration.h"
enum class SesameDeviceStatus {
/**
* @brief The initial state of the device after boot
*/
initial,
/**
* @brief The device has configured the individual parts,
* but has no the ethernet hardware detected.
*/
configuredButNoEthernetHardware,
/**
* @brief The device has ethernet hardware, but no ethernet link
*/
ethernetHardwareButNoLink,
/**
* @brief The device has an ethernet link, but no IP address.
*/
ethernetLinkButNoIP,
/**
* @brief The device has an IP address, but no socket connection
*/
ipAddressButNoSocketConnection,
};
class SesameController: public ServerConnectionCallbacks {
public:
SesameController();
void configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig);
SesameController(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig);
void loop(uint32_t millis);
private:
SesameDeviceStatus status = SesameDeviceStatus::initial;
uint32_t currentTime = 0;
ServerConnection server;
@ -26,7 +55,7 @@ private:
// An EthernetUDP instance to send and receive packets over UDP
EthernetUDP udp;
EthernetHardwareStatus ethernetStatus;
EthernetConfiguration ethernetConfig;
bool ethernetIsConfigured = false;
@ -42,6 +71,31 @@ private:
uint32_t currentServerChallenge;
SignedMessage outgoingMessage;
// MARK: Ethernet
void initializeSpiBusForEthernetModule();
/**
* @brief Checks to ensure that Ethernet hardware is available
*
* @return true The hardware is available
* @return false The hardware is missing
*/
bool hasAvailableEthernetHardware();
/**
* @brief Check that an active ethernet link is available
*
* @return true Link is available
* @return false Link is absent
*/
bool hasEthernetLink();
void configureEthernet();
void startUDP();
void stopUDP();
bool hasCurrentChallenge() {
return currentChallengeExpiry > currentTime;

View File

@ -46,14 +46,16 @@ class ServerConnection {
public:
ServerConnection();
ServerConnection(ServerConfiguration configuration);
/**
* @brief Set the configuration and the callback handler
*
* @param callback The handler to handle messages and errors
*/
void configure(ServerConfiguration configuration, ServerConnectionCallbacks* callbacks);
void setCallbacks(ServerConnectionCallbacks* callbacks);
void shouldConnect(bool connect);
/**
* @brief Call this function regularly to handle socket operations.
@ -73,7 +75,8 @@ public:
private:
uint32_t currentTime;
uint32_t currentTime = 0;
bool shouldBeConnected = false;
bool socketIsConnected() {
return webSocket.isConnected();
@ -81,15 +84,12 @@ private:
void connect();
void disconnect();
bool shouldReconnect = true;
bool isConnecting = false;
bool isDisconnecting = false;
uint32_t connectionTimeout = 0;
uint32_t nextReconnectAttemptMs = 0;
void didDisconnect();
void didConnect();
void didChangeConnectionState(bool isConnected);
ServerConfiguration configuration;

View File

@ -52,15 +52,10 @@ public:
/**
* @brief Construct a new servo controller
*/
ServoController();
/**
* @brief Configure the servo
*
* @param The configuration for the servo
*/
void configure(ServoConfiguration configuration);
ServoController(ServoConfiguration configuration);
/**
* @brief Update the servo state periodically

View File

@ -5,64 +5,135 @@
#include <SPI.h>
#include <Ethernet.h>
SesameController::SesameController() {
SesameController::SesameController(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig)
: ethernetConfig(ethernetConfig), keyConfig(keyConfig), servo(servoConfig), server(serverConfig) {
}
void SesameController::configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig) {
this->ethernetConfig = ethernetConfig;
this->keyConfig = keyConfig;
// Ensure source of random numbers without WiFi and Bluetooth
enableCrypto();
// Initialize SPI interface to Ethernet module
void SesameController::initializeSpiBusForEthernetModule() {
SPI.begin(ethernetConfig.spiPinSclk, ethernetConfig.spiPinMiso, ethernetConfig.spiPinMosi, ethernetConfig.spiPinSS); //SCLK, MISO, MOSI, SS
pinMode(ethernetConfig.spiPinSS, OUTPUT);
Ethernet.init(ethernetConfig.spiPinSS);
if (Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.dhcpLeaseTimeoutMs, ethernetConfig.dhcpLeaseResponseTimeoutMs) == 1) {
Serial.print("[INFO] DHCP assigned IP ");
Serial.println(Ethernet.localIP());
ethernetIsConfigured = true;
} else {
// Check for Ethernet hardware present
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("[ERROR] Ethernet shield not found.");
} else if (Ethernet.linkStatus() == LinkOFF) {
Serial.println("[ERROR] Ethernet cable is not connected.");
} else if (Ethernet.linkStatus() == Unknown) {
Serial.println("[ERROR] Ethernet cable status unknown.");
} else if (Ethernet.linkStatus() == LinkON) {
Serial.println("[INFO] Ethernet cable is connected.");
// Try to configure using IP address instead of DHCP
Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.manualIp, ethernetConfig.manualDnsAddress);
Serial.print("[WARNING] DHCP failed, using self-assigned IP ");
Serial.println(Ethernet.localIP());
ethernetIsConfigured = true;
}
}
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");
udp.begin(ethernetConfig.udpPort);
Serial.println("[INFO] Local UDP connection configured");
}
void SesameController::loop(uint32_t millis) {
currentTime = millis;
//server.loop(millis);
checkLocalMessage();
switch (status) {
case SesameDeviceStatus::initial:
// In initial state, first configure SPI and
enableCrypto(); // Ensure source of random numbers without WiFi and Bluetooth
initializeSpiBusForEthernetModule();
// Direct messages and errors over the websocket to the controller
server.setCallbacks(this);
configureEthernet();
status = SesameDeviceStatus::configuredButNoEthernetHardware;
Serial.println("[INFO] State: initial -> noHardware");
// Directly check for ethernet hardware
// break;
case SesameDeviceStatus::configuredButNoEthernetHardware:
if (!hasAvailableEthernetHardware()) {
// No ethernet hardware found, wait
// TODO: Try rebooting after some time as a potential fix?
break;
}
status = SesameDeviceStatus::ethernetHardwareButNoLink;
Serial.println("[INFO] State: noHardware -> noLink");
// Directly check for Ethernet link
// break;
case SesameDeviceStatus::ethernetHardwareButNoLink:
if (!hasEthernetLink()) {
if (!hasAvailableEthernetHardware()) {
status = SesameDeviceStatus::configuredButNoEthernetHardware;
Serial.println("[INFO] State: noLink -> noHardware");
break;
}
// Wait for ethernet link
// TODO: Try rebooting after some time as a potential fix?
break;
}
status = SesameDeviceStatus::ethernetLinkButNoIP;
Serial.println("[INFO] State: noLink -> noIP");
// Directly check for socket connection
// break;
case SesameDeviceStatus::ethernetLinkButNoIP:
if (!hasEthernetLink()) {
if (hasAvailableEthernetHardware()) {
status = SesameDeviceStatus::ethernetHardwareButNoLink;
Serial.println("[INFO] State: noIP -> noLink");
} else {
status = SesameDeviceStatus::configuredButNoEthernetHardware;
Serial.println("[INFO] State: noIP -> noHardware");
}
break;
}
startUDP();
status = SesameDeviceStatus::ipAddressButNoSocketConnection;
Serial.println("[INFO] State: noIP -> noSocket");
// Directly check for socket connection
// break;
case SesameDeviceStatus::ipAddressButNoSocketConnection:
if (!hasEthernetLink()) {
server.shouldConnect(false);
stopUDP();
if (!hasAvailableEthernetHardware()) {
status = SesameDeviceStatus::configuredButNoEthernetHardware;
Serial.println("[INFO] State: noSocket -> noHardware");
} else {
status = SesameDeviceStatus::ethernetHardwareButNoLink;
Serial.println("[INFO] State: noSocket -> noLink");
}
break;
}
server.shouldConnect(true);
server.loop(millis);
checkLocalMessage();
break;
}
}
bool SesameController::hasAvailableEthernetHardware() {
EthernetHardwareStatus ethernetStatus = Ethernet.hardwareStatus();
static bool didNotify = false;
if (ethernetStatus != EthernetW5500) {
if (!didNotify) {
Serial.print("[ERROR] No Ethernet hardware found: ");
Serial.println(ethernetStatus);
didNotify = true;
}
return false;
}
return true;
}
bool SesameController::hasEthernetLink() {
return Ethernet.linkStatus() == EthernetLinkStatus::LinkON;
}
void SesameController::configureEthernet() {
if (Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.dhcpLeaseTimeoutMs, ethernetConfig.dhcpLeaseResponseTimeoutMs) == 1) {
Serial.print("[INFO] DHCP assigned IP ");
Serial.println(Ethernet.localIP());
return;
}
Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.manualIp, ethernetConfig.manualDnsAddress);
Serial.print("[WARNING] DHCP failed, using self-assigned IP ");
Serial.println(Ethernet.localIP());
}
void SesameController::startUDP() {
udp.begin(ethernetConfig.udpPort);
}
void SesameController::stopUDP() {
udp.stop();
}
// MARK: Local
void SesameController::checkLocalMessage() {

View File

@ -22,53 +22,51 @@
#include "controller.h"
#include "config.h"
SesameController controller{};
ServoConfiguration servoConfig {
.pwmTimer = pwmTimer,
.pwmFrequency = servoFrequency,
.pin = servoPin,
.openDuration = lockOpeningDuration,
.pressedValue = servoPressedState,
.releasedValue = servoReleasedState,
};
ServerConfiguration serverConfig {
.url = serverUrl,
.port = serverPort,
.path = serverPath,
.key = serverAccessKey,
.reconnectTime = 5000,
.socketHeartbeatIntervalMs = socketHeartbeatIntervalMs,
.socketHeartbeatTimeoutMs = socketHeartbeatTimeoutMs,
.socketHeartbeatFailureReconnectCount = socketHeartbeatFailureReconnectCount,
};
EthernetConfiguration ethernetConfig {
.macAddress = ethernetMacAddress,
.spiPinMiso = spiPinMiso,
.spiPinMosi = spiPinMosi,
.spiPinSclk = spiPinSclk,
.spiPinSS = spiPinSS,
.dhcpLeaseTimeoutMs = dhcpLeaseTimeoutMs,
.dhcpLeaseResponseTimeoutMs = dhcpLeaseResponseTimeoutMs,
.manualIp = manualIpAddress,
.manualDnsAddress = manualDnsServerAddress,
.udpPort = localUdpPort,
};
KeyConfiguration keyConfig {
.remoteKey = remoteKey,
.localKey = localKey,
.challengeExpiryMs = challengeExpiryMs,
};
SesameController controller(servoConfig, serverConfig, ethernetConfig, keyConfig);
void setup() {
Serial.begin(serialBaudRate);
Serial.setDebugOutput(true);
Serial.println("[INFO] Device started");
ServoConfiguration servoConfig {
.pwmTimer = pwmTimer,
.pwmFrequency = servoFrequency,
.pin = servoPin,
.openDuration = lockOpeningDuration,
.pressedValue = servoPressedState,
.releasedValue = servoReleasedState,
};
ServerConfiguration serverConfig {
.url = serverUrl,
.port = serverPort,
.path = serverPath,
.key = serverAccessKey,
.reconnectTime = 5000,
.socketHeartbeatIntervalMs = socketHeartbeatIntervalMs,
.socketHeartbeatTimeoutMs = socketHeartbeatTimeoutMs,
.socketHeartbeatFailureReconnectCount = socketHeartbeatFailureReconnectCount,
};
EthernetConfiguration ethernetConfig {
.macAddress = ethernetMacAddress,
.spiPinMiso = spiPinMiso,
.spiPinMosi = spiPinMosi,
.spiPinSclk = spiPinSclk,
.spiPinSS = spiPinSS,
.dhcpLeaseTimeoutMs = dhcpLeaseTimeoutMs,
.dhcpLeaseResponseTimeoutMs = dhcpLeaseResponseTimeoutMs,
.manualIp = manualIpAddress,
.manualDnsAddress = manualDnsServerAddress,
.udpPort = localUdpPort,
};
KeyConfiguration keyConfig {
.remoteKey = remoteKey,
.localKey = localKey,
.challengeExpiryMs = challengeExpiryMs,
};
controller.configure(servoConfig, serverConfig, ethernetConfig, keyConfig);
}
void loop() {

View File

@ -1,26 +1,25 @@
#include "server.h"
constexpr int32_t pingInterval = 10000;
constexpr uint32_t pongTimeout = 5000;
uint8_t disconnectTimeoutCount = 3;
ServerConnection::ServerConnection(ServerConfiguration configuration) : configuration(configuration) { }
ServerConnection::ServerConnection() { }
void ServerConnection::configure(ServerConfiguration configuration, ServerConnectionCallbacks *callbacks) {
void ServerConnection::setCallbacks(ServerConnectionCallbacks *callbacks) {
controller = callbacks;
this->configuration = configuration;
}
void ServerConnection::connect() {
if (webSocket.isConnected()) {
void ServerConnection::shouldConnect(bool connect) {
if (connect == shouldBeConnected) {
return;
}
if (controller == NULL) {
Serial.println("[ERROR] No callbacks set for server");
return;
}
shouldBeConnected = connect;
nextReconnectAttemptMs = currentTime;
}
isConnecting = true;
void ServerConnection::connect() {
Serial.printf("[INFO] Connecting to %s:%d%s\n", configuration.url, configuration.port, configuration.path);
connectionTimeout = currentTime + configuration.socketHeartbeatIntervalMs;
webSocket.begin(configuration.url, configuration.port, configuration.path);
@ -33,47 +32,76 @@ void ServerConnection::connect() {
webSocket.setReconnectInterval(configuration.reconnectTime);
}
void ServerConnection::didDisconnect() {
if (shouldReconnect || isConnecting) {
return; // Disconnect already registered.
void ServerConnection::didChangeConnectionState(bool isConnected) {
static bool wasConnected = false;
if (isConnected) {
Serial.println("[INFO] Socket connected, enabling heartbeat");
isConnecting = false;
webSocket.enableHeartbeat(configuration.socketHeartbeatIntervalMs, configuration.socketHeartbeatTimeoutMs, configuration.socketHeartbeatFailureReconnectCount);
} else {
isDisconnecting = false;
if (!wasConnected && shouldBeConnected && nextReconnectAttemptMs < currentTime) {
nextReconnectAttemptMs = currentTime + configuration.socketHeartbeatIntervalMs;
Serial.println("[INFO] Socket disconnected, setting reconnect time");
} else if (wasConnected) {
Serial.println("[INFO] Socket disconnected");
}
}
Serial.println("[INFO] Socket disconnected");
nextReconnectAttemptMs = currentTime + configuration.socketHeartbeatIntervalMs;
shouldReconnect = true;
}
void ServerConnection::didConnect() {
isConnecting = false;
Serial.println("[INFO] Socket connected");
webSocket.enableHeartbeat(configuration.socketHeartbeatIntervalMs, configuration.socketHeartbeatTimeoutMs, configuration.socketHeartbeatFailureReconnectCount);
}
void ServerConnection::disconnect() {
webSocket.disconnect();
wasConnected = isConnected;
}
void ServerConnection::loop(uint32_t millis) {
currentTime = millis;
webSocket.loop();
if (shouldReconnect && !isConnecting) {
shouldReconnect = false;
if (shouldBeConnected) {
if (isDisconnecting) {
return; // Wait for disconnect to finish, then it will be reconnected
}
if (isConnecting) {
if (millis > connectionTimeout) {
// Cancel connection attempt
Serial.println("[INFO] Canceling socket connection attempt");
isDisconnecting = true;
isConnecting = false;
webSocket.disconnect();
}
return; // Wait for connect to finish
}
if (webSocket.isConnected()) {
return; // Already connected
}
if (controller == NULL) {
return;
}
if (millis < nextReconnectAttemptMs) {
return; // Wait for next reconnect
}
isConnecting = true;
connect();
}
if (isConnecting && millis > connectionTimeout) {
Serial.println("[INFO] Failed to connect");
disconnect();
shouldReconnect = true;
isConnecting = false;
} else {
if (isDisconnecting) {
return; // Wait for disconnect
}
if (isConnecting) {
return; // Wait until connection is established, then it will be disconnected
}
if (!webSocket.isConnected()) {
return;
}
isDisconnecting = true;
Serial.println("[INFO] Disconnecting socket");
webSocket.disconnect();
}
}
void ServerConnection::webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
didDisconnect();
didChangeConnectionState(false);
break;
case WStype_CONNECTED:
didConnect();
didChangeConnectionState(true);
break;
case WStype_TEXT:
controller->sendServerError(MessageResult::TextReceivedOverSocket);

View File

@ -2,8 +2,6 @@
#include <esp32-hal.h> // For `millis()`
ServoController::ServoController() { }
void performReset(void * pvParameters) {
ServoController* servo = (ServoController *) pvParameters;
for(;;){
@ -12,7 +10,7 @@ void performReset(void * pvParameters) {
}
}
void ServoController::configure(ServoConfiguration configuration) {
ServoController::ServoController(ServoConfiguration configuration) {
// Create a task that runs on a different core,
// So that it's always executed