chore: checkpoint app work before firmware sync
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
#include "voice_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* NOTE: Do not use ctype.h (isalnum/isalpha/isdigit/isspace) in this app. Those
|
||||
* functions read the `_ctype_` table, which is resolved from the flashed firmware
|
||||
* at runtime; the firmware's table does not behave correctly for side-loaded ELF
|
||||
* apps, so isalnum('a') can return false. Use explicit ASCII range checks instead. */
|
||||
|
||||
static bool is_digit(unsigned char c) { return c >= '0' && c <= '9'; }
|
||||
|
||||
static bool is_space(unsigned char c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
|
||||
}
|
||||
|
||||
static bool is_alnum(unsigned char c) {
|
||||
return is_digit(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
}
|
||||
|
||||
static bool copy_part(char* destination, size_t destination_size, const char* start, size_t length) {
|
||||
if (length == 0 || length >= destination_size) return false;
|
||||
memcpy(destination, start, length);
|
||||
destination[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_loopback(const char* host) {
|
||||
return strcmp(host, "localhost") == 0 || strcmp(host, "::1") == 0 || strncmp(host, "127.", 4) == 0;
|
||||
}
|
||||
|
||||
static bool valid_identifier(const char* value) {
|
||||
if (value == NULL || *value == '\0') return false;
|
||||
for (const unsigned char* p = (const unsigned char*)value; *p; ++p) {
|
||||
if (!is_alnum(*p) && *p != '-' && *p != '_' && *p != '.') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pv_parse_endpoint(const char* url, PvEndpoint* endpoint) {
|
||||
if (url == NULL || endpoint == NULL || strncmp(url, "ws://", 5) != 0) return false;
|
||||
const char* authority = url + 5;
|
||||
const char* path = strchr(authority, '/');
|
||||
const char* authority_end = path ? path : authority + strlen(authority);
|
||||
const char* colon = NULL;
|
||||
for (const char* p = authority; p < authority_end; ++p) {
|
||||
if (*p == ':') {
|
||||
if (colon != NULL) return false;
|
||||
colon = p;
|
||||
}
|
||||
if (is_space((unsigned char)*p) || *p == '@' || *p == '?' || *p == '#') return false;
|
||||
}
|
||||
size_t host_length = (size_t)((colon ? colon : authority_end) - authority);
|
||||
if (!copy_part(endpoint->host, sizeof(endpoint->host), authority, host_length) || is_loopback(endpoint->host)) return false;
|
||||
endpoint->port = 80;
|
||||
if (colon != NULL) {
|
||||
unsigned long port = 0;
|
||||
for (const char* p = colon + 1; p < authority_end; ++p) {
|
||||
if (!is_digit((unsigned char)*p)) return false;
|
||||
port = port * 10U + (unsigned long)(*p - '0');
|
||||
if (port > 65535U) return false;
|
||||
}
|
||||
if (port == 0) return false;
|
||||
endpoint->port = (uint16_t)port;
|
||||
}
|
||||
return path == NULL ? copy_part(endpoint->path, sizeof(endpoint->path), "/", 1)
|
||||
: copy_part(endpoint->path, sizeof(endpoint->path), path, strlen(path));
|
||||
}
|
||||
|
||||
bool pv_make_start_json(char* out, size_t out_size, const char* session_id, const char* device_id) {
|
||||
if (out == NULL || !valid_identifier(session_id) || !valid_identifier(device_id)) return false;
|
||||
int written = snprintf(out, out_size,
|
||||
"{\"v\":1,\"event\":\"start\",\"session_id\":\"%s\",\"device_id\":\"%s\",\"audio\":{\"format\":\"pcm_s16le\",\"sample_rate\":16000,\"channels\":1,\"sample_width\":2}}",
|
||||
session_id, device_id);
|
||||
return written > 0 && (size_t)written < out_size;
|
||||
}
|
||||
|
||||
bool pv_valid_pcm_chunk(size_t bytes) {
|
||||
return bytes > 0 && bytes <= PV_PCM_CHUNK_MAX && (bytes % 2U) == 0;
|
||||
}
|
||||
|
||||
bool pv_valid_downstream_audio(const char* format, int sample_rate, int channels, int sample_width, size_t byte_length) {
|
||||
return format != NULL && strcmp(format, "pcm_s16le") == 0 && sample_rate > 0 && sample_rate <= 48000 &&
|
||||
channels == 1 && sample_width == 2 && byte_length > 0 && byte_length <= PV_DOWNSTREAM_MAX &&
|
||||
(byte_length % 2U) == 0;
|
||||
}
|
||||
|
||||
bool pv_binary_matches_metadata(size_t expected_bytes, size_t received_bytes) {
|
||||
return expected_bytes > 0 && expected_bytes == received_bytes;
|
||||
}
|
||||
|
||||
uint32_t pv_retry_delay_seconds(unsigned attempt) {
|
||||
uint32_t delay = 1;
|
||||
while (attempt > 0 && delay < 30) {
|
||||
delay *= 2;
|
||||
--attempt;
|
||||
}
|
||||
return delay > 30 ? 30 : delay;
|
||||
}
|
||||
|
||||
PvState pv_disconnect_state(bool endpoint_valid) {
|
||||
return endpoint_valid ? PV_RECONNECTING : PV_FAILED;
|
||||
}
|
||||
|
||||
const char* pv_state_label(PvState state) {
|
||||
switch (state) {
|
||||
case PV_CONNECTING: return "CONNECTING";
|
||||
case PV_STREAMING: return "STREAMING";
|
||||
case PV_RECONNECTING: return "RECONNECTING";
|
||||
case PV_FAILED: return "FAILED";
|
||||
default: return "FAILED";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define PV_PROTOCOL_VERSION 1
|
||||
#define PV_PCM_CHUNK_MAX 16384U
|
||||
#define PV_DOWNSTREAM_MAX 65536U
|
||||
|
||||
typedef enum {
|
||||
PV_CONNECTING,
|
||||
PV_STREAMING,
|
||||
PV_RECONNECTING,
|
||||
PV_FAILED,
|
||||
} PvState;
|
||||
|
||||
typedef struct {
|
||||
char host[64];
|
||||
char path[96];
|
||||
uint16_t port;
|
||||
} PvEndpoint;
|
||||
|
||||
bool pv_parse_endpoint(const char* url, PvEndpoint* endpoint);
|
||||
bool pv_make_start_json(char* out, size_t out_size, const char* session_id, const char* device_id);
|
||||
bool pv_valid_pcm_chunk(size_t bytes);
|
||||
bool pv_valid_downstream_audio(const char* format, int sample_rate, int channels, int sample_width, size_t byte_length);
|
||||
bool pv_binary_matches_metadata(size_t expected_bytes, size_t received_bytes);
|
||||
uint32_t pv_retry_delay_seconds(unsigned attempt);
|
||||
PvState pv_disconnect_state(bool endpoint_valid);
|
||||
const char* pv_state_label(PvState state);
|
||||
@@ -2,239 +2,166 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/inet.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_random.h>
|
||||
#include <lwip/inet.h>
|
||||
#include <lwip/sockets.h>
|
||||
|
||||
static int recv_all(int fd, void* buf, size_t len) {
|
||||
size_t total = 0;
|
||||
char* p = (char*)buf;
|
||||
while (total < len) {
|
||||
int r = lwip_recv(fd, p + total, len - total, 0);
|
||||
if (r <= 0) {
|
||||
return -1;
|
||||
}
|
||||
total += r;
|
||||
#define TAG "PipecatVoiceWs"
|
||||
#define WS_HEADER_LIMIT 1024U
|
||||
#define WS_CONTROL_LIMIT 125U
|
||||
|
||||
static int send_all(int fd, const uint8_t* data, size_t length) {
|
||||
size_t sent = 0;
|
||||
while (sent < length) {
|
||||
int result = lwip_send(fd, data + sent, length - sent, 0);
|
||||
if (result <= 0) return -1;
|
||||
sent += (size_t)result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint16_t my_htons(uint16_t val) {
|
||||
return (uint16_t)(((val & 0xff) << 8) | ((val & 0xff00) >> 8));
|
||||
static int recv_all(int fd, uint8_t* data, size_t length) {
|
||||
size_t received = 0;
|
||||
while (received < length) {
|
||||
int result = lwip_recv(fd, data + received, length - received, 0);
|
||||
if (result <= 0) return -1;
|
||||
received += (size_t)result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ws_connect(const char* host, int port, const char* path, const char* device_id, const char* auth_key) {
|
||||
static int discard(int fd, uint64_t length) {
|
||||
uint8_t buffer[256];
|
||||
while (length > 0) {
|
||||
size_t chunk = length > sizeof(buffer) ? sizeof(buffer) : (size_t)length;
|
||||
if (recv_all(fd, buffer, chunk) < 0) return -1;
|
||||
length -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_frame(int fd, uint8_t opcode, const uint8_t* payload, size_t length) {
|
||||
if (length > 65535U || ((opcode & 0x08U) && length > WS_CONTROL_LIMIT)) return -1;
|
||||
uint8_t header[8];
|
||||
size_t header_length = 2;
|
||||
header[0] = 0x80U | opcode;
|
||||
if (length < 126U) {
|
||||
header[1] = 0x80U | (uint8_t)length;
|
||||
} else {
|
||||
header[1] = 0x80U | 126U;
|
||||
header[2] = (uint8_t)(length >> 8U);
|
||||
header[3] = (uint8_t)length;
|
||||
header_length = 4;
|
||||
}
|
||||
uint8_t mask[4];
|
||||
uint32_t random = esp_random();
|
||||
memcpy(mask, &random, sizeof(mask));
|
||||
memcpy(header + header_length, mask, sizeof(mask));
|
||||
header_length += sizeof(mask);
|
||||
if (send_all(fd, header, header_length) < 0) return -1;
|
||||
|
||||
uint8_t chunk[512];
|
||||
size_t offset = 0;
|
||||
while (offset < length) {
|
||||
size_t count = length - offset > sizeof(chunk) ? sizeof(chunk) : length - offset;
|
||||
for (size_t i = 0; i < count; ++i) chunk[i] = payload[offset + i] ^ mask[(offset + i) % sizeof(mask)];
|
||||
if (send_all(fd, chunk, count) < 0) return -1;
|
||||
offset += count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ws_connect(const char* host, int port, const char* path, const char* device_id, const char* api_key) {
|
||||
if (host == NULL || path == NULL || device_id == NULL || api_key == NULL || port < 1 || port > 65535) return -1;
|
||||
int fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = my_htons(port);
|
||||
addr.sin_addr.s_addr = ipaddr_addr(host);
|
||||
|
||||
if (lwip_connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "socket create failed");
|
||||
return -1;
|
||||
}
|
||||
struct sockaddr_in address = {0};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons((uint16_t)port);
|
||||
address.sin_addr.s_addr = ipaddr_addr(host);
|
||||
if (address.sin_addr.s_addr == IPADDR_NONE) {
|
||||
ESP_LOGW(TAG, "endpoint address parse failed");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set socket receive timeout (e.g. 90 seconds) to prevent blocking indefinitely
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 90;
|
||||
tv.tv_usec = 0;
|
||||
lwip_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
// Send HTTP upgrade handshake request
|
||||
char req[1024];
|
||||
snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"Authorization: Bearer %s\r\n"
|
||||
"X-Device-ID: %s\r\n"
|
||||
"\r\n",
|
||||
path, host, port, auth_key, device_id);
|
||||
|
||||
if (lwip_send(fd, req, strlen(req), 0) < 0) {
|
||||
if (lwip_connect(fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
|
||||
ESP_LOGW(TAG, "TCP connect failed");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read HTTP response headers until we hit "\r\n\r\n"
|
||||
char header_buf[1024];
|
||||
size_t header_len = 0;
|
||||
while (header_len < sizeof(header_buf) - 1) {
|
||||
char c;
|
||||
int r = lwip_recv(fd, &c, 1, 0);
|
||||
if (r <= 0) {
|
||||
struct timeval timeout = {.tv_sec = 15, .tv_usec = 0};
|
||||
lwip_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
char request[WS_HEADER_LIMIT];
|
||||
int request_length = snprintf(request, sizeof(request),
|
||||
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: MDEyMzQ1Njc4OWFiY2RlZg==\r\nSec-WebSocket-Version: 13\r\n"
|
||||
"Authorization: Bearer %s\r\nX-Device-ID: %s\r\n\r\n",
|
||||
path, host, port, api_key, device_id);
|
||||
if (request_length < 0 || (size_t)request_length >= sizeof(request) || send_all(fd, (const uint8_t*)request, (size_t)request_length) < 0) {
|
||||
ESP_LOGW(TAG, "WebSocket upgrade request failed");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
char response[WS_HEADER_LIMIT];
|
||||
size_t length = 0;
|
||||
while (length + 1 < sizeof(response)) {
|
||||
if (recv_all(fd, (uint8_t*)&response[length], 1) < 0) {
|
||||
ESP_LOGW(TAG, "WebSocket upgrade response failed");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
header_buf[header_len++] = c;
|
||||
header_buf[header_len] = '\0';
|
||||
|
||||
if (header_len >= 4 && strcmp(header_buf + header_len - 4, "\r\n\r\n") == 0) {
|
||||
break;
|
||||
}
|
||||
response[++length] = '\0';
|
||||
if (length >= 4 && memcmp(response + length - 4, "\r\n\r\n", 4) == 0) break;
|
||||
}
|
||||
|
||||
// Verify HTTP 101 Switching Protocols response status
|
||||
if (strstr(header_buf, "HTTP/1.1 101") == NULL && strstr(header_buf, "HTTP/1.0 101") == NULL) {
|
||||
if (length + 1 >= sizeof(response) || strstr(response, " 101 ") == NULL) {
|
||||
ESP_LOGW(TAG, "WebSocket upgrade rejected");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "WebSocket upgrade accepted");
|
||||
return fd;
|
||||
}
|
||||
|
||||
int ws_send(int fd, const uint8_t* data, size_t len, bool binary) {
|
||||
uint8_t header[10];
|
||||
size_t header_len = 0;
|
||||
|
||||
header[0] = binary ? 0x82 : 0x81;
|
||||
|
||||
if (len < 126) {
|
||||
header[1] = 0x80 | (uint8_t)len;
|
||||
header_len = 2;
|
||||
} else {
|
||||
header[1] = 0x80 | 126;
|
||||
header[2] = (uint8_t)((len >> 8) & 0xFF);
|
||||
header[3] = (uint8_t)(len & 0xFF);
|
||||
header_len = 4;
|
||||
}
|
||||
|
||||
// Use fixed client mask for performance: 0x12, 0x34, 0x56, 0x78
|
||||
uint8_t mask[4] = { 0x12, 0x34, 0x56, 0x78 };
|
||||
memcpy(header + header_len, mask, 4);
|
||||
header_len += 4;
|
||||
|
||||
// Send WebSocket frame header
|
||||
int sent = lwip_send(fd, header, header_len, 0);
|
||||
if (sent < 0) return -1;
|
||||
|
||||
// Mask the payload
|
||||
uint8_t* masked = malloc(len);
|
||||
if (masked == NULL) return -1;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
masked[i] = data[i] ^ mask[i % 4];
|
||||
}
|
||||
|
||||
// Send masked payload
|
||||
sent = lwip_send(fd, masked, len, 0);
|
||||
free(masked);
|
||||
|
||||
return sent >= 0 ? 0 : -1;
|
||||
int ws_send(int fd, const uint8_t* data, size_t length, bool binary) {
|
||||
if (fd < 0 || data == NULL || length == 0) return -1;
|
||||
return send_frame(fd, binary ? 0x02U : 0x01U, data, length);
|
||||
}
|
||||
|
||||
int ws_recv(int fd, int* out_opcode, uint8_t* payload, size_t max_len) {
|
||||
int ws_recv(int fd, int* opcode, bool* final, uint8_t* payload, size_t maximum) {
|
||||
uint8_t header[2];
|
||||
if (recv_all(fd, header, 2) < 0) {
|
||||
return -1;
|
||||
if (fd < 0 || recv_all(fd, header, sizeof(header)) < 0) return -1;
|
||||
uint64_t length = header[1] & 0x7fU;
|
||||
if (length == 126U) {
|
||||
uint8_t extended[2];
|
||||
if (recv_all(fd, extended, sizeof(extended)) < 0) return -1;
|
||||
length = ((uint64_t)extended[0] << 8U) | extended[1];
|
||||
} else if (length == 127U) {
|
||||
uint8_t extended[8];
|
||||
if (recv_all(fd, extended, sizeof(extended)) < 0) return -1;
|
||||
length = 0;
|
||||
for (size_t i = 0; i < sizeof(extended); ++i) length = (length << 8U) | extended[i];
|
||||
}
|
||||
|
||||
int opcode = header[0] & 0x0F;
|
||||
if (out_opcode != NULL) {
|
||||
*out_opcode = opcode;
|
||||
bool masked = (header[1] & 0x80U) != 0;
|
||||
uint8_t mask[4] = {0};
|
||||
if (masked && recv_all(fd, mask, sizeof(mask)) < 0) return -1;
|
||||
uint8_t frame_opcode = header[0] & 0x0fU;
|
||||
if (((frame_opcode & 0x08U) && (length > WS_CONTROL_LIMIT || !(header[0] & 0x80U))) || length > maximum) {
|
||||
if (discard(fd, length) < 0) return -1;
|
||||
return -2;
|
||||
}
|
||||
|
||||
int masked = (header[1] & 0x80) != 0;
|
||||
size_t len = header[1] & 0x7F;
|
||||
|
||||
if (len == 126) {
|
||||
uint8_t ext_len[2];
|
||||
if (recv_all(fd, ext_len, 2) < 0) return -1;
|
||||
len = ((size_t)ext_len[0] << 8) | ext_len[1];
|
||||
} else if (len == 127) {
|
||||
uint8_t ext_len[8];
|
||||
if (recv_all(fd, ext_len, 8) < 0) return -1;
|
||||
// Parse 64-bit length into size_t
|
||||
len = ((size_t)ext_len[4] << 24) | ((size_t)ext_len[5] << 16) | ((size_t)ext_len[6] << 8) | ext_len[7];
|
||||
}
|
||||
|
||||
if (masked) {
|
||||
uint8_t mask[4];
|
||||
if (recv_all(fd, mask, 4) < 0) return -1;
|
||||
|
||||
if (len > max_len) {
|
||||
ESP_LOGE("websocket", "ws_recv overflow (masked): len=%u, max_len=%u", (unsigned)len, (unsigned)max_len);
|
||||
// Buffer overflow, skip payload to align stream
|
||||
size_t to_discard = len;
|
||||
uint8_t discard_buf[256];
|
||||
while (to_discard > 0) {
|
||||
size_t chunk = to_discard < sizeof(discard_buf) ? to_discard : sizeof(discard_buf);
|
||||
if (recv_all(fd, discard_buf, chunk) < 0) return -1;
|
||||
to_discard -= chunk;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (recv_all(fd, payload, len) < 0) return -1;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
payload[i] ^= mask[i % 4];
|
||||
}
|
||||
} else {
|
||||
if (len > max_len) {
|
||||
ESP_LOGE("websocket", "ws_recv overflow (unmasked): len=%u, max_len=%u", (unsigned)len, (unsigned)max_len);
|
||||
size_t to_discard = len;
|
||||
uint8_t discard_buf[256];
|
||||
while (to_discard > 0) {
|
||||
size_t chunk = to_discard < sizeof(discard_buf) ? to_discard : sizeof(discard_buf);
|
||||
if (recv_all(fd, discard_buf, chunk) < 0) return -1;
|
||||
to_discard -= chunk;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (recv_all(fd, payload, len) < 0) return -1;
|
||||
}
|
||||
|
||||
return (int)len;
|
||||
if (length > 0 && recv_all(fd, payload, (size_t)length) < 0) return -1;
|
||||
if (masked) for (size_t i = 0; i < (size_t)length; ++i) payload[i] ^= mask[i % sizeof(mask)];
|
||||
if (opcode) *opcode = frame_opcode;
|
||||
if (final) *final = (header[0] & 0x80U) != 0;
|
||||
return (int)length;
|
||||
}
|
||||
|
||||
void ws_close(int fd) {
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
int ws_send_pong(int fd, const uint8_t* payload, size_t len) {
|
||||
uint8_t header[10];
|
||||
size_t header_len = 0;
|
||||
|
||||
header[0] = 0x8A; // FIN | PONG (0x0A)
|
||||
|
||||
if (len < 126) {
|
||||
header[1] = 0x80 | (uint8_t)len;
|
||||
header_len = 2;
|
||||
} else {
|
||||
header[1] = 0x80 | 126;
|
||||
header[2] = (uint8_t)((len >> 8) & 0xFF);
|
||||
header[3] = (uint8_t)(len & 0xFF);
|
||||
header_len = 4;
|
||||
}
|
||||
|
||||
uint8_t mask[4] = { 0x12, 0x34, 0x56, 0x78 };
|
||||
memcpy(header + header_len, mask, 4);
|
||||
header_len += 4;
|
||||
|
||||
int sent = lwip_send(fd, header, header_len, 0);
|
||||
if (sent < 0) return -1;
|
||||
|
||||
if (len > 0 && payload != NULL) {
|
||||
uint8_t* masked = malloc(len);
|
||||
if (masked == NULL) return -1;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
masked[i] = payload[i] ^ mask[i % 4];
|
||||
}
|
||||
sent = lwip_send(fd, masked, len, 0);
|
||||
free(masked);
|
||||
}
|
||||
|
||||
return sent >= 0 ? 0 : -1;
|
||||
}
|
||||
int ws_send_pong(int fd, const uint8_t* payload, size_t length) { return send_frame(fd, 0x0aU, payload, length); }
|
||||
int ws_send_close(int fd) { return send_frame(fd, 0x08U, NULL, 0); }
|
||||
void ws_close(int fd) { if (fd >= 0) close(fd); }
|
||||
@@ -14,10 +14,10 @@ extern "C" {
|
||||
* @param port Port number (e.g. 8642)
|
||||
* @param path WebSocket path (e.g. "/api/esp32/voice/ws")
|
||||
* @param device_id Unique device identifier
|
||||
* @param auth_key Hermes Bearer API key
|
||||
* @param api_key Optional profile API key; never compiled into firmware
|
||||
* @return Socket file descriptor on success, or -1 on failure
|
||||
*/
|
||||
int ws_connect(const char* host, int port, const char* path, const char* device_id, const char* auth_key);
|
||||
int ws_connect(const char* host, int port, const char* path, const char* device_id, const char* api_key);
|
||||
|
||||
/**
|
||||
* Send a WebSocket frame.
|
||||
@@ -37,7 +37,7 @@ int ws_send(int fd, const uint8_t* data, size_t len, bool binary);
|
||||
* @param max_len Maximum length of the payload buffer
|
||||
* @return Received payload length on success, -1 on connection failure, or -2 on buffer overflow
|
||||
*/
|
||||
int ws_recv(int fd, int* out_opcode, uint8_t* payload, size_t max_len);
|
||||
int ws_recv(int fd, int* out_opcode, bool* out_final, uint8_t* payload, size_t max_len);
|
||||
|
||||
/**
|
||||
* Close a WebSocket connection.
|
||||
@@ -54,6 +54,9 @@ void ws_close(int fd);
|
||||
*/
|
||||
int ws_send_pong(int fd, const uint8_t* payload, size_t len);
|
||||
|
||||
/** Send a clean WebSocket close control frame before closing the socket. */
|
||||
int ws_send_close(int fd);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user