update firmware v1.2.4 (#16)

This commit is contained in:
Forairaaaaa
2026-04-20 16:27:36 +08:00
committed by GitHub
parent 605b575fcc
commit dd34f9e0ec
94 changed files with 3615 additions and 41513 deletions
@@ -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");
}
-8
View File
@@ -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
+2 -1
View File
@@ -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);
+23 -1
View File
@@ -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();
+44 -7
View File
@@ -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)
{
}
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+246
View File
@@ -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;
}
+122
View File
@@ -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);
}
+10 -4
View File
@@ -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);
}
+465
View File
@@ -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>());
}
+10 -1
View File
@@ -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()
+67
View File
@@ -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;
}
+2 -2
View File
@@ -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";
-53
View File
@@ -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);
+52 -11
View File
@@ -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
+6 -2
View File
@@ -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
}
+10 -3
View File
@@ -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;
}