113 lines
4.5 KiB
C
113 lines
4.5 KiB
C
#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";
|
|
}
|
|
} |