can now recieve visemes through websocket

websocket
Jake 2026-02-10 00:35:47 +08:00
parent d4683aa385
commit 263bb2590f
9 changed files with 438 additions and 92 deletions

View File

@ -652,4 +652,22 @@ Key files:
- `protocol.h/cpp` - Packet format and CRC implementation - `protocol.h/cpp` - Packet format and CRC implementation
- `commands.h/cpp` - Command handlers - `commands.h/cpp` - Command handlers
- `sensors.h/cpp` - IMU and radar drivers - `sensors.h/cpp` - IMU and radar drivers
- `websocket_client.h/cpp` - WiFi, WebSocket client + server
---
## WebSocket Server
The firmware runs a WebSocket server on port **81** alongside the outbound client.
Any device on the same WiFi network can connect and send protocol-framed packets.
**URL:** `ws://<robot-ip>:81/`
All commands from the serial protocol are supported over WebSocket:
`VSME`, `MSET`, `BHVR`, `SSET`, `FPLY`, `FSTP`, etc.
Packets must use the same binary framing as serial:
`0xA5 0x5A + TAG(4) + LEN(2) + SEQ(2) + PAYLOAD(N) + CRC16(2)`
Up to 2 simultaneous WebSocket client connections are supported.

View File

@ -284,7 +284,7 @@ private:
std::vector<Viseme> visemes; std::vector<Viseme> visemes;
// Configuration // Configuration
static constexpr unsigned long TIMEOUT_MS = 3000; // 3 second timeout static constexpr unsigned long TIMEOUT_MS = 200; // 3 second timeout
static constexpr uint16_t DEFAULT_POSITION = 2047; // Center/rest position static constexpr uint16_t DEFAULT_POSITION = 2047; // Center/rest position
// Helper to find viseme by ID // Helper to find viseme by ID

View File

@ -87,10 +87,10 @@ bool MotorStreamState::shouldStream() {
// ============================================================================ // ============================================================================
void dispatchCommand() { void dispatchCommand() {
const char* tag = getReceivedTag(); dispatchCommand(getReceivedTag(), getReceivedPayload(), getReceivedPayloadLen());
const uint8_t* payload = getReceivedPayload(); }
uint16_t len = getReceivedPayloadLen();
void dispatchCommand(const char tag[4], const uint8_t* payload, uint16_t len) {
// Identity & Config // Identity & Config
if (tagMatches(tag, Tag::IDENT)) { if (tagMatches(tag, Tag::IDENT)) {
handleIdent(payload, len); handleIdent(payload, len);

View File

@ -47,6 +47,9 @@ extern MotorStreamState motorStream;
// Process a received packet - call after receivePacket() returns true // Process a received packet - call after receivePacket() returns true
void dispatchCommand(); void dispatchCommand();
// Dispatch a command by tag and payload directly (for WebSocket server, etc.)
void dispatchCommand(const char tag[4], const uint8_t* payload, uint16_t len);
// ============================================================================ // ============================================================================
// Individual Command Handlers // Individual Command Handlers
// ============================================================================ // ============================================================================

View File

@ -559,9 +559,6 @@ void setup() {
delay(500); delay(500);
Serial.println("\n[HansonServo] Starting..."); Serial.println("\n[HansonServo] Starting...");
// WebSocket client (WiFi + connect to remote, receive FACE packets)
websocketSetup();
// Initialize servo manager // Initialize servo manager
servoManager.init(); servoManager.init();
Serial.println("[HansonServo] Servos initialized"); Serial.println("[HansonServo] Servos initialized");
@ -645,6 +642,9 @@ void setup() {
behaviorManager.setBehaviorEnabled(BEHAVIOR_IDLE, true); behaviorManager.setBehaviorEnabled(BEHAVIOR_IDLE, true);
} }
// WebSocket client + server (WiFi must be after config load so credentials are ready)
websocketSetup();
// Initialize idle behavior motors (needs config.motors to be loaded) // Initialize idle behavior motors (needs config.motors to be loaded)
std::vector<uint8_t> allMotorIDs; std::vector<uint8_t> allMotorIDs;
for (const Motor& motor : config.motors) { for (const Motor& motor : config.motors) {

View File

@ -650,13 +650,20 @@ bool RobotConfig::loadFromFFatV2(const char* path, BehaviorManager* behaviorMana
case KEY_WIFI_SETTINGS: { case KEY_WIFI_SETTINGS: {
if (type == TYPE_WIFI_SETTINGS) { if (type == TYPE_WIFI_SETTINGS) {
readStr(file, wifiSettings.ssid, 32); // TODO: re-enable when SSET wifi writes are working
readStr(file, wifiSettings.password, 64); // For now, skip loading and use hardcoded defaults from WiFiSettings struct
readStr(file, wifiSettings.host, 63); // readStr(file, wifiSettings.ssid, 32);
wifiSettings.port = readU16(file); // readStr(file, wifiSettings.password, 64);
readStr(file, wifiSettings.path, 31); // readStr(file, wifiSettings.host, 63);
// wifiSettings.port = readU16(file);
// readStr(file, wifiSettings.path, 31);
// Serial.println("[Config] WiFi settings loaded");
Serial.println("[Config] WiFi settings loaded"); // Skip the stored blob so file position stays correct
for (int s = 0; s < 3; s++) { uint8_t l = file.read(); for (uint8_t k = 0; k < l; k++) file.read(); }
file.read(); file.read(); // port
for (int s = 0; s < 2; s++) { uint8_t l = file.read(); for (uint8_t k = 0; k < l; k++) file.read(); }
Serial.println("[Config] WiFi settings skipped (using hardcoded)");
} else { } else {
// Skip: 5 length-prefixed strings + 2 byte port - can't know exact size // Skip: 5 length-prefixed strings + 2 byte port - can't know exact size
// Best effort: skip based on stored lengths // Best effort: skip based on stored lengths

232
tools/viseme_sender.py Normal file
View File

@ -0,0 +1,232 @@
"""
Viseme Sender - sends viseme packets to the robot over WebSocket.
Usage:
python viseme_sender.py [robot_ip]
Default IP: 192.168.1.x (auto-discovered or specify as argument)
Connects to ws://<ip>:81
Controls:
0-9, A = send viseme 0-10
Space = send neutral (viseme 0 / sil)
Q / Esc = quit
L = request viseme list (VLST)
"""
import asyncio
import struct
import sys
try:
import websockets
except ImportError:
print("Missing dependency. Install with: pip install websockets")
sys.exit(1)
# ---------------------------------------------------------------------------
# Protocol helpers
# ---------------------------------------------------------------------------
SYNC = b'\xA5\x5A'
def crc16_ccitt(data: bytes, init: int = 0xFFFF) -> int:
crc = init
for b in data:
crc ^= b << 8
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc <<= 1
crc &= 0xFFFF
return crc
_seq = 0
def build_packet(tag: str, payload: bytes = b'') -> bytes:
global _seq
tag_bytes = tag.encode('ascii')[:4].ljust(4, b'\x00')
length = len(payload)
header_tail = tag_bytes + struct.pack('<HH', length, _seq)
crc_data = header_tail + payload
crc = crc16_ccitt(crc_data)
packet = SYNC + crc_data + struct.pack('<H', crc)
_seq = (_seq + 1) & 0xFFFF
return packet
def parse_packet(data: bytes):
"""Parse a protocol packet, return (tag, payload) or None."""
if len(data) < 12:
return None
if data[0:2] != SYNC:
return None
tag = data[2:6].decode('ascii', errors='replace')
length = struct.unpack('<H', data[6:8])[0]
# seq = struct.unpack('<H', data[8:10])[0]
if len(data) < 10 + length + 2:
return None
payload = data[10:10 + length]
received_crc = struct.unpack('<H', data[10 + length:12 + length])[0]
computed_crc = crc16_ccitt(data[2:10 + length])
if received_crc != computed_crc:
return None
return tag, payload
# ---------------------------------------------------------------------------
# Viseme table
# ---------------------------------------------------------------------------
VISEMES = {
0: 'sil',
1: 'AA',
2: 'AE',
3: 'AH',
4: 'AO',
5: 'EH',
6: 'IH',
7: 'IY',
8: 'OW',
9: 'UH',
10: 'UW',
}
KEY_MAP = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14,
'f': 15, 'g': 16, 'h': 17, 'i': 18, 'j': 19,
' ': 0,
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
async def main():
ip = sys.argv[1] if len(sys.argv) > 1 else '192.168.1.1'
uri = f'ws://{ip}:81'
print(f'Connecting to {uri} ...')
try:
ws = await websockets.connect(uri)
except Exception as e:
print(f'Connection failed: {e}')
return
print(f'Connected to {uri}')
print()
print('Viseme keys:')
for k, vid in sorted(KEY_MAP.items()):
label = VISEMES.get(vid, f'viseme {vid}')
key_label = 'Space' if k == ' ' else k.upper()
print(f' [{key_label}] -> {vid}: {label}')
print(' [L] -> list visemes from device')
print(' [Q/Esc] -> quit')
print()
# Background task to receive and print responses
async def receiver():
try:
async for message in ws:
if isinstance(message, bytes):
result = parse_packet(message)
if result:
tag, payload = result
if tag == 'VLST':
print_viseme_list(payload)
elif tag == 'ACK!':
print(f' <- ACK')
elif tag.startswith('NAC'):
msg = payload.decode('ascii', errors='replace') if payload else ''
print(f' <- NACK: {msg}')
elif tag == 'MSGE':
msg = payload.decode('ascii', errors='replace')
print(f' <- MSG: {msg}')
else:
print(f' <- [{tag}] {len(payload)} bytes')
except websockets.exceptions.ConnectionClosed:
pass
recv_task = asyncio.create_task(receiver())
# Input loop (runs in executor to avoid blocking)
loop = asyncio.get_event_loop()
try:
while True:
key = await loop.run_in_executor(None, get_key)
if key is None or key in ('q', '\x1b'):
break
if key == 'l':
pkt = build_packet('VLST')
await ws.send(pkt)
print(' -> VLST (list visemes)')
continue
if key in KEY_MAP:
vid = KEY_MAP[key]
pkt = build_packet('VSME', bytes([vid]))
await ws.send(pkt)
label = VISEMES.get(vid, f'viseme {vid}')
print(f' -> VSME {vid}: {label}')
except (KeyboardInterrupt, EOFError):
pass
finally:
recv_task.cancel()
await ws.close()
print('Disconnected.')
def print_viseme_list(payload: bytes):
if len(payload) < 1:
print(' <- VLST: (empty)')
return
count = payload[0]
print(f' <- VLST: {count} visemes')
pos = 1
for _ in range(count):
if pos + 4 >= len(payload):
break
vid = payload[pos]
label = payload[pos+1:pos+4].decode('ascii', errors='replace')
motor_count = payload[pos+4]
pos += 5
motors = []
for _ in range(motor_count):
if pos + 3 > len(payload):
break
mid = payload[pos]
mpos = struct.unpack('<H', payload[pos+1:pos+3])[0]
motors.append(f'{mid}={mpos}')
pos += 3
print(f' [{vid:2d}] "{label}" motors: {", ".join(motors) if motors else "(none)"}')
# ---------------------------------------------------------------------------
# Cross-platform single key input
# ---------------------------------------------------------------------------
def get_key() -> str | None:
"""Block until a single key is pressed, return it lowercase."""
try:
import msvcrt # Windows
ch = msvcrt.getwch()
return ch.lower()
except ImportError:
pass
# Unix / macOS
import tty, termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
return ch.lower()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print('\nBye.')

View File

@ -1,6 +1,7 @@
#include "websocket_client.h" #include "websocket_client.h"
#include "sensors.h" #include "sensors.h"
#include "protocol.h" #include "protocol.h"
#include "commands.h"
#include <ArduinoWebsockets.h> #include <ArduinoWebsockets.h>
#include <WiFi.h> #include <WiFi.h>
@ -8,6 +9,10 @@ using namespace websockets;
WiFiSettings wifiSettings; // Global runtime instance WiFiSettings wifiSettings; // Global runtime instance
// ============================================================================
// Outbound client (connects to Radxa for FACE/ALIV)
// ============================================================================
static WebsocketsClient client; static WebsocketsClient client;
static bool s_connected = false; static bool s_connected = false;
static unsigned long lastReconnectAttempt = 0; static unsigned long lastReconnectAttempt = 0;
@ -18,70 +23,48 @@ static String buildWsUrl() {
} }
// ============================================================================ // ============================================================================
// Packet parsing for WebSocket binary messages // Inbound server (accepts commands from computer)
// Uses the same protocol format: 0xA5 0x5A TAG(4) LEN(2) SEQ(2) PAYLOAD(N) CRC(2)
// ============================================================================ // ============================================================================
static void processPacketPayload(const char tag[4], const uint8_t* payload, uint16_t len) { static WebsocketsServer* server = nullptr;
if (memcmp(tag, Tag::FACE, 4) == 0) { constexpr uint8_t MAX_SERVER_CLIENTS = 2;
// Face detection data - feed to FaceDetect sensor static WebsocketsClient serverClients[MAX_SERVER_CLIENTS];
// SensorManager will send the FACE packet over serial static bool serverClientActive[MAX_SERVER_CLIENTS] = {false};
faceDetect.feedPayload(payload, len);
}
else if (memcmp(tag, Tag::ALIV, 4) == 0) {
// ALIV payload: [component_id:1][alive:1]
if (len >= 2) {
uint8_t componentId = payload[0];
uint8_t alive = payload[1];
// Component 3 = Face detection (Radxa) // ============================================================================
if (componentId == 3) { // Shared protocol packet parser
faceDetect.setAlive(alive != 0); // ============================================================================
}
}
// Forward ALIV as a proper protocol packet over serial
sendPacket(Tag::ALIV, payload, len);
}
}
static void parseProtocolMessage(const uint8_t* data, size_t len) { // Parse protocol-framed binary data, call handler for each valid packet
// Walk through the message looking for protocol packets static void parseProtocolPackets(const uint8_t* data, size_t len,
// Format: SYNC0(0xA5) SYNC1(0x5A) TAG(4) LEN(2) SEQ(2) PAYLOAD(N) CRC(2) void (*handler)(const char tag[4], const uint8_t* payload, uint16_t payloadLen))
{
size_t pos = 0; size_t pos = 0;
while (pos + 12 <= len) { // Minimum packet: 2 sync + 4 tag + 2 len + 2 seq + 0 payload + 2 crc = 12 while (pos + 12 <= len) {
// Look for sync bytes
if (data[pos] != 0xA5 || data[pos + 1] != 0x5A) { if (data[pos] != 0xA5 || data[pos + 1] != 0x5A) {
pos++; pos++;
continue; continue;
} }
// Read tag
char tag[4]; char tag[4];
memcpy(tag, &data[pos + 2], 4); memcpy(tag, &data[pos + 2], 4);
// Read length (LE)
uint16_t payloadLen = data[pos + 6] | (data[pos + 7] << 8); uint16_t payloadLen = data[pos + 6] | (data[pos + 7] << 8);
// Sanity check size_t totalPacketLen = 2 + 4 + 2 + 2 + payloadLen + 2;
size_t totalPacketLen = 2 + 4 + 2 + 2 + payloadLen + 2; // sync + tag + len + seq + payload + crc if (pos + totalPacketLen > len) break;
if (pos + totalPacketLen > len) {
// Incomplete packet
break;
}
// Payload starts after sync(2) + tag(4) + len(2) + seq(2) = offset 10
const uint8_t* payload = &data[pos + 10]; const uint8_t* payload = &data[pos + 10];
// Verify CRC over tag + len + seq + payload // Verify CRC
const uint8_t* crcData = &data[pos + 2]; // starts at tag const uint8_t* crcData = &data[pos + 2];
uint16_t crcDataLen = 4 + 2 + 2 + payloadLen; // tag + len + seq + payload uint16_t crcDataLen = 4 + 2 + 2 + payloadLen;
uint16_t computed = crc16Compute(crcData, crcDataLen); uint16_t computed = crc16Compute(crcData, crcDataLen);
uint16_t received = data[pos + 10 + payloadLen] | (data[pos + 10 + payloadLen + 1] << 8); uint16_t received = data[pos + 10 + payloadLen] | (data[pos + 10 + payloadLen + 1] << 8);
if (computed == received) { if (computed == received) {
processPacketPayload(tag, payload, payloadLen); handler(tag, payload, payloadLen);
} }
pos += totalPacketLen; pos += totalPacketLen;
@ -89,46 +72,84 @@ static void parseProtocolMessage(const uint8_t* data, size_t len) {
} }
// ============================================================================ // ============================================================================
// WebSocket callbacks // Client packet handler (FACE/ALIV from Radxa)
// ============================================================================ // ============================================================================
static void onMessage(WebsocketsMessage message) { static void handleClientPacket(const char tag[4], const uint8_t* payload, uint16_t len) {
std::string raw = message.rawData(); if (memcmp(tag, Tag::FACE, 4) == 0) {
size_t len = raw.size(); faceDetect.feedPayload(payload, len);
if (len == 0) return; }
else if (memcmp(tag, Tag::ALIV, 4) == 0) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(raw.data()); if (len >= 2) {
parseProtocolMessage(data, len); uint8_t componentId = payload[0];
uint8_t alive = payload[1];
if (componentId == 3) {
faceDetect.setAlive(alive != 0);
}
}
sendPacket(Tag::ALIV, payload, len);
}
} }
static void onEvent(WebsocketsEvent event, String data) { // ============================================================================
// Server packet handler (any command from computer)
// ============================================================================
static void handleServerPacket(const char tag[4], const uint8_t* payload, uint16_t len) {
// Route to the standard command dispatcher - handles VSME, MSET, BHVR, SSET, etc.
dispatchCommand(tag, payload, len);
}
// ============================================================================
// Client callbacks
// ============================================================================
static void onClientMessage(WebsocketsMessage message) {
std::string raw = message.rawData();
if (raw.empty()) return;
parseProtocolPackets(reinterpret_cast<const uint8_t*>(raw.data()), raw.size(), handleClientPacket);
}
static void onClientEvent(WebsocketsEvent event, String data) {
switch (event) { switch (event) {
case WebsocketsEvent::ConnectionOpened: case WebsocketsEvent::ConnectionOpened:
s_connected = true; s_connected = true;
Serial.println("[WebSocket] Connected"); Serial.println("[WS Client] Connected");
break; break;
case WebsocketsEvent::ConnectionClosed: case WebsocketsEvent::ConnectionClosed:
s_connected = false; s_connected = false;
Serial.println("[WebSocket] Disconnected"); Serial.println("[WS Client] Disconnected");
break;
case WebsocketsEvent::GotPing:
break;
case WebsocketsEvent::GotPong:
break;
default:
break; break;
default: break;
} }
} }
// ============================================================================
// Server client callbacks (created per-client via lambda in loop)
// ============================================================================
static void onServerMessage(WebsocketsMessage message) {
std::string raw = message.rawData();
if (raw.empty()) return;
parseProtocolPackets(reinterpret_cast<const uint8_t*>(raw.data()), raw.size(), handleServerPacket);
}
static void onServerEvent(WebsocketsEvent event, String data) {
// Connection events are handled in the accept logic
}
// ============================================================================ // ============================================================================
// Public API // Public API
// ============================================================================ // ============================================================================
void websocketSetup() { void websocketSetup() {
Serial.print("[WiFi] SSID: '");
Serial.print(wifiSettings.ssid);
Serial.println("'");
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(wifiSettings.ssid, wifiSettings.password); WiFi.begin(wifiSettings.ssid, wifiSettings.password);
Serial.print("[WebSocket] WiFi connecting to "); Serial.print("[WiFi] Connecting");
Serial.print(wifiSettings.ssid);
int attempts = 0; int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) { while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500); delay(500);
@ -137,22 +158,33 @@ void websocketSetup() {
} }
Serial.println(); Serial.println();
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WebSocket] WiFi failed"); Serial.println("[WiFi] Failed to connect");
return; return;
} }
Serial.print("[WebSocket] WiFi OK "); Serial.print("[WiFi] OK ");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
client.onMessage(onMessage); // --- Start outbound client (to Radxa) ---
client.onEvent(onEvent); client.onMessage(onClientMessage);
client.onEvent(onClientEvent);
String url = buildWsUrl(); String url = buildWsUrl();
Serial.println("[WebSocket] Connecting to " + url); Serial.println("[WS Client] Connecting to " + url);
if (client.connect(url)) { if (client.connect(url)) {
s_connected = true; s_connected = true;
Serial.println("[WebSocket] Connected"); Serial.println("[WS Client] Connected");
} else { } else {
Serial.println("[WebSocket] Connect failed (will retry in loop)"); Serial.println("[WS Client] Connect failed (will retry)");
}
// --- Start inbound server (allocated after WiFi to avoid early init issues) ---
server = new WebsocketsServer();
server->listen(WS_SERVER_PORT);
if (server->available()) {
Serial.print("[WS Server] Listening on port ");
Serial.println(WS_SERVER_PORT);
} else {
Serial.println("[WS Server] Failed to start");
} }
} }
@ -162,6 +194,7 @@ void websocketLoop() {
return; return;
} }
// --- Outbound client ---
if (!s_connected && !client.available()) { if (!s_connected && !client.available()) {
unsigned long now = millis(); unsigned long now = millis();
if (now - lastReconnectAttempt >= RECONNECT_INTERVAL) { if (now - lastReconnectAttempt >= RECONNECT_INTERVAL) {
@ -169,7 +202,7 @@ void websocketLoop() {
String url = buildWsUrl(); String url = buildWsUrl();
if (client.connect(url)) { if (client.connect(url)) {
s_connected = true; s_connected = true;
Serial.println("[WebSocket] Reconnected"); Serial.println("[WS Client] Reconnected");
} }
} }
} }
@ -177,6 +210,46 @@ void websocketLoop() {
if (client.available()) { if (client.available()) {
client.poll(); client.poll();
} }
// --- Inbound server: accept new connections ---
if (server && server->available() && server->poll()) {
WebsocketsClient newClient = server->accept();
if (newClient.available()) {
// Find an empty slot
bool accepted = false;
for (uint8_t i = 0; i < MAX_SERVER_CLIENTS; i++) {
if (!serverClientActive[i] || !serverClients[i].available()) {
serverClients[i] = std::move(newClient);
serverClients[i].onMessage(onServerMessage);
serverClients[i].onEvent(onServerEvent);
serverClientActive[i] = true;
Serial.print("[WS Server] Client connected (slot ");
Serial.print(i);
Serial.println(")");
accepted = true;
break;
}
}
if (!accepted) {
Serial.println("[WS Server] Max clients reached, rejecting");
newClient.close();
}
}
}
// --- Inbound server: poll existing clients ---
for (uint8_t i = 0; i < MAX_SERVER_CLIENTS; i++) {
if (serverClientActive[i]) {
if (serverClients[i].available()) {
serverClients[i].poll();
} else {
serverClientActive[i] = false;
Serial.print("[WS Server] Client disconnected (slot ");
Serial.print(i);
Serial.println(")");
}
}
}
} }
bool websocketConnected() { bool websocketConnected() {
@ -184,15 +257,21 @@ bool websocketConnected() {
} }
void websocketReconnect() { void websocketReconnect() {
// Close existing connection and force reconnect with new settings
client.close(); client.close();
s_connected = false; s_connected = false;
// Reconnect WiFi if SSID changed
WiFi.disconnect(); WiFi.disconnect();
WiFi.begin(wifiSettings.ssid, wifiSettings.password); WiFi.begin(wifiSettings.ssid, wifiSettings.password);
Serial.print("[WebSocket] Reconnecting WiFi to "); Serial.print("[WiFi] Reconnecting to ");
Serial.println(wifiSettings.ssid); Serial.println(wifiSettings.ssid);
lastReconnectAttempt = 0; // Force immediate reconnect attempt in loop lastReconnectAttempt = 0;
}
uint8_t websocketServerClientCount() {
uint8_t count = 0;
for (uint8_t i = 0; i < MAX_SERVER_CLIENTS; i++) {
if (serverClientActive[i] && serverClients[i].available()) count++;
}
return count;
} }

View File

@ -1,8 +1,9 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
// WebSocket client: connect to remote device, receive bytes (e.g. FACE packets). // WebSocket client + server
// Extension to the serial protocol - same packet concepts over WebSocket. // Client: connects out to Radxa to receive FACE/ALIV packets
// Server: listens for incoming connections to accept any protocol command (VSME, MSET, etc.)
// Runtime-configurable WiFi + WebSocket settings (persisted to NVM) // Runtime-configurable WiFi + WebSocket settings (persisted to NVM)
struct WiFiSettings { struct WiFiSettings {
@ -15,14 +16,20 @@ struct WiFiSettings {
extern WiFiSettings wifiSettings; extern WiFiSettings wifiSettings;
// WebSocket server port (for incoming commands from computer)
constexpr uint16_t WS_SERVER_PORT = 81;
// Call once from setup() after Serial is ready // Call once from setup() after Serial is ready
void websocketSetup(); void websocketSetup();
// Call every loop() - handles connect, reconnect, and incoming messages // Call every loop() - handles client + server
void websocketLoop(); void websocketLoop();
// True when connected and ready to send/receive // True when outbound client is connected
bool websocketConnected(); bool websocketConnected();
// Force reconnect (call after settings change) // Force client reconnect (call after settings change)
void websocketReconnect(); void websocketReconnect();
// Number of server clients currently connected
uint8_t websocketServerClientCount();