mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-27 19:12:40 +00:00
update firmware v1.2.4 (#16)
This commit is contained in:
@@ -177,6 +177,8 @@ void CoreS3AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gp
|
||||
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
|
||||
ESP_LOGI(TAG, "Duplex channels created");
|
||||
}
|
||||
|
||||
|
||||
@@ -92,12 +92,6 @@ void disply_lvgl_unlock()
|
||||
display->LvglUnlock();
|
||||
}
|
||||
|
||||
void display_setup_xiaozhi_ui()
|
||||
{
|
||||
auto display = static_cast<DISPLAY_TYPE*>(Board::GetInstance().GetDisplay());
|
||||
display->SetupXiaoZhiUI();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Application */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@@ -110,8 +104,6 @@ void xiaozhi_board_init()
|
||||
|
||||
void start_xiaozhi_app()
|
||||
{
|
||||
display_setup_xiaozhi_ui();
|
||||
|
||||
set_xiaozhi_mode(true);
|
||||
|
||||
// Initialize and run the application
|
||||
|
||||
@@ -37,7 +37,6 @@ void toggle_xiaozhi_chat_state();
|
||||
void disply_lvgl_lock();
|
||||
void disply_lvgl_unlock();
|
||||
lv_disp_t* display_get_lvgl_display();
|
||||
void display_setup_xiaozhi_ui();
|
||||
|
||||
void xiaozhi_board_init();
|
||||
void start_xiaozhi_app();
|
||||
@@ -49,6 +48,8 @@ int board_get_battery_level();
|
||||
bool board_is_battery_charging();
|
||||
void board_set_backlight_brightness(uint8_t brightness, bool permanent = false);
|
||||
uint8_t board_get_backlight_brightness();
|
||||
void board_set_speaker_volume(uint8_t volume, bool permanent = false);
|
||||
uint8_t board_get_speaker_volume();
|
||||
|
||||
void app_play_sound(const std::string_view& sound);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_ili9341.h>
|
||||
#include <esp_timer.h>
|
||||
// #include "esp32_camera.h"
|
||||
#include "stackchan_camera.h"
|
||||
#include "hal_bridge.h"
|
||||
|
||||
@@ -59,6 +58,7 @@ public:
|
||||
WriteReg(0x90, 0xBF);
|
||||
WriteReg(0x94, 33 - 5);
|
||||
WriteReg(0x95, 33 - 5);
|
||||
WriteReg(0x27, 0x00);
|
||||
|
||||
auto ret = setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_700MA);
|
||||
if (!ret) {
|
||||
@@ -541,6 +541,28 @@ uint8_t hal_bridge::board_get_backlight_brightness()
|
||||
}
|
||||
}
|
||||
|
||||
void hal_bridge::board_set_speaker_volume(uint8_t volume, bool permanent)
|
||||
{
|
||||
auto& board = Board::GetInstance();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (audio_codec) {
|
||||
if (permanent) {
|
||||
audio_codec->SetOutputVolume(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t hal_bridge::board_get_speaker_volume()
|
||||
{
|
||||
int volume = 70;
|
||||
Settings settings("audio", false);
|
||||
volume = settings.GetInt("output_volume", volume);
|
||||
if (volume <= 0) {
|
||||
volume = 10;
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
void hal_bridge::toggle_xiaozhi_chat_state()
|
||||
{
|
||||
auto& app = Application::GetInstance();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <esp_psram.h>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <src/misc/cache/lv_cache.h>
|
||||
#include <settings.h>
|
||||
#include <lvgl.h>
|
||||
#include <lvgl_theme.h>
|
||||
@@ -228,8 +229,16 @@ lv_disp_t* StackChanAvatarDisplay::GetLvglDisplay()
|
||||
|
||||
#include <hal/board/hal_bridge.h>
|
||||
|
||||
void StackChanAvatarDisplay::SetupXiaoZhiUI()
|
||||
void StackChanAvatarDisplay::SetupUI()
|
||||
{
|
||||
// Prevent duplicate calls - if already called, return early
|
||||
if (setup_ui_called_) {
|
||||
ESP_LOGW(TAG, "SetupUI() called multiple times, skipping duplicate call");
|
||||
return;
|
||||
}
|
||||
|
||||
Display::SetupUI(); // Mark SetupUI as called
|
||||
|
||||
auto& stackchan = GetStackChan();
|
||||
|
||||
if (stackchan.hasAvatar()) {
|
||||
@@ -243,7 +252,11 @@ void StackChanAvatarDisplay::SetupXiaoZhiUI()
|
||||
|
||||
auto avatar = std::make_unique<DefaultAvatar>();
|
||||
avatar->init(lv_screen_active());
|
||||
avatar->getPanel()->onClick().connect([]() { hal_bridge::toggle_xiaozhi_chat_state(); });
|
||||
avatar->getPanel()->onClick().connect([]() {
|
||||
if (hal_bridge::is_xiaozhi_ready()) {
|
||||
hal_bridge::toggle_xiaozhi_chat_state();
|
||||
}
|
||||
});
|
||||
|
||||
stackchan.attachAvatar(std::move(avatar));
|
||||
stackchan.addModifier(std::make_unique<BreathModifier>());
|
||||
@@ -283,7 +296,7 @@ void StackChanAvatarDisplay::SetEmotion(const char* emotion)
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
ESP_LOGE(TAG, "SetEmotion: %s", emotion);
|
||||
// ESP_LOGE(TAG, "SetEmotion: %s", emotion);
|
||||
|
||||
auto& avatar = stackchan.avatar();
|
||||
|
||||
@@ -335,12 +348,16 @@ void StackChanAvatarDisplay::SetEmotion(const char* emotion)
|
||||
|
||||
void StackChanAvatarDisplay::SetChatMessage(const char* role, const char* content)
|
||||
{
|
||||
if (!setup_ui_called_) {
|
||||
ESP_LOGW(TAG, "SetChatMessage('%s', '%s') called before SetupUI() - message will be lost!", role, content);
|
||||
}
|
||||
|
||||
auto& stackchan = GetStackChan();
|
||||
if (!stackchan.hasAvatar()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "SetChatMessage: role=%s, content=%s", role ? role : "null", content ? content : "null");
|
||||
// ESP_LOGE(TAG, "SetChatMessage: role=%s, content=%s", role ? role : "null", content ? content : "null");
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
@@ -351,6 +368,20 @@ void StackChanAvatarDisplay::SetChatMessage(const char* role, const char* conten
|
||||
}
|
||||
}
|
||||
|
||||
void StackChanAvatarDisplay::ClearChatMessages()
|
||||
{
|
||||
auto& stackchan = GetStackChan();
|
||||
if (!stackchan.hasAvatar()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
stackchan.avatar().clearSpeech();
|
||||
|
||||
ESP_LOGI(TAG, "Chat messages cleared");
|
||||
}
|
||||
|
||||
void StackChanAvatarDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image)
|
||||
{
|
||||
DisplayLockGuard lock(this);
|
||||
@@ -380,6 +411,10 @@ void StackChanAvatarDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image)
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, 6000 * 1000));
|
||||
}
|
||||
|
||||
void StackChanAvatarDisplay::UpdateStatusBar(bool update_all)
|
||||
{
|
||||
}
|
||||
|
||||
void StackChanAvatarDisplay::SetTheme(Theme* theme)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetTheme: %s", theme->name().c_str());
|
||||
@@ -407,9 +442,7 @@ bool hal_bridge::is_xiaozhi_ready()
|
||||
|
||||
void StackChanAvatarDisplay::SetStatus(const char* status)
|
||||
{
|
||||
ESP_LOGE(TAG, "SetStatus: %s", status);
|
||||
|
||||
LvglDisplay::SetStatus(status);
|
||||
// ESP_LOGE(TAG, "SetStatus: %s", status);
|
||||
|
||||
auto& stackchan = GetStackChan();
|
||||
if (!stackchan.hasAvatar()) {
|
||||
@@ -491,3 +524,7 @@ void StackChanAvatarDisplay::SetStatus(const char* status)
|
||||
avatar.setSpeech("");
|
||||
}
|
||||
}
|
||||
|
||||
void StackChanAvatarDisplay::ShowNotification(const char* notification, int duration_ms)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -38,11 +38,14 @@ public:
|
||||
// Override Display methods to control Robot
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void ClearChatMessages() override;
|
||||
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
|
||||
virtual void UpdateStatusBar(bool update_all = false) override;
|
||||
virtual void SetupUI() override;
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
virtual void SetStatus(const char* status) override;
|
||||
virtual void ShowNotification(const char* notification, int duration_ms = 3000) override;
|
||||
|
||||
void SetupXiaoZhiUI();
|
||||
void LvglLock();
|
||||
void LvglUnlock();
|
||||
lv_disp_t* GetLvglDisplay();
|
||||
|
||||
+42
-28
@@ -48,6 +48,7 @@ void Hal::init()
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <system_info.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_timer.h>
|
||||
#include <esp_mac.h>
|
||||
@@ -86,8 +87,38 @@ void Hal::reboot()
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
static void _confirm_ota_image_if_stable()
|
||||
{
|
||||
constexpr uint32_t ota_confirm_delay_ms = 20000;
|
||||
static bool ota_confirm_checked = false;
|
||||
if (ota_confirm_checked || GetHAL().millis() < ota_confirm_delay_ms) {
|
||||
return;
|
||||
}
|
||||
ota_confirm_checked = true;
|
||||
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
if (running == nullptr) {
|
||||
mclog::tagError(_tag, "failed to get running partition for ota confirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
esp_ota_img_states_t ota_state;
|
||||
if (esp_ota_get_state_partition(running, &ota_state) != ESP_OK) {
|
||||
mclog::tagError(_tag, "failed to get ota state for partition: {}", running->label);
|
||||
return;
|
||||
}
|
||||
|
||||
mclog::tagInfo(_tag, "ota confirm check: partition={}, state={}", running->label, static_cast<int>(ota_state));
|
||||
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
mclog::tagInfo(_tag, "ota image is stable, marking current app valid");
|
||||
esp_ota_mark_app_valid_cancel_rollback();
|
||||
}
|
||||
}
|
||||
|
||||
void Hal::updateHeapStatusLog()
|
||||
{
|
||||
_confirm_ota_image_if_stable();
|
||||
|
||||
static uint32_t last_log_tick = 0;
|
||||
if (millis() - last_log_tick < 10000) {
|
||||
return;
|
||||
@@ -164,7 +195,7 @@ void Hal::startXiaozhi()
|
||||
});
|
||||
|
||||
// Start stackchan update task
|
||||
xTaskCreate(_stackchan_update_task, "stackchan", 4096, NULL, 5, NULL);
|
||||
xTaskCreatePinnedToCore(_stackchan_update_task, "stackchan", 4096, NULL, 5, NULL, 1);
|
||||
|
||||
hal_bridge::start_xiaozhi_app();
|
||||
}
|
||||
@@ -211,6 +242,16 @@ uint8_t Hal::getBackLightBrightness()
|
||||
return hal_bridge::board_get_backlight_brightness();
|
||||
}
|
||||
|
||||
void Hal::setSpeakerVolume(uint8_t volume, bool permanent)
|
||||
{
|
||||
hal_bridge::board_set_speaker_volume(volume, permanent);
|
||||
}
|
||||
|
||||
uint8_t Hal::getSpeakerVolume()
|
||||
{
|
||||
return hal_bridge::board_get_speaker_volume();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Lvgl */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@@ -252,33 +293,6 @@ void Hal::lvgl_init()
|
||||
hal_bridge::disply_lvgl_unlock();
|
||||
}
|
||||
|
||||
lv_timer_t* _timer_stackchan_update = NULL;
|
||||
|
||||
void Hal::startStackChanAutoUpdate(int fps)
|
||||
{
|
||||
mclog::tagInfo(_tag, "start stack chan auto update with fps: {}", fps);
|
||||
|
||||
if (_timer_stackchan_update) {
|
||||
mclog::tagWarn(_tag, "stack chan auto update already started");
|
||||
return;
|
||||
}
|
||||
|
||||
_timer_stackchan_update = lv_timer_create([](lv_timer_t* timer) { GetStackChan().update(); }, 1000 / fps, NULL);
|
||||
}
|
||||
|
||||
void Hal::stopStackChanAutoUpdate()
|
||||
{
|
||||
mclog::tagInfo(_tag, "stop stack chan auto update");
|
||||
|
||||
if (!_timer_stackchan_update) {
|
||||
mclog::tagWarn(_tag, "stack chan auto update already stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
lv_timer_delete(_timer_stackchan_update);
|
||||
_timer_stackchan_update = NULL;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Warm Reboot */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
+56
-6
@@ -73,6 +73,23 @@ enum class CommonLogLevel {
|
||||
Error,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
*/
|
||||
namespace app_center {
|
||||
|
||||
struct AppInfo_t {
|
||||
std::string name;
|
||||
std::string iconUrl;
|
||||
std::string description;
|
||||
std::string firmwareUrl;
|
||||
};
|
||||
|
||||
using AppInfoList_t = std::vector<AppInfo_t>;
|
||||
|
||||
}; // namespace app_center
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
@@ -84,6 +101,15 @@ enum class WifiStatus {
|
||||
High,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
*/
|
||||
struct UserAccountInfo_t {
|
||||
std::string username;
|
||||
std::string deviceName;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
@@ -97,6 +123,7 @@ public:
|
||||
_panel->setAlign(LV_ALIGN_CENTER);
|
||||
_panel->setBorderWidth(0);
|
||||
_panel->setBgOpa(0);
|
||||
_panel->setPaddingAll(0);
|
||||
|
||||
_label_logo = std::make_unique<uitk::lvgl_cpp::Label>(_panel->get());
|
||||
_label_logo->setTextFont(&lv_font_montserrat_24);
|
||||
@@ -109,12 +136,19 @@ public:
|
||||
_label_msg->setTextColor(lv_color_hex(0xBFBFBF));
|
||||
_label_msg->align(LV_ALIGN_CENTER, 0, 14);
|
||||
_label_msg->setText("Starting up ...");
|
||||
|
||||
_label_version = std::make_unique<uitk::lvgl_cpp::Label>(_panel->get());
|
||||
_label_version->setTextFont(&lv_font_montserrat_14);
|
||||
_label_version->setTextColor(lv_color_hex(0x8B8B8B));
|
||||
_label_version->align(LV_ALIGN_BOTTOM_RIGHT, -7, -6);
|
||||
_label_version->setText("V" FIRMWARE_VERSION);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<uitk::lvgl_cpp::Container> _panel;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Label> _label_logo;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Label> _label_msg;
|
||||
std::unique_ptr<uitk::lvgl_cpp::Label> _label_version;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,14 +201,11 @@ public:
|
||||
bool isBleConnected();
|
||||
void startAppConfigServer();
|
||||
bool isAppConfiged();
|
||||
void resetAppConfiged();
|
||||
|
||||
/* --------------------------------- HeadPet -------------------------------- */
|
||||
uitk::Signal<HeadPetGesture> onHeadPetGesture;
|
||||
|
||||
/* -------------------------------- StackChan ------------------------------- */
|
||||
void startStackChanAutoUpdate(int fps); // Start the auto update with lvgl timer
|
||||
void stopStackChanAutoUpdate();
|
||||
|
||||
/* ----------------------------------- RGB ---------------------------------- */
|
||||
void setRgbColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b);
|
||||
void showRgbColor(uint8_t r, uint8_t g, uint8_t b);
|
||||
@@ -205,7 +236,6 @@ public:
|
||||
void syncSystemTimeToRtc();
|
||||
void setTimezone(std::string_view tz);
|
||||
std::string getTimezone();
|
||||
void resetTimezoneConfig();
|
||||
|
||||
/* --------------------------------- EspNow --------------------------------- */
|
||||
uitk::Signal<const std::vector<uint8_t>&> onEspNowData;
|
||||
@@ -223,13 +253,33 @@ public:
|
||||
WifiStatus getWifiStatus();
|
||||
void startSntp();
|
||||
|
||||
/* -------------------------------- App center ------------------------------- */
|
||||
app_center::AppInfoList_t fetchAppList();
|
||||
void launchApp(std::string_view url, std::function<void(int)> onProgress);
|
||||
|
||||
/* --------------------------------- EzData --------------------------------- */
|
||||
void startEzDataService(std::function<void(std::string_view)> onStartLog);
|
||||
uitk::Signal<std::string_view> onEzdataPairCode;
|
||||
|
||||
/* ------------------------------- User Acount ------------------------------ */
|
||||
UserAccountInfo_t getUserAccountInfo();
|
||||
bool updateAccountInfo(std::function<void(std::string_view)> onLog);
|
||||
bool unbindAccount(std::function<void(std::string_view)> onLog);
|
||||
|
||||
/* ----------------------------------- OTA ---------------------------------- */
|
||||
bool updateFirmware(std::function<void(std::string_view)> onLog);
|
||||
|
||||
/* ---------------------------------- Audio --------------------------------- */
|
||||
void setSpeakerVolume(uint8_t volume, bool permanent = false);
|
||||
uint8_t getSpeakerVolume();
|
||||
|
||||
private:
|
||||
bool _xiaozhi_start_requested = false;
|
||||
|
||||
void xiaozhi_board_init();
|
||||
void lvgl_init();
|
||||
void xiaozhi_mcp_init();
|
||||
void ble_init();
|
||||
void ble_init(bool useAltUuid);
|
||||
void servo_init();
|
||||
void head_touch_init();
|
||||
void io_expander_init();
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "hal.h"
|
||||
#include "utils/secret_logic/secret_logic.h"
|
||||
#include <settings.h>
|
||||
#include <mooncake_log.h>
|
||||
#include <memory>
|
||||
#include <board.h>
|
||||
#include <cJSON.h>
|
||||
|
||||
static const std::string_view _tag = "HAL-Account";
|
||||
|
||||
static const std::string_view _setting_ns = "account";
|
||||
static const std::string_view _setting_username_key = "username";
|
||||
static const std::string_view _setting_device_name_key = "device_name";
|
||||
|
||||
static const std::string_view _get_user_account_info_url = "http://47.113.125.164:12800/stackChan/device/user";
|
||||
static const std::string_view _get_device_info_url = "http://47.113.125.164:12800/stackChan/device/info";
|
||||
static const std::string_view _unbind_user_account_info_url = "http://47.113.125.164:12800/stackChan/device/unbind";
|
||||
|
||||
static bool request_authorized_get(std::string_view token, std::string_view url, std::string &response,
|
||||
std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
auto &board = Board::GetInstance();
|
||||
auto network = board.GetNetwork();
|
||||
auto http = network->CreateHttp(0);
|
||||
|
||||
if (!http) {
|
||||
mclog::tagError(_tag, "failed to create http client");
|
||||
onLog("Failed to create HTTP client");
|
||||
return false;
|
||||
}
|
||||
|
||||
http->SetHeader("Authorization", std::string(token));
|
||||
if (!http->Open("GET", std::string(url))) {
|
||||
mclog::tagError(_tag, "failed to open http request: {}", url);
|
||||
onLog("Failed to connect to server");
|
||||
return false;
|
||||
}
|
||||
|
||||
int status_code = http->GetStatusCode();
|
||||
if (status_code != 200) {
|
||||
mclog::tagError(_tag, "http get failed, status code: {}, url: {}", status_code, url);
|
||||
onLog("HTTP Request Failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
response = http->ReadAll();
|
||||
mclog::tagInfo(_tag, "response from {}: {}", url, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool fetch_username(std::string_view token, std::string &username, std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
onLog("Updating user account info...");
|
||||
|
||||
std::string response;
|
||||
if (!request_authorized_get(token, _get_user_account_info_url, response, onLog)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse(response.c_str());
|
||||
if (!root) {
|
||||
mclog::tagError(_tag, "failed to parse user account json");
|
||||
onLog("Failed to parse response");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
cJSON *code = cJSON_GetObjectItem(root, "code");
|
||||
cJSON *data = cJSON_GetObjectItem(root, "data");
|
||||
|
||||
const char *username_value = nullptr;
|
||||
if (data && cJSON_IsString(data)) {
|
||||
username_value = data->valuestring;
|
||||
} else if (data && cJSON_IsObject(data)) {
|
||||
cJSON *username_json = cJSON_GetObjectItem(data, "username");
|
||||
if (username_json && cJSON_IsString(username_json)) {
|
||||
username_value = username_json->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
if (code && cJSON_IsNumber(code) && code->valueint == 0 && username_value != nullptr) {
|
||||
username = username_value;
|
||||
success = true;
|
||||
} else {
|
||||
mclog::tagError(_tag, "invalid user account response format or error code");
|
||||
onLog("Invalid response from server");
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool fetch_device_name(std::string_view token, std::string &deviceName,
|
||||
std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
onLog("Updating device info...");
|
||||
|
||||
std::string response;
|
||||
if (!request_authorized_get(token, _get_device_info_url, response, onLog)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse(response.c_str());
|
||||
if (!root) {
|
||||
mclog::tagError(_tag, "failed to parse device info json");
|
||||
onLog("Failed to parse response");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
cJSON *code = cJSON_GetObjectItem(root, "code");
|
||||
cJSON *data = cJSON_GetObjectItem(root, "data");
|
||||
cJSON *target = root;
|
||||
|
||||
if (code && cJSON_IsNumber(code)) {
|
||||
if (code->valueint != 0) {
|
||||
mclog::tagError(_tag, "device info request failed, code: {}", code->valueint);
|
||||
onLog("Invalid response from server");
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
if (data && cJSON_IsObject(data)) {
|
||||
target = data;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *name_json = cJSON_GetObjectItem(target, "name");
|
||||
if (name_json && cJSON_IsString(name_json)) {
|
||||
deviceName = name_json->valuestring;
|
||||
success = true;
|
||||
} else {
|
||||
mclog::tagError(_tag, "invalid device info response format");
|
||||
onLog("Invalid response from server");
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return success;
|
||||
}
|
||||
|
||||
UserAccountInfo_t Hal::getUserAccountInfo()
|
||||
{
|
||||
UserAccountInfo_t info;
|
||||
Settings settings(_setting_ns.data(), false);
|
||||
info.username = settings.GetString(_setting_username_key.data(), "Account Info");
|
||||
info.deviceName = settings.GetString(_setting_device_name_key.data(), "");
|
||||
return info;
|
||||
}
|
||||
|
||||
bool Hal::updateAccountInfo(std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
std::string token = secret_logic::generate_auth_token();
|
||||
|
||||
std::string username;
|
||||
if (!fetch_username(token, username, onLog)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string device_name;
|
||||
if (!fetch_device_name(token, device_name, onLog)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Settings settings(_setting_ns.data(), true);
|
||||
settings.SetString(_setting_username_key.data(), username);
|
||||
settings.SetString(_setting_device_name_key.data(), device_name);
|
||||
|
||||
mclog::tagInfo(_tag, "account updated: username={}, device_name={}", username, device_name);
|
||||
onLog(std::string("Account updated: ") + username);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Hal::unbindAccount(std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
std::string token = secret_logic::generate_auth_token();
|
||||
|
||||
auto &board = Board::GetInstance();
|
||||
auto network = board.GetNetwork();
|
||||
auto http = network->CreateHttp(0);
|
||||
|
||||
if (!http) {
|
||||
mclog::tagError(_tag, "failed to create http client");
|
||||
onLog("Failed to create HTTP client");
|
||||
return false;
|
||||
}
|
||||
|
||||
http->SetHeader("Authorization", token);
|
||||
http->SetContent(std::string());
|
||||
mclog::tagInfo(_tag, "requesting to unbind account...");
|
||||
onLog("Unbinding account...");
|
||||
|
||||
if (!http->Open("POST", std::string(_unbind_user_account_info_url))) {
|
||||
mclog::tagError(_tag, "failed to open http request");
|
||||
onLog("Failed to connect to server");
|
||||
return false;
|
||||
}
|
||||
|
||||
int status_code = http->GetStatusCode();
|
||||
if (status_code != 200) {
|
||||
mclog::tagError(_tag, "http post failed, status code: {}", status_code);
|
||||
onLog("HTTP Request Failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string response = http->ReadAll();
|
||||
mclog::tagInfo(_tag, "response: {}", response);
|
||||
|
||||
cJSON *root = cJSON_Parse(response.c_str());
|
||||
if (!root) {
|
||||
mclog::tagError(_tag, "failed to parse json");
|
||||
onLog("Failed to parse response");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
cJSON *code = cJSON_GetObjectItem(root, "code");
|
||||
|
||||
if (code && cJSON_IsNumber(code) && code->valueint == 0) {
|
||||
Settings settings(_setting_ns.data(), true);
|
||||
settings.SetString(_setting_username_key.data(), "Account Info");
|
||||
settings.SetString(_setting_device_name_key.data(), "");
|
||||
mclog::tagInfo(_tag, "account unbound successfully");
|
||||
onLog("Account unbound successfully");
|
||||
success = true;
|
||||
} else {
|
||||
mclog::tagError(_tag, "invalid response format or error code");
|
||||
cJSON *msg = cJSON_GetObjectItem(root, "message");
|
||||
if (msg && cJSON_IsString(msg)) {
|
||||
onLog(std::string("Unbind failed: ") + msg->valuestring);
|
||||
} else {
|
||||
onLog("Invalid response from server");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (success) {
|
||||
resetAppConfiged();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "hal.h"
|
||||
#include "utils/ota/ota.h"
|
||||
#include <mooncake_log.h>
|
||||
#include <memory>
|
||||
#include <board.h>
|
||||
#include <cJSON.h>
|
||||
|
||||
static const std::string_view _tag = "HAL-AppCenter";
|
||||
|
||||
static const std::string_view _app_info_list_url = "http://47.113.125.164:12800/stackChan/apps";
|
||||
|
||||
static const char *get_json_string(cJSON *item, std::initializer_list<const char *> keys)
|
||||
{
|
||||
for (const auto *key : keys) {
|
||||
cJSON *value = cJSON_GetObjectItemCaseSensitive(item, key);
|
||||
if (cJSON_IsString(value) && value->valuestring != nullptr) {
|
||||
return value->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
app_center::AppInfoList_t Hal::fetchAppList()
|
||||
{
|
||||
app_center::AppInfoList_t app_list;
|
||||
auto &board = Board::GetInstance();
|
||||
auto network = board.GetNetwork();
|
||||
|
||||
auto http = network->CreateHttp(0);
|
||||
if (!http->Open("GET", std::string(_app_info_list_url))) {
|
||||
mclog::tagError(_tag, "failed to open http connection");
|
||||
return app_list;
|
||||
}
|
||||
|
||||
if (http->GetStatusCode() != 200) {
|
||||
mclog::tagError(_tag, "failed to fetch app list, status code: {}", http->GetStatusCode());
|
||||
http->Close();
|
||||
return app_list;
|
||||
}
|
||||
|
||||
std::string data = http->ReadAll();
|
||||
http->Close();
|
||||
|
||||
cJSON *root = cJSON_Parse(data.c_str());
|
||||
if (root == NULL) {
|
||||
mclog::tagError(_tag, "failed to parse json response");
|
||||
return app_list;
|
||||
}
|
||||
|
||||
cJSON *app_array = nullptr;
|
||||
if (cJSON_IsArray(root)) {
|
||||
app_array = root;
|
||||
} else if (cJSON_IsObject(root)) {
|
||||
cJSON *code = cJSON_GetObjectItemCaseSensitive(root, "code");
|
||||
if (cJSON_IsNumber(code) && code->valueint != 0) {
|
||||
cJSON *message = cJSON_GetObjectItemCaseSensitive(root, "message");
|
||||
mclog::tagError(_tag, "failed to fetch app list, code: {}, message: {}", code->valueint,
|
||||
cJSON_IsString(message) ? message->valuestring : "unknown");
|
||||
cJSON_Delete(root);
|
||||
return app_list;
|
||||
}
|
||||
|
||||
app_array = cJSON_GetObjectItemCaseSensitive(root, "data");
|
||||
if (!cJSON_IsArray(app_array)) {
|
||||
mclog::tagError(_tag, "invalid app list response: data is not an array");
|
||||
cJSON_Delete(root);
|
||||
return app_list;
|
||||
}
|
||||
}
|
||||
|
||||
if (cJSON_IsArray(app_array)) {
|
||||
cJSON *item = NULL;
|
||||
cJSON_ArrayForEach(item, app_array)
|
||||
{
|
||||
app_center::AppInfo_t info;
|
||||
|
||||
if (const char *name = get_json_string(item, {"appName", "name"})) {
|
||||
info.name = name;
|
||||
}
|
||||
|
||||
if (const char *icon = get_json_string(item, {"iconUrl", "appIcon", "icon"})) {
|
||||
info.iconUrl = icon;
|
||||
}
|
||||
|
||||
if (const char *desc = get_json_string(item, {"description", "appDescription", "desc"})) {
|
||||
info.description = desc;
|
||||
}
|
||||
|
||||
if (const char *url = get_json_string(item, {"firmwareUrl", "downloadUrl", "otaUrl", "url"})) {
|
||||
info.firmwareUrl = url;
|
||||
}
|
||||
|
||||
app_list.push_back(info);
|
||||
}
|
||||
} else {
|
||||
mclog::tagError(_tag, "invalid app list response: unexpected json shape");
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return app_list;
|
||||
}
|
||||
|
||||
static std::function<void(int)> _on_progress = nullptr;
|
||||
static void ota_callback(int progress)
|
||||
{
|
||||
if (_on_progress) {
|
||||
_on_progress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
void Hal::launchApp(std::string_view url, std::function<void(int)> onProgress)
|
||||
{
|
||||
mclog::tagInfo(_tag, "launching app from url: {}", url);
|
||||
_on_progress = onProgress;
|
||||
start_ota_update(url.data(), ota_callback);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ static uint8_t _handle_ble_battery_read(void)
|
||||
return 96;
|
||||
}
|
||||
|
||||
void Hal::ble_init()
|
||||
void Hal::ble_init(bool useAltUuid)
|
||||
{
|
||||
mclog::tagInfo(_tag, "init");
|
||||
|
||||
@@ -61,7 +61,7 @@ void Hal::ble_init()
|
||||
};
|
||||
stackchan_ble_register_callbacks(&ble_callbacks);
|
||||
|
||||
ble_prph_init();
|
||||
ble_prph_init(useAltUuid);
|
||||
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_EFUSE_FACTORY);
|
||||
@@ -72,7 +72,7 @@ void Hal::ble_init()
|
||||
void Hal::startBleServer()
|
||||
{
|
||||
mclog::tagInfo(_tag, "start ble server");
|
||||
ble_init();
|
||||
ble_init(false);
|
||||
}
|
||||
|
||||
bool Hal::isBleConnected()
|
||||
@@ -273,7 +273,7 @@ void Hal::startAppConfigServer()
|
||||
{
|
||||
mclog::tagInfo(_tag, "start app config server");
|
||||
|
||||
ble_init();
|
||||
ble_init(true);
|
||||
|
||||
mooncake::GetMooncake().extensionManager()->createAbility(std::make_unique<AppConfigServerWorker>());
|
||||
}
|
||||
@@ -283,3 +283,9 @@ bool Hal::isAppConfiged()
|
||||
Settings settings("app_config", false);
|
||||
return settings.GetBool("is_configed", false);
|
||||
}
|
||||
|
||||
void Hal::resetAppConfiged()
|
||||
{
|
||||
Settings settings("app_config", true);
|
||||
settings.SetBool("is_configed", false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "hal.h"
|
||||
#include <stackchan/stackchan.h>
|
||||
#include <mooncake.h>
|
||||
#include <mooncake_log.h>
|
||||
#include <ArduinoJson.hpp>
|
||||
#include <cstdint>
|
||||
#include <board.h>
|
||||
#include <mqtt.h>
|
||||
#include <esp_log.h>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
using namespace stackchan;
|
||||
|
||||
static std::string _tag = "EzData";
|
||||
|
||||
class EzData {
|
||||
public:
|
||||
enum class CmdType : int {
|
||||
// --- 数据操作 (100-102) ---
|
||||
DeviceAddData = 100, ///< 设备端新增数据
|
||||
DeviceUpdateData = 101, ///< 设备端修改数据
|
||||
DeviceDeleteData = 102, ///< 设备端删除数据
|
||||
|
||||
// --- 数据查询 (103-104) ---
|
||||
DeviceQueryList = 103, ///< 设备端查询数据列表
|
||||
DeviceQueryDetail = 104, ///< 设备端查询数据详情
|
||||
|
||||
// --- 文件上传 (105) ---
|
||||
DeviceUploadFile = 105, ///< 设备端上传文件 (通知)
|
||||
|
||||
// --- 用户操作 (106-109) ---
|
||||
UserScanCode = 106, ///< 用户端扫码
|
||||
UserUpdateData = 107, ///< 用户端修改数据
|
||||
UserDeleteData = 108, ///< 用户端删除数据
|
||||
UserAddData = 109, ///< 用户端新增数据
|
||||
|
||||
// --- 设备操作 (112) ---
|
||||
DeviceGetMatchCode = 112, ///< 设备端获取匹配码
|
||||
|
||||
// --- 错误 (500) ---
|
||||
Error = 500 ///< 设备端请求错误
|
||||
};
|
||||
|
||||
std::function<void(void)> onConnected;
|
||||
std::function<void(std::string_view)> onPairCodeReceived;
|
||||
std::function<void(std::string_view, const ArduinoJson::JsonVariant&)> onUserUpdateData;
|
||||
|
||||
void init()
|
||||
{
|
||||
_connect();
|
||||
}
|
||||
|
||||
void update()
|
||||
{
|
||||
if (!_mqtt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_mqtt->IsConnected()) {
|
||||
if (GetHAL().millis() - _last_reconnect_attempt > 5000) {
|
||||
ESP_LOGI(_tag.c_str(), "Reconnecting...");
|
||||
_connect();
|
||||
}
|
||||
} else {
|
||||
_process_messages();
|
||||
_check_callbacks_timeout();
|
||||
}
|
||||
}
|
||||
|
||||
void sendPacket(CmdType type, const std::function<void(ArduinoJson::JsonObject&)>& bodyBuilder = nullptr)
|
||||
{
|
||||
if (!_mqtt || !_mqtt->IsConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArduinoJson::JsonDocument doc;
|
||||
doc["deviceToken"] = _token;
|
||||
|
||||
ArduinoJson::JsonObject body = doc["body"].to<ArduinoJson::JsonObject>();
|
||||
body["requestType"] = static_cast<int>(type);
|
||||
|
||||
if (bodyBuilder) {
|
||||
bodyBuilder(body);
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
ArduinoJson::serializeJson(doc, payload);
|
||||
|
||||
ESP_LOGI(_tag.c_str(), "Sending Packet Type: %d", (int)type);
|
||||
_mqtt->Publish(_pub_topic, payload, 0);
|
||||
}
|
||||
|
||||
void requestPairCode()
|
||||
{
|
||||
ESP_LOGI(_tag.c_str(), "Requesting pair code");
|
||||
sendPacket(CmdType::DeviceGetMatchCode);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void addData(std::string_view name, T value)
|
||||
{
|
||||
sendPacket(CmdType::DeviceAddData, [&name, &value](ArduinoJson::JsonObject& body) {
|
||||
body["name"] = name;
|
||||
body["value"] = value;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void modifyData(std::string_view name, T value)
|
||||
{
|
||||
sendPacket(CmdType::DeviceUpdateData, [&name, &value](ArduinoJson::JsonObject& body) {
|
||||
body["name"] = name;
|
||||
body["value"] = value;
|
||||
});
|
||||
}
|
||||
|
||||
void getData(std::string_view name, std::function<void(const ArduinoJson::JsonVariant&)> onData,
|
||||
std::function<void()> onFailed)
|
||||
{
|
||||
// 注册回调
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_callbacks_mutex);
|
||||
_get_data_callbacks[name.data()] = {onData, onFailed, GetHAL().millis()};
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
sendPacket(CmdType::DeviceQueryDetail, [&name](ArduinoJson::JsonObject& body) { body["name"] = name; });
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Mqtt> _mqtt;
|
||||
uint32_t _last_reconnect_attempt = 0;
|
||||
std::string _sub_topic;
|
||||
std::string _pub_topic;
|
||||
std::string _token;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::queue<std::string> _msg_queue;
|
||||
|
||||
struct GetDataCallback_t {
|
||||
std::function<void(const ArduinoJson::JsonVariant&)> onData;
|
||||
std::function<void()> onFailed;
|
||||
uint32_t requestTime;
|
||||
};
|
||||
std::mutex _callbacks_mutex;
|
||||
std::map<std::string, GetDataCallback_t> _get_data_callbacks;
|
||||
|
||||
std::string _get_device_token()
|
||||
{
|
||||
const std::string url = "https://ezdata2.m5stack.com/api/v2/device/registerMac";
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
auto network = board.GetNetwork();
|
||||
|
||||
auto http = network->CreateHttp(0);
|
||||
if (!http) {
|
||||
ESP_LOGE(_tag.c_str(), "failed to create HTTP instance");
|
||||
return "";
|
||||
}
|
||||
|
||||
ArduinoJson::JsonDocument doc;
|
||||
doc["deviceType"] = "CoreS3";
|
||||
doc["mac"] = GetHAL().getFactoryMacString();
|
||||
|
||||
std::string payload;
|
||||
ArduinoJson::serializeJson(doc, payload);
|
||||
|
||||
http->SetHeader("Content-Type", "application/json");
|
||||
http->SetContent(std::move(payload));
|
||||
|
||||
if (!http->Open("POST", url)) {
|
||||
ESP_LOGE(_tag.c_str(), "failed to open HTTP connection");
|
||||
return "";
|
||||
}
|
||||
|
||||
int status_code = http->GetStatusCode();
|
||||
|
||||
if (status_code != 200) {
|
||||
ESP_LOGE(_tag.c_str(), "HTTP request failed, status code: %d", status_code);
|
||||
http->Close();
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string response = http->ReadAll();
|
||||
http->Close();
|
||||
|
||||
doc.clear();
|
||||
auto error = ArduinoJson::deserializeJson(doc, response);
|
||||
if (error) {
|
||||
ESP_LOGE(_tag.c_str(), "failed to parse JSON response: %s", error.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
if (doc["code"] == 200 && doc["msg"] == "OK") {
|
||||
if (doc["data"].is<std::string>()) {
|
||||
return doc["data"].as<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(_tag.c_str(), "failed to get token from response");
|
||||
return "";
|
||||
}
|
||||
|
||||
void _connect()
|
||||
{
|
||||
_mqtt.reset();
|
||||
|
||||
_token = _get_device_token();
|
||||
if (_token.empty()) {
|
||||
ESP_LOGE(_tag.c_str(), "failed to get device token");
|
||||
_last_reconnect_attempt = GetHAL().millis();
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(_tag.c_str(), "get token %s", _token.c_str());
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
auto network = board.GetNetwork();
|
||||
|
||||
_mqtt = network->CreateMqtt(1);
|
||||
if (!_mqtt) {
|
||||
ESP_LOGE(_tag.c_str(), "Failed to create MQTT instance");
|
||||
_last_reconnect_attempt = GetHAL().millis();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string mac = GetHAL().getFactoryMacString();
|
||||
std::string client_id = fmt::format("ez{}ez", mac);
|
||||
_sub_topic = fmt::format("$ezdata/{}/down", _token);
|
||||
_pub_topic = fmt::format("$ezdata/{}/up", _token);
|
||||
|
||||
_mqtt->OnConnected([this]() {
|
||||
ESP_LOGI(_tag.c_str(), "Connected");
|
||||
_mqtt->Subscribe(_sub_topic, 0);
|
||||
ESP_LOGI(_tag.c_str(), "Subscribed: %s", _sub_topic.c_str());
|
||||
|
||||
if (onConnected) {
|
||||
onConnected();
|
||||
}
|
||||
});
|
||||
|
||||
_mqtt->OnDisconnected([this]() { ESP_LOGI(_tag.c_str(), "Disconnected"); });
|
||||
|
||||
_mqtt->OnMessage([this](const std::string&, const std::string& payload) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_msg_queue.push(payload);
|
||||
});
|
||||
|
||||
ESP_LOGI(_tag.c_str(), "Connecting to EzData as %s", client_id.c_str());
|
||||
_mqtt->Connect("uiflow2.m5stack.com", 1883, client_id, _token, "");
|
||||
|
||||
_last_reconnect_attempt = GetHAL().millis();
|
||||
}
|
||||
|
||||
void _check_callbacks_timeout()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_callbacks_mutex);
|
||||
uint32_t now = GetHAL().millis();
|
||||
for (auto it = _get_data_callbacks.begin(); it != _get_data_callbacks.end();) {
|
||||
if (now - it->second.requestTime > 5000) {
|
||||
ESP_LOGW(_tag.c_str(), "GetData timeout for %s", it->first.c_str());
|
||||
if (it->second.onFailed) {
|
||||
it->second.onFailed();
|
||||
}
|
||||
it = _get_data_callbacks.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _process_messages()
|
||||
{
|
||||
std::vector<std::string> messages;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
while (!_msg_queue.empty()) {
|
||||
messages.push_back(std::move(_msg_queue.front()));
|
||||
_msg_queue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& msg : messages) {
|
||||
_handle_message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void _handle_message(std::string_view payload)
|
||||
{
|
||||
ArduinoJson::JsonDocument doc;
|
||||
auto error = ArduinoJson::deserializeJson(doc, payload);
|
||||
if (error) {
|
||||
ESP_LOGE(_tag.c_str(), "DeserializeJson failed: %s", error.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc["code"].is<int>()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int code = doc["code"];
|
||||
if (code != 200) {
|
||||
ESP_LOGW(_tag.c_str(), "EzData response error code: %d", code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc["cmd"].is<int>()) {
|
||||
return;
|
||||
}
|
||||
CmdType cmd = static_cast<CmdType>(doc["cmd"].as<int>());
|
||||
// auto body = doc["body"];
|
||||
|
||||
ESP_LOGI(_tag.c_str(), "Received Cmd: %d", (int)cmd);
|
||||
|
||||
switch (cmd) {
|
||||
case CmdType::DeviceQueryList:
|
||||
ESP_LOGI(_tag.c_str(), "Device list received");
|
||||
break;
|
||||
case CmdType::DeviceGetMatchCode: {
|
||||
if (doc["body"]["pairCode"].is<std::string>()) {
|
||||
std::string pair_code = doc["body"]["pairCode"].as<std::string>();
|
||||
ESP_LOGI(_tag.c_str(), "Pair code: %s", pair_code.c_str());
|
||||
|
||||
if (onPairCodeReceived) {
|
||||
onPairCodeReceived(pair_code);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CmdType::DeviceQueryDetail: {
|
||||
// {"body": {"createTime": 1751274100000, "dataToken": "...", "id": "...", "name": "adasd",
|
||||
// "updateTime": 1751274100000, "value": "ddsad"}}
|
||||
if (doc["body"].is<ArduinoJson::JsonObject>()) {
|
||||
std::string name;
|
||||
ArduinoJson::JsonVariant value = doc["body"]["value"];
|
||||
|
||||
if (doc["body"]["name"].is<std::string>()) {
|
||||
name = doc["body"]["name"].as<std::string>();
|
||||
}
|
||||
|
||||
if (!name.empty()) {
|
||||
std::lock_guard<std::mutex> lock(_callbacks_mutex);
|
||||
auto it = _get_data_callbacks.find(name);
|
||||
if (it != _get_data_callbacks.end()) {
|
||||
if (it->second.onData) {
|
||||
it->second.onData(value);
|
||||
}
|
||||
_get_data_callbacks.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CmdType::UserUpdateData: {
|
||||
if (doc["body"].is<ArduinoJson::JsonObject>()) {
|
||||
std::string name;
|
||||
ArduinoJson::JsonVariant value = doc["body"]["value"];
|
||||
|
||||
if (doc["body"]["name"].is<std::string>()) {
|
||||
name = doc["body"]["name"].as<std::string>();
|
||||
}
|
||||
|
||||
if (!name.empty() && onUserUpdateData) {
|
||||
onUserUpdateData(name, value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class EzdataWorker : public mooncake::BasicAbility {
|
||||
public:
|
||||
EzdataWorker()
|
||||
{
|
||||
_service = std::make_unique<EzData>();
|
||||
|
||||
_service->onConnected = [this]() { _service->requestPairCode(); };
|
||||
_service->onPairCodeReceived = [this](std::string_view pairCode) { GetHAL().onEzdataPairCode.emit(pairCode); };
|
||||
_service->onUserUpdateData = [this](std::string_view name, const ArduinoJson::JsonVariant& value) {
|
||||
handle_user_update_data(name, value);
|
||||
};
|
||||
|
||||
_service->init();
|
||||
|
||||
setup_data();
|
||||
}
|
||||
|
||||
void onRunning() override
|
||||
{
|
||||
_service->update();
|
||||
}
|
||||
|
||||
void onDestroy() override
|
||||
{
|
||||
_service.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<EzData> _service;
|
||||
int _pitch_servo_speed = 500;
|
||||
int _yaw_servo_speed = 500;
|
||||
|
||||
const std::string_view KEY_PITCH_SERVO_ANGLE = "SERVO.Y.ANGLE";
|
||||
const std::string_view KEY_PITCH_SERVO_SPEED = "SERVO.Y.SPEED";
|
||||
const std::string_view KEY_YAW_SERVO_ANGLE = "SERVO.X.ANGLE";
|
||||
const std::string_view KEY_YAW_SERVO_SPEED = "SERVO.X.SPEED";
|
||||
|
||||
void setup_data()
|
||||
{
|
||||
auto& motion = GetStackChan().motion();
|
||||
|
||||
_service->addData(KEY_PITCH_SERVO_ANGLE, ((float)motion.getCurrentPitchAngle() / 10.0f));
|
||||
_service->addData(KEY_YAW_SERVO_ANGLE, ((float)motion.getCurrentYawAngle() / 10.0f));
|
||||
_service->addData(KEY_PITCH_SERVO_SPEED, _pitch_servo_speed);
|
||||
_service->addData(KEY_YAW_SERVO_SPEED, _yaw_servo_speed);
|
||||
}
|
||||
|
||||
void handle_user_update_data(std::string_view name, const ArduinoJson::JsonVariant& value)
|
||||
{
|
||||
mclog::tagInfo(_tag, "on user update data {}", name);
|
||||
|
||||
auto& motion = GetStackChan().motion();
|
||||
|
||||
if (name == KEY_PITCH_SERVO_ANGLE) {
|
||||
if (value.is<float>()) {
|
||||
float angle = value.as<float>();
|
||||
motion.movePitchWithSpeed(angle * 10, _pitch_servo_speed);
|
||||
}
|
||||
} else if (name == KEY_YAW_SERVO_ANGLE) {
|
||||
if (value.is<float>()) {
|
||||
float angle = value.as<float>();
|
||||
motion.moveYawWithSpeed(angle * 10, _yaw_servo_speed);
|
||||
}
|
||||
} else if (name == KEY_PITCH_SERVO_SPEED) {
|
||||
if (value.is<int>()) {
|
||||
_pitch_servo_speed = value.as<int>();
|
||||
}
|
||||
} else if (name == KEY_YAW_SERVO_SPEED) {
|
||||
if (value.is<int>()) {
|
||||
_yaw_servo_speed = value.as<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void Hal::startEzDataService(std::function<void(std::string_view)> onStartLog)
|
||||
{
|
||||
mclog::tagInfo(_tag, "start ezdata service");
|
||||
|
||||
startNetwork(onStartLog);
|
||||
|
||||
onStartLog("Connecting to\nserver...");
|
||||
mooncake::GetMooncake().extensionManager()->createAbility(std::make_unique<EzdataWorker>());
|
||||
}
|
||||
@@ -15,8 +15,10 @@
|
||||
#include <ctime>
|
||||
#include <sys/time.h>
|
||||
#include <esp_sntp.h>
|
||||
#include <atomic>
|
||||
|
||||
static std::string _tag = "Network";
|
||||
static std::string _tag = "Network";
|
||||
static bool _is_network_connected = false;
|
||||
|
||||
static void time_sync_notification_cb(struct timeval* tv)
|
||||
{
|
||||
@@ -44,6 +46,11 @@ void Hal::startSntp()
|
||||
|
||||
void Hal::startNetwork(std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
if (_is_network_connected) {
|
||||
mclog::tagInfo(_tag, "network already connected");
|
||||
return;
|
||||
}
|
||||
|
||||
std::atomic<bool> network_connected = false;
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
@@ -108,6 +115,8 @@ void Hal::startNetwork(std::function<void(std::string_view)> onLog)
|
||||
board.SetNetworkEventCallback(nullptr);
|
||||
|
||||
startSntp();
|
||||
|
||||
_is_network_connected = true;
|
||||
}
|
||||
|
||||
WifiStatus Hal::getWifiStatus()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "hal.h"
|
||||
#include <mooncake_log.h>
|
||||
#include <memory>
|
||||
#include <ota.h>
|
||||
|
||||
static const std::string_view _tag = "HAL-OTA";
|
||||
|
||||
bool Hal::updateFirmware(std::function<void(std::string_view)> onLog)
|
||||
{
|
||||
onLog("Checking firmware updates...");
|
||||
|
||||
Ota ota;
|
||||
esp_err_t err = ota.CheckVersion();
|
||||
if (err != ESP_OK) {
|
||||
mclog::tagError(_tag, "failed to check firmware version: {}", esp_err_to_name(err));
|
||||
onLog("Failed to check firmware updates");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ota.HasNewVersion()) {
|
||||
ota.MarkCurrentVersionValid();
|
||||
mclog::tagInfo(_tag, "no new firmware version available");
|
||||
onLog("Already up to date");
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string &firmware_url = ota.GetFirmwareUrl();
|
||||
const std::string &firmware_version = ota.GetFirmwareVersion();
|
||||
if (firmware_url.empty()) {
|
||||
mclog::tagError(_tag, "firmware update available but url is empty");
|
||||
onLog("Invalid firmware update info");
|
||||
return false;
|
||||
}
|
||||
|
||||
mclog::tagInfo(_tag, "new firmware available: version={}, url={}", firmware_version, firmware_url);
|
||||
if (!firmware_version.empty()) {
|
||||
onLog(std::string("New firmware found: ") + firmware_version);
|
||||
} else {
|
||||
onLog("New firmware found");
|
||||
}
|
||||
|
||||
onLog("Starting firmware upgrade...");
|
||||
bool upgrade_success = Ota::Upgrade(firmware_url, [&](int progress, size_t speed) {
|
||||
auto msg = fmt::format("Upgrading firmware: {}% at {}KB/s", progress, speed / 1024);
|
||||
mclog::tagInfo(_tag, "upgrade progress: {}", msg);
|
||||
onLog(msg);
|
||||
});
|
||||
|
||||
if (!upgrade_success) {
|
||||
mclog::tagError(_tag, "firmware upgrade failed: version={}, url={}", firmware_version, firmware_url);
|
||||
onLog("Firmware upgrade failed, rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
reboot();
|
||||
return false;
|
||||
}
|
||||
|
||||
mclog::tagInfo(_tag, "firmware upgrade successful, rebooting");
|
||||
onLog("Upgrade successful, rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
reboot();
|
||||
return true;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ void Hal::servo_init()
|
||||
|
||||
ServoConfig_t yaw_servo_config;
|
||||
yaw_servo_config.id = 1;
|
||||
yaw_servo_config.defaultZeroPos = 450;
|
||||
yaw_servo_config.defaultZeroPos = 460;
|
||||
yaw_servo_config.angleLimit = Vector2i(-1280, 1280);
|
||||
yaw_servo_config.rawPosLimit = Vector2i(0, 1000);
|
||||
yaw_servo_config.settingNs = "servo";
|
||||
@@ -179,7 +179,7 @@ void Hal::servo_init()
|
||||
|
||||
ServoConfig_t pitch_servo_config;
|
||||
pitch_servo_config.id = 2;
|
||||
pitch_servo_config.defaultZeroPos = 125;
|
||||
pitch_servo_config.defaultZeroPos = 620;
|
||||
pitch_servo_config.angleLimit = Vector2i(0, 900);
|
||||
pitch_servo_config.rawPosLimit = Vector2i(0, 1000);
|
||||
pitch_servo_config.settingNs = "servo";
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <lvgl_image.h>
|
||||
#include <wifi_manager.h>
|
||||
#include "utils/jpeg_to_image/jpeg_decoder.h"
|
||||
#include "audio/audio_service.h"
|
||||
#include "utils/secret_logic/secret_logic.h"
|
||||
|
||||
static std::string _tag = "WS-Avatar";
|
||||
@@ -94,9 +93,6 @@ public:
|
||||
ESP_LOGI(_tag.c_str(), "Sending EndCall");
|
||||
sendPacket(DataType::EndCall, nullptr, 0);
|
||||
});
|
||||
|
||||
// Initialize audio service
|
||||
setupAudio();
|
||||
}
|
||||
|
||||
void connect()
|
||||
@@ -134,18 +130,6 @@ public:
|
||||
});
|
||||
|
||||
_websocket->OnData([this](const char* data, size_t len, bool binary) {
|
||||
// Fast path for Audio packets to avoid jitter from main thread loop
|
||||
if (binary && len > 5 && static_cast<DataType>(data[0]) == DataType::Opus) {
|
||||
// Determine raw pointer and size safely
|
||||
const uint8_t* u_data = reinterpret_cast<const uint8_t*>(data);
|
||||
|
||||
// Directly construct and push to AudioService
|
||||
auto packet = std::make_unique<AudioStreamPacket>();
|
||||
packet->payload.assign(u_data + 5, u_data + len);
|
||||
_audio_service.PushPacketToDecodeQueue(std::move(packet));
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_msg_queue.push({binary, std::vector<uint8_t>(data, data + len)});
|
||||
});
|
||||
@@ -373,13 +357,9 @@ public:
|
||||
break;
|
||||
}
|
||||
case DataType::StartAudioStream: {
|
||||
ESP_LOGI(_tag.c_str(), "Start Audio Stream");
|
||||
_is_audio_streaming = true;
|
||||
break;
|
||||
}
|
||||
case DataType::StopAudioStream: {
|
||||
ESP_LOGI(_tag.c_str(), "Stop Audio Stream");
|
||||
_is_audio_streaming = false;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -452,44 +432,11 @@ private:
|
||||
uint32_t _last_heartbeat_time = 0;
|
||||
bool _is_streaming = false;
|
||||
bool _is_video_mode = false;
|
||||
std::atomic<bool> _is_audio_streaming{false};
|
||||
std::mutex _mutex;
|
||||
std::queue<ReceivedMessage> _msg_queue;
|
||||
|
||||
// Audio
|
||||
AudioService _audio_service;
|
||||
std::mutex _send_mutex;
|
||||
|
||||
void setupAudio()
|
||||
{
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
_audio_service.Initialize(codec);
|
||||
|
||||
AudioServiceCallbacks callbacks;
|
||||
callbacks.on_send_queue_available = [this]() {
|
||||
while (true) {
|
||||
auto packet = _audio_service.PopPacketFromSendQueue();
|
||||
if (!packet) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Only send audio if streaming is enabled
|
||||
if (_is_audio_streaming) {
|
||||
if (!packet->payload.empty()) {
|
||||
sendPacket(DataType::Opus, packet->payload.data(), packet->payload.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_audio_service.SetCallbacks(callbacks);
|
||||
|
||||
codec->SetInputGain(0.0f);
|
||||
_audio_service.Start();
|
||||
_audio_service.EnableVoiceProcessing(true);
|
||||
_audio_service.EnableDeviceAec(true);
|
||||
}
|
||||
|
||||
void sendPacket(DataType type, const uint8_t* data, size_t len)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_send_mutex);
|
||||
|
||||
@@ -32,8 +32,35 @@
|
||||
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
static uint8_t ext_adv_pattern_1[] = {
|
||||
0x02, 0x01, 0x06, 0x03, 0x03, 0xab, 0xcd, 0x03, 0x03, 0x18, 0x11, 0x11, 0X09, 'n', 'i',
|
||||
'm', 'b', 'l', 'e', '-', 'b', 'l', 'e', 'p', 'r', 'p', 'h', '-', 'e',
|
||||
0x02,
|
||||
BLE_HS_ADV_TYPE_FLAGS,
|
||||
0x06,
|
||||
0x03,
|
||||
BLE_HS_ADV_TYPE_COMP_UUIDS16,
|
||||
0xab,
|
||||
0xcd,
|
||||
0x03,
|
||||
BLE_HS_ADV_TYPE_COMP_UUIDS16,
|
||||
0x18,
|
||||
0x11,
|
||||
0x11,
|
||||
BLE_HS_ADV_TYPE_COMP_NAME,
|
||||
'n',
|
||||
'i',
|
||||
'm',
|
||||
'b',
|
||||
'l',
|
||||
'e',
|
||||
'-',
|
||||
'b',
|
||||
'l',
|
||||
'e',
|
||||
'p',
|
||||
'r',
|
||||
'p',
|
||||
'h',
|
||||
'-',
|
||||
'e',
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -50,6 +77,8 @@ static uint16_t cids[MYNEWT_VAL(BLE_EATT_CHAN_NUM)];
|
||||
static uint16_t bearers;
|
||||
#endif
|
||||
|
||||
static bool s_use_alt_uuid = false;
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
#if NIMBLE_BLE_CONNECT
|
||||
@@ -103,8 +132,8 @@ static void ext_bleprph_advertise(void)
|
||||
|
||||
params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
params.secondary_phy = BLE_HCI_LE_PHY_2M;
|
||||
// params.tx_power = 127;
|
||||
params.sid = 1;
|
||||
params.tx_power = 127;
|
||||
params.sid = 1;
|
||||
|
||||
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
@@ -229,10 +258,17 @@ static void bleprph_advertise(void)
|
||||
// fields.num_uuids16 = 1;
|
||||
// fields.uuids16_is_complete = 1;
|
||||
|
||||
ble_uuid128_t stackchan_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
fields.uuids128 = &stackchan_uuid;
|
||||
fields.num_uuids128 = 1;
|
||||
fields.uuids128_is_complete = 1;
|
||||
ble_uuid128_t stackchan_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
ble_uuid128_t stackchan_uuid_alt = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE_ALT);
|
||||
|
||||
if (s_use_alt_uuid) {
|
||||
fields.uuids128 = &stackchan_uuid_alt;
|
||||
} else {
|
||||
fields.uuids128 = &stackchan_uuid;
|
||||
}
|
||||
|
||||
fields.num_uuids128 = 1;
|
||||
fields.uuids128_is_complete = 1;
|
||||
|
||||
rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc != 0) {
|
||||
@@ -600,8 +636,9 @@ void bleprph_host_task(void *param)
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
void ble_prph_init(void)
|
||||
void ble_prph_init(bool use_alt_uuid)
|
||||
{
|
||||
s_use_alt_uuid = use_alt_uuid;
|
||||
int rc;
|
||||
esp_err_t ret;
|
||||
|
||||
@@ -647,14 +684,18 @@ void ble_prph_init(void)
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ID;
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(STATIC_PASSKEY) && NIMBLE_BLE_CONNECT
|
||||
ble_sm_configure_static_passkey(456789, true);
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_GATTS)
|
||||
rc = gatt_svr_init();
|
||||
rc = gatt_svr_init(use_alt_uuid);
|
||||
assert(rc == 0);
|
||||
#endif
|
||||
|
||||
#if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
/* Set the default device name. */
|
||||
rc = ble_svc_gap_device_name_set("Stack-Chan");
|
||||
rc = ble_svc_gap_device_name_set("StackChan");
|
||||
assert(rc == 0);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ struct ble_gatt_register_ctxt;
|
||||
#define STACKCHAN_SVC_UUID_BASE \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe0, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Service UUID Alt: e2e5e5ff-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_SVC_UUID_BASE_ALT \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xff, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Motion Characteristic UUID: e2e5e5e1-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_CHR_MOTION_UUID \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe1, 0xe5, 0xe5, 0xe2
|
||||
@@ -160,9 +164,9 @@ void stackchan_ble_set_conn_handle(uint16_t conn_handle);
|
||||
bool stackchan_ble_is_connected(void);
|
||||
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
int gatt_svr_init(void);
|
||||
int gatt_svr_init(bool use_alt_uuid);
|
||||
|
||||
void ble_prph_init(void);
|
||||
void ble_prph_init(bool use_alt_uuid);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
#define MAX_NOTIFY 5
|
||||
|
||||
/* Stack-Chan Service */
|
||||
static const ble_uuid128_t stackchan_svc_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
static const ble_uuid128_t stackchan_svc_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
static const ble_uuid128_t stackchan_svc_uuid_alt = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE_ALT);
|
||||
|
||||
static const ble_uuid128_t stackchan_chr_motion_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_MOTION_UUID);
|
||||
|
||||
@@ -77,7 +78,7 @@ static int stackchan_svc_access(uint16_t conn_handle, uint16_t attr_handle, stru
|
||||
|
||||
static int battery_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
static struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
{
|
||||
/*** Stack-Chan Service ***/
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
@@ -321,7 +322,7 @@ void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
int gatt_svr_init(void)
|
||||
int gatt_svr_init(bool use_alt_uuid)
|
||||
{
|
||||
int rc;
|
||||
|
||||
@@ -339,6 +340,12 @@ int gatt_svr_init(void)
|
||||
ble_svc_gap_init();
|
||||
ble_svc_gatt_init();
|
||||
|
||||
if (use_alt_uuid) {
|
||||
gatt_svr_svcs[0].uuid = &stackchan_svc_uuid_alt.u;
|
||||
} else {
|
||||
gatt_svr_svcs[0].uuid = &stackchan_svc_uuid.u;
|
||||
}
|
||||
|
||||
rc = ble_gatts_count_cfg(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
@@ -32,7 +32,7 @@ static int enter_passkey_handler(int argc, char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%s", pkey);
|
||||
sscanf(argv[1], "%7s", pkey);
|
||||
ESP_LOGI("You entered", "%s %s", argv[0], argv[1]);
|
||||
num = pkey[0];
|
||||
|
||||
@@ -45,7 +45,9 @@ static int enter_passkey_handler(int argc, char *argv[])
|
||||
xQueueSend(cli_handle, &key, 0);
|
||||
}
|
||||
} else {
|
||||
sscanf(pkey, "%d", &key);
|
||||
if (sscanf(pkey, "%d", &key) != 1) {
|
||||
key = 0;
|
||||
}
|
||||
xQueueSend(cli_handle, &key, 0);
|
||||
}
|
||||
|
||||
@@ -84,14 +86,14 @@ static void scli_task(void *arg)
|
||||
QueueHandle_t uart_queue;
|
||||
uart_event_t event;
|
||||
|
||||
uart_driver_install(uart_num, 256, 0, 8, &uart_queue, 0);
|
||||
ESP_ERROR_CHECK(uart_driver_install(uart_num, 256, 0, 8, &uart_queue, 0));
|
||||
/* Initialize the console */
|
||||
esp_console_config_t console_config = {
|
||||
.max_cmdline_args = 8,
|
||||
.max_cmdline_length = 256,
|
||||
};
|
||||
|
||||
esp_console_init(&console_config);
|
||||
ESP_ERROR_CHECK(esp_console_init(&console_config));
|
||||
|
||||
while (!stop) {
|
||||
i = 0;
|
||||
@@ -107,6 +109,9 @@ static void scli_task(void *arg)
|
||||
}
|
||||
if (event.type == UART_DATA) {
|
||||
while (uart_read_bytes(uart_num, (uint8_t *)&linebuf[i], 1, 0)) {
|
||||
if (i >= sizeof(linebuf) - 1) {
|
||||
break;
|
||||
}
|
||||
if (linebuf[i] == '\r') {
|
||||
uart_write_bytes(uart_num, "\r\n", 2);
|
||||
} else {
|
||||
@@ -115,13 +120,17 @@ static void scli_task(void *arg)
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} while ((i < 255) && linebuf[i - 1] != '\r');
|
||||
|
||||
} while ((i < 255) && (i == 0 || linebuf[i - 1] != '\r'));
|
||||
if (stop) {
|
||||
break;
|
||||
}
|
||||
/* Remove the truncating \r\n */
|
||||
linebuf[strlen((char *)linebuf) - 1] = '\0';
|
||||
ret = esp_console_run((char *)linebuf, &cmd_ret);
|
||||
size_t len = strlen((char *)linebuf);
|
||||
if (len > 0) {
|
||||
linebuf[len - 1] = '\0';
|
||||
}
|
||||
ret = esp_console_run((char *)linebuf, &cmd_ret);
|
||||
if (ret < 0) {
|
||||
break;
|
||||
}
|
||||
@@ -144,3 +153,40 @@ int scli_init(void)
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int scli_deinit(void)
|
||||
{
|
||||
if (cli_task == NULL) {
|
||||
return ESP_OK; // Already deinitialized
|
||||
}
|
||||
|
||||
// Signal task to exit
|
||||
stop = 1;
|
||||
|
||||
// Wait for task to exit (it will clean up UART and console)
|
||||
int timeout_ms = 200;
|
||||
while (timeout_ms > 0 && cli_task != NULL && eTaskGetState(cli_task) != eDeleted) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
timeout_ms -= 10;
|
||||
}
|
||||
|
||||
// Force delete if still running (shouldn't happen if task exits properly)
|
||||
if (cli_task != NULL && eTaskGetState(cli_task) != eDeleted) {
|
||||
vTaskDelete(cli_task);
|
||||
// If force-deleted, clean up resources manually
|
||||
uart_driver_delete(0);
|
||||
esp_console_deinit();
|
||||
}
|
||||
cli_task = NULL;
|
||||
|
||||
// Clean up queue
|
||||
if (cli_handle != NULL) {
|
||||
vQueueDelete(cli_handle);
|
||||
cli_handle = NULL;
|
||||
}
|
||||
|
||||
// Reset stop flag
|
||||
stop = 0;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user