diff --git a/include/controller.h b/include/controller.h index 0230f75..578dabf 100644 --- a/include/controller.h +++ b/include/controller.h @@ -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; diff --git a/include/server.h b/include/server.h index f8e18a2..1b18421 100644 --- a/include/server.h +++ b/include/server.h @@ -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; diff --git a/include/servo.h b/include/servo.h index 89741e1..802665f 100644 --- a/include/servo.h +++ b/include/servo.h @@ -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 diff --git a/src/controller.cpp b/src/controller.cpp index 92d59c3..caf3d50 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -5,64 +5,135 @@ #include #include -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() { diff --git a/src/main.cpp b/src/main.cpp index c64615e..d3ed7b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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() { diff --git a/src/server.cpp b/src/server.cpp index 886d4b4..0e47dad 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -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); diff --git a/src/servo.cpp b/src/servo.cpp index 224b7d5..b2e3a69 100644 --- a/src/servo.cpp +++ b/src/servo.cpp @@ -2,8 +2,6 @@ #include // 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