feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
#include "platform_client.h"
|
||||
#include "ble_cloud.h"
|
||||
#include "call_client.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
@@ -21,7 +24,7 @@
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define PLATFORM_API_ORIGIN "https://121.41.46.238"
|
||||
#define PLATFORM_RESPONSE_MAX (32 * 1024)
|
||||
#define PLATFORM_RESPONSE_MAX (40 * 1024)
|
||||
#define PLATFORM_FIRMWARE_VERSION "1.2.2-echoear-companion.3"
|
||||
#define PLATFORM_HARDWARE_AGENT_SDK_VERSION "2.0.0"
|
||||
|
||||
@@ -53,6 +56,15 @@ static SemaphoreHandle_t s_http_mutex;
|
||||
static unsigned s_http_connection_count;
|
||||
static uint32_t s_heartbeat_interval_ms = 15000;
|
||||
static TickType_t s_heartbeat_due;
|
||||
static atomic_uint s_local_lease_deadline;
|
||||
static atomic_bool s_local_lease_active;
|
||||
|
||||
bool platform_conversation_audio_authorized(void)
|
||||
{
|
||||
if (ble_cloud_configured()) return call_client_audio_allowed();
|
||||
return atomic_load(&s_local_lease_active) &&
|
||||
(int32_t)(atomic_load(&s_local_lease_deadline) - xTaskGetTickCount()) > 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint32_t version;
|
||||
@@ -159,7 +171,7 @@ esp_err_t platform_identity_save(const char *device_id, const char *device_secre
|
||||
|
||||
bool platform_identity_is_configured(void)
|
||||
{
|
||||
return strlen(s_device_id) == 36 && strlen(s_device_secret) >= 32;
|
||||
return ble_cloud_configured() || (strlen(s_device_id) == 36 && strlen(s_device_secret) >= 32);
|
||||
}
|
||||
|
||||
const char *platform_hardware_id(void)
|
||||
@@ -264,10 +276,10 @@ static esp_err_t platform_request_impl(const char *method, const char *path, con
|
||||
const char *idempotency_key, char **response_out)
|
||||
{
|
||||
s_last_error_code[0] = '\0';
|
||||
if (!platform_identity_is_configured()) return ESP_ERR_INVALID_STATE;
|
||||
if (!platform_identity_is_configured() && !ble_cloud_configured()) return ESP_ERR_INVALID_STATE;
|
||||
if (body == NULL) body = "";
|
||||
char url[384];
|
||||
snprintf(url, sizeof(url), "%s%s", PLATFORM_API_ORIGIN, path);
|
||||
snprintf(url, sizeof(url), "%s%s", ble_cloud_configured()?ble_cloud_origin():PLATFORM_API_ORIGIN, path);
|
||||
response_buffer_t response = {
|
||||
.data = calloc(1, PLATFORM_RESPONSE_MAX), .capacity = PLATFORM_RESPONSE_MAX,
|
||||
};
|
||||
@@ -281,6 +293,7 @@ static esp_err_t platform_request_impl(const char *method, const char *path, con
|
||||
.buffer_size = 2048,
|
||||
.buffer_size_tx = 2048,
|
||||
.keep_alive_enable = true,
|
||||
.disable_auto_redirect = true,
|
||||
};
|
||||
if (!s_http_client) s_http_client = esp_http_client_init(&config);
|
||||
esp_http_client_handle_t client = s_http_client;
|
||||
@@ -300,12 +313,19 @@ static esp_err_t platform_request_impl(const char *method, const char *path, con
|
||||
timestamp_now(timestamp);
|
||||
random_hex(nonce, 16);
|
||||
sha256_hex(body, digest);
|
||||
if (ble_cloud_configured()) {
|
||||
char request_id[37]; if (!idempotency_key) { ble_random_uuid(request_id); idempotency_key=request_id; }
|
||||
if (err == ESP_OK) err=ble_cloud_headers(client,method,path,body,idempotency_key);
|
||||
// Header values are copied by ESP-IDF; keep the idempotency pointer valid below.
|
||||
idempotency_key=NULL;
|
||||
} else {
|
||||
if (err == ESP_OK) err = signature_header(method, path, timestamp, nonce, digest, signature);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Id", s_device_id);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Timestamp", timestamp);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Nonce", nonce);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Content-SHA256", digest);
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "X-Device-Signature", signature);
|
||||
}
|
||||
if (err == ESP_OK) err = esp_http_client_set_header(client, "Accept", "application/json");
|
||||
if (err == ESP_OK && idempotency_key != NULL) err = esp_http_client_set_header(client, "Idempotency-Key", idempotency_key);
|
||||
if (err == ESP_OK && body[0] != '\0') {
|
||||
@@ -331,6 +351,7 @@ static esp_err_t platform_request_impl(const char *method, const char *path, con
|
||||
cJSON *root = cJSON_Parse(response.data);
|
||||
cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
|
||||
cJSON *code = cJSON_GetObjectItemCaseSensitive(error, "code");
|
||||
if (!cJSON_IsString(code)) code=cJSON_GetObjectItemCaseSensitive(root,"code");
|
||||
if (cJSON_IsString(code) && code->valuestring) {
|
||||
size_t length = strlen(code->valuestring);
|
||||
if (length > 0 && length < sizeof(s_last_error_code) &&
|
||||
@@ -351,7 +372,7 @@ static esp_err_t platform_request_impl(const char *method, const char *path, con
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t platform_request(const char *method, const char *path, const char *body,
|
||||
esp_err_t platform_request(const char *method, const char *path, const char *body,
|
||||
const char *idempotency_key, char **response_out)
|
||||
{
|
||||
if (!s_http_mutex) return ESP_ERR_INVALID_STATE;
|
||||
@@ -673,45 +694,65 @@ void platform_agent_session_free(platform_agent_session_t *session)
|
||||
memset(session, 0, sizeof(*session));
|
||||
}
|
||||
|
||||
const char *platform_runtime_instance_id(void) { return s_volc.instance_id; }
|
||||
const char *platform_runtime_product_key(void) { return s_volc.product_key; }
|
||||
const char *platform_runtime_device_name(void) { return s_volc.device_name; }
|
||||
const char *platform_runtime_device_secret(void) { return s_volc.device_secret; }
|
||||
const char *platform_runtime_instance_id(void) { return ble_cloud_configured()?call_client_credential()->instance_id:s_volc.instance_id; }
|
||||
const char *platform_runtime_product_key(void) { return ble_cloud_configured()?call_client_credential()->product_key:s_volc.product_key; }
|
||||
const char *platform_runtime_device_name(void) { return ble_cloud_configured()?call_client_credential()->device_name:s_volc.device_name; }
|
||||
const char *platform_runtime_device_secret(void) { return ble_cloud_configured()?call_client_credential()->device_secret:s_volc.device_secret; }
|
||||
bool platform_runtime_rtc(void) { return ble_cloud_configured(); }
|
||||
const char *platform_runtime_rtc_app_id(void) { return call_client_rtc_app_id(); }
|
||||
void platform_runtime_engine_ready(bool ready) { call_client_engine_ready(ready); }
|
||||
void platform_conversation_observe(const char *type) { call_client_observe(type); }
|
||||
void platform_conversation_stop_result(bool started,int result) { call_client_stop_result(started,result); }
|
||||
|
||||
esp_err_t platform_conversation_begin(void)
|
||||
{
|
||||
if (ble_cloud_configured()) return call_client_begin();
|
||||
atomic_store(&s_local_lease_active, false);
|
||||
platform_agent_session_free(&s_active_session);
|
||||
ESP_RETURN_ON_ERROR(recover_pending_session(), TAG, "recover interrupted session");
|
||||
s_event_sequence = 0;
|
||||
s_turn_sequence = 0;
|
||||
return platform_create_agent_session(&s_active_session);
|
||||
TickType_t requested_at = xTaskGetTickCount();
|
||||
esp_err_t result = platform_create_agent_session(&s_active_session);
|
||||
if (result == ESP_OK) {
|
||||
atomic_store(&s_local_lease_deadline, requested_at + pdMS_TO_TICKS(45000));
|
||||
atomic_store(&s_local_lease_active, true);
|
||||
s_heartbeat_due = requested_at + pdMS_TO_TICKS(5000);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const char *platform_conversation_bot_id(void) { return s_active_session.bot_id; }
|
||||
const char *platform_conversation_update_json(void) { return s_active_session.session_update_json; }
|
||||
const char *platform_conversation_bot_id(void) { return ble_cloud_configured()?call_client_bot_id():s_active_session.bot_id; }
|
||||
const char *platform_conversation_update_json(void) { return ble_cloud_configured()?call_client_params():s_active_session.session_update_json; }
|
||||
|
||||
esp_err_t platform_conversation_started(const char *provider_connection_ref)
|
||||
{
|
||||
if (ble_cloud_configured()) { call_client_observe("MEDIA_CONNECTED"); return ESP_OK; }
|
||||
if (provider_connection_ref == NULL || provider_connection_ref[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
esp_err_t err = platform_report_session_started(&s_active_session, provider_connection_ref);
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(s_heartbeat_interval_ms);
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(5000);
|
||||
return err;
|
||||
}
|
||||
|
||||
int platform_conversation_poll(void)
|
||||
{
|
||||
if (!s_active_session.conversation_id[0]) return 1;
|
||||
if (ble_cloud_configured()) return call_client_poll();
|
||||
if (!s_active_session.conversation_id[0] || !platform_conversation_audio_authorized()) return 1;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if ((int32_t)(now - s_heartbeat_due) < 0) return 0;
|
||||
esp_err_t err = platform_report_session_event(&s_active_session, "session_updated", NULL);
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(err == ESP_OK ? s_heartbeat_interval_ms : 5000);
|
||||
if (err == ESP_ERR_INVALID_STATE) return 1;
|
||||
s_heartbeat_due = xTaskGetTickCount() + pdMS_TO_TICKS(5000);
|
||||
if (err == ESP_ERR_INVALID_STATE) { atomic_store(&s_local_lease_active, false); return 1; }
|
||||
// Do not let an HTTP request that outlived the existing deadline resurrect audio.
|
||||
if (!platform_conversation_audio_authorized()) { atomic_store(&s_local_lease_active, false); return 1; }
|
||||
if (err == ESP_OK) atomic_store(&s_local_lease_deadline, now + pdMS_TO_TICKS(45000));
|
||||
if (err != ESP_OK) ESP_LOGW(TAG, "PLATFORM_HEARTBEAT failed=%s", esp_err_to_name(err));
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t platform_conversation_turn(const char *speaker, const char *source_id, const char *text)
|
||||
{
|
||||
if (ble_cloud_configured()) return call_client_turn(speaker,source_id,text);
|
||||
if (s_active_session.native_subtitle_primary) return ESP_OK;
|
||||
if (speaker == NULL || text == NULL || text[0] == '\0' || s_active_session.conversation_id[0] == '\0') return ESP_ERR_INVALID_ARG;
|
||||
char timestamp[32], path[256], key[96], random[33];
|
||||
@@ -758,6 +799,8 @@ esp_err_t platform_conversation_turn(const char *speaker, const char *source_id,
|
||||
|
||||
void platform_conversation_finish(bool failed, const char *error_code)
|
||||
{
|
||||
if (ble_cloud_configured()) { call_client_finish(failed); return; }
|
||||
atomic_store(&s_local_lease_active,false);
|
||||
if (s_active_session.conversation_id[0]) {
|
||||
esp_err_t err = platform_report_session_event(&s_active_session,
|
||||
failed ? "session_failed" : "session_ended",
|
||||
|
||||
Reference in New Issue
Block a user