Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 47 additions & 2 deletions CustomCharacteristic.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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://<device-ip>: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
See code for more references/info in BLE_Custom_Characteristic.cpp
4 changes: 4 additions & 0 deletions include/Main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 4 additions & 0 deletions include/WebsocketAppender.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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];
};
6 changes: 6 additions & 0 deletions src/BLE_Custom_Characteristic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ This characteristic allows for reading and writing various user configuration pa
#include <Power_Table.h>
#include <BLE_Custom_Characteristic.h>
#include <Constants.h>
#include <Main.h>
#include <WebsocketAppender.h>

void BLE_ss2kCustomCharacteristic::setupService(NimBLEServer *pServer) {
pSmartSpin2kService = spinBLEServer.pServer->createService(SMARTSPIN2K_SERVICE_UUID);
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down
72 changes: 72 additions & 0 deletions src/WebsocketAppender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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;
}
Expand Down