diff --git a/CHANGELOG.md b/CHANGELOG.md index 09734ba3..2f0708bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Changed + +### Hardware + + +## [25.12.11] + +### Added + ### Changed - links card update diff --git a/CustomCharacteristic.md b/CustomCharacteristic.md index 3943639e..b9f8dcf5 100644 --- a/CustomCharacteristic.md +++ b/CustomCharacteristic.md @@ -1,10 +1,21 @@ Please use and refer to the following notes for use of the custom characteristic: -Custom Characteristic for userConfig Variable manipulation via BLE +Custom Characteristic for userConfig Variable manipulation via BLE and WebSocket SMARTSPIN2K_SERVICE_UUID "77776277-7877-7774-4466-896665500000" SMARTSPIN2K_CHARACTERISTIC_UUID "77776277-7877-7774-4466-896665500001" +## Access Methods + +Custom characteristic commands can be sent through multiple channels: + +1. **BLE (Bluetooth Low Energy)**: Using the SmartSpin2k custom characteristic UUID +2. **DirCon Protocol**: Via TCP on port 8081 (see DirCon documentation) +3. **WebSocket**: Via WebSocket connection on port 8080 (NEW) + +The WebSocket server accepts the same binary command format as BLE. This allows web applications +and other WebSocket clients to control SmartSpin2k settings using the same command structure. + An example follows to read/write 26.3kph to simulatedSpeed: simulatedSpeed is a float and first needs to be converted to int by *10 for transmission, so convert 26.3kph to 263 (multiply by 10) @@ -79,7 +90,41 @@ From BLE_common.h *syncMode will disable the movement of the stepper motor by forcing stepperPosition = targetPosition prior to the motor control. While this mode is enabled, it allows the client to set parameters like incline and shifterPosition without moving the motor from it's current position. Once the parameters are set, this mode should be turned back off and SS2K will resume normal operation. +## Using WebSocket for Custom Characteristic Commands + +The WebSocket server (port 8080) provides full bidirectional communication for custom characteristic +commands in the same format as BLE. To send a command and receive responses via WebSocket: + +1. Connect to `ws://:8080` +2. Send a binary WebSocket message with the command bytes +3. Receive the response as a binary message back through the same WebSocket connection + +Example JavaScript code: +```javascript +const ws = new WebSocket('ws://192.168.1.100:8080'); +ws.binaryType = 'arraybuffer'; + +ws.onopen = () => { + // Write 263 to simulatedSpeed (0x06) + const command = new Uint8Array([0x02, 0x06, 0x07, 0x01]); + ws.send(command.buffer); +}; + +ws.onmessage = (event) => { + if (event.data instanceof ArrayBuffer) { + // Binary response from custom characteristic command + const response = new Uint8Array(event.data); + console.log('Response:', Array.from(response).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')); + } else { + // Text message (logging output) + console.log('Log:', event.data); + } +}; +``` + +Note: Only WebSocket clients that have sent at least one command will receive command responses. +Clients that only listen for logs will not receive custom characteristic responses. This characteristic also notifies when a shift is preformed or the button is pressed. -See code for more references/info in BLE_Server.cpp starting on line 534 \ No newline at end of file +See code for more references/info in BLE_Custom_Characteristic.cpp \ No newline at end of file diff --git a/include/Main.h b/include/Main.h index bf1bfeb9..4b795f15 100644 --- a/include/Main.h +++ b/include/Main.h @@ -114,3 +114,7 @@ extern SS2K *ss2k; // Main program variable that stores most everything extern userParameters *userConfig; extern RuntimeParameters *rtConfig; + +// WebSocket appender for logging and custom characteristic commands +class WebSocketAppender; +extern WebSocketAppender webSocketAppender; diff --git a/include/WebsocketAppender.h b/include/WebsocketAppender.h index c893ba56..8ea08232 100644 --- a/include/WebsocketAppender.h +++ b/include/WebsocketAppender.h @@ -17,6 +17,7 @@ class WebSocketAppender : public ILogAppender { WebSocketAppender(); void Log(const char* message); void Loop(); + void SendResponse(const uint8_t* data, size_t length); private: static const uint16_t port = 8080; @@ -26,7 +27,10 @@ class WebSocketAppender : public ILogAppender { uint8_t GetClientsCount(); void AddClient(WebsocketsClient* client); void CheckConnectedClients(); + void HandleIncomingMessages(); + void OnMessageReceived(WebsocketsClient& client, WebsocketsMessage message); WebsocketsServer _webSocketsServer; WebsocketsClient* _clients[maxClients]; + bool _clientSentCommand[maxClients]; }; diff --git a/src/BLE_Custom_Characteristic.cpp b/src/BLE_Custom_Characteristic.cpp index f4d92dca..068f9259 100644 --- a/src/BLE_Custom_Characteristic.cpp +++ b/src/BLE_Custom_Characteristic.cpp @@ -87,6 +87,8 @@ This characteristic allows for reading and writing various user configuration pa #include #include #include +#include +#include void BLE_ss2kCustomCharacteristic::setupService(NimBLEServer *pServer) { pSmartSpin2kService = spinBLEServer.pServer->createService(SMARTSPIN2K_SERVICE_UUID); @@ -825,6 +827,8 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { #endif if (returnString == "") { pCharacteristic->setValue(returnValue, returnLength); + // Send response to WebSocket clients that have sent commands + webSocketAppender.SendResponse(returnValue, returnLength); } else { // Need to send a string instead uint8_t returnChar[returnString.length() + 2]; returnChar[0] = cc_success; @@ -833,6 +837,8 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { returnChar[i + 2] = returnString[i]; } pCharacteristic->setValue(returnChar, returnString.length() + 2); + // Send response to WebSocket clients that have sent commands + webSocketAppender.SendResponse(returnChar, returnString.length() + 2); } pCharacteristic->indicate(); diff --git a/src/WebsocketAppender.cpp b/src/WebsocketAppender.cpp index bf715e1d..3c065906 100644 --- a/src/WebsocketAppender.cpp +++ b/src/WebsocketAppender.cpp @@ -7,14 +7,17 @@ // see: https://github.com/gilmaimon/ArduinoWebsockets #include "WebsocketAppender.h" +#include "BLE_Custom_Characteristic.h" WebSocketAppender::WebSocketAppender() { for (uint8_t index = 0; index < maxClients; index++) { _clients[index] = NULL; + _clientSentCommand[index] = false; } } void WebSocketAppender::Initialize() { _webSocketsServer.listen(WebSocketAppender::port); } + void WebSocketAppender::Loop() { // CheckConnectedClients(); if (WiFi.status() == WL_CONNECTED && GetClientsCount() < maxClients) { @@ -26,6 +29,9 @@ void WebSocketAppender::Loop() { WebsocketsClient client = _webSocketsServer.accept(); AddClient(new WebsocketsClient(client)); } + + // Handle incoming messages from connected clients + HandleIncomingMessages(); } void WebSocketAppender::Log(const char* message) { @@ -40,6 +46,7 @@ void WebSocketAppender::Log(const char* message) { if (!client->available() || !client->send(message)) { _clients[index] = NULL; + _clientSentCommand[index] = false; // Serial.println("Remove disconnected websocket client from Log()."); client->close(); delete client; @@ -62,6 +69,11 @@ void WebSocketAppender::AddClient(WebsocketsClient* client) { for (uint8_t index = 0; index < maxClients; index++) { if (_clients[index] == NULL) { _clients[index] = client; + _clientSentCommand[index] = false; + // Set up message callback for this client + client->onMessage([this](WebsocketsClient& client, WebsocketsMessage message) { + this->OnMessageReceived(client, message); + }); return; } } @@ -77,6 +89,66 @@ void WebSocketAppender::CheckConnectedClients() { if (!client->available()) { // Serial.println("Remove disconnected websocket client."); _clients[index] = NULL; + _clientSentCommand[index] = false; + client->close(); + delete client; + } + } +} + +void WebSocketAppender::HandleIncomingMessages() { + for (uint8_t index = 0; index < maxClients; index++) { + WebsocketsClient* client = _clients[index]; + if (client == NULL) { + continue; + } + + if (client->available()) { + // Poll for messages - the onMessage callback will be triggered + client->poll(); + } + } +} + +void WebSocketAppender::OnMessageReceived(WebsocketsClient& client, WebsocketsMessage message) { + // Only process binary or text messages + if (!message.isBinary() && !message.isText()) { + return; + } + + // Mark this client as having sent a command so it receives responses + for (uint8_t index = 0; index < maxClients; index++) { + if (_clients[index] == &client) { + _clientSentCommand[index] = true; + break; + } + } + + // Convert message data to std::string for processing + std::string rxValue; + if (message.isBinary()) { + // Binary data - directly use the raw data + rxValue = std::string(message.c_str(), message.length()); + } else { + // Text data - also use as-is (could be hex-encoded or similar) + rxValue = std::string(message.c_str(), message.length()); + } + + // Process the custom characteristic command using the existing BLE processing logic + BLE_ss2kCustomCharacteristic::process(rxValue); +} + +void WebSocketAppender::SendResponse(const uint8_t* data, size_t length) { + // Send response to all clients that have previously sent a command + for (uint8_t index = 0; index < maxClients; index++) { + WebsocketsClient* client = _clients[index]; + if (client == NULL || !_clientSentCommand[index]) { + continue; + } + + if (!client->available() || !client->sendBinary((const char*)data, length)) { + _clients[index] = NULL; + _clientSentCommand[index] = false; client->close(); delete client; }