397 lines
18 KiB
C
397 lines
18 KiB
C
// Direct chat: fetch parameters, talk to Volcano, report end. Subtitles come from Volcano.
|
|||
|
|
#include "call_client.h"
|
||
|
|
#include "firmware_version.h"
|
||
|
|
#include "../components/volc_conv_ai/src/base/volc_device_manager.h"
|
||
|
|
#include "ble_cloud.h"
|
||
|
|
#include "cJSON.h"
|
||
|
|
#include "esp_log.h"
|
||
|
|
#include "esp_timer.h"
|
||
|
|
#include "freertos/FreeRTOS.h"
|
||
|
|
#include "freertos/semphr.h"
|
||
|
|
#include "freertos/task.h"
|
||
|
|
#include "mbedtls/platform_util.h"
|
||
|
|
#include "mbedtls/sha256.h"
|
||
|
|
#include "nvs.h"
|
||
|
|
#include <stdatomic.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <sys/time.h>
|
||
|
|
|
||
|
|
#define AUDIO_PROFILE "echoear-pcm16-16k-v1"
|
||
|
|
#define FIRMWARE DEVICE_FIRMWARE_VERSION
|
||
|
|
#define QUEUE_SIZE 12
|
||
|
|
typedef struct {
|
||
|
|
unsigned version;
|
||
|
|
int64_t binding, config;
|
||
|
|
platform_volc_credential_t credential;
|
||
|
|
char activation[37], attempt[37], result_id[37], rtc_app[129];
|
||
|
|
int pending; // 1=contact possible; 2=successful registration saved; 3=unknown
|
||
|
|
} voice_store_t;
|
||
|
|
typedef struct {
|
||
|
|
unsigned version;
|
||
|
|
char session[37], boot[37], end_id[37], reason[24];
|
||
|
|
bool finished, failed, sdk_stopped;
|
||
|
|
} call_store_t;
|
||
|
|
static voice_store_t voice;
|
||
|
|
static call_store_t call;
|
||
|
|
static atomic_bool audio_allowed;
|
||
|
|
static atomic_uint lease_deadline;
|
||
|
|
static char *params;
|
||
|
|
static char bot[201];
|
||
|
|
static bool boot_loaded, retry_started, engine_ready, sdk_initialized, sdk_stopped;
|
||
|
|
static bool maintenance;
|
||
|
|
static atomic_bool standby_mode;
|
||
|
|
static int64_t renew_due;
|
||
|
|
static StaticSemaphore_t state_mutex_storage;
|
||
|
|
static SemaphoreHandle_t state_mutex;
|
||
|
|
static const char *TAG = "call_client";
|
||
|
|
static char start_error[80];
|
||
|
|
const char *call_client_start_error(void) { return start_error; }
|
||
|
|
static void retry_task(void *unused);
|
||
|
|
static void finish_pending(void);
|
||
|
|
|
||
|
|
static const char *str(const cJSON *j, const char *name) {
|
||
|
|
const cJSON *v = cJSON_GetObjectItemCaseSensitive(j, name);
|
||
|
|
return cJSON_IsString(v) ? v->valuestring : "";
|
||
|
|
}
|
||
|
|
static int64_t num(const cJSON *j, const char *name) {
|
||
|
|
const cJSON *v = cJSON_GetObjectItemCaseSensitive(j, name);
|
||
|
|
return cJSON_IsNumber(v) ? (int64_t)v->valuedouble : -1;
|
||
|
|
}
|
||
|
|
static int64_t wall(void) {
|
||
|
|
struct timeval t;
|
||
|
|
gettimeofday(&t, NULL);
|
||
|
|
return (int64_t)t.tv_sec * 1000 + t.tv_usec / 1000;
|
||
|
|
}
|
||
|
|
static int64_t mono(void) { return esp_timer_get_time() / 1000; }
|
||
|
|
static esp_err_t persist(const char *key, const void *data, size_t len) {
|
||
|
|
nvs_handle_t n;
|
||
|
|
esp_err_t e = nvs_open("voice", NVS_READWRITE, &n);
|
||
|
|
if (e != ESP_OK)
|
||
|
|
return e;
|
||
|
|
e = data ? nvs_set_blob(n, key, data, len) : nvs_erase_key(n, key);
|
||
|
|
if (e == ESP_ERR_NVS_NOT_FOUND)
|
||
|
|
e = ESP_OK;
|
||
|
|
if (e == ESP_OK)
|
||
|
|
e = nvs_commit(n);
|
||
|
|
nvs_close(n);
|
||
|
|
return e;
|
||
|
|
}
|
||
|
|
static void restore(const char *key, void *data, size_t len) {
|
||
|
|
nvs_handle_t n;
|
||
|
|
if (nvs_open("voice", NVS_READONLY, &n) != ESP_OK)
|
||
|
|
return;
|
||
|
|
size_t size = len;
|
||
|
|
if (nvs_get_blob(n, key, data, &size) != ESP_OK || size != len)
|
||
|
|
memset(data, 0, len);
|
||
|
|
nvs_close(n);
|
||
|
|
}
|
||
|
|
// Save the five fields from ble-bind before reporting BLE success. Boot still
|
||
|
|
// obtains the current binding/config metadata for SDK initialization and ACK.
|
||
|
|
esp_err_t call_client_save_bind_voice(const cJSON *data) {
|
||
|
|
voice_store_t saved = {.version=1};
|
||
|
|
const char *instance=str(data,"instance_id"), *product=str(data,"product_key"),
|
||
|
|
*name=str(data,"device_name"), *secret=str(data,"device_secret"), *app=str(data,"RTCAppID");
|
||
|
|
if (!instance[0] || strlen(instance)>=sizeof(saved.credential.instance_id) ||
|
||
|
|
!product[0] || strlen(product)>=sizeof(saved.credential.product_key) ||
|
||
|
|
!name[0] || strlen(name)>=sizeof(saved.credential.device_name) ||
|
||
|
|
strlen(secret)<16 || strlen(secret)>=sizeof(saved.credential.device_secret) ||
|
||
|
|
!app[0] || strlen(app)>=sizeof(saved.rtc_app)) return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
snprintf(saved.credential.instance_id,sizeof(saved.credential.instance_id),"%s",instance);
|
||
|
|
snprintf(saved.credential.product_key,sizeof(saved.credential.product_key),"%s",product);
|
||
|
|
snprintf(saved.credential.device_name,sizeof(saved.credential.device_name),"%s",name);
|
||
|
|
snprintf(saved.credential.device_secret,sizeof(saved.credential.device_secret),"%s",secret);
|
||
|
|
snprintf(saved.rtc_app,sizeof(saved.rtc_app),"%s",app);
|
||
|
|
esp_err_t err=persist("credential",&saved,sizeof(saved));
|
||
|
|
mbedtls_platform_zeroize(&saved,sizeof(saved));
|
||
|
|
return err;
|
||
|
|
}
|
||
|
|
static esp_err_t post(const char *path, cJSON *body, const char *rid, cJSON **out) {
|
||
|
|
char *raw = cJSON_PrintUnformatted(body), *response = NULL;
|
||
|
|
if (!raw)
|
||
|
|
return ESP_ERR_NO_MEM;
|
||
|
|
esp_err_t e = platform_request("POST", path, raw, rid, &response);
|
||
|
|
// An idle keep-alive connection may have been closed by the gateway. Chat
|
||
|
|
// requests are keyed by session_id, so retry once on a transport failure.
|
||
|
|
if (e != ESP_OK && !platform_last_error_code()[0] && strstr(path,"/device/chat/")) {
|
||
|
|
if (response) { mbedtls_platform_zeroize(response,strlen(response)); free(response); response=NULL; }
|
||
|
|
e = platform_request("POST",path,raw,rid,&response);
|
||
|
|
}
|
||
|
|
mbedtls_platform_zeroize(raw,strlen(raw));
|
||
|
|
free(raw);
|
||
|
|
if (e == ESP_OK) {
|
||
|
|
cJSON *root = cJSON_Parse(response), *data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
||
|
|
if (!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root, "ok")) || !cJSON_IsObject(data))
|
||
|
|
e = ESP_ERR_INVALID_RESPONSE;
|
||
|
|
else if (out) {
|
||
|
|
*out = cJSON_Duplicate(data, true);
|
||
|
|
cJSON *server_time = cJSON_GetObjectItemCaseSensitive(root,"server_time_ms");
|
||
|
|
if (*out && cJSON_IsNumber(server_time))
|
||
|
|
cJSON_AddNumberToObject(*out,"_server_time_ms",server_time->valuedouble);
|
||
|
|
}
|
||
|
|
cJSON_Delete(root);
|
||
|
|
}
|
||
|
|
if (response) {
|
||
|
|
mbedtls_platform_zeroize(response, strlen(response));
|
||
|
|
free(response);
|
||
|
|
}
|
||
|
|
return e;
|
||
|
|
}
|
||
|
|
const platform_volc_credential_t *call_client_credential(void) { return &voice.credential; }
|
||
|
|
const char *call_client_rtc_app_id(void) { return voice.rtc_app; }
|
||
|
|
|
||
|
|
static bool saved_voice_valid(void) {
|
||
|
|
return voice.version == 1 &&
|
||
|
|
memchr(voice.credential.instance_id, 0, sizeof(voice.credential.instance_id)) &&
|
||
|
|
memchr(voice.credential.product_key, 0, sizeof(voice.credential.product_key)) &&
|
||
|
|
memchr(voice.credential.device_name, 0, sizeof(voice.credential.device_name)) &&
|
||
|
|
memchr(voice.credential.device_secret, 0, sizeof(voice.credential.device_secret)) &&
|
||
|
|
memchr(voice.rtc_app, 0, sizeof(voice.rtc_app)) &&
|
||
|
|
voice.credential.instance_id[0] && voice.credential.product_key[0] &&
|
||
|
|
voice.credential.device_name[0] && strlen(voice.credential.device_secret) >= 16 &&
|
||
|
|
voice.rtc_app[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
esp_err_t call_client_boot(void) {
|
||
|
|
start_error[0] = 0;
|
||
|
|
if (!state_mutex) state_mutex = xSemaphoreCreateMutexStatic(&state_mutex_storage);
|
||
|
|
if (!state_mutex) return ESP_ERR_NO_MEM;
|
||
|
|
atomic_store(&audio_allowed, false);
|
||
|
|
if (!boot_loaded) {
|
||
|
|
restore("credential", &voice, sizeof(voice));
|
||
|
|
restore("direct_call", &call, sizeof(call));
|
||
|
|
if (call.version != 2 || !memchr(call.session,0,sizeof(call.session)) ||
|
||
|
|
!memchr(call.end_id,0,sizeof(call.end_id))) memset(&call,0,sizeof(call));
|
||
|
|
if (call.session[0]) {
|
||
|
|
call.finished = true; call.failed = true; call.sdk_stopped = false;
|
||
|
|
snprintf(call.reason,sizeof(call.reason),"reboot");
|
||
|
|
}
|
||
|
|
boot_loaded = true;
|
||
|
|
}
|
||
|
|
if (ble_cloud_sync_time() != ESP_OK) return ESP_FAIL;
|
||
|
|
if (call.finished) {
|
||
|
|
finish_pending();
|
||
|
|
if (call.session[0]) return ESP_ERR_INVALID_STATE;
|
||
|
|
}
|
||
|
|
// A retained credential is not proof of a current household/elder binding.
|
||
|
|
cJSON *j=cJSON_CreateObject(), *data=NULL;
|
||
|
|
esp_err_t e=post("/api/v1/device/ble-state",j,NULL,&data);
|
||
|
|
cJSON_Delete(j);
|
||
|
|
if (e!=ESP_OK) { cJSON_Delete(data); return e; }
|
||
|
|
bool bound=cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(data,"bound"));
|
||
|
|
cJSON_Delete(data); data=NULL;
|
||
|
|
if (!bound) { snprintf(start_error,sizeof(start_error),"DEVICE_UNBOUND"); return ESP_ERR_INVALID_STATE; }
|
||
|
|
// Normal boot uses the five fields saved from ble-bind. Recovery only.
|
||
|
|
if (!saved_voice_valid()) {
|
||
|
|
j=cJSON_CreateObject();
|
||
|
|
e=post("/api/v1/device/voice-config",j,NULL,&data); cJSON_Delete(j);
|
||
|
|
if (e==ESP_OK && !strcmp(str(data,"voice_status"),"ready")) {
|
||
|
|
e=call_client_save_bind_voice(cJSON_GetObjectItemCaseSensitive(data,"voice"));
|
||
|
|
} else if(e==ESP_OK) {
|
||
|
|
snprintf(start_error,sizeof(start_error),"%s",
|
||
|
|
!strcmp(str(data,"voice_status"),"preparing") ? "VOICE_PREPARING" : "VOICE_RECOVERY_REQUIRED");
|
||
|
|
e=ESP_ERR_INVALID_STATE;
|
||
|
|
}
|
||
|
|
cJSON *secret=cJSON_GetObjectItemCaseSensitive(cJSON_GetObjectItemCaseSensitive(data,"voice"),"device_secret");
|
||
|
|
if(cJSON_IsString(secret))mbedtls_platform_zeroize(secret->valuestring,strlen(secret->valuestring));
|
||
|
|
cJSON_Delete(data);
|
||
|
|
if(e!=ESP_OK)return e;
|
||
|
|
}
|
||
|
|
if (!retry_started) {
|
||
|
|
if(xTaskCreate(retry_task,"call_sync",8192,NULL,4,NULL)!=pdPASS)return ESP_ERR_NO_MEM;
|
||
|
|
retry_started=true;
|
||
|
|
}
|
||
|
|
ESP_LOGI(TAG,"Direct chat ready; firmware=%s credentials=platform-issued",FIRMWARE);
|
||
|
|
return ESP_OK;
|
||
|
|
}
|
||
|
|
|
||
|
|
void call_client_engine_ready(bool initialized) {
|
||
|
|
sdk_initialized=initialized;
|
||
|
|
engine_ready=initialized; // Local SDK initialization, no platform activation ACK.
|
||
|
|
}
|
||
|
|
bool call_client_audio_allowed(void) {
|
||
|
|
if (!atomic_load(&audio_allowed)) return false;
|
||
|
|
if ((int32_t)(atomic_load(&lease_deadline) - (uint32_t)mono()) <= 0) {
|
||
|
|
atomic_store(&audio_allowed, false);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
bool call_client_is_engine_ready(void) { return engine_ready; }
|
||
|
|
const char *call_client_bot_id(void) { return bot; }
|
||
|
|
const char *call_client_params(void) { return params ? params : "{}"; }
|
||
|
|
// No device transcript queue or event upload: provider callbacks own records.
|
||
|
|
void call_client_observe(const char *type) { (void)type; }
|
||
|
|
int volc_platform_recording_prepare(const char *app, const char *room, const char *user) {
|
||
|
|
if (!app || !room || !user || strlen(app)>128 || strlen(room)>128 || strlen(user)>128 || !state_mutex) return ESP_ERR_INVALID_ARG;
|
||
|
|
char session[37]={0};
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
if (call.session[0] && !call.finished && call_client_audio_allowed()) snprintf(session,sizeof(session),"%s",call.session);
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
if (!session[0]) return ESP_ERR_INVALID_STATE;
|
||
|
|
cJSON *body=cJSON_CreateObject(), *data=NULL;
|
||
|
|
if (!body) return ESP_ERR_NO_MEM;
|
||
|
|
cJSON_AddStringToObject(body,"session_id",session);
|
||
|
|
cJSON_AddStringToObject(body,"device_id",ble_factory_id());
|
||
|
|
cJSON_AddStringToObject(body,"rtc_app_id",app);
|
||
|
|
cJSON_AddStringToObject(body,"room_id",room);
|
||
|
|
cJSON_AddStringToObject(body,"rtc_user_id",user);
|
||
|
|
esp_err_t result=post("/api/v1/device/chat/recording",body,NULL,&data);
|
||
|
|
ESP_LOGI(TAG,"Cloud conversation recording: %s",result==ESP_OK ? str(data,"recording") : "unavailable");
|
||
|
|
cJSON_Delete(data);cJSON_Delete(body);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
esp_err_t call_client_turn(const char *speaker, const char *id, const char *text) {
|
||
|
|
(void)speaker; (void)id; (void)text; return ESP_OK;
|
||
|
|
}
|
||
|
|
static esp_err_t fetch_parameters(bool initial) {
|
||
|
|
int64_t requested_at = mono();
|
||
|
|
cJSON *j = cJSON_CreateObject(), *data = NULL;
|
||
|
|
cJSON_AddStringToObject(j, "session_id", call.session);
|
||
|
|
cJSON_AddStringToObject(j, "device_id", ble_factory_id());
|
||
|
|
cJSON_AddStringToObject(j, "boot_id", call.boot);
|
||
|
|
esp_err_t e = post("/api/v1/device/chat/start", j, NULL, &data);
|
||
|
|
cJSON_Delete(j);
|
||
|
|
// A slow reply cannot reopen an expired local authorization.
|
||
|
|
if (!initial && !call_client_audio_allowed()) e = ESP_ERR_TIMEOUT;
|
||
|
|
if (e == ESP_OK) {
|
||
|
|
// Compare server timestamps with each other: boot-time clock skew must
|
||
|
|
// not reject a valid 45-second lease. Subtract the full request duration.
|
||
|
|
int64_t server_now = num(data,"_server_time_ms");
|
||
|
|
int64_t lease = num(data,"lease_expires_at_ms") - (server_now > 0 ? server_now : wall());
|
||
|
|
if (server_now > 0) lease -= mono() - requested_at;
|
||
|
|
cJSON *p = cJSON_GetObjectItemCaseSensitive(data, "params");
|
||
|
|
if (strcmp(str(data,"session_id"),call.session) || !cJSON_IsObject(p) ||
|
||
|
|
lease <= 0 || lease > 45000 || num(data,"expires_at_ms") <= (server_now > 0 ? server_now : wall()) ||
|
||
|
|
!str(data,"bot_id")[0]) e = ESP_ERR_INVALID_RESPONSE;
|
||
|
|
else {
|
||
|
|
if (initial) {
|
||
|
|
char *encoded = cJSON_PrintUnformatted(p);
|
||
|
|
if (!encoded || strlen(encoded) > 16384) {
|
||
|
|
free(encoded); e = ESP_ERR_NO_MEM;
|
||
|
|
} else {
|
||
|
|
free(params); params = encoded;
|
||
|
|
snprintf(bot, sizeof(bot), "%s", str(data,"bot_id"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (e == ESP_OK) {
|
||
|
|
atomic_store(&lease_deadline, (uint32_t)(mono() + lease));
|
||
|
|
atomic_store(&audio_allowed, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
cJSON_Delete(data);
|
||
|
|
renew_due = mono() + 10000;
|
||
|
|
if (e != ESP_OK && initial) atomic_store(&audio_allowed, false);
|
||
|
|
return e;
|
||
|
|
}
|
||
|
|
static void finish_pending(void) {
|
||
|
|
if (!call.session[0] || !call.finished) return;
|
||
|
|
atomic_store(&audio_allowed, false);
|
||
|
|
cJSON *j = cJSON_CreateObject();
|
||
|
|
cJSON_AddStringToObject(j,"session_id",call.session);
|
||
|
|
cJSON_AddStringToObject(j,"device_id",ble_factory_id());
|
||
|
|
cJSON_AddStringToObject(j,"reason",call.reason[0] ? call.reason : "disconnected");
|
||
|
|
cJSON_AddBoolToObject(j,"sdk_stopped",call.sdk_stopped);
|
||
|
|
esp_err_t e = post("/api/v1/device/chat/end",j,call.end_id,NULL);
|
||
|
|
cJSON_Delete(j);
|
||
|
|
if (e == ESP_OK && persist("direct_call",NULL,0) == ESP_OK) memset(&call,0,sizeof(call));
|
||
|
|
if (params) {
|
||
|
|
mbedtls_platform_zeroize(params,strlen(params)); free(params); params=NULL;
|
||
|
|
}
|
||
|
|
ESP_LOGI(TAG,"chat end acknowledged=%d",e == ESP_OK);
|
||
|
|
}
|
||
|
|
esp_err_t call_client_begin(void) {
|
||
|
|
start_error[0] = '\0';
|
||
|
|
if (!state_mutex) return ESP_ERR_INVALID_STATE;
|
||
|
|
if (!engine_ready && sdk_initialized) call_client_engine_ready(true);
|
||
|
|
if (!engine_ready) return ESP_ERR_INVALID_STATE;
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
if (call.finished) finish_pending();
|
||
|
|
if (maintenance || atomic_load(&standby_mode) || call.session[0]) { xSemaphoreGive(state_mutex); return ESP_ERR_INVALID_STATE; }
|
||
|
|
memset(&call,0,sizeof(call)); call.version=2;
|
||
|
|
ble_random_uuid(call.session); ble_random_uuid(call.end_id);
|
||
|
|
snprintf(call.boot,sizeof(call.boot),"%s",ble_cloud_boot_id());
|
||
|
|
sdk_stopped=false;
|
||
|
|
esp_err_t e = persist("direct_call",&call,sizeof(call));
|
||
|
|
if (e == ESP_OK) e=fetch_parameters(true);
|
||
|
|
if (e != ESP_OK) {
|
||
|
|
snprintf(start_error,sizeof(start_error),"%s",platform_last_error_code());
|
||
|
|
call.finished=true; call.failed=true;
|
||
|
|
snprintf(call.reason,sizeof(call.reason),"start_failed");
|
||
|
|
persist("direct_call",&call,sizeof(call)); finish_pending();
|
||
|
|
}
|
||
|
|
xSemaphoreGive(state_mutex); return e;
|
||
|
|
}
|
||
|
|
int call_client_poll(void) {
|
||
|
|
if (!call_client_audio_allowed()) return 1;
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
if (mono() >= renew_due) {
|
||
|
|
esp_err_t e=fetch_parameters(false);
|
||
|
|
// A known refusal is terminal. Transient transport failures get only
|
||
|
|
// the remaining previously granted time, never a new local lease.
|
||
|
|
if (e != ESP_OK) {
|
||
|
|
const char *code=platform_last_error_code();
|
||
|
|
if (code && (!strcmp(code,"CHAT_ENDED") || !strcmp(code,"DEVICE_BINDING_CHANGED") ||
|
||
|
|
!strcmp(code,"VOICE_CHAT_CONSENT_REQUIRED") || !strcmp(code,"DEVICE_NOT_READY") ||
|
||
|
|
!strcmp(code,"MEMORY_CONSENT_REVOKED") || !strcmp(code,"DEVICE_UNBOUND") ||
|
||
|
|
!strcmp(code,"VOICE_AGENT_NOT_READY"))) atomic_store(&audio_allowed,false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
int stopped=!call_client_audio_allowed();
|
||
|
|
xSemaphoreGive(state_mutex); return stopped;
|
||
|
|
}
|
||
|
|
void call_client_stop_result(bool started,int result) { sdk_stopped=started && result==0; }
|
||
|
|
void call_client_finish(bool failed) {
|
||
|
|
atomic_store(&audio_allowed,false);
|
||
|
|
if (!state_mutex) return;
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
if (call.session[0] && !call.finished) {
|
||
|
|
call.finished=true; call.failed=failed; call.sdk_stopped=sdk_stopped;
|
||
|
|
snprintf(call.reason,sizeof(call.reason),"%s",failed ? "disconnected" : "hangup");
|
||
|
|
persist("direct_call",&call,sizeof(call));
|
||
|
|
}
|
||
|
|
finish_pending();
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
}
|
||
|
|
static void retry_task(void *unused) {
|
||
|
|
(void)unused;
|
||
|
|
for (;;) {
|
||
|
|
vTaskDelay(pdMS_TO_TICKS(10000));
|
||
|
|
if (xSemaphoreTake(state_mutex,0)==pdTRUE) {
|
||
|
|
if (call.finished) finish_pending();
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool call_client_busy(void) {
|
||
|
|
if (!state_mutex || xSemaphoreTake(state_mutex,0)!=pdTRUE) return true;
|
||
|
|
bool busy=maintenance || call.session[0]!=0;
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
return busy;
|
||
|
|
}
|
||
|
|
bool call_client_standby(void) { return atomic_load(&standby_mode); }
|
||
|
|
esp_err_t call_client_set_standby(bool standby) {
|
||
|
|
if (!state_mutex) { atomic_store(&standby_mode,standby); return ESP_OK; }
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
if (call.session[0] || !maintenance) { xSemaphoreGive(state_mutex); return ESP_ERR_INVALID_STATE; }
|
||
|
|
atomic_store(&standby_mode,standby);
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
return ESP_OK;
|
||
|
|
}
|
||
|
|
bool call_client_enter_maintenance(void) {
|
||
|
|
if (!state_mutex || xSemaphoreTake(state_mutex,0)!=pdTRUE) return false;
|
||
|
|
bool allowed=!maintenance && !call.session[0];
|
||
|
|
if (allowed) maintenance=true;
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
return allowed;
|
||
|
|
}
|
||
|
|
void call_client_leave_maintenance(void) {
|
||
|
|
if (!state_mutex) return;
|
||
|
|
xSemaphoreTake(state_mutex,portMAX_DELAY);
|
||
|
|
maintenance=false;
|
||
|
|
xSemaphoreGive(state_mutex);
|
||
|
|
}
|