323 lines
17 KiB
C
323 lines
17 KiB
C
#include "device_control.h"
|
|||
|
|
#include "device_ota.h"
|
||
|
|
#include "device_power.h"
|
||
|
|
#include "device_screen_wake.h"
|
||
|
|
#include "ble_cloud.h"
|
||
|
|
#include "call_client.h"
|
||
|
|
#include "firmware_version.h"
|
||
|
|
#include "cJSON.h"
|
||
|
|
#include "esp_heap_caps.h"
|
||
|
|
#include "esp_log.h"
|
||
|
|
#include "esp_system.h"
|
||
|
|
#include "esp_timer.h"
|
||
|
|
#include "esp_wifi.h"
|
||
|
|
#include "freertos/FreeRTOS.h"
|
||
|
|
#include "freertos/task.h"
|
||
|
|
#include "freertos/idf_additions.h"
|
||
|
|
#include "mqtt_client.h"
|
||
|
|
#include "nvs.h"
|
||
|
|
#include "nvs_flash.h"
|
||
|
|
#include "volc_hal.h"
|
||
|
|
#include "volc_hal_player.h"
|
||
|
|
#include "mbedtls/platform_util.h"
|
||
|
|
#include <stdatomic.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
|
||
|
|
static const char *TAG="device_control";
|
||
|
|
static esp_mqtt_client_handle_t mqtt;
|
||
|
|
static atomic_bool connected;
|
||
|
|
static atomic_bool stopping;
|
||
|
|
static char status_topic[180], ota_topic[180];
|
||
|
|
static int64_t applied_revision;
|
||
|
|
static char device_display_name[97];
|
||
|
|
static bool started;
|
||
|
|
static TaskHandle_t control_worker;
|
||
|
|
|
||
|
|
static const char *text(const cJSON *j,const char *key) {
|
||
|
|
const cJSON *v=cJSON_GetObjectItemCaseSensitive(j,key);
|
||
|
|
return cJSON_IsString(v)?v->valuestring:"";
|
||
|
|
}
|
||
|
|
static void secret_free(char *s) {
|
||
|
|
if(s){mbedtls_platform_zeroize(s,strlen(s));free(s);}
|
||
|
|
}
|
||
|
|
static esp_err_t post(const char *path,cJSON *body,cJSON **data) {
|
||
|
|
char *raw=cJSON_PrintUnformatted(body),*response=NULL;
|
||
|
|
if(!raw)return ESP_ERR_NO_MEM;
|
||
|
|
esp_err_t err=platform_request("POST",path,raw,NULL,&response);
|
||
|
|
secret_free(raw);
|
||
|
|
if(err==ESP_OK) {
|
||
|
|
cJSON *root=response?cJSON_Parse(response):NULL;
|
||
|
|
const cJSON *value=cJSON_GetObjectItemCaseSensitive(root,"data");
|
||
|
|
if(!cJSON_IsObject(value))err=ESP_ERR_INVALID_RESPONSE;
|
||
|
|
else if(data){*data=cJSON_Duplicate(value,true);if(!*data)err=ESP_ERR_NO_MEM;}
|
||
|
|
cJSON_Delete(root);
|
||
|
|
}
|
||
|
|
secret_free(response);
|
||
|
|
return err;
|
||
|
|
}
|
||
|
|
static esp_err_t save_result(const char *result) {
|
||
|
|
nvs_handle_t n;
|
||
|
|
esp_err_t err=nvs_open("control",NVS_READWRITE,&n);
|
||
|
|
if(err!=ESP_OK)return err;
|
||
|
|
err=result?nvs_set_str(n,"result",result):nvs_erase_key(n,"result");
|
||
|
|
if(err==ESP_ERR_NVS_NOT_FOUND)err=ESP_OK;
|
||
|
|
if(err==ESP_OK)err=nvs_commit(n);
|
||
|
|
nvs_close(n);return err;
|
||
|
|
}
|
||
|
|
// Persist terminal receipts before sending them. Commands are delivered at
|
||
|
|
// least once; an offline receipt must not cause another reboot on redelivery.
|
||
|
|
static bool flush_result(void) {
|
||
|
|
nvs_handle_t n;
|
||
|
|
esp_err_t err=nvs_open("control",NVS_READONLY,&n);
|
||
|
|
// Only an absent namespace means no pending receipt. I/O failures must
|
||
|
|
// block fetching another command, or a pending reboot may run again.
|
||
|
|
if(err==ESP_ERR_NVS_NOT_FOUND)return true;
|
||
|
|
if(err!=ESP_OK)return false;
|
||
|
|
size_t len=0;err=nvs_get_str(n,"result",NULL,&len);
|
||
|
|
if(err==ESP_ERR_NVS_NOT_FOUND){nvs_close(n);return true;}
|
||
|
|
if(err!=ESP_OK || len<2 || len>4096){nvs_close(n);return false;}
|
||
|
|
char *raw=malloc(len);
|
||
|
|
if(!raw){nvs_close(n);return false;}
|
||
|
|
err=nvs_get_str(n,"result",raw,&len);nvs_close(n);
|
||
|
|
cJSON *j=err==ESP_OK?cJSON_ParseWithLength(raw,len):NULL;free(raw);
|
||
|
|
if(!j)return false;
|
||
|
|
// A reboot succeeds only after the boot ID changes, never before restart.
|
||
|
|
const char *boot=text(j,"_reboot_from");
|
||
|
|
if(boot[0] && !strcmp(boot,ble_cloud_boot_id())){cJSON_Delete(j);return false;}
|
||
|
|
cJSON_DeleteItemFromObjectCaseSensitive(j,"_reboot_from");
|
||
|
|
err=post("/api/v1/device/control/ack",j,NULL);cJSON_Delete(j);
|
||
|
|
if(err==ESP_OK || !strcmp(platform_last_error_code(),"DEVICE_COMMAND_STATE_CONFLICT"))
|
||
|
|
return save_result(NULL)==ESP_OK;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
static esp_err_t ack(const char *id,const char *status,cJSON *result,bool reboot) {
|
||
|
|
cJSON *j=cJSON_CreateObject();
|
||
|
|
cJSON_AddStringToObject(j,"command_id",id);cJSON_AddStringToObject(j,"status",status);
|
||
|
|
cJSON_AddItemToObject(j,"result_summary",result?cJSON_Duplicate(result,true):cJSON_CreateObject());
|
||
|
|
esp_err_t err;
|
||
|
|
if(!strcmp(status,"ACKNOWLEDGED"))err=post("/api/v1/device/control/ack",j,NULL);
|
||
|
|
else {
|
||
|
|
if(reboot)cJSON_AddStringToObject(j,"_reboot_from",ble_cloud_boot_id());
|
||
|
|
char *raw=cJSON_PrintUnformatted(j);err=raw?save_result(raw):ESP_ERR_NO_MEM;free(raw);
|
||
|
|
if(err==ESP_OK && !reboot)flush_result();
|
||
|
|
}
|
||
|
|
cJSON_Delete(j);return err;
|
||
|
|
}
|
||
|
|
static esp_err_t apply_config(const cJSON *config,bool maintenance_held) {
|
||
|
|
cJSON *revision=cJSON_GetObjectItemCaseSensitive(config,"revision");
|
||
|
|
cJSON *desired=cJSON_GetObjectItemCaseSensitive(config,"desired_config");
|
||
|
|
if(!cJSON_IsNumber(revision) || revision->valuedouble<1 || !cJSON_IsObject(desired))return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
const cJSON *category=NULL;
|
||
|
|
cJSON_ArrayForEach(category,desired) {
|
||
|
|
// Do not claim that arbitrary categories were applied by this firmware.
|
||
|
|
if(!strcmp(category->string,"voice_runtime") && cJSON_IsString(category) &&
|
||
|
|
!strcmp(category->valuestring,"volc_hardware_agent"))continue;
|
||
|
|
if(strcmp(category->string,"volume") && strcmp(category->string,"identity") && strcmp(category->string,"power"))return ESP_ERR_NOT_SUPPORTED;
|
||
|
|
}
|
||
|
|
const cJSON *power=cJSON_GetObjectItemCaseSensitive(desired,"power");
|
||
|
|
const cJSON *mode=cJSON_GetObjectItemCaseSensitive(power,"mode");
|
||
|
|
if(power && (!cJSON_IsObject(power) || cJSON_GetArraySize(power)!=1 || !cJSON_IsString(mode) || (strcmp(mode->valuestring,"active") && strcmp(mode->valuestring,"standby"))))return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
const cJSON *identity=cJSON_GetObjectItemCaseSensitive(desired,"identity");
|
||
|
|
const cJSON *name=cJSON_GetObjectItemCaseSensitive(identity,"display_name");
|
||
|
|
if(identity) {
|
||
|
|
if(!cJSON_IsObject(identity) || cJSON_GetArraySize(identity)!=1 || !cJSON_IsString(name))return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
size_t len=strlen(name->valuestring);
|
||
|
|
if(len>=sizeof(device_display_name) || (len && (name->valuestring[0]==' ' || name->valuestring[len-1]==' ')))return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
for(size_t i=0;i<len;i++)if((unsigned char)name->valuestring[i]<32 || (unsigned char)name->valuestring[i]==127)return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
}
|
||
|
|
cJSON *volume=cJSON_GetObjectItemCaseSensitive(desired,"volume");
|
||
|
|
cJSON *speaker=cJSON_GetObjectItemCaseSensitive(volume,"speaker");
|
||
|
|
if(volume) {
|
||
|
|
if(!cJSON_IsNumber(speaker) || speaker->valuedouble!=speaker->valueint || speaker->valueint<0 || speaker->valueint>100)return ESP_ERR_INVALID_RESPONSE;
|
||
|
|
if(volc_hal_get_device_volume()!=speaker->valueint){
|
||
|
|
if(volc_hal_set_device_volume(speaker->valueint)!=ESP_OK)return ESP_FAIL;
|
||
|
|
}
|
||
|
|
nvs_handle_t n;
|
||
|
|
if(nvs_open("control",NVS_READWRITE,&n)!=ESP_OK)return ESP_FAIL;
|
||
|
|
int32_t stored=-1;nvs_get_i32(n,"speaker",&stored);
|
||
|
|
esp_err_t e=ESP_OK;
|
||
|
|
if(stored!=speaker->valueint){e=nvs_set_i32(n,"speaker",speaker->valueint);if(e==ESP_OK)e=nvs_commit(n);}
|
||
|
|
nvs_close(n);if(e!=ESP_OK)return e;
|
||
|
|
}
|
||
|
|
if(identity && strcmp(device_display_name,name->valuestring)) {
|
||
|
|
nvs_handle_t n;
|
||
|
|
if(nvs_open("control",NVS_READWRITE,&n)!=ESP_OK)return ESP_FAIL;
|
||
|
|
esp_err_t e=nvs_set_str(n,"display_name",name->valuestring);
|
||
|
|
if(e==ESP_OK)e=nvs_commit(n);
|
||
|
|
char readback[sizeof(device_display_name)];size_t size=sizeof(readback);
|
||
|
|
if(e==ESP_OK)e=nvs_get_str(n,"display_name",readback,&size);
|
||
|
|
nvs_close(n);
|
||
|
|
if(e!=ESP_OK || strcmp(readback,name->valuestring))return ESP_FAIL;
|
||
|
|
memcpy(device_display_name,readback,size);
|
||
|
|
}
|
||
|
|
if(power && device_power_apply(mode->valuestring,maintenance_held)!=ESP_OK)return ESP_FAIL;
|
||
|
|
applied_revision=(int64_t)revision->valuedouble;
|
||
|
|
return ESP_OK;
|
||
|
|
}
|
||
|
|
static cJSON *diagnostics(void) {
|
||
|
|
cJSON *j=cJSON_CreateObject(),*audio=cJSON_AddObjectToObject(j,"audio"),
|
||
|
|
*network=cJSON_AddObjectToObject(j,"network"),*storage=cJSON_AddObjectToObject(j,"storage");
|
||
|
|
int volume=volc_hal_get_device_volume();
|
||
|
|
cJSON_AddBoolToObject(audio,"codec_ready",volume>=0);
|
||
|
|
cJSON_AddNumberToObject(audio,"speaker",volume);
|
||
|
|
cJSON_AddStringToObject(audio,"scope","codec state; microphone and speaker listening test requires user");
|
||
|
|
wifi_ap_record_t ap={0};bool online=esp_wifi_sta_get_ap_info(&ap)==ESP_OK;
|
||
|
|
cJSON_AddBoolToObject(network,"connected",online);
|
||
|
|
if(online)cJSON_AddNumberToObject(network,"rssi",ap.rssi);
|
||
|
|
nvs_stats_t stats={0};esp_err_t e=nvs_get_stats(NULL,&stats);
|
||
|
|
cJSON_AddBoolToObject(storage,"nvs_readable",e==ESP_OK);
|
||
|
|
if(e==ESP_OK)cJSON_AddNumberToObject(storage,"free_entries",stats.free_entries);
|
||
|
|
// Check an actual write/commit/read path in our own namespace only.
|
||
|
|
nvs_handle_t n;bool write_ok=false;uint32_t readback=0,nonce=esp_random();
|
||
|
|
if(nvs_open("control",NVS_READWRITE,&n)==ESP_OK) {
|
||
|
|
write_ok=nvs_set_u32(n,"probe",nonce)==ESP_OK && nvs_commit(n)==ESP_OK &&
|
||
|
|
nvs_get_u32(n,"probe",&readback)==ESP_OK && readback==nonce;
|
||
|
|
nvs_erase_key(n,"probe");nvs_commit(n);nvs_close(n);
|
||
|
|
}
|
||
|
|
cJSON_AddBoolToObject(storage,"write_verified",write_ok);
|
||
|
|
cJSON_AddItemToObject(j,"power",device_power_report());
|
||
|
|
cJSON_AddNumberToObject(j,"internal_heap_free",heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
|
||
|
|
cJSON_AddNumberToObject(j,"uptime_ms",esp_timer_get_time()/1000);
|
||
|
|
return j;
|
||
|
|
}
|
||
|
|
static void command(const cJSON *cmd,const cJSON *config) {
|
||
|
|
const char *id=text(cmd,"id"),*type=text(cmd,"type");
|
||
|
|
if(strlen(id)!=36 || !type[0])return;
|
||
|
|
if(!call_client_enter_maintenance())return;
|
||
|
|
if(ack(id,"ACKNOWLEDGED",NULL,false)!=ESP_OK){call_client_leave_maintenance();return;}
|
||
|
|
cJSON *result=NULL;bool success=false;
|
||
|
|
if(!strcmp(type,"reboot")) {
|
||
|
|
if(ack(id,"SUCCEEDED",NULL,true)==ESP_OK){device_control_prepare_restart();vTaskDelay(pdMS_TO_TICKS(100));esp_restart();}
|
||
|
|
} else if(!strcmp(type,"diagnostics")) { result=diagnostics();success=result!=NULL; }
|
||
|
|
else if(!strcmp(type,"apply_config"))success=apply_config(config,true)==ESP_OK;
|
||
|
|
if(!result) {
|
||
|
|
result=cJSON_CreateObject();
|
||
|
|
cJSON_AddStringToObject(result,"message",success?"Configuration applied":"Command unsupported or hardware operation failed");
|
||
|
|
}
|
||
|
|
ack(id,success?"SUCCEEDED":"FAILED",result,false);cJSON_Delete(result);
|
||
|
|
call_client_leave_maintenance();
|
||
|
|
}
|
||
|
|
void device_control_prepare_restart(void) {
|
||
|
|
atomic_store(&stopping,true);
|
||
|
|
atomic_store(&connected,false);
|
||
|
|
// stop sends MQTT DISCONNECT and closes the transport before returning.
|
||
|
|
// Otherwise a fast reboot can leave stale presence until the next session.
|
||
|
|
if(mqtt && esp_mqtt_client_stop(mqtt)!=ESP_OK)
|
||
|
|
ESP_LOGW(TAG,"MQTT shutdown failed; presence will expire normally");
|
||
|
|
}
|
||
|
|
void device_control_heartbeat(void) {
|
||
|
|
if(!mqtt || atomic_load(&stopping) || !atomic_load(&connected))return;
|
||
|
|
cJSON *j=cJSON_CreateObject();
|
||
|
|
cJSON_AddBoolToObject(j,"online",true);
|
||
|
|
cJSON_AddStringToObject(j,"firmware_version",device_ota_version());
|
||
|
|
cJSON_AddNumberToObject(j,"version_code",device_ota_version_code());
|
||
|
|
cJSON_AddBoolToObject(j,"ota_supported",device_ota_supported());
|
||
|
|
char *raw=cJSON_PrintUnformatted(j);
|
||
|
|
if(raw){esp_mqtt_client_enqueue(mqtt,status_topic,raw,0,1,0,false);free(raw);}
|
||
|
|
cJSON_Delete(j);
|
||
|
|
}
|
||
|
|
static void mqtt_event(void *arg,esp_event_base_t base,int32_t id,void *event_data) {
|
||
|
|
static char incoming[1536];
|
||
|
|
static size_t used,expected;
|
||
|
|
esp_mqtt_event_handle_t e=event_data;
|
||
|
|
if(id==MQTT_EVENT_CONNECTED){esp_mqtt_client_subscribe(e->client,ota_topic,1);}
|
||
|
|
else if(id==MQTT_EVENT_SUBSCRIBED){atomic_store(&connected,true);device_control_heartbeat();}
|
||
|
|
else if(id==MQTT_EVENT_DISCONNECTED)atomic_store(&connected,false);
|
||
|
|
else if(id==MQTT_EVENT_DATA){
|
||
|
|
if(e->current_data_offset==0){
|
||
|
|
used=expected=0;
|
||
|
|
if(e->topic_len!=(int)strlen(ota_topic) || memcmp(e->topic,ota_topic,e->topic_len) ||
|
||
|
|
e->total_data_len<1 || e->total_data_len>=sizeof(incoming))return;
|
||
|
|
expected=e->total_data_len;
|
||
|
|
}
|
||
|
|
if(!expected || e->current_data_offset!=(int)used || e->data_len<0 || used+e->data_len>expected){used=expected=0;return;}
|
||
|
|
memcpy(incoming+used,e->data,e->data_len);used+=e->data_len;
|
||
|
|
if(used==expected){incoming[used]=0;cJSON *j=cJSON_Parse(incoming);
|
||
|
|
device_ota_request(text(j,"task_id"));cJSON_Delete(j);used=expected=0;}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
static esp_err_t start_mqtt(void) {
|
||
|
|
cJSON *body=cJSON_CreateObject(),*j=NULL;
|
||
|
|
esp_err_t err=post("/api/v1/device/mqtt-config",body,&j);cJSON_Delete(body);
|
||
|
|
if(err!=ESP_OK)return err;
|
||
|
|
const char *broker=text(j,"broker_url"),*client=text(j,"client_id"),*user=text(j,"username"),*password=text(j,"password");
|
||
|
|
if(!broker[0] || !client[0] || !password[0] || strlen(text(j,"status_topic"))>=sizeof(status_topic) || strlen(text(j,"ota_topic"))>=sizeof(ota_topic)) {cJSON_Delete(j);return ESP_ERR_INVALID_RESPONSE;}
|
||
|
|
snprintf(status_topic,sizeof(status_topic),"%s",text(j,"status_topic"));
|
||
|
|
snprintf(ota_topic,sizeof(ota_topic),"%s",text(j,"ota_topic"));
|
||
|
|
esp_mqtt_client_config_t cfg={.broker.address.uri=broker,
|
||
|
|
.credentials.client_id=client,.credentials.username=user,.credentials.authentication.password=password,
|
||
|
|
.session.keepalive=60,.session.last_will={.topic=status_topic,.msg="{\"online\":false}",.qos=1,.retain=0},
|
||
|
|
.task.stack_size=4096,.buffer.size=2048,.buffer.out_size=512,.outbox.limit=4096};
|
||
|
|
mqtt=esp_mqtt_client_init(&cfg);
|
||
|
|
if(password[0])mbedtls_platform_zeroize((char*)password,strlen(password));
|
||
|
|
cJSON_Delete(j);
|
||
|
|
if(!mqtt)return ESP_ERR_NO_MEM;
|
||
|
|
esp_mqtt_client_register_event(mqtt,ESP_EVENT_ANY_ID,mqtt_event,NULL);
|
||
|
|
err=esp_mqtt_client_start(mqtt);
|
||
|
|
if(err!=ESP_OK){esp_mqtt_client_destroy(mqtt);mqtt=NULL;}
|
||
|
|
return err;
|
||
|
|
}
|
||
|
|
static void control_task(void *arg) {
|
||
|
|
int64_t mqtt_due=0,heartbeat_due=0,diagnostic_due=0;
|
||
|
|
for(;;) {
|
||
|
|
int64_t now=esp_timer_get_time()/1000;
|
||
|
|
if (device_screen_wake_poll(applied_revision) != ESP_OK)
|
||
|
|
ESP_LOGW(TAG, "Screen wake pending; local hardware/storage operation will retry");
|
||
|
|
if(!mqtt && now>=mqtt_due){start_mqtt();mqtt_due=now+30000;}
|
||
|
|
if(now>=heartbeat_due){device_control_heartbeat();heartbeat_due=now+30000;}
|
||
|
|
device_ota_poll();
|
||
|
|
int speaker=volc_hal_get_device_volume();
|
||
|
|
if(now>=diagnostic_due){
|
||
|
|
ESP_LOGI(TAG,"state mqtt=%d speaker=%d revision=%lld ota=%d internal_free=%u",
|
||
|
|
atomic_load(&connected),speaker,(long long)applied_revision,device_ota_supported(),
|
||
|
|
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
|
||
|
|
diagnostic_due=now+30000;
|
||
|
|
}
|
||
|
|
wifi_ap_record_t ap={0};
|
||
|
|
if(speaker>=0 && esp_wifi_sta_get_ap_info(&ap)==ESP_OK && flush_result()) {
|
||
|
|
cJSON *j=cJSON_CreateObject(),*data=NULL;
|
||
|
|
cJSON_AddStringToObject(j,"firmware_version",device_ota_version());
|
||
|
|
cJSON_AddNumberToObject(j,"reported_revision",applied_revision);
|
||
|
|
cJSON_AddNumberToObject(j,"speaker",speaker);
|
||
|
|
cJSON_AddStringToObject(j,"display_name",device_display_name);
|
||
|
|
cJSON_AddItemToObject(j,"power",device_power_report());
|
||
|
|
cJSON_AddNumberToObject(j,"rssi",ap.rssi);
|
||
|
|
cJSON_AddBoolToObject(j,"busy",call_client_busy());
|
||
|
|
device_screen_wake_add_report(j);
|
||
|
|
esp_err_t e=post("/api/v1/device/control/sync",j,&data);cJSON_Delete(j);
|
||
|
|
if(e==ESP_OK && device_screen_wake_ack(data)==ESP_OK) {
|
||
|
|
cJSON *config=cJSON_GetObjectItemCaseSensitive(data,"configuration");
|
||
|
|
apply_config(config,false);
|
||
|
|
cJSON *commands=cJSON_GetObjectItemCaseSensitive(data,"commands");
|
||
|
|
const cJSON *item=cJSON_GetArrayItem(commands,0);
|
||
|
|
if(item)command(item,config);
|
||
|
|
} else if(!strcmp(platform_last_error_code(),"CONFIG_REVISION_CONFLICT"))applied_revision=0;
|
||
|
|
cJSON_Delete(data);
|
||
|
|
}
|
||
|
|
ulTaskNotifyTake(pdTRUE,pdMS_TO_TICKS(5000));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
esp_err_t device_control_start(void) {
|
||
|
|
if(started)return ESP_OK;
|
||
|
|
esp_err_t wake_init = device_screen_wake_init();
|
||
|
|
if (wake_init != ESP_OK) return wake_init;
|
||
|
|
nvs_handle_t n;
|
||
|
|
if(nvs_open("control",NVS_READONLY,&n)==ESP_OK){
|
||
|
|
int32_t volume;
|
||
|
|
if(nvs_get_i32(n,"speaker",&volume)==ESP_OK && volume>=0 && volume<=100)volc_hal_set_device_volume(volume);
|
||
|
|
size_t size=sizeof(device_display_name);
|
||
|
|
if(nvs_get_str(n,"display_name",device_display_name,&size)!=ESP_OK)device_display_name[0]=0;
|
||
|
|
nvs_close(n);
|
||
|
|
}
|
||
|
|
if(xTaskCreateWithCaps(control_task,"device_control",12288,NULL,3,&control_worker,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)!=pdPASS)return ESP_ERR_NO_MEM;
|
||
|
|
started=true;ESP_LOGI(TAG,"Device control worker started");return ESP_OK;
|
||
|
|
}
|
||
|
|
bool device_control_request_screen_wake(void) {
|
||
|
|
if (!started || !control_worker || atomic_load(&stopping)) return false;
|
||
|
|
device_screen_wake_request();
|
||
|
|
xTaskNotifyGive(control_worker);
|
||
|
|
return true;
|
||
|
|
}
|