update firmware source code to v0.18 (#9)

This commit is contained in:
Forairaaaaa
2026-03-25 11:11:14 +08:00
committed by GitHub
parent 5001b7081b
commit 605b575fcc
123 changed files with 24590 additions and 1899 deletions
+3 -8
View File
@@ -104,14 +104,8 @@ void display_setup_xiaozhi_ui()
void xiaozhi_board_init()
{
// Initialize the default event loop
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Init board
auto& board = Board::GetInstance();
// test
board.GetBacklight()->SetBrightness(100);
}
void start_xiaozhi_app()
@@ -120,9 +114,10 @@ void start_xiaozhi_app()
set_xiaozhi_mode(true);
// Launch the application
// Initialize and run the application
auto& app = Application::GetInstance();
app.Start();
app.Initialize();
app.Run(); // This function runs the main event loop and never returns
}
void app_play_sound(const std::string_view& sound)
+8 -2
View File
@@ -19,7 +19,8 @@ struct TouchPoint_t {
struct Data_t {
TouchPoint_t touchPoint;
bool isXiaozhiMode = false;
bool isXiaozhiMode = false;
bool isXiaozhiModeToggleEnabled = false;
};
void lock();
@@ -31,6 +32,7 @@ TouchPoint_t get_touch_point();
bool is_xiaozhi_mode();
void set_xiaozhi_mode(bool mode);
void toggle_xiaozhi_chat_state();
void disply_lvgl_lock();
void disply_lvgl_unlock();
@@ -39,10 +41,14 @@ void display_setup_xiaozhi_ui();
void xiaozhi_board_init();
void start_xiaozhi_app();
bool is_xiaozhi_ready();
i2c_master_bus_handle_t board_get_i2c_bus();
StackChanCamera* board_get_camera();
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 app_play_sound(const std::string_view& sound);
+89 -34
View File
@@ -7,6 +7,7 @@
#include "power_save_timer.h"
#include "i2c_device.h"
#include "axp2101.h"
#include "settings.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
@@ -51,7 +52,7 @@ public:
uint8_t data = ReadReg(0x90);
data |= 0b10110100;
WriteReg(0x90, data);
WriteReg(0x99, (0b11110 - 5));
// WriteReg(0x99, (0b11110 - 5));
WriteReg(0x97, (0b11110 - 2));
WriteReg(0x69, 0b00110101);
WriteReg(0x30, 0b111111);
@@ -65,12 +66,32 @@ public:
} else {
ESP_LOGI(TAG, "Set charge current success");
}
SetBrightness(0);
}
void SetBrightness(uint8_t brightness)
{
brightness = ((brightness + 641) >> 5);
WriteReg(0x99, brightness);
if (brightness == 0) {
// DLDO1 off
uint8_t val = ReadReg(0x90);
WriteReg(0x90, val & 0x7F);
} else {
// 映射计算:将 1~100 映射到 寄存器值 20~28
// 公式:MinReg + (input * (MaxReg - MinReg) / MaxInput)
// 20 + (brightness * 8 / 100)
if (brightness > 100) {
brightness = 100;
}
uint8_t reg_val = 20 + ((uint16_t)brightness * 8 / 100);
WriteReg(0x99, reg_val);
// Make sure DLDO1 on
uint8_t val = ReadReg(0x90);
if (!(val & 0x80)) {
WriteReg(0x90, val | 0x80);
}
}
}
/**
@@ -262,37 +283,11 @@ private:
void PollTouchpad()
{
static bool was_touched = false;
static int64_t touch_start_time = 0;
const int64_t TOUCH_THRESHOLD_MS = 500; // 触摸时长阈值,超过500ms视为长按
ft6336_->UpdateTouchPoint();
auto& touch_point = ft6336_->GetTouchPoint();
if (!hal_bridge::is_xiaozhi_mode()) {
hal_bridge::set_touch_point(touch_point.num, touch_point.x, touch_point.y);
return;
}
// 检测触摸开始
if (touch_point.num > 0 && !was_touched) {
was_touched = true;
touch_start_time = esp_timer_get_time() / 1000; // 转换为毫秒
}
// 检测触摸释放
else if (touch_point.num == 0 && was_touched) {
was_touched = false;
int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time;
// 只有短触才触发
if (touch_duration < TOUCH_THRESHOLD_MS) {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
}
}
// Update hal touch point
hal_bridge::set_touch_point(touch_point.num, touch_point.x, touch_point.y);
}
void InitializeFt6336TouchPad()
@@ -461,12 +456,12 @@ public:
return true;
}
virtual void SetPowerSaveMode(bool enabled) override
virtual void SetPowerSaveLevel(PowerSaveLevel level) override
{
if (!enabled) {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
WifiBoard::SetPowerSaveLevel(level);
}
virtual Backlight* GetBacklight() override
@@ -495,3 +490,63 @@ StackChanCamera* hal_bridge::board_get_camera()
auto camera = (StackChanCamera*)board.GetCamera();
return camera;
}
int hal_bridge::board_get_battery_level()
{
auto& board = Board::GetInstance();
int level = 0;
bool charging = false;
bool discharging = false;
if (board.GetBatteryLevel(level, charging, discharging)) {
return level;
} else {
return 100;
}
}
bool hal_bridge::board_is_battery_charging()
{
auto& board = Board::GetInstance();
int level = 0;
bool charging = false;
bool discharging = false;
if (board.GetBatteryLevel(level, charging, discharging)) {
return charging;
} else {
return false;
}
}
void hal_bridge::board_set_backlight_brightness(uint8_t brightness, bool permanent)
{
auto& board = Board::GetInstance();
auto backlight = board.GetBacklight();
if (backlight) {
backlight->SetBrightness(brightness, false);
if (permanent) {
Settings settings("display", true);
settings.SetInt("brightness", brightness);
}
}
}
uint8_t hal_bridge::board_get_backlight_brightness()
{
auto& board = Board::GetInstance();
auto backlight = board.GetBacklight();
if (backlight) {
return backlight->brightness();
} else {
return 0;
}
}
void hal_bridge::toggle_xiaozhi_chat_state()
{
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
// EnterWifiConfigMode();
return;
}
app.ToggleChatState();
}
+124 -65
View File
@@ -1,16 +1,24 @@
#include "stackchan_camera.h"
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <unistd.h>
#include "board.h"
#include "display.h"
#include <errno.h>
#include <esp_heap_caps.h>
#include <cstdio>
#include <cstring>
#include "esp_imgfx_color_convert.h"
#include "esp_video_device.h"
#include "esp_video_init.h"
#include "jpg/image_to_jpeg.h"
#include "linux/videodev2.h"
#include "board.h"
#include "display.h"
#include "stackchan_camera.h"
#include "esp_jpeg_common.h"
#include "jpg/image_to_jpeg.h"
#include "jpg/jpeg_to_image.h"
#include "lvgl_display.h"
#include "mcp_server.h"
#include "system_info.h"
@@ -23,7 +31,8 @@
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
#undef LOG_LOCAL_LEVEL
#define LOG_LOCAL_LEVEL MAX(CONFIG_LOG_DEFAULT_LEVEL, ESP_LOG_DEBUG)
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
#include <esp_log.h> // should be after LOCAL_LOG_LEVEL definition
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
#ifdef CONFIG_IDF_TARGET_ESP32P4
@@ -47,12 +56,6 @@
#endif // target
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
#include <errno.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <cstdio>
#include <cstring>
#define TAG "StackChanCamera"
#if defined(CONFIG_CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER) || defined(CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP)
@@ -134,7 +137,7 @@ StackChanCamera::StackChanCamera(const esp_video_init_config_t& config)
#endif
#if CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
else if (config.usb_uvc != nullptr) {
video_device_name = ESP_VIDEO_USB_UVC_DEVICE_NAME(config.usb_uvc->uvc.uvc_dev_num);
video_device_name = ESP_VIDEO_USB_UVC_DEVICE_NAME(0);
}
#endif
@@ -201,12 +204,9 @@ StackChanCamera::StackChanCamera(const esp_video_init_config_t& config)
return 0;
case V4L2_PIX_FMT_RGB565:
return 1;
case V4L2_PIX_FMT_YUV420:
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
case V4L2_PIX_FMT_YUV420: // 软件 JPEG 编码器不支持 YUV420 格式
return 2;
#else
// 软件 JPEG 编码器不支持 YUV420 格式
[[fallthrough]];
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
case V4L2_PIX_FMT_GREY:
case V4L2_PIX_FMT_YUV422P:
@@ -218,17 +218,21 @@ StackChanCamera::StackChanCamera(const esp_video_init_config_t& config)
auto get_rank = [](uint32_t fmt) -> int {
switch (fmt) {
case V4L2_PIX_FMT_YUV422P:
return 0;
return 10;
case V4L2_PIX_FMT_RGB565:
return 1;
return 11;
case V4L2_PIX_FMT_RGB24:
return 2;
return 12;
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
case V4L2_PIX_FMT_YUV420:
return 3;
return 13;
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
case V4L2_PIX_FMT_JPEG:
return 5;
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
case V4L2_PIX_FMT_GREY:
return 4;
return 20;
default:
return 1 << 29; // unsupported
}
@@ -333,7 +337,7 @@ StackChanCamera::StackChanCamera(const esp_video_init_config_t& config)
// 当启用 ISP 时,ISP 需要一些照片来初始化参数,因此开启后后台拍摄5s照片并丢弃
xTaskCreate(
[](void* arg) {
StackChanCamera* self = static_cast<StackChanCamera*>(arg);
Esp32Camera* self = static_cast<Esp32Camera*>(arg);
uint16_t capture_count = 0;
TickType_t start = xTaskGetTickCount();
TickType_t duration = 5000 / portTICK_PERIOD_MS; // 5s
@@ -419,7 +423,7 @@ bool StackChanCamera::Capture()
frame_.len = buf.bytesused;
frame_.data = (uint8_t*)heap_caps_malloc(frame_.len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!frame_.data) {
ESP_LOGE(TAG, "alloc frame copy failed");
ESP_LOGE(TAG, "alloc frame copy failed: need allocate %lu bytes", buf.bytesused);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -442,6 +446,9 @@ bool StackChanCamera::Capture()
case V4L2_PIX_FMT_YUYV:
case V4L2_PIX_FMT_YUV420:
case V4L2_PIX_FMT_GREY:
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
case V4L2_PIX_FMT_JPEG:
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
{
auto src16 = (uint16_t*)mmap_buffers_[buf.index].start;
@@ -488,7 +495,7 @@ bool StackChanCamera::Capture()
break;
}
default:
ESP_LOGE(TAG, "unsupported sensor format: 0x%08x", sensor_format_);
ESP_LOGE(TAG, "unsupported sensor format: 0x%08lx", sensor_format_);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -530,7 +537,7 @@ bool StackChanCamera::Capture()
rotate_cfg.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888;
break;
default:
ESP_LOGE(TAG, "unsupported sensor format: 0x%08x", sensor_format_);
ESP_LOGE(TAG, "unsupported sensor format: 0x%08lx", sensor_format_);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -581,11 +588,11 @@ bool StackChanCamera::Capture()
ppa_srm_color_mode_t ppa_color_mode;
switch (frame_.format) {
case V4L2_PIX_FMT_RGB565:
rotate_src = (uint8_t*)frame_.data;
rotate_src = (uint8_t*)frame_.data;
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB565;
break;
case V4L2_PIX_FMT_RGB24:
rotate_src = (uint8_t*)frame_.data;
rotate_src = (uint8_t*)frame_.data;
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB888;
break;
case V4L2_PIX_FMT_YUYV: {
@@ -600,9 +607,9 @@ bool StackChanCamera::Capture()
return false;
}
esp_imgfx_color_convert_cfg_t convert_cfg = {
.in_res = {.width = static_cast<int16_t>(frame_.width),
.height = static_cast<int16_t>(frame_.height)},
.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_YUYV,
.in_res = {.width = static_cast<int16_t>(frame_.width),
.height = static_cast<int16_t>(frame_.height)},
.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_YUYV,
.out_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888,
};
esp_imgfx_color_convert_handle_t convert_handle = nullptr;
@@ -617,11 +624,11 @@ bool StackChanCamera::Capture()
return false;
}
esp_imgfx_data_t convert_input_data = {
.data = frame_.data,
.data = frame_.data,
.data_len = frame_.len,
};
esp_imgfx_data_t convert_output_data = {
.data = rotate_src,
.data = rotate_src,
.data_len = static_cast<uint32_t>(frame_.width * frame_.height * 3),
};
err = esp_imgfx_color_convert_process(convert_handle, &convert_input_data, &convert_output_data);
@@ -641,11 +648,11 @@ bool StackChanCamera::Capture()
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB888;
heap_caps_free(frame_.data);
frame_.data = rotate_src;
frame_.len = frame_.width * frame_.height * 3;
frame_.len = frame_.width * frame_.height * 3;
break;
}
default:
ESP_LOGE(TAG, "unsupported sensor format for PPA rotation: 0x%08x", sensor_format_);
ESP_LOGE(TAG, "unsupported sensor format for PPA rotation: 0x%08lx", sensor_format_);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -664,7 +671,7 @@ bool StackChanCamera::Capture()
ppa_client_handle_t ppa_client = nullptr;
ppa_client_config_t client_cfg = {
.oper_type = PPA_OPERATION_SRM,
.oper_type = PPA_OPERATION_SRM,
.max_pending_trans_num = 1,
};
esp_err_t err = ppa_register_client(&client_cfg, &ppa_client);
@@ -681,29 +688,29 @@ bool StackChanCamera::Capture()
ppa_srm_rotation_angle_t ppa_angle = IMAGE_ROTATION_ANGLE;
ppa_srm_oper_config_t srm_cfg = {};
srm_cfg.in.buffer = (void*)rotate_src;
srm_cfg.in.pic_w = sensor_width_;
srm_cfg.in.pic_h = sensor_height_;
srm_cfg.in.block_w = sensor_width_;
srm_cfg.in.block_h = sensor_height_;
srm_cfg.in.block_offset_x = 0;
srm_cfg.in.block_offset_y = 0;
srm_cfg.in.srm_cm = ppa_color_mode;
srm_cfg.in.buffer = (void*)rotate_src;
srm_cfg.in.pic_w = sensor_width_;
srm_cfg.in.pic_h = sensor_height_;
srm_cfg.in.block_w = sensor_width_;
srm_cfg.in.block_h = sensor_height_;
srm_cfg.in.block_offset_x = 0;
srm_cfg.in.block_offset_y = 0;
srm_cfg.in.srm_cm = ppa_color_mode;
srm_cfg.out.buffer = (void*)rotate_dst;
srm_cfg.out.buffer_size = frame_.len;
srm_cfg.out.pic_w = frame_.width;
srm_cfg.out.pic_h = frame_.height;
srm_cfg.out.buffer = (void*)rotate_dst;
srm_cfg.out.buffer_size = frame_.len;
srm_cfg.out.pic_w = frame_.width;
srm_cfg.out.pic_h = frame_.height;
srm_cfg.out.block_offset_x = 0;
srm_cfg.out.block_offset_y = 0;
srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
// 等比例缩放 1.0
srm_cfg.scale_x = 1.0f;
srm_cfg.scale_y = 1.0f;
srm_cfg.scale_x = 1.0f;
srm_cfg.scale_y = 1.0f;
srm_cfg.rotation_angle = ppa_angle;
srm_cfg.mode = PPA_TRANS_MODE_BLOCKING;
srm_cfg.user_data = nullptr;
srm_cfg.mode = PPA_TRANS_MODE_BLOCKING;
srm_cfg.user_data = nullptr;
err = ppa_do_scale_rotate_mirror(ppa_client, &srm_cfg);
if (err != ESP_OK) {
@@ -719,8 +726,8 @@ bool StackChanCamera::Capture()
(void)ppa_unregister_client(ppa_client);
frame_.data = rotate_dst;
frame_.len = frame_.width * frame_.height * 2;
frame_.data = rotate_dst;
frame_.len = frame_.width * frame_.height * 2;
frame_.format = V4L2_PIX_FMT_RGB565;
heap_caps_free(rotate_src);
rotate_src = nullptr;
@@ -806,6 +813,33 @@ bool StackChanCamera::Capture()
lvgl_image_size = frame_.len; // fallthrough 时兼顾 YUYV 与 RGB565
break;
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
case V4L2_PIX_FMT_JPEG: {
uint8_t* out_data = nullptr; // out data is allocated by jpeg_to_image
size_t out_len = 0;
size_t out_width = 0;
size_t out_height = 0;
size_t out_stride = 0;
esp_err_t ret =
jpeg_to_image(frame_.data, frame_.len, &out_data, &out_len, &out_width, &out_height, &out_stride);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to decode JPEG image: %d (%s)", (int)ret, esp_err_to_name(ret));
if (out_data) {
heap_caps_free(out_data);
out_data = nullptr;
}
return false;
}
data = out_data;
w = out_width;
h = out_height;
lvgl_image_size = out_len;
stride = out_stride;
break;
}
#endif
default:
ESP_LOGE(TAG, "unsupported frame format: 0x%08lx", frame_.format);
return false;
@@ -845,7 +879,7 @@ bool StackChanCamera::StreamCaptures()
frame_.len = buf.bytesused;
frame_.data = (uint8_t*)heap_caps_malloc(frame_.len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!frame_.data) {
ESP_LOGE(TAG, "alloc frame copy failed");
ESP_LOGE(TAG, "alloc frame copy failed: need allocate %lu bytes", buf.bytesused);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -868,6 +902,9 @@ bool StackChanCamera::StreamCaptures()
case V4L2_PIX_FMT_YUYV:
case V4L2_PIX_FMT_YUV420:
case V4L2_PIX_FMT_GREY:
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
case V4L2_PIX_FMT_JPEG:
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
{
auto src16 = (uint16_t*)mmap_buffers_[buf.index].start;
@@ -914,7 +951,7 @@ bool StackChanCamera::StreamCaptures()
break;
}
default:
ESP_LOGE(TAG, "unsupported sensor format: 0x%08x", sensor_format_);
ESP_LOGE(TAG, "unsupported sensor format: 0x%08lx", sensor_format_);
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
}
@@ -1005,16 +1042,31 @@ std::string StackChanCamera::Explain(const std::string& question)
uint16_t w = frame_.width ? frame_.width : 320;
uint16_t h = frame_.height ? frame_.height : 240;
v4l2_pix_fmt_t enc_fmt = frame_.format;
image_to_jpeg_cb(
frame_.data, frame_.len, w, h, enc_fmt, 80,
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
auto jpeg_queue = (QueueHandle_t)arg;
JpegChunk chunk = {.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM), .len = len};
memcpy(chunk.data, data, len);
bool ok = image_to_jpeg_cb(
frame_.data, frame_.len, w, h, enc_fmt, 80,
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
auto jpeg_queue = static_cast<QueueHandle_t>(arg);
JpegChunk chunk = {.data = nullptr, .len = len};
if (index == 0 && data != nullptr && len > 0) {
chunk.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (chunk.data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for JPEG chunk", len);
chunk.len = 0;
} else {
memcpy(chunk.data, data, len);
}
} else {
chunk.len = 0; // Sentinel or error
}
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
return len;
},
jpeg_queue);
},
jpeg_queue);
if (!ok) {
JpegChunk chunk = {.data = nullptr, .len = 0};
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
}
});
auto network = Board::GetInstance().GetNetwork();
@@ -1066,7 +1118,8 @@ std::string StackChanCamera::Explain(const std::string& question)
}
// 第三块:JPEG数据
size_t total_sent = 0;
size_t total_sent = 0;
bool saw_terminator = false;
while (true) {
JpegChunk chunk;
if (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) != pdPASS) {
@@ -1074,6 +1127,7 @@ std::string StackChanCamera::Explain(const std::string& question)
break;
}
if (chunk.data == nullptr) {
saw_terminator = true;
break; // The last chunk
}
http->Write((const char*)chunk.data, chunk.len);
@@ -1085,6 +1139,11 @@ std::string StackChanCamera::Explain(const std::string& question)
// 清理队列
vQueueDelete(jpeg_queue);
if (!saw_terminator || total_sent == 0) {
ESP_LOGE(TAG, "JPEG encoder failed or produced empty output");
throw std::runtime_error("Failed to encode image to JPEG");
}
{
// 第四块:multipart尾部
std::string multipart_footer;
+37 -6
View File
@@ -130,7 +130,7 @@ StackChanAvatarDisplay::StackChanAvatarDisplay(esp_lcd_panel_io_handle_t panel_i
.panel_handle = panel_,
.control_handle = nullptr,
.buffer_size = static_cast<uint32_t>(width_ * 20),
.double_buffer = true,
.double_buffer = false,
.trans_size = 0,
.hres = static_cast<uint32_t>(width_),
.vres = static_cast<uint32_t>(height_),
@@ -177,6 +177,18 @@ StackChanAvatarDisplay::StackChanAvatarDisplay(esp_lcd_panel_io_handle_t panel_i
};
esp_timer_create(&preview_timer_args, &preview_timer_);
// Create boot logo label if not warm boot
if (GetHAL().getWarmRebootTarget() < 0) {
ESP_LOGI(TAG, "Create boot logo label");
Lock();
{
uitk::lvgl_cpp::ScreenActive screen;
screen.setBgColor(lv_color_hex(0x000000));
}
GetHAL().bootLogo = std::make_unique<BootLogo>();
Unlock();
}
// Robot will be created later in SetupXiaoZhiUI()
}
@@ -214,6 +226,8 @@ lv_disp_t* StackChanAvatarDisplay::GetLvglDisplay()
return display_;
}
#include <hal/board/hal_bridge.h>
void StackChanAvatarDisplay::SetupXiaoZhiUI()
{
auto& stackchan = GetStackChan();
@@ -229,19 +243,20 @@ void StackChanAvatarDisplay::SetupXiaoZhiUI()
auto avatar = std::make_unique<DefaultAvatar>();
avatar->init(lv_screen_active());
avatar->getPanel()->onClick().connect([]() { hal_bridge::toggle_xiaozhi_chat_state(); });
stackchan.attachAvatar(std::move(avatar));
stackchan.addModifier(std::make_unique<BreathModifier>());
blink_modifier_id_ = stackchan.addModifier(std::make_unique<BlinkModifier>());
stackchan.addModifier(std::make_unique<HeadPetModifier>());
// stackchan.addModifier(std::make_unique<IMUMotionModifier>());
stackchan.addModifier(std::make_unique<ImuEventModifier>());
preview_image_ = lv_image_create(lv_screen_active());
lv_obj_set_size(preview_image_, 192, 144);
lv_obj_align(preview_image_, LV_ALIGN_CENTER, 0, -25);
lv_obj_set_size(preview_image_, 320, 240);
lv_obj_align(preview_image_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
GetHAL().startStackChanAutoUpdate(24);
// GetHAL().startStackChanAutoUpdate(24);
ESP_LOGI(TAG, "Avatar created and started");
}
@@ -296,6 +311,8 @@ void StackChanAvatarDisplay::SetEmotion(const char* emotion)
if (idle_motion_modifier_id_ >= 0) {
stackchan.removeModifier(idle_motion_modifier_id_);
idle_motion_modifier_id_ = -1;
stackchan.removeModifier(idle_expression_modifier_id_);
idle_expression_modifier_id_ = -1;
}
// Return to default pose
@@ -381,6 +398,13 @@ void StackChanAvatarDisplay::SetTheme(Theme* theme)
stackchan.avatar().setSpeechTextFont((void*)text_font);
}
#include <hal/board/hal_bridge.h>
static bool _is_xiaozhi_ready = false;
bool hal_bridge::is_xiaozhi_ready()
{
return _is_xiaozhi_ready;
}
void StackChanAvatarDisplay::SetStatus(const char* status)
{
ESP_LOGE(TAG, "SetStatus: %s", status);
@@ -413,6 +437,8 @@ void StackChanAvatarDisplay::SetStatus(const char* status)
GetHAL().refreshRgb();
} else if (strcmp(status, Lang::Strings::STANDBY) == 0) {
_is_xiaozhi_ready = true;
if (speaking_modifier_id_ >= 0) {
// Stop speaking
stackchan.removeModifier(speaking_modifier_id_);
@@ -432,13 +458,16 @@ void StackChanAvatarDisplay::SetStatus(const char* status)
GetHAL().setRgbColor(0, 0, 0, 50);
GetHAL().refreshRgb();
} else {
avatar.setSpeech(status);
}
if (is_idle) {
// Start idle motion
ESP_LOGW(TAG, "Start idle motion");
if (idle_motion_modifier_id_ < 0) {
idle_motion_modifier_id_ = stackchan.addModifier(std::make_unique<IdleMotionModifier>());
idle_motion_modifier_id_ = stackchan.addModifier(std::make_unique<IdleMotionModifier>());
idle_expression_modifier_id_ = stackchan.addModifier(std::make_unique<IdleExpressionModifier>());
}
} else {
// Stop idle motion
@@ -446,6 +475,8 @@ void StackChanAvatarDisplay::SetStatus(const char* status)
if (idle_motion_modifier_id_ >= 0) {
stackchan.removeModifier(idle_motion_modifier_id_);
idle_motion_modifier_id_ = -1;
stackchan.removeModifier(idle_expression_modifier_id_);
idle_expression_modifier_id_ = -1;
}
// if (!is_listening) {
@@ -16,6 +16,7 @@ private:
esp_lcd_panel_handle_t panel_ = nullptr;
int speaking_modifier_id_ = -1;
int idle_motion_modifier_id_ = -1;
int idle_expression_modifier_id_ = -1;
int blink_modifier_id_ = -1;
bool is_sleeping_ = false;
@@ -0,0 +1,271 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "PCF8563_Class.hpp"
#include "esp_log.h"
#include <cstring>
#include <cstdlib>
static const char* TAG = "PCF8563";
namespace m5 {
static std::uint8_t bcd2ToByte(std::uint8_t value)
{
return ((value >> 4) * 10) + (value & 0x0F);
}
static std::uint8_t byteToBcd2(std::uint8_t value)
{
std::uint_fast8_t bcdhigh = value / 10;
return (bcdhigh << 4) | (value - (bcdhigh * 10));
}
PCF8563_Class::PCF8563_Class(i2c_master_bus_handle_t i2c_bus_handle, uint8_t addr) : _addr(addr), _init(false)
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = _addr,
.scl_speed_hz = 400000,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus_handle, &dev_cfg, &_i2c_dev));
}
PCF8563_Class::~PCF8563_Class()
{
if (_i2c_dev) {
i2c_master_bus_rm_device(_i2c_dev);
}
}
bool PCF8563_Class::begin()
{
// TimerCamera's internal RTC sometimes failed to initialize, so execute a dummy write first
writeRegister8(0x00, 0x00);
_init = (writeRegister8(0x00, 0x00) == ESP_OK) && (writeRegister8(0x0E, 0x03) == ESP_OK);
return _init;
}
esp_err_t PCF8563_Class::writeRegister8(uint8_t reg, uint8_t value)
{
uint8_t buf[2] = {reg, value};
return i2c_master_transmit(_i2c_dev, buf, sizeof(buf), 1000);
}
uint8_t PCF8563_Class::readRegister8(uint8_t reg)
{
uint8_t val = 0;
esp_err_t err = i2c_master_transmit_receive(_i2c_dev, &reg, 1, &val, 1, 1000);
if (err != ESP_OK) {
ESP_LOGE(TAG, "readRegister8 failed: %s", esp_err_to_name(err));
return 0;
}
return val;
}
esp_err_t PCF8563_Class::writeRegister(uint8_t reg, const uint8_t* data, size_t len)
{
if (len == 0) return ESP_OK;
uint8_t* buf = (uint8_t*)malloc(len + 1);
if (!buf) return ESP_ERR_NO_MEM;
buf[0] = reg;
memcpy(buf + 1, data, len);
esp_err_t err = i2c_master_transmit(_i2c_dev, buf, len + 1, 1000);
free(buf);
return err;
}
esp_err_t PCF8563_Class::readRegister(uint8_t reg, uint8_t* data, size_t len)
{
return i2c_master_transmit_receive(_i2c_dev, &reg, 1, data, len, 1000);
}
esp_err_t PCF8563_Class::bitOn(uint8_t reg, uint8_t mask)
{
uint8_t val = readRegister8(reg);
return writeRegister8(reg, val | mask);
}
esp_err_t PCF8563_Class::bitOff(uint8_t reg, uint8_t mask)
{
uint8_t val = readRegister8(reg);
return writeRegister8(reg, val & ~mask);
}
bool PCF8563_Class::getVoltLow(void)
{
return readRegister8(0x02) & 0x80; // RTCC_VLSEC_MASK
}
bool PCF8563_Class::getDateTime(rtc_date_t* date, rtc_time_t* time)
{
if (!_init) return false;
std::uint8_t buf[7] = {0};
int start_reg = (time != nullptr) ? 0x02 : 0x05;
int len = ((date != nullptr) ? 4 : 0) + ((time != nullptr) ? 3 : 0);
if (len == 0) return false;
if (readRegister(start_reg, buf, len) != ESP_OK) {
return false;
}
int idx = 0;
if (time) {
time->seconds = bcd2ToByte(buf[idx++] & 0x7f);
time->minutes = bcd2ToByte(buf[idx++] & 0x7f);
time->hours = bcd2ToByte(buf[idx++] & 0x3f);
}
if (date) {
date->date = bcd2ToByte(buf[idx++] & 0x3f);
date->weekDay = bcd2ToByte(buf[idx++] & 0x07);
date->month = bcd2ToByte(buf[idx++] & 0x1f);
date->year = bcd2ToByte(buf[idx] & 0xff) + ((0x80 & buf[idx - 1]) ? 1900 : 2000);
}
return true;
}
bool PCF8563_Class::setDateTime(const rtc_date_t* date, const rtc_time_t* time)
{
if (!_init) return false;
std::uint8_t buf[7] = {0};
int idx = 0;
int reg_start = 0x05;
if (time) {
reg_start = 0x02;
buf[idx++] = byteToBcd2(time->seconds);
buf[idx++] = byteToBcd2(time->minutes);
buf[idx++] = byteToBcd2(time->hours);
}
if (date) {
buf[idx++] = byteToBcd2(date->date);
buf[idx++] = (uint8_t)(0x07u & date->weekDay);
buf[idx++] = (std::uint8_t)(byteToBcd2(date->month) + (date->year < 2000 ? 0x80 : 0));
buf[idx++] = byteToBcd2(date->year % 100);
}
if (idx == 0) {
return false;
}
return writeRegister(reg_start, buf, idx) == ESP_OK;
}
std::uint32_t PCF8563_Class::setTimerIRQ(std::uint32_t msec)
{
if (!_init) return 0;
std::uint8_t reg_value = readRegister8(0x01) & ~0x0C;
std::uint32_t afterSeconds = (msec + 500) / 1000;
if (afterSeconds <= 0) { // disable timer
writeRegister8(0x01, reg_value & ~0x01);
writeRegister8(0x0E, 0x03);
return 0;
}
std::size_t div = 1;
std::uint8_t type_value = 0x82;
if (afterSeconds < 270) {
if (afterSeconds > 255) {
afterSeconds = 255;
}
} else {
div = 60;
afterSeconds = (afterSeconds + 30) / div;
if (afterSeconds > 255) {
afterSeconds = 255;
}
type_value = 0x83;
}
writeRegister8(0x0E, type_value);
writeRegister8(0x0F, afterSeconds);
writeRegister8(0x01, (reg_value | 0x01) & ~0x80);
return afterSeconds * div * 1000;
}
int PCF8563_Class::setAlarmIRQ(const rtc_date_t* date, const rtc_time_t* time)
{
if (!_init) return 0;
union {
std::uint32_t raw;
std::uint8_t buf[4];
} data_u;
data_u.raw = ~0; // 0xFFFFFFFF
bool irq_enable = false;
if (time) {
if (time->minutes >= 0) {
irq_enable = true;
data_u.buf[0] = byteToBcd2(time->minutes) & 0x7f;
}
if (time->hours >= 0) {
irq_enable = true;
data_u.buf[1] = byteToBcd2(time->hours) & 0x3f;
}
}
if (date) {
if (date->date >= 0) {
irq_enable = true;
data_u.buf[2] = byteToBcd2(date->date) & 0x3f;
}
if (date->weekDay >= 0) {
irq_enable = true;
data_u.buf[3] = byteToBcd2(date->weekDay) & 0x07;
}
}
writeRegister(0x09, data_u.buf, 4);
if (irq_enable) {
bitOn(0x01, 0x02);
} else {
bitOff(0x01, 0x02);
}
return irq_enable;
}
bool PCF8563_Class::getIRQstatus(void)
{
return _init && (0x0C & readRegister8(0x01));
}
void PCF8563_Class::clearIRQ(void)
{
if (!_init) {
return;
}
bitOff(0x01, 0x0C);
}
void PCF8563_Class::disableIRQ(void)
{
if (!_init) {
return;
}
// disable alerm (bit7:1=disabled)
static constexpr const std::uint8_t buf[4] = {0x80, 0x80, 0x80, 0x80};
writeRegister(0x09, buf, 4);
// disable timer (bit7:0=disabled)
writeRegister8(0x0E, 0);
// clear flag and INT enable bits
writeRegister8(0x01, 0x00);
}
} // namespace m5
@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint>
#include "driver/i2c_master.h"
#include "esp_err.h"
namespace m5 {
struct rtc_time_t {
int8_t hours;
int8_t minutes;
int8_t seconds;
};
struct rtc_date_t {
int8_t weekDay;
int8_t month;
int8_t date;
int16_t year;
};
class PCF8563_Class {
public:
static constexpr uint8_t DEFAULT_ADDRESS = 0x51;
PCF8563_Class(i2c_master_bus_handle_t i2c_bus_handle, uint8_t addr = DEFAULT_ADDRESS);
~PCF8563_Class();
bool begin();
bool getDateTime(rtc_date_t* date, rtc_time_t* time);
bool setDateTime(const rtc_date_t* date, const rtc_time_t* time);
/// Set timer IRQ
uint32_t setTimerIRQ(uint32_t timer_msec);
/// Set alarm by time
int setAlarmIRQ(const rtc_date_t* date, const rtc_time_t* time);
bool getIRQstatus(void);
void clearIRQ(void);
void disableIRQ(void);
bool getVoltLow(void);
private:
i2c_master_dev_handle_t _i2c_dev;
uint8_t _addr;
bool _init;
esp_err_t writeRegister8(uint8_t reg, uint8_t value);
uint8_t readRegister8(uint8_t reg);
esp_err_t writeRegister(uint8_t reg, const uint8_t* data, size_t len);
esp_err_t readRegister(uint8_t reg, uint8_t* data, size_t len);
esp_err_t bitOn(uint8_t reg, uint8_t mask);
esp_err_t bitOff(uint8_t reg, uint8_t mask);
};
} // namespace m5
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "PY32IOExpander_Class.hpp"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint>
+118 -21
View File
@@ -9,7 +9,7 @@
#include <nvs_flash.h>
static std::unique_ptr<Hal> _hal_instance;
static const std::string _tag = "HAL";
static const std::string_view _tag = "HAL";
Hal& GetHAL()
{
@@ -32,17 +32,14 @@ void Hal::init()
}
ESP_ERROR_CHECK(ret);
// ble_init();
xiaozhi_board_init();
xiaozhi_mcp_init();
head_touch_init();
io_expander_init();
rtc_init();
imu_init();
servo_init();
lvgl_init();
reminder_init();
// startWebSocketAvatar();
}
/* -------------------------------------------------------------------------- */
@@ -50,6 +47,7 @@ void Hal::init()
/* -------------------------------------------------------------------------- */
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <system_info.h>
#include <esp_system.h>
#include <esp_timer.h>
#include <esp_mac.h>
@@ -88,10 +86,23 @@ void Hal::reboot()
esp_restart();
}
void Hal::updateHeapStatusLog()
{
static uint32_t last_log_tick = 0;
if (millis() - last_log_tick < 10000) {
return;
}
last_log_tick = millis();
SystemInfo::PrintHeapStats();
}
/* -------------------------------------------------------------------------- */
/* Xiaozhi */
/* -------------------------------------------------------------------------- */
#include "board/hal_bridge.h"
#include <stackchan/stackchan.h>
#include <apps/common/common.h>
#include <assets/assets.h>
void Hal::xiaozhi_board_init()
{
@@ -100,13 +111,81 @@ void Hal::xiaozhi_board_init()
hal_bridge::xiaozhi_board_init();
}
static void _stackchan_update_task(void* param)
{
bool is_xiaozhi_ready = false;
bool is_setup_done = false;
while (1) {
vTaskDelay(pdMS_TO_TICKS(10));
tools::update_reminders();
LvglLockGuard lock;
GetStackChan().update();
if (!is_xiaozhi_ready) {
is_xiaozhi_ready = hal_bridge::is_xiaozhi_ready();
continue;
}
if (!is_setup_done) {
// Setup when xiaozhi ready
GetHAL().startSntp();
view::create_home_indicator([]() { GetHAL().requestWarmReboot(0); }, 0x81DBBD, 0x134233);
view::create_status_bar(0x81DBBD, 0x134233);
is_setup_done = true;
}
view::update_home_indicator();
view::update_status_bar();
}
}
void Hal::startXiaozhi()
{
mclog::tagInfo(_tag, "start xiaozhi");
auto& motion = GetStackChan().motion();
motion.setAutoAngleSyncEnabled(true);
motion.setAutoTorqueReleaseEnabled(true);
// Setup reminder handler
tools::on_reminder_triggered().clear();
tools::on_reminder_triggered().connect([](int id, std::string_view msg) {
mclog::tagInfo(_tag, "reminder triggered: id: {}, msg: {}", id, msg);
{
LvglLockGuard lock;
auto& avatar = GetStackChan().avatar();
avatar.addDecorator(std::make_unique<view::ReminderView>(lv_screen_active(), msg));
}
hal_bridge::app_play_sound(OGG_NEW_NOTIFICATION);
});
// Start stackchan update task
xTaskCreate(_stackchan_update_task, "stackchan", 4096, NULL, 5, NULL);
hal_bridge::start_xiaozhi_app();
}
uint8_t Hal::getBatteryLevel()
{
return hal_bridge::board_get_battery_level();
}
bool Hal::isBatteryCharging()
{
return hal_bridge::board_is_battery_charging();
}
void Hal::factoryReset()
{
mclog::tagInfo(_tag, "start factory reset");
ESP_ERROR_CHECK(nvs_flash_erase());
reboot();
}
/* -------------------------------------------------------------------------- */
/* Display */
/* -------------------------------------------------------------------------- */
@@ -122,6 +201,16 @@ void Hal::lvglUnlock()
hal_bridge::disply_lvgl_unlock();
}
void Hal::setBackLightBrightness(uint8_t brightness, bool permanent)
{
hal_bridge::board_set_backlight_brightness(brightness, permanent);
}
uint8_t Hal::getBackLightBrightness()
{
return hal_bridge::board_get_backlight_brightness();
}
/* -------------------------------------------------------------------------- */
/* Lvgl */
/* -------------------------------------------------------------------------- */
@@ -131,13 +220,7 @@ void Hal::lvglUnlock()
static void lvgl_read_cb(lv_indev_t* indev, lv_indev_data_t* data)
{
hal_bridge::lock();
auto bridge_data = hal_bridge::get_data();
if (bridge_data.isXiaozhiMode) {
hal_bridge::unlock();
data->state = LV_INDEV_STATE_RELEASED;
return;
}
auto& bridge_data = hal_bridge::get_data();
// mclog::tagInfo(_tag, "touchpoint: {}, x: {}, y: {}", bridge_data.touchPoint.num, bridge_data.touchPoint.x,
// bridge_data.touchPoint.y);
@@ -197,23 +280,37 @@ void Hal::stopStackChanAutoUpdate()
}
/* -------------------------------------------------------------------------- */
/* Reminder */
/* Warm Reboot */
/* -------------------------------------------------------------------------- */
#include "hal/utils/reminder/reminder.h"
#include <settings.h>
#include <string_view>
int Hal::createReminder(int duration_s, const std::string& message)
static std::string_view _warm_boot_nvs_ns = "warm_boot";
static std::string_view _warm_boot_nvs_key = "app_index";
void Hal::requestWarmReboot(int appIndex)
{
return ReminderManager::GetInstance().CreateReminder(duration_s, message);
mclog::tagInfo(_tag, "warm reboot request to app index: {}", appIndex);
{
Settings settings(_warm_boot_nvs_ns.data(), true);
settings.SetInt(_warm_boot_nvs_key.data(), appIndex);
}
delay(100);
esp_restart();
}
void Hal::stopReminder(int id)
int Hal::getWarmRebootTarget()
{
ReminderManager::GetInstance().StopReminder(id);
Settings settings(_warm_boot_nvs_ns.data(), false);
return settings.GetInt(_warm_boot_nvs_key.data(), -1);
}
void Hal::reminder_init()
void Hal::clearWarmRebootRequest()
{
mclog::tagInfo(_tag, "reminder init");
mclog::tagInfo(_tag, "clear warm reboot request");
ReminderManager::GetInstance().Start();
Settings settings(_warm_boot_nvs_ns.data(), true);
settings.SetInt(_warm_boot_nvs_key.data(), -1);
}
+94 -8
View File
@@ -8,8 +8,10 @@
#include <cstdint>
#include <string>
#include <lvgl.h>
#include <functional>
#include <smooth_ui_toolkit.hpp>
#include <uitk/short_namespace.hpp>
#include <smooth_lvgl.hpp>
#include <array>
#include <lvgl_image.h>
#include <string_view>
@@ -61,6 +63,60 @@ enum class AppConfigEvent {
WifiConnected,
};
/**
* @brief
*
*/
enum class CommonLogLevel {
Info = 0,
Warning,
Error,
};
/**
* @brief
*
*/
enum class WifiStatus {
None = 0,
Low,
Medium,
High,
};
/**
* @brief
*
*/
class BootLogo {
public:
BootLogo()
{
_panel = std::make_unique<uitk::lvgl_cpp::Container>(lv_screen_active());
_panel->setSize(320, 240);
_panel->setAlign(LV_ALIGN_CENTER);
_panel->setBorderWidth(0);
_panel->setBgOpa(0);
_label_logo = std::make_unique<uitk::lvgl_cpp::Label>(_panel->get());
_label_logo->setTextFont(&lv_font_montserrat_24);
_label_logo->setTextColor(lv_color_hex(0xFFFFFF));
_label_logo->align(LV_ALIGN_CENTER, 0, -14);
_label_logo->setText("STACKCHAN");
_label_msg = std::make_unique<uitk::lvgl_cpp::Label>(_panel->get());
_label_msg->setTextFont(&lv_font_montserrat_16);
_label_msg->setTextColor(lv_color_hex(0xBFBFBF));
_label_msg->align(LV_ALIGN_CENTER, 0, 14);
_label_msg->setText("Starting up ...");
}
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;
};
/**
* @brief
*
@@ -76,20 +132,35 @@ public:
std::array<uint8_t, 6> getFactoryMac();
std::string getFactoryMacString(std::string divider = "");
void reboot();
void updateHeapStatusLog();
uint8_t getBatteryLevel();
bool isBatteryCharging();
void factoryReset();
/* --------------------------------- Display -------------------------------- */
lv_indev_t* lvTouchpad = nullptr;
std::unique_ptr<BootLogo> bootLogo;
void lvglLock();
void lvglUnlock();
void setBackLightBrightness(uint8_t brightness, bool permanent = false);
uint8_t getBackLightBrightness();
/* --------------------------------- Xiaozhi -------------------------------- */
void requestXiaozhiStart()
{
_xiaozhi_start_requested = true;
}
bool isXiaozhiStartRequested()
{
return _xiaozhi_start_requested;
}
void startXiaozhi();
/* ----------------------------------- BLE ---------------------------------- */
uitk::Signal<const char*> onBleMotionData;
uitk::Signal<const char*> onBleAvatarData;
uitk::Signal<const char*> onBleConfigData;
uitk::Signal<const char*> onBleAnimationData;
uitk::Signal<const char*> onBleRgbData;
uitk::Signal<AppConfigEvent> onAppConfigEvent;
void startBleServer();
@@ -122,24 +193,39 @@ public:
uitk::Signal<bool> onWsVideoModeChange;
uitk::Signal<std::shared_ptr<LvglImage>> onWsVideoFrame;
uitk::Signal<std::string_view> onWsDanceData;
uitk::Signal<CommonLogLevel, std::string_view> onWsLog;
void startWebSocketAvatar();
/* -------------------------------- Reminder -------------------------------- */
uitk::Signal<int, std::string> onReminderTriggered;
int createReminder(int duration_s, const std::string& message);
void stopReminder(int id);
void startWebSocketAvatarService(std::function<void(std::string_view)> onStartLog);
/* ----------------------------------- IMU ---------------------------------- */
uitk::Signal<ImuMotionEvent> onImuMotionEvent;
/* ---------------------------------- Time ---------------------------------- */
void syncRtcTimeToSystem();
void syncSystemTimeToRtc();
void setTimezone(std::string_view tz);
std::string getTimezone();
void resetTimezoneConfig();
/* --------------------------------- EspNow --------------------------------- */
uitk::Signal<const std::vector<uint8_t>&> onEspNowData;
void startEspNow(int channel);
bool espNowSend(const std::vector<uint8_t>& data, const uint8_t* destAddr = nullptr);
void setLaserEnabled(bool enabled);
/* ------------------------------- Warm Reboot ------------------------------ */
void requestWarmReboot(int appIndex);
int getWarmRebootTarget();
void clearWarmRebootRequest();
/* --------------------------------- Network -------------------------------- */
void startNetwork(std::function<void(std::string_view)> onLog);
WifiStatus getWifiStatus();
void startSntp();
private:
bool _xiaozhi_start_requested = false;
void xiaozhi_board_init();
void lvgl_init();
void xiaozhi_mcp_init();
@@ -147,8 +233,8 @@ private:
void servo_init();
void head_touch_init();
void io_expander_init();
void reminder_init();
void imu_init();
void rtc_init();
};
Hal& GetHAL();
+79 -29
View File
@@ -5,12 +5,14 @@
*/
#include "hal.h"
#include "utils/bleprph/bleprph.h"
#include "utils/secret_logic/secret_logic.h"
#include <ArduinoJson.hpp>
#include <mooncake_log.h>
#include <esp_mac.h>
#include <mooncake.h>
#include <settings.h>
#include <esp_mac.h>
static const std::string _tag = "HAL-BLE";
static const std::string_view _tag = "HAL-BLE";
static int _handle_ble_motion_write(const char* json_data, uint16_t len, uint16_t conn_handle)
{
@@ -28,15 +30,15 @@ static int _handle_ble_avatar_write(const char* json_data, uint16_t len, uint16_
static int _handle_ble_config_write(const char* json_data, uint16_t len, uint16_t conn_handle)
{
mclog::tagInfo(_tag, "on config:\n{}", json_data);
// mclog::tagInfo(_tag, "on config:\n{}", json_data);
GetHAL().onBleConfigData.emit(json_data);
return 0;
}
static int _handle_ble_animation_write(const char* json_data, uint16_t len, uint16_t conn_handle)
static int _handle_ble_rgb_write(const char* json_data, uint16_t len, uint16_t conn_handle)
{
mclog::tagInfo(_tag, "on animation:\n{}", json_data);
GetHAL().onBleAnimationData.emit(json_data);
// mclog::tagInfo(_tag, "on rgb:\n{}", json_data);
GetHAL().onBleRgbData.emit(json_data);
return 0;
}
@@ -54,7 +56,7 @@ void Hal::ble_init()
.motion_cb = _handle_ble_motion_write,
.avatar_cb = _handle_ble_avatar_write,
.config_cb = _handle_ble_config_write,
.animation_cb = _handle_ble_animation_write,
.rgb_cb = _handle_ble_rgb_write,
.battery_read_cb = _handle_ble_battery_read,
};
stackchan_ble_register_callbacks(&ble_callbacks);
@@ -85,6 +87,7 @@ bool Hal::isBleConnected()
#include <string_view>
#include <queue>
#include <mutex>
#include <atomic>
class WifiConfigServer {
public:
@@ -94,26 +97,29 @@ public:
_was_connected = stackchan_ble_is_connected();
// Setup WifiStation callbacks
auto& wifi = StackChanWifiStation::GetInstance();
wifi.OnConnect([this](const std::string& ssid) {
mclog::tagInfo(_tag, "Wifi Connecting to {}", ssid);
_wifi_station = std::make_unique<StackChanWifiStation>();
_wifi_station->OnConnect([this](const std::string& ssid) {
mclog::tagInfo(_tag, "wifi Connecting to {}", ssid);
_is_wifi_connecting = true;
notify_state(0, "wifiConnecting");
});
wifi.OnConnected([this](const std::string& ssid) {
mclog::tagInfo(_tag, "Wifi Connected to {}", ssid);
_wifi_station->OnConnected([this](const std::string& ssid) {
mclog::tagInfo(_tag, "wifi Connected to {}", ssid);
_is_wifi_connecting = false;
notify_state(1, "wifiConnected");
GetHAL().onAppConfigEvent.emit(AppConfigEvent::WifiConnected);
Settings settings("app_config", true);
settings.SetBool("is_configed", true);
});
wifi.OnConnectFailed([this](const std::string& ssid) {
mclog::tagInfo(_tag, "Wifi Connect Failed to {}", ssid);
_wifi_station->OnConnectFailed([this](const std::string& ssid) {
mclog::tagInfo(_tag, "wifi Connect Failed to {}", ssid);
_is_wifi_connecting = false;
notify_state(2, "wifiConnectFailed");
GetHAL().onAppConfigEvent.emit(AppConfigEvent::WifiConnectFailed);
});
wifi.Start();
_wifi_station->Start();
}
void update()
@@ -151,6 +157,8 @@ private:
std::queue<std::string> _msg_queue;
std::mutex _mutex;
bool _was_connected = false;
std::atomic<bool> _is_wifi_connecting{false};
std::unique_ptr<StackChanWifiStation> _wifi_station;
void on_config_data(const char* json_data)
{
@@ -170,15 +178,37 @@ private:
if (doc["cmd"] == "setWifi") {
handle_set_wifi(doc["data"]);
} else if (doc["cmd"] == "getWifiStatus") {
handle_get_wifi_status();
} else if (doc["cmd"] == "handshake") {
std::string data = doc["data"].as<std::string>();
handle_handshake(data);
}
}
void handle_get_wifi_status()
{
if (_wifi_station->IsConnected()) {
notify_state(1, "wifiConnected");
} else if (_is_wifi_connecting) {
notify_state(0, "wifiConnecting");
} else {
notify_state(3, "wifiDisconnected");
}
}
void handle_set_wifi(ArduinoJson::JsonObject data)
{
if (_is_wifi_connecting) {
mclog::tagWarn(_tag, "busy connecting, ignoring setWifi");
notify_state(2, "wifiConnectFailed: Busy");
return;
}
const char* ssid = data["ssid"];
const char* password = data["password"];
mclog::tagInfo("WifiConfigServer", "get wifi config: {} / {}", ssid, password);
mclog::tagInfo(_tag, "get wifi config: {} / {}", ssid, password);
// Notify state: connecting
notify_state(0, "wifiConnecting");
@@ -187,12 +217,16 @@ private:
connect_wifi(ssid, password);
}
void handle_handshake(std::string_view data)
{
auto token = secret_logic::generate_handshake_token(data);
notify_state(4, token.c_str());
}
void connect_wifi(const char* ssid, const char* password)
{
auto& wifi = StackChanWifiStation::GetInstance();
// Save to NVS (compatible with Xiaozhi) and connect
wifi.AddAuth(ssid, password);
_wifi_station->AddAuth(ssid, password);
}
void notify_state(int type, const char* state)
@@ -208,16 +242,32 @@ private:
}
};
static void _app_config_server_task(void* param)
{
auto server = std::make_unique<WifiConfigServer>();
server->init();
while (1) {
server->update();
vTaskDelay(pdMS_TO_TICKS(50));
class AppConfigServerWorker : public mooncake::BasicAbility {
public:
void onCreate() override
{
_server = std::make_unique<WifiConfigServer>();
_server->init();
}
}
void onRunning() override
{
if (GetHAL().millis() - _last_tick < 50) {
return;
}
_last_tick = GetHAL().millis();
_server->update();
}
void onDestroy() override
{
_server.reset();
}
private:
std::unique_ptr<WifiConfigServer> _server;
uint32_t _last_tick = 0;
};
void Hal::startAppConfigServer()
{
@@ -225,7 +275,7 @@ void Hal::startAppConfigServer()
ble_init();
xTaskCreate(_app_config_server_task, "appconfig", 6000, NULL, 10, NULL);
mooncake::GetMooncake().extensionManager()->createAbility(std::make_unique<AppConfigServerWorker>());
}
bool Hal::isAppConfiged()
+3 -3
View File
@@ -23,7 +23,7 @@
#include <espnow_utils.h>
#include <esp_check.h>
static const std::string _tag = "HAL-EspNow";
static const std::string_view _tag = "HAL-EspNow";
static EventGroupHandle_t s_wifi_event_group = NULL;
static const int WIFI_CONNECTED_BIT = BIT0;
@@ -60,7 +60,7 @@ static void _wifi_init(int channel = 1)
// ESP_ERROR_CHECK(nvs_flash_init());
// ESP_ERROR_CHECK(esp_netif_init());
// ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t* sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
@@ -164,7 +164,7 @@ void Hal::setLaserEnabled(bool enabled)
return;
}
const gpio_num_t laser_pin = GPIO_NUM_9;
const gpio_num_t laser_pin = GPIO_NUM_2;
if (!is_inited) {
gpio_reset_pin(laser_pin);
+5 -5
View File
@@ -10,7 +10,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static const std::string _tag = "HAL-HeadTouch";
static const std::string_view _tag = "HAL-HeadTouch";
// 触摸状态
enum class TouchState { IDLE, TOUCHED, SWIPING };
@@ -68,7 +68,7 @@ public:
current_state = TouchState::TOUCHED;
initial_position = data.get_position();
gesture = HeadPetGesture::Press;
// mclog::tagDebug(_tag, "Touch detected at position: {}", initial_position);
// mclog::tagInfo(_tag, "Touch detected at position: {}", initial_position);
}
break;
@@ -84,11 +84,11 @@ public:
if (delta > config.swipe_threshold) {
current_state = TouchState::SWIPING;
gesture = HeadPetGesture::SwipeForward;
// mclog::tagDebug(_tag, "Swipe forward detected, delta: {}", delta);
// mclog::tagInfo(_tag, "Swipe forward detected, delta: {}", delta);
} else if (delta < -config.swipe_threshold) {
current_state = TouchState::SWIPING;
gesture = HeadPetGesture::SwipeBackward;
// mclog::tagDebug(_tag, "Swipe backward detected, delta: {}", delta);
// mclog::tagInfo(_tag, "Swipe backward detected, delta: {}", delta);
}
}
break;
@@ -158,5 +158,5 @@ void Hal::head_touch_init()
si12t_init(&si12t_cfg, &si12t);
si12t_setup(si12t, SI12T_TYPE_LOW, SI12T_SENSITIVITY_LEVEL_3);
xTaskCreate(_head_touch_update_task, "headtouch", 4096, si12t, 5, NULL);
xTaskCreateWithCaps(_head_touch_update_task, "headtouch", 1024 * 6, si12t, 5, NULL, MALLOC_CAP_SPIRAM);
}
+7 -6
View File
@@ -10,13 +10,14 @@
#include <mooncake_log.h>
#include <memory>
static const std::string _tag = "HAL-IMU";
static const std::string_view _tag = "HAL-IMU";
static std::unique_ptr<BMI270> _bmi270;
static void _imu_task(void* param)
{
auto motion_detector = std::make_unique<MotionDetector>();
motion_detector->setShakeThreshold(16.0f);
while (1) {
if (_bmi270 && _bmi270->update()) {
@@ -29,10 +30,10 @@ static void _imu_task(void* param)
mclog::tagInfo(_tag, "Shake Detected!");
GetHAL().onImuMotionEvent.emit(ImuMotionEvent::Shake);
}
if (motion_detector->isPickUpDetected()) {
mclog::tagInfo(_tag, "Pick Up Detected!");
GetHAL().onImuMotionEvent.emit(ImuMotionEvent::PickUp);
}
// if (motion_detector->isPickUpDetected()) {
// mclog::tagInfo(_tag, "Pick Up Detected!");
// GetHAL().onImuMotionEvent.emit(ImuMotionEvent::PickUp);
// }
}
vTaskDelay(pdMS_TO_TICKS(100));
}
@@ -52,5 +53,5 @@ void Hal::imu_init()
}
mclog::tagInfo(_tag, "BMI270 init ok");
// xTaskCreate(_imu_task, "imu", 4096, NULL, 5, NULL);
xTaskCreateWithCaps(_imu_task, "imu", 4096, NULL, 5, NULL, MALLOC_CAP_SPIRAM);
}
+1 -1
View File
@@ -9,7 +9,7 @@
#include <mooncake_log.h>
#include <memory>
static const std::string _tag = "HAL-IOE";
static const std::string_view _tag = "HAL-IOE";
static std::unique_ptr<m5::PY32IOExpander_Class> _io_expander;
+116 -29
View File
@@ -7,10 +7,11 @@
#include <mooncake_log.h>
#include <mcp_server.h>
#include <stackchan/stackchan.h>
#include <apps/common/common.h>
using namespace stackchan;
static const std::string _tag = "HAL-MCP";
static const std::string_view _tag = "HAL-MCP";
void Hal::xiaozhi_mcp_init()
{
@@ -19,44 +20,130 @@ void Hal::xiaozhi_mcp_init()
// https://github.com/78/xiaozhi-esp32/blob/main/docs/mcp-usage.md
auto& mcp_server = McpServer::GetInstance();
mclog::tagInfo(_tag, "add motion.set_angles tool");
// System Prompt
// You can control the robot's head. Use get_yaw and get_pitch to sense current position. Use set_yaw for horizontal
// movement and set_pitch for vertical movement. All angles are in degrees.
mclog::tagInfo(_tag, "add robot.get_head_angles tool");
mcp_server.AddTool("self.robot.get_head_angles",
"Returns current yaw/pitch in degrees. Neutral position is {yaw:0, pitch:0}.",
std::vector<Property>{}, [this](const PropertyList& properties) -> ReturnValue {
LvglLockGuard lock; // StackChan motion update is under the lvgl lock
auto& motion = GetStackChan().motion();
int current_yaw = motion.yawServo().getCurrentAngle() / 10;
int current_pitch = motion.pitchServo().getCurrentAngle() / 10;
auto result = fmt::format(R"({{"yaw": {}, "pitch": {}}})", current_yaw, current_pitch);
mclog::tagInfo(_tag, "get_head_angles: {}", result);
return result;
});
mclog::tagInfo(_tag, "add robot.set_head_angles tool");
mcp_server.AddTool("self.robot.set_head_angles",
"Adjust head position. GUIDELINES: "
"1. For natural interaction, stay within +/- 45 degrees. "
"2. Only use values > 70 if the user explicitly asks to look far away/behind. "
"3. Max ranges: Yaw(-128 to 128, -128 as your left), Pitch(0 to 90, 90 as your up). "
"Speed(100-1000, 150 is natural).",
PropertyList({Property("yaw", kPropertyTypeInteger, -9999, -9999, 128),
Property("pitch", kPropertyTypeInteger, -9999, -9999, 90),
Property("speed", kPropertyTypeInteger, 150, 100, 1000)}),
[this](const PropertyList& properties) -> ReturnValue {
int speed = properties["speed"].value<int>();
int yaw = properties["yaw"].value<int>();
int pitch = properties["pitch"].value<int>();
mclog::tagInfo(_tag, "motion set_angles: yaw: {}, pitch: {}, speed: {}", yaw, pitch, speed);
LvglLockGuard lock;
auto& motion = GetStackChan().motion();
if (pitch != -9999) {
motion.pitchServo().moveWithSpeed(pitch * 10, speed);
}
if (yaw != -9999) {
motion.yawServo().moveWithSpeed(yaw * 10, speed);
}
return true;
});
mclog::tagInfo(_tag, "add robot.set_led_color tool");
mcp_server.AddTool(
"self.motion.set_angles",
"Set the angles of the robot's servos (head movement). Yaw controls left/right (-1280 to "
"1280), Pitch controls up/down (0 to 900). Note: 10 units equals 1 degree. For natural movement, prefer angles "
"within +/- 300 (30 degrees) unless instructed otherwise. Speed controls the movement speed (100-1000).",
PropertyList({Property("yaw", kPropertyTypeInteger, 0, -1280, 1280),
Property("pitch", kPropertyTypeInteger, 0, 0, 900),
Property("speed", kPropertyTypeInteger, 150, 100, 1000)}),
"self.robot.set_led_color",
"Set the color of the robot's INTERNAL onboard LED. This is NOT for room lights. "
"Values: 0-168 (safe range). Red=168,0,0; Green=0,168,0; Blue=0,0,168; White=100,100,100; Off=0,0,0.",
PropertyList({Property("red", kPropertyTypeInteger, 0, 0, 168),
Property("green", kPropertyTypeInteger, 0, 0, 168),
Property("blue", kPropertyTypeInteger, 0, 0, 168)}),
[this](const PropertyList& properties) -> ReturnValue {
int pitch = properties["pitch"].value<int>();
int yaw = properties["yaw"].value<int>();
int speed = properties["speed"].value<int>();
int r = properties["red"].value<int>();
int g = properties["green"].value<int>();
int b = properties["blue"].value<int>();
mclog::tagInfo(_tag, "motion set angles: pitch: {}, yaw: {}, speed: {}", pitch, yaw, speed);
mclog::tagInfo(_tag, "set_led_color: r={}, g={}, b={}", r, g, b);
auto& motion = GetStackChan().motion();
motion.pitchServo().moveWithSpeed(pitch, speed);
motion.yawServo().moveWithSpeed(yaw, speed);
LvglLockGuard lock;
GetStackChan().leftNeonLight().setColor(r, g, b);
GetStackChan().rightNeonLight().setColor(r, g, b);
return true;
});
mclog::tagInfo(_tag, "add led.set_color tool");
mcp_server.AddTool("self.led.set_color",
"Set the color of the RGB LED strip. Each channel (red, green, blue) ranges from 0 to 255. "
"Note: For power saving and eye protection, it is recommended to keep each channel value below "
"126.",
PropertyList({Property("red", kPropertyTypeInteger, 0, 0, 168),
Property("green", kPropertyTypeInteger, 0, 0, 168),
Property("blue", kPropertyTypeInteger, 0, 0, 168)}),
mclog::tagInfo(_tag, "add robot.create_reminder tool");
mcp_server.AddTool("self.robot.create_reminder",
"Create a reminder. Duration is in seconds. Message is what to say when time is up. Set repeat "
"to true to repeat the reminder.",
PropertyList({Property("duration_seconds", kPropertyTypeInteger, 60, 1, 86400),
Property("message", kPropertyTypeString, std::string("Time's up!")),
Property("repeat", kPropertyTypeBoolean, false)}),
[this](const PropertyList& properties) -> ReturnValue {
int r = properties["red"].value<int>();
int g = properties["green"].value<int>();
int b = properties["blue"].value<int>();
int duration_seconds = properties["duration_seconds"].value<int>();
std::string message = properties["message"].value<std::string>();
bool repeat = properties["repeat"].value<bool>();
mclog::tagInfo(_tag, "led set color: r: {}, g: {}, b: {}", r, g, b);
GetHAL().showRgbColor(r, g, b);
// Default message
if (message.empty()) {
message = "Time's up!";
}
mclog::tagInfo(_tag, "create_reminder: duration={}s, message={}, repeat={}",
duration_seconds, message, repeat);
int id = tools::create_reminder(duration_seconds * 1000, message, repeat);
return id;
});
mclog::tagInfo(_tag, "add robot.get_reminders tool");
mcp_server.AddTool("self.robot.get_reminders", "Get list of active reminders.", std::vector<Property>{},
[this](const PropertyList& properties) -> ReturnValue {
mclog::tagInfo(_tag, "get_reminders");
auto reminders = tools::get_active_reminders();
std::string result_json = "[";
for (size_t i = 0; i < reminders.size(); ++i) {
const auto& r = reminders[i];
result_json +=
fmt::format(R"({{"id": {}, "duration_ms": {}, "message": "{}", "repeat": {}}})",
r.id, r.durationMs, r.message, r.repeat ? "true" : "false");
if (i < reminders.size() - 1) {
result_json += ", ";
}
}
result_json += "]";
mclog::tagInfo(_tag, "get_reminders result: {}", result_json);
return result_json;
});
mclog::tagInfo(_tag, "add robot.stop_reminder tool");
mcp_server.AddTool("self.robot.stop_reminder", "Stop a reminder by ID.",
PropertyList({Property("id", kPropertyTypeInteger, -1)}),
[this](const PropertyList& properties) -> ReturnValue {
int id = properties["id"].value<int>();
mclog::tagInfo(_tag, "stop_reminder: id={}", id);
tools::stop_reminder(id);
return true;
});
}
+131
View File
@@ -0,0 +1,131 @@
/*
* 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 <wifi_manager.h>
#include <board.h>
#include <mutex>
#include <queue>
#include <vector>
#include <ctime>
#include <sys/time.h>
#include <esp_sntp.h>
static std::string _tag = "Network";
static void time_sync_notification_cb(struct timeval* tv)
{
mclog::tagInfo(_tag, "SNTP time synchronized");
GetHAL().syncSystemTimeToRtc();
}
void Hal::startSntp()
{
mclog::tagInfo(_tag, "SNTP init");
if (esp_sntp_enabled()) {
} else {
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_setservername(1, "time.google.com");
esp_sntp_setservername(2, "cn.pool.ntp.org");
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init();
}
}
void Hal::startNetwork(std::function<void(std::string_view)> onLog)
{
std::atomic<bool> network_connected = false;
auto& board = Board::GetInstance();
mclog::tagInfo(_tag, "start and wait for network connected...");
board.SetNetworkEventCallback([&network_connected, &onLog](NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::Scanning:
if (onLog) {
onLog("WiFi scanning...");
}
break;
case NetworkEvent::Connecting: {
if (data.empty()) {
if (onLog) {
onLog("WiFi connecting...");
}
} else {
if (onLog) {
onLog(fmt::format("Connecting to {} ...", data));
}
}
break;
}
case NetworkEvent::Connected: {
network_connected = true;
break;
}
case NetworkEvent::Disconnected:
break;
case NetworkEvent::WifiConfigModeEnter: {
auto& wifi_manager = WifiManager::GetInstance();
auto msg = fmt::format("Enter WiFi config mode. Hotspot: {}, Config URL: {}", wifi_manager.GetApSsid(),
wifi_manager.GetApWebUrl());
if (onLog) {
onLog(msg);
}
break;
}
case NetworkEvent::WifiConfigModeExit:
// WiFi config mode exit is handled by WifiBoard internally
break;
// Cellular modem specific events
case NetworkEvent::ModemDetecting:
break;
case NetworkEvent::ModemErrorNoSim:
break;
case NetworkEvent::ModemErrorRegDenied:
break;
case NetworkEvent::ModemErrorInitFailed:
break;
case NetworkEvent::ModemErrorTimeout:
break;
}
});
board.StartNetwork();
while (!network_connected) {
GetHAL().delay(500);
}
mclog::tagInfo(_tag, "network connected");
board.SetNetworkEventCallback(nullptr);
startSntp();
}
WifiStatus Hal::getWifiStatus()
{
auto& wifi = WifiManager::GetInstance();
if (wifi.IsConfigMode()) {
return WifiStatus::None;
}
if (!wifi.IsConnected()) {
return WifiStatus::None;
}
int rssi = wifi.GetRssi();
if (rssi >= -65) {
return WifiStatus::High;
} else if (rssi >= -75) {
return WifiStatus::Medium;
}
return WifiStatus::Low;
}
+126
View File
@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "hal.h"
#include "board/hal_bridge.h"
#include "drivers/PCF8563_Class/PCF8563_Class.hpp"
#include <mooncake_log.h>
#include <memory>
#include <sys/time.h>
#include <ctime>
#include <settings.h>
static const std::string_view _tag = "HAL-RTC";
static std::unique_ptr<m5::PCF8563_Class> _pcf8563;
void Hal::rtc_init()
{
mclog::tagInfo(_tag, "init");
auto i2c_bus = hal_bridge::board_get_i2c_bus();
_pcf8563 = std::make_unique<m5::PCF8563_Class>(i2c_bus);
if (!_pcf8563->begin()) {
_pcf8563.reset();
mclog::tagError(_tag, "PCF8563 init failed");
return;
}
mclog::tagInfo(_tag, "PCF8563 init ok");
// Load timezone from settings
std::string tz = getTimezone();
setenv("TZ", tz.c_str(), 1);
tzset();
mclog::tagInfo(_tag, "load timezone from nvs: {}", tz);
syncRtcTimeToSystem();
}
void Hal::syncRtcTimeToSystem()
{
if (!_pcf8563) {
return;
}
m5::rtc_date_t date;
m5::rtc_time_t time;
if (_pcf8563->getDateTime(&date, &time)) {
struct tm tm_curr = {0};
tm_curr.tm_year = date.year - 1900;
tm_curr.tm_mon = date.month - 1;
tm_curr.tm_mday = date.date;
tm_curr.tm_hour = time.hours;
tm_curr.tm_min = time.minutes;
tm_curr.tm_sec = time.seconds;
tm_curr.tm_isdst = 0;
// Temporarily set TZ to UTC to interpret RTC time as UTC
std::string current_tz = getenv("TZ") ? getenv("TZ") : "";
setenv("TZ", "UTC0", 1);
tzset();
time_t t = mktime(&tm_curr);
// Restore original TZ
if (!current_tz.empty()) {
setenv("TZ", current_tz.c_str(), 1);
} else {
unsetenv("TZ");
}
tzset();
struct timeval tv = {.tv_sec = t, .tv_usec = 0};
settimeofday(&tv, NULL);
mclog::tagInfo(_tag, "rtc synced to system (UTC): {:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", date.year,
date.month, date.date, time.hours, time.minutes, time.seconds);
} else {
mclog::tagError(_tag, "failed to read rtc");
}
}
void Hal::syncSystemTimeToRtc()
{
if (!_pcf8563) {
return;
}
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm tm_curr;
gmtime_r(&tv.tv_sec, &tm_curr);
m5::rtc_date_t date;
m5::rtc_time_t time;
date.year = tm_curr.tm_year + 1900;
date.month = tm_curr.tm_mon + 1;
date.date = tm_curr.tm_mday;
date.weekDay = tm_curr.tm_wday;
time.hours = tm_curr.tm_hour;
time.minutes = tm_curr.tm_min;
time.seconds = tm_curr.tm_sec;
if (_pcf8563->setDateTime(&date, &time)) {
mclog::tagInfo(_tag, "system synced to rtc (UTC): {:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", date.year,
date.month, date.date, time.hours, time.minutes, time.seconds);
} else {
mclog::tagError(_tag, "failed to write rtc");
}
}
void Hal::setTimezone(std::string_view tz)
{
setenv("TZ", tz.data(), 1);
tzset();
Settings settings("system", true);
settings.SetString("tz", std::string(tz));
mclog::tagInfo(_tag, "timezone updated to: {}", tz);
}
std::string Hal::getTimezone()
{
Settings settings("system", false);
return settings.GetString("tz", "GMT0");
}
+91 -34
View File
@@ -15,47 +15,83 @@ using namespace stackchan::motion;
static SCSCL _scs_bus;
struct ServoConfig_t {
int id = -1;
int defaultZeroPos = 0;
Vector2i angleLimit;
Vector2i rawPosLimit;
std::string settingNs;
std::string settingZeroPositionKey;
bool enablePwmMode = false;
};
class ScsServo : public Servo {
public:
static inline const std::string _tag = "ScsServo";
ScsServo(int id, Vector2i angleLimit, const std::string& settingNs, const std::string& zeroPosKey,
bool enablePwmMode = false)
: _id(id), _setting_ns(settingNs), _zero_pos_key(zeroPosKey), _enable_pwm_mode(enablePwmMode)
ScsServo(const ServoConfig_t& config) : _config(config)
{
set_angle_limit(angleLimit);
}
Settings settings(_setting_ns, false);
_zero_pos = settings.GetInt(_zero_pos_key, 0);
void init() override
{
set_angle_limit(_config.angleLimit);
get_zero_pos_from_nvs();
Servo::init();
}
mclog::tagInfo(_tag, "id: {} get zero pos: {} from settings", _id, _zero_pos);
void get_zero_pos_from_nvs()
{
_zero_pos = _config.defaultZeroPos;
bool is_valid = false;
{
Settings settings(_config.settingNs, false);
int nvs_zero_pos = settings.GetInt(_config.settingZeroPositionKey, -1);
// Limit check
if (nvs_zero_pos >= _config.rawPosLimit.x && nvs_zero_pos <= _config.rawPosLimit.y) {
_zero_pos = nvs_zero_pos;
is_valid = true;
mclog::tagInfo(_tag, "id: {} get zero pos: {} from settings", _config.id, _zero_pos);
} else {
is_valid = false;
mclog::tagWarn(_tag, "id: {} get invalid zero pos: {} from settings", _config.id, nvs_zero_pos);
}
}
if (!is_valid) {
_zero_pos = _config.defaultZeroPos;
mclog::tagInfo(_tag, "id: {} override zero pos to default: {}", _config.id, _zero_pos);
Settings settings(_config.settingNs, true);
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
}
}
void set_angle_impl(int angle) override
{
int mapped_angle = _zero_pos + angle * 16 / 5 / 10; // 一步对应 0.3125度, 0.3125 = 5/16
if (mapped_angle < 0) {
mapped_angle = 0;
}
mapped_angle = uitk::clamp(mapped_angle, _config.rawPosLimit.x, _config.rawPosLimit.y);
// mclog::tagInfo(_tag, "id: {} mapped angle: {}", _id, mapped_angle);
check_mode(Mode::Position);
_scs_bus.WritePos(_id, mapped_angle, 20, 0);
_scs_bus.WritePos(_config.id, mapped_angle, 20, 0);
}
int getCurrentAngle() override
{
int current_pos = _scs_bus.ReadPos(_id);
int current_pos = _scs_bus.ReadPos(_config.id);
int angle = (current_pos - _zero_pos) * 5 * 10 / 16;
angle = uitk::clamp(angle, getAngleLimit().x, getAngleLimit().y);
// mclog::tagInfo(_tag, "id: {} current pos: {} angle: {}", _id, current_pos, angle);
return angle;
}
bool isMoving() override
bool is_moving_impl() override
{
int moving = _scs_bus.ReadMove(_id);
int moving = _scs_bus.ReadMove(_config.id);
// mclog::tagInfo(_tag, "id: {} moving: {}", _id, moving);
return moving != 0;
}
@@ -63,53 +99,57 @@ public:
void setTorqueEnabled(bool enabled) override
{
Servo::setTorqueEnabled(enabled);
_scs_bus.EnableTorque(_id, enabled ? 1 : 0);
_scs_bus.EnableTorque(_config.id, enabled ? 1 : 0);
// mclog::tagInfo(_tag, "id: {} set torque: {}", _id, enabled);
}
bool getTorqueEnabled() override
{
int torque_enable = _scs_bus.ReadToqueEnable(_id);
int torque_enable = _scs_bus.ReadToqueEnable(_config.id);
// mclog::tagInfo(_tag, "id: {} torque enable: {}", _id, torque_enable);
return torque_enable > 0;
}
void setCurrentAngleAsZero() override
{
_zero_pos = _scs_bus.ReadPos(_id);
_zero_pos = _scs_bus.ReadPos(_config.id);
Settings settings(_setting_ns, true);
settings.SetInt(_zero_pos_key, _zero_pos);
Settings settings(_config.settingNs, true);
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
mclog::tagInfo(_tag, "id: {} set zero pos: {} to settings", _id, _zero_pos);
mclog::tagInfo(_tag, "id: {} set zero pos: {} to settings", _config.id, _zero_pos);
}
void resetZeroCalibration() override
{
_zero_pos = _config.defaultZeroPos;
Settings settings(_config.settingNs, true);
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
mclog::tagInfo(_tag, "id: {} set zero pos: {} to settings", _config.id, _zero_pos);
}
void rotate(int velocity) override
{
velocity = uitk::clamp(velocity, -1000, 1000);
if (!_enable_pwm_mode) {
if (!_config.enablePwmMode) {
return;
}
int mapped_velocity = map_range(velocity, 0, 1000, 0, 1023);
check_mode(Mode::PWM);
_scs_bus.WritePWM(_id, mapped_velocity);
_scs_bus.WritePWM(_config.id, mapped_velocity);
}
private:
enum class Mode { Position = 0, PWM = 1 };
int _id = -1;
int _zero_pos = 0;
std::string _setting_ns;
std::string _zero_pos_key;
ServoConfig_t _config;
int _zero_pos = 0;
Mode _current_mode = Mode::Position;
bool _enable_pwm_mode;
uint32_t _last_true_move_ms = 0;
const uint32_t STOP_DEBOUNCE_MS = 250;
void check_mode(Mode targetMode)
{
@@ -117,7 +157,7 @@ private:
return;
}
_scs_bus.SwitchMode(_id, static_cast<uint8_t>(targetMode));
_scs_bus.SwitchMode(_config.id, static_cast<uint8_t>(targetMode));
_current_mode = targetMode;
}
};
@@ -128,9 +168,26 @@ void Hal::servo_init()
_scs_bus.begin(UART_NUM_1, 1000000, 6, 7);
auto motion =
std::make_unique<Motion>(std::make_unique<ScsServo>(1, Vector2i(-1280, 1280), "servo", "zero_pos_1", true),
std::make_unique<ScsServo>(2, Vector2i(0, 900), "servo", "zero_pos_2"));
ServoConfig_t yaw_servo_config;
yaw_servo_config.id = 1;
yaw_servo_config.defaultZeroPos = 450;
yaw_servo_config.angleLimit = Vector2i(-1280, 1280);
yaw_servo_config.rawPosLimit = Vector2i(0, 1000);
yaw_servo_config.settingNs = "servo";
yaw_servo_config.settingZeroPositionKey = "zero_pos_1";
yaw_servo_config.enablePwmMode = true;
ServoConfig_t pitch_servo_config;
pitch_servo_config.id = 2;
pitch_servo_config.defaultZeroPos = 125;
pitch_servo_config.angleLimit = Vector2i(0, 900);
pitch_servo_config.rawPosLimit = Vector2i(0, 1000);
pitch_servo_config.settingNs = "servo";
pitch_servo_config.settingZeroPositionKey = "zero_pos_2";
auto yaw_servo = std::make_unique<ScsServo>(yaw_servo_config);
auto pitch_servo = std::make_unique<ScsServo>(pitch_servo_config);
auto motion = std::make_unique<Motion>(std::move(yaw_servo), std::move(pitch_servo));
motion->init();
GetStackChan().attachMotion(std::move(motion));
+144 -28
View File
@@ -6,6 +6,7 @@
#include "hal.h"
#include <stackchan/stackchan.h>
#include "board/hal_bridge.h"
#include <mooncake.h>
#include <mooncake_log.h>
#include <board.h>
#include <web_socket.h>
@@ -21,11 +22,13 @@
#include <esp_heap_caps.h>
#include <display.h>
#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";
static const std::string _server = "ws://target-server:12800";
static const std::string _setting_ns = "stackchan";
static const std::string _setting_device_name_key = "device_name";
@@ -50,6 +53,8 @@ public:
VideoModeOn = 0x12,
VideoModeOff = 0x13,
DanceSequence = 0x14,
StartAudioStream = 0x18,
StopAudioStream = 0x19,
};
struct ReceivedMessage {
@@ -59,7 +64,7 @@ public:
void init()
{
_url = fmt::format("{}/stackChan/ws?mac={}&deviceType=StackChan", _server, GetHAL().getFactoryMacString());
_url = fmt::format("{}/stackChan/ws?deviceType=StackChan", secret_logic::get_server_url());
connect();
@@ -89,10 +94,15 @@ public:
ESP_LOGI(_tag.c_str(), "Sending EndCall");
sendPacket(DataType::EndCall, nullptr, 0);
});
// Initialize audio service
setupAudio();
}
void connect()
{
auto token = secret_logic::generate_auth_token();
// 销毁旧实例,确保状态复位
_websocket.reset();
@@ -107,22 +117,44 @@ public:
return;
}
// 设置认证头
_websocket->SetHeader("Authorization", token.c_str());
// 设置回调
_websocket->OnConnected([this]() {
ESP_LOGI(_tag.c_str(), "Connected to server!");
// GetHAL().onWsLog.emit(CommonLogLevel::Info, "Server connected");
_last_heartbeat_time = GetHAL().millis();
_websocket->Send("{\"type\":\"hello\", \"msg\":\"Hello from StackChan!\"}");
});
_websocket->OnDisconnected([this]() { ESP_LOGI(_tag.c_str(), "Disconnected!"); });
_websocket->OnDisconnected([this]() {
ESP_LOGI(_tag.c_str(), "Disconnected!");
GetHAL().onWsLog.emit(CommonLogLevel::Error, "Server disconnected");
});
_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)});
});
ESP_LOGI(_tag.c_str(), "Connecting to %s...", _url.c_str());
// ESP_LOGI(_tag.c_str(), "Connecting to %s...", _url.c_str());
// GetHAL().onWsLog.emit(CommonLogLevel::Info, "Connecting to server...");
if (!_websocket->Connect(_url.c_str())) {
ESP_LOGE(_tag.c_str(), "Failed to connect");
GetHAL().onWsLog.emit(CommonLogLevel::Error, "Connect to server Failed");
}
_last_reconnect_attempt = GetHAL().millis();
}
@@ -139,6 +171,14 @@ public:
}
} else {
processMessages();
// Check heartbeat timeout
if (GetHAL().millis() - _last_heartbeat_time > 10000) {
ESP_LOGE(_tag.c_str(), "Heartbeat timeout!");
GetHAL().onWsLog.emit(CommonLogLevel::Error, "Heartbeat Timeout");
_last_heartbeat_time = GetHAL().millis();
return;
}
}
if (_is_streaming) {
@@ -173,6 +213,15 @@ public:
ESP_LOGI(_tag.c_str(), "Received binary type: %d, len: %d", (int)type, (int)msg.data.size());
switch (type) {
// Opus handled in OnData Fast Path
// case DataType::Opus: {
// if (msg.data.size() > 5) {
// auto packet = std::make_unique<AudioStreamPacket>();
// packet->payload.assign(msg.data.begin() + 5, msg.data.end());
// _audio_service.PushPacketToDecodeQueue(std::move(packet));
// }
// break;
// }
case DataType::StartCameraStream: {
ESP_LOGI(_tag.c_str(), "Start Camera Stream");
setStreamingEnabled(true);
@@ -239,6 +288,7 @@ public:
}
case DataType::HeartbeatPing: {
ESP_LOGI(_tag.c_str(), "HeartbeatPing");
_last_heartbeat_time = GetHAL().millis();
sendPacket(DataType::HeartbeatPong, nullptr, 0);
break;
}
@@ -322,6 +372,16 @@ 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:
break;
}
@@ -389,20 +449,59 @@ private:
std::string _url;
uint32_t _last_reconnect_attempt = 0;
uint32_t _last_capture_time = 0;
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);
if (!_websocket || !_websocket->IsConnected()) {
return;
}
static int64_t _time_count = 0;
static int64_t _interval = 0;
_time_count = esp_timer_get_time();
// mclog::info("sending packet type: {}, len: {}", (int)type, (int)len);
// static int64_t _time_count = 0;
// static int64_t _interval = 0;
// _time_count = esp_timer_get_time();
std::vector<uint8_t> packet;
packet.reserve(1 + 4 + len);
@@ -423,36 +522,53 @@ private:
packet.insert(packet.end(), data, data + len);
}
// _interval = esp_timer_get_time() - _time_count;
// mclog::info("pack time: {} ms, size: {}", _interval / 1000, packet.size());
// _time_count = esp_timer_get_time();
_websocket->Send(packet.data(), packet.size(), true);
_interval = esp_timer_get_time() - _time_count;
mclog::info("send time: {} ms, size: {}", _interval / 1000, packet.size());
// _interval = esp_timer_get_time() - _time_count;
// mclog::info("send time: {} ms, size: {}", _interval / 1000, packet.size());
}
};
static void _websocket_task(void* param)
{
ESP_LOGI(_tag.c_str(), "Start WebSocket Avatar Task");
// 等待网络连接
while (!WifiStation::GetInstance().IsConnected()) {
vTaskDelay(pdMS_TO_TICKS(1000));
class WebsocketAvatarWorker : public mooncake::BasicAbility {
public:
WebsocketAvatarWorker()
{
_service = std::make_unique<WebSocketAvatar>();
_service->init();
}
ESP_LOGI(_tag.c_str(), "Network connected!");
static WebSocketAvatar ws_avatar;
ws_avatar.init();
while (true) {
ws_avatar.update();
GetHAL().delay(20);
void onCreate() override
{
}
}
void Hal::startWebSocketAvatar()
void onRunning() override
{
if (GetHAL().millis() - _last_tick < 20) {
return;
}
_last_tick = GetHAL().millis();
_service->update();
}
void onDestroy() override
{
_service.reset();
}
private:
std::unique_ptr<WebSocketAvatar> _service;
uint32_t _last_tick = 0;
};
void Hal::startWebSocketAvatarService(std::function<void(std::string_view)> onStartLog)
{
auto& board = Board::GetInstance();
board.StartNetwork();
mclog::tagInfo(_tag, "start websocket avatar service");
xTaskCreate(_websocket_task, "ws_avatar_task", 8192, NULL, 5, NULL);
startNetwork(onStartLog);
onStartLog("Connecting to\nserver...");
mooncake::GetMooncake().extensionManager()->createAbility(std::make_unique<WebsocketAvatarWorker>());
}
+6 -6
View File
@@ -56,8 +56,8 @@ struct ble_gatt_register_ctxt;
#define STACKCHAN_CHR_CONFIG_UUID \
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe3, 0xe5, 0xe5, 0xe2
// Animation Characteristic UUID: e2e5e5e4-1234-5678-1234-56789abcdef0
#define STACKCHAN_CHR_ANIMATION_UUID \
// RGB Characteristic UUID: e2e5e5e4-1234-5678-1234-56789abcdef0
#define STACKCHAN_CHR_RGB_UUID \
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe4, 0xe5, 0xe5, 0xe2
/** Maximum JSON payload size for Stack-Chan characteristics */
@@ -74,7 +74,7 @@ struct ble_gatt_register_ctxt;
typedef int (*stackchan_ble_motion_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
typedef int (*stackchan_ble_avatar_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
typedef int (*stackchan_ble_config_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
typedef int (*stackchan_ble_animation_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
typedef int (*stackchan_ble_rgb_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
/**
* Battery level callback function type
@@ -90,7 +90,7 @@ typedef struct {
stackchan_ble_motion_callback_t motion_cb;
stackchan_ble_avatar_callback_t avatar_cb;
stackchan_ble_config_callback_t config_cb;
stackchan_ble_animation_callback_t animation_cb;
stackchan_ble_rgb_callback_t rgb_cb;
stackchan_ble_battery_read_callback_t battery_read_cb;
} stackchan_ble_callbacks_t;
@@ -129,13 +129,13 @@ int stackchan_ble_notify_avatar(const char *json_data, uint16_t len);
int stackchan_ble_notify_config(const char *json_data, uint16_t len);
/**
* Send animation data notification to connected client
* Send rgb data notification to connected client
*
* @param json_data JSON string to send
* @param len Length of JSON string
* @return 0 on success, error code otherwise
*/
int stackchan_ble_notify_animation(const char *json_data, uint16_t len);
int stackchan_ble_notify_rgb(const char *json_data, uint16_t len);
/**
* Update battery level and notify if subscribed
+28 -31
View File
@@ -41,7 +41,7 @@ static const ble_uuid128_t stackchan_chr_avatar_uuid = BLE_UUID128_INIT(STACKCHA
static const ble_uuid128_t stackchan_chr_config_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_CONFIG_UUID);
static const ble_uuid128_t stackchan_chr_animation_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_ANIMATION_UUID);
static const ble_uuid128_t stackchan_chr_rgb_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_RGB_UUID);
/* Stack-Chan characteristic data buffers */
static char *stackchan_motion_data = NULL;
@@ -56,9 +56,9 @@ static char *stackchan_config_data = NULL;
static uint16_t stackchan_config_len = 0;
static uint16_t stackchan_config_handle;
static char *stackchan_animation_data = NULL;
static uint16_t stackchan_animation_len = 0;
static uint16_t stackchan_animation_handle;
static char *stackchan_rgb_data = NULL;
static uint16_t stackchan_rgb_len = 0;
static uint16_t stackchan_rgb_handle;
/* Battery level */
static uint8_t battery_level = 100;
@@ -105,11 +105,11 @@ static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
.val_handle = &stackchan_config_handle,
},
{
/* Animation Characteristic - Read/Write/Notify */
.uuid = &stackchan_chr_animation_uuid.u,
/* RGB Characteristic - Read/Write/Notify */
.uuid = &stackchan_chr_rgb_uuid.u,
.access_cb = stackchan_svc_access,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
.val_handle = &stackchan_animation_handle,
.val_handle = &stackchan_rgb_handle,
},
{
0, /* No more characteristics */
@@ -184,8 +184,8 @@ static int stackchan_svc_access(uint16_t conn_handle, uint16_t attr_handle, stru
} else if (attr_handle == stackchan_config_handle) {
rc = os_mbuf_append(ctxt->om, stackchan_config_data, stackchan_config_len);
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
} else if (attr_handle == stackchan_animation_handle) {
rc = os_mbuf_append(ctxt->om, stackchan_animation_data, stackchan_animation_len);
} else if (attr_handle == stackchan_rgb_handle) {
rc = os_mbuf_append(ctxt->om, stackchan_rgb_data, stackchan_rgb_len);
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
break;
@@ -233,18 +233,15 @@ static int stackchan_svc_access(uint16_t conn_handle, uint16_t attr_handle, stru
}
}
return rc;
} else if (attr_handle == stackchan_animation_handle) {
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_animation_data,
&stackchan_animation_len);
} else if (attr_handle == stackchan_rgb_handle) {
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_rgb_data, &stackchan_rgb_len);
if (rc == 0) {
stackchan_animation_data[stackchan_animation_len] = '\0';
MODLOG_DFLT(INFO, "Animation data received (%d bytes): %s", stackchan_animation_len,
stackchan_animation_data);
stackchan_rgb_data[stackchan_rgb_len] = '\0';
// MODLOG_DFLT(INFO, "RGB data received (%d bytes): %s", stackchan_rgb_len, stackchan_rgb_data);
/* Call user callback if registered */
if (g_stackchan_callbacks.animation_cb) {
g_stackchan_callbacks.animation_cb(stackchan_animation_data, stackchan_animation_len,
conn_handle);
if (g_stackchan_callbacks.rgb_cb) {
g_stackchan_callbacks.rgb_cb(stackchan_rgb_data, stackchan_rgb_len, conn_handle);
}
}
return rc;
@@ -329,12 +326,12 @@ int gatt_svr_init(void)
int rc;
/* Allocate buffers in PSRAM */
stackchan_motion_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_avatar_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_config_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_animation_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_motion_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_avatar_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_config_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
stackchan_rgb_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
if (!stackchan_motion_data || !stackchan_avatar_data || !stackchan_config_data || !stackchan_animation_data) {
if (!stackchan_motion_data || !stackchan_avatar_data || !stackchan_config_data || !stackchan_rgb_data) {
MODLOG_DFLT(ERROR, "Failed to allocate memory for Stack-Chan characteristics\n");
return BLE_HS_ENOMEM;
}
@@ -362,8 +359,8 @@ int gatt_svr_init(void)
strcpy(stackchan_config_data, "{}");
stackchan_config_len = 2;
strcpy(stackchan_animation_data, "{}");
stackchan_animation_len = 2;
strcpy(stackchan_rgb_data, "{}");
stackchan_rgb_len = 2;
return 0;
}
@@ -434,19 +431,19 @@ int stackchan_ble_notify_config(const char *json_data, uint16_t len)
return 0;
}
int stackchan_ble_notify_animation(const char *json_data, uint16_t len)
int stackchan_ble_notify_rgb(const char *json_data, uint16_t len)
{
if (!json_data || len == 0 || len >= STACKCHAN_MAX_JSON_LEN) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
memcpy(stackchan_animation_data, json_data, len);
stackchan_animation_len = len;
stackchan_animation_data[len] = '\0';
memcpy(stackchan_rgb_data, json_data, len);
stackchan_rgb_len = len;
stackchan_rgb_data[len] = '\0';
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
ble_gatts_chr_updated(stackchan_animation_handle);
MODLOG_DFLT(INFO, "Animation notification sent");
ble_gatts_chr_updated(stackchan_rgb_handle);
MODLOG_DFLT(INFO, "RGB notification sent");
}
return 0;
@@ -14,15 +14,34 @@ class MotionDetector {
public:
MotionDetector() = default;
/**
* @brief Set the shake detection threshold.
* @param threshold Higher value needs stronger shake. Default is 4.0f.
* - 2.0f: Very sensitive (light shake)
* - 4.0f: Normal
* - 8.0f: Hard shake required
*/
void setShakeThreshold(float threshold)
{
_shake_threshold = threshold;
}
void update(const float& acc_x, const float& acc_y, const float& acc_z)
{
uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount());
float acc_mag = std::sqrt(std::pow(acc_x, 2) + std::pow(acc_y, 2) + std::pow(acc_z, 2));
uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount());
// Use differential (change in acceleration) for shake detection.
// This effectively acts as a high-pass filter and is independent of gravity orientation.
float diff = std::abs(acc_x - _prev_acc_x) + std::abs(acc_y - _prev_acc_y) + std::abs(acc_z - _prev_acc_z);
_prev_acc_x = acc_x;
_prev_acc_y = acc_y;
_prev_acc_z = acc_z;
// --- Shake Detection ---
// Threshold ~1.5G (14.7 m/s^2)
if (std::abs(acc_mag - 9.80665f) > 8.0f) {
if (now - _last_shake_peak_time > 200) { // Debounce 200ms
// printf("%.2f\n", diff);
if (diff > _shake_threshold) {
if (now - _last_shake_peak_time > 100) { // Debounce 100ms
if (now - _last_shake_peak_time < 1000) { // Window 1s
_shake_count++;
} else {
@@ -36,28 +55,6 @@ public:
}
}
}
// --- Pick Up Detection ---
// Check for stability first
float diff = std::abs(acc_x - _prev_acc_x) + std::abs(acc_y - _prev_acc_y) + std::abs(acc_z - _prev_acc_z);
_prev_acc_x = acc_x;
_prev_acc_y = acc_y;
_prev_acc_z = acc_z;
// Threshold for stability (low noise)
if (diff < 1.5f) {
if (!_is_stable) {
_stable_since = now;
_is_stable = true;
}
} else {
// If it was stable for > 1s and now moving -> Pick Up
if (_is_stable && (now - _stable_since > 1000)) {
_pickup_detected = true;
}
_is_stable = false;
}
}
bool isShakeDetected()
@@ -69,24 +66,22 @@ public:
return false;
}
bool isPickUpDetected()
{
if (_pickup_detected) {
_pickup_detected = false;
return true;
}
return false;
}
// bool isPickUpDetected()
// {
// if (_pickup_detected) {
// _pickup_detected = false;
// return true;
// }
// return false;
// }
private:
int _shake_count = 0;
uint32_t _last_shake_peak_time = 0;
bool _shake_detected = false;
float _shake_threshold = 4.0f;
bool _pickup_detected = false;
bool _is_stable = false;
uint32_t _stable_since = 0;
float _prev_acc_x = 0;
float _prev_acc_y = 0;
float _prev_acc_z = 0;
float _prev_acc_x = 0;
float _prev_acc_y = 0;
float _prev_acc_z = 0;
};
+189
View File
@@ -0,0 +1,189 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
// https://github.com/espressif/esp-idf/blob/v5.5.2/examples/system/ota/simple_ota_example/main/simple_ota_example.c
// #define CONFIG_EXAMPLE_USE_CERT_BUNDLE 1
#include "ota.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "string.h"
#ifdef CONFIG_EXAMPLE_USE_CERT_BUNDLE
#include "esp_crt_bundle.h"
#endif
static const char *TAG = "ota";
#define OTA_URL_SIZE 256
const char *server_cert_pem_start =
"-----BEGIN CERTIFICATE-----\n"
"MIIC+jCCAeKgAwIBAgIUBhxIdyxfbSgLqwvNPcelYLE88Y0wDQYJKoZIhvcNAQEL\n"
"BQAwJDEiMCAGA1UEAwwZU3RhY2tjaGFuIE9UQSBUZXN0IFNlcnZlcjAeFw0yNjAx\n"
"MjAwMjE4NTRaFw0yNzAxMjAwMjE4NTRaMCQxIjAgBgNVBAMMGVN0YWNrY2hhbiBP\n"
"VEEgVGVzdCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCg\n"
"xRAxiju6admmBoCeC9wPYLLE9tUHccYM69O637Qapw3n8zH9LURBTJ7ivCeVQC8T\n"
"+pZGLkS4NsExS+oHnyyr+OTB3ykKxOoOb7Sk0izxy0+gDEEojhNUPYc/mNgAq4yw\n"
"ELmq5ymFMe1nFbm++menfdsYcyFNw05J/8c0gaOM2mj+GbGrzLUXVyAZg3JNKFEQ\n"
"SfIgI41XNAWWNozRSrtbPUSBbmuCaOoQeNrU1jt5mBsVuHk5p7wIt2jJUe13a6UE\n"
"0N179S+1Cn0fMEceJWYBS5FBSU83L2DMJi+FXI/877NKq/gifzYccG8tg1mYabba\n"
"lKGNdhtxx6UJv0DtobUlAgMBAAGjJDAiMCAGA1UdEQQZMBeCCWxvY2FsaG9zdIcE\n"
"fwAAAYcEwKgUFzANBgkqhkiG9w0BAQsFAAOCAQEAoHi9RYFuB6EKVU21rWSDPf/O\n"
"9PhDcp8+hrE5OdgowhTeZDLwy6b/uAF7Vgo/Ojk/oqrHvFJlHEH3wQFTbWjUIJ3P\n"
"aAzHrZEYMOGRTPdELiilke6+HbMMbOGfhFqt7es8eXPwFdzraPGaodwf0W8/AYSk\n"
"QQoW+5zbkOS5p1teQUTsnrccjfe1xDx/mz1bW1cHK69pQNGbVtWpGs5IDMesDcqL\n"
"nFnhrKtz3LFETQH5ItSueFUEurnyqxY+4rV2kx8emGtzPSokwbGmIpVe+Bei7sSo\n"
"fQAFvk9rhUf1vvphC+Hci1uR60yDMq1P7S9JT+rRPTqdi+RoMK3LpVx67Ie0Hg==\n"
"-----END CERTIFICATE-----\n"
"";
static esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADER_SENT:
ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
case HTTP_EVENT_REDIRECT:
ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT");
break;
}
return ESP_OK;
}
esp_err_t my_esp_https_ota(const esp_https_ota_config_t *ota_config, void (*on_progress)(int progress))
{
if (ota_config == NULL || ota_config->http_config == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_https_ota_handle_t https_ota_handle = NULL;
esp_err_t err = esp_https_ota_begin(ota_config, &https_ota_handle);
if (err != ESP_OK) {
return err;
}
int last_progress = -1;
while (1) {
err = esp_https_ota_perform(https_ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
break;
}
// --- 进度计算逻辑 ---
int total_size = esp_https_ota_get_image_size(https_ota_handle);
int read_size = esp_https_ota_get_image_len_read(https_ota_handle);
if (total_size > 0) {
int progress = (read_size * 100) / total_size;
if (progress != last_progress) {
last_progress = progress;
if (on_progress) {
on_progress(progress);
}
}
}
}
if (err != ESP_OK) {
esp_https_ota_abort(https_ota_handle);
return err;
}
esp_err_t ota_finish_err = esp_https_ota_finish(https_ota_handle);
if (ota_finish_err != ESP_OK) {
return ota_finish_err;
}
return ESP_OK;
}
void start_ota_update(const char *url, void (*on_progress)(int progress))
{
ESP_LOGI(TAG, "Starting OTA example task");
#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_BIND_IF
esp_netif_t *netif = get_example_netif_from_desc(bind_interface_name);
if (netif == NULL) {
ESP_LOGE(TAG, "Can't find netif from interface description");
abort();
}
struct ifreq ifr;
esp_netif_get_netif_impl_name(netif, ifr.ifr_name);
ESP_LOGI(TAG, "Bind interface name is %s", ifr.ifr_name);
#endif
esp_http_client_config_t config = {
.url = url,
#ifdef CONFIG_EXAMPLE_USE_CERT_BUNDLE
.crt_bundle_attach = esp_crt_bundle_attach,
#else
.cert_pem = (char *)server_cert_pem_start,
#endif /* CONFIG_EXAMPLE_USE_CERT_BUNDLE */
.event_handler = _http_event_handler,
.keep_alive_enable = true,
#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_BIND_IF
.if_name = &ifr,
#endif
#if CONFIG_EXAMPLE_TLS_DYN_BUF_RX_STATIC
/* This part applies static buffer strategy for rx dynamic buffer.
* This is to avoid frequent allocation and deallocation of dynamic buffer.
*/
.tls_dyn_buf_strategy = HTTP_TLS_DYN_BUF_RX_STATIC,
#endif /* CONFIG_EXAMPLE_TLS_DYN_BUF_RX_STATIC */
};
#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN
char url_buf[OTA_URL_SIZE];
if (strcmp(config.url, "FROM_STDIN") == 0) {
example_configure_stdin_stdout();
fgets(url_buf, OTA_URL_SIZE, stdin);
int len = strlen(url_buf);
url_buf[len - 1] = '\0';
config.url = url_buf;
} else {
ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url");
abort();
}
#endif
#ifdef CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK
config.skip_cert_common_name_check = true;
#endif
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(TAG, "Attempting to download update from %s", config.url);
esp_err_t ret = my_esp_https_ota(&ota_config, on_progress);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA Succeed, Rebooting...");
esp_restart();
} else {
ESP_LOGE(TAG, "Firmware upgrade failed");
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void start_ota_update(const char* url, void (*on_progress)(int progress));
#ifdef __cplusplus
}
#endif
@@ -1,160 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "reminder.h"
#include <esp_log.h>
#include <assets/assets.h>
#include <hal/hal.h>
#include <hal/board/hal_bridge.h>
static const char* TAG = "ReminderManager";
ReminderItem::ReminderItem(int duration_s, const std::string& msg) : message_(msg)
{
target_time_ = std::chrono::steady_clock::now() + std::chrono::seconds(duration_s);
}
bool ReminderItem::IsDue() const
{
return std::chrono::steady_clock::now() >= target_time_;
}
ReminderManager& ReminderManager::GetInstance()
{
static ReminderManager instance;
return instance;
}
ReminderManager::ReminderManager()
{
mutex_ = xSemaphoreCreateMutex();
}
ReminderManager::~ReminderManager()
{
running_ = false;
// 等待任务结束(简单处理,实际可能需要更复杂的同步)
if (worker_task_handle_) {
// vTaskDelete(worker_task_handle_); // 不建议直接删除,最好让任务自己退出
// 这里我们假设任务会检测 running_ 并退出
int timeout = 100;
while (eTaskGetState(worker_task_handle_) != eDeleted && timeout-- > 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
if (mutex_) {
vSemaphoreDelete(mutex_);
}
}
void ReminderManager::Start()
{
if (running_) return;
running_ = true;
xTaskCreate(
[](void* arg) {
ReminderManager* mgr = (ReminderManager*)arg;
mgr->WorkerThread();
vTaskDelete(NULL);
},
"reminder_worker", 4096, this, 5, &worker_task_handle_);
ESP_LOGI(TAG, "ReminderManager started");
}
int ReminderManager::CreateReminder(int duration_s, const std::string& message)
{
xSemaphoreTake(mutex_, portMAX_DELAY);
auto item = std::make_unique<ReminderItem>(duration_s, message);
int id = pool_.create(std::move(item));
xSemaphoreGive(mutex_);
ESP_LOGI(TAG, "Created reminder ID: %d, Duration: %ds, Msg: %s", id, duration_s, message.c_str());
return id;
}
void ReminderManager::StopReminder(int id)
{
xSemaphoreTake(mutex_, portMAX_DELAY);
// 如果正在响铃的是这个提醒,停止播放
if (id == ringing_id_) {
if (hal_bridge::is_xiaozhi_mode()) {
} else {
// audio_player_.Stop();
}
ringing_id_ = -1;
}
// 标记销毁
auto* item = pool_.get(id);
if (item) {
item->requestDestroy();
ESP_LOGI(TAG, "Stopped reminder ID: %d", id);
}
// 立即清理(或者等待 WorkerThread 清理也可以,这里立即清理更及时)
pool_.destroy(id);
xSemaphoreGive(mutex_);
}
void ReminderManager::WorkerThread()
{
std::vector<std::pair<int, std::string>> triggered_list;
while (running_) {
triggered_list.clear();
{
xSemaphoreTake(mutex_, portMAX_DELAY);
// 1. 检查所有提醒
pool_.forEach([&](ReminderItem* item, int id) {
if (!item->IsTriggered() && item->IsDue()) {
item->SetTriggered(true);
triggered_list.push_back({id, item->GetMessage()});
}
});
// 2. 清理已销毁的对象
pool_.cleanup();
xSemaphoreGive(mutex_);
}
// 3. 处理触发的提醒(在锁外执行,防止死锁)
for (const auto& pair : triggered_list) {
int id = pair.first;
const std::string& msg = pair.second;
ESP_LOGI(TAG, "Reminder triggered! ID: %d, Msg: %s", id, msg.c_str());
// 更新响铃 ID
{
xSemaphoreTake(mutex_, portMAX_DELAY);
// 再次检查对象是否存在(可能在触发前一瞬间被删除了)
if (pool_.get(id) == nullptr) {
xSemaphoreGive(mutex_);
continue;
}
ringing_id_ = id;
xSemaphoreGive(mutex_);
}
// 播放铃声 (循环)
if (!OGG_CAMERA_SHUTTER.empty()) {
if (hal_bridge::is_xiaozhi_mode()) {
hal_bridge::app_play_sound(OGG_CAMERA_SHUTTER);
} else {
// audio_player_.Play(reinterpret_cast<const uint8_t*>(OGG_CAMERA_SHUTTER.data()),
// OGG_CAMERA_SHUTTER.size(), true);
}
} else {
ESP_LOGW(TAG, "No ringtone data available");
}
// 发出信号
GetHAL().onReminderTriggered.emit(id, msg);
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
@@ -1,74 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <string>
#include <chrono>
#include <vector>
#include <memory>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include "stackchan/utils/object_pool.h"
#include "hal/utils/simple_audio_player/simple_audio_player.h"
class ReminderItem : public stackchan::Poolable {
public:
ReminderItem(int duration_s, const std::string& msg);
bool IsDue() const;
bool IsTriggered() const
{
return triggered_;
}
void SetTriggered(bool t)
{
triggered_ = t;
}
const std::string& GetMessage() const
{
return message_;
}
private:
std::string message_;
std::chrono::steady_clock::time_point target_time_;
bool triggered_ = false;
};
class ReminderManager {
public:
static ReminderManager& GetInstance();
// 初始化并启动后台线程
void Start();
// 创建一个提醒
// duration_s: 多少秒后提醒
// message: 提醒内容
// 返回: 提醒 ID
int CreateReminder(int duration_s, const std::string& message);
// 停止/关闭提醒
// id: 提醒 ID
void StopReminder(int id);
private:
ReminderManager();
~ReminderManager();
void WorkerThread();
SemaphoreHandle_t mutex_ = nullptr;
stackchan::ObjectPool<ReminderItem> pool_;
// SimpleAudioPlayer audio_player_;
TaskHandle_t worker_task_handle_ = nullptr;
volatile bool running_ = false;
int ringing_id_ = -1;
};
@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "secret_logic.h"
namespace secret_logic {
__attribute__((weak)) std::string get_server_url()
{
return "http://localhost:3000";
}
__attribute__((weak)) std::string generate_auth_token()
{
return "hi-stack-chan";
}
__attribute__((weak)) std::string generate_handshake_token(std::string_view data)
{
return "hi-stack-chan";
}
} // namespace secret_logic
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <string>
#include <string_view>
namespace secret_logic {
std::string get_server_url();
std::string generate_auth_token();
std::string generate_handshake_token(std::string_view data);
} // namespace secret_logic
@@ -1,211 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "simple_audio_player.h"
#include <opus_decoder.h>
#include <opus_resampler.h>
#include <esp_log.h>
#include <cstring>
#include "board.h"
#include "audio_codec.h"
static const char* TAG = "SimpleAudioPlayer";
#define OPUS_FRAME_DURATION_MS 60
SimpleAudioPlayer::SimpleAudioPlayer()
{
codec_ = Board::GetInstance().GetAudioCodec();
codec_->Start();
// 初始化解码器和重采样器,默认参数,后续会根据 OGG 头调整
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
output_resampler_ = std::make_unique<OpusResampler>();
}
SimpleAudioPlayer::~SimpleAudioPlayer()
{
Stop();
}
struct PlayerParams {
SimpleAudioPlayer* player;
const uint8_t* data;
size_t size;
bool loop;
};
bool SimpleAudioPlayer::Play(const uint8_t* data, size_t size, bool loop)
{
Stop(); // 停止之前的播放
is_playing_ = true;
stop_requested_ = false;
PlayerParams* params = new PlayerParams{this, data, size, loop};
BaseType_t ret = xTaskCreate(
[](void* arg) {
PlayerParams* p = (PlayerParams*)arg;
p->player->PlaybackTask(p->data, p->size, p->loop);
delete p;
vTaskDelete(NULL);
},
"simple_player", 4096 * 2, params, 5, &task_handle_);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create playback task");
delete params;
is_playing_ = false;
return false;
}
return true;
}
void SimpleAudioPlayer::Stop()
{
stop_requested_ = true;
if (task_handle_) {
int timeout = 100;
while (is_playing_ && timeout-- > 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
bool SimpleAudioPlayer::IsPlaying() const
{
return is_playing_;
}
void SimpleAudioPlayer::PlaybackTask(const uint8_t* data, size_t size, bool loop)
{
if (!codec_->output_enabled()) {
codec_->EnableOutput(true);
}
const uint8_t* buf = data;
auto find_page = [&](size_t start) -> size_t {
for (size_t i = start; i + 4 <= size; ++i) {
if (buf[i] == 'O' && buf[i + 1] == 'g' && buf[i + 2] == 'g' && buf[i + 3] == 'S') return i;
}
return static_cast<size_t>(-1);
};
do {
size_t offset = 0;
bool seen_head = false;
bool seen_tags = false;
int sample_rate = 16000; // 默认值
// 如果是循环播放,重置解码器状态以避免爆音
if (loop) {
opus_decoder_->ResetState();
}
while (!stop_requested_) {
// 确保输出已启用(防止被 AudioService 的自动省电逻辑关闭)
if (!codec_->output_enabled()) {
codec_->EnableOutput(true);
}
size_t pos = find_page(offset);
if (pos == static_cast<size_t>(-1)) break;
offset = pos;
if (offset + 27 > size) break;
const uint8_t* page = buf + offset;
uint8_t page_segments = page[26];
size_t seg_table_off = offset + 27;
if (seg_table_off + page_segments > size) break;
size_t body_size = 0;
for (size_t i = 0; i < page_segments; ++i) body_size += page[27 + i];
size_t body_off = seg_table_off + page_segments;
if (body_off + body_size > size) break;
// Parse packets using lacing
size_t cur = body_off;
size_t seg_idx = 0;
while (seg_idx < page_segments && !stop_requested_) {
size_t pkt_len = 0;
size_t pkt_start = cur;
bool continued = false;
do {
uint8_t l = page[27 + seg_idx++];
pkt_len += l;
cur += l;
continued = (l == 255);
} while (continued && seg_idx < page_segments);
if (pkt_len == 0) continue;
const uint8_t* pkt_ptr = buf + pkt_start;
if (!seen_head) {
if (pkt_len >= 19 && std::memcmp(pkt_ptr, "OpusHead", 8) == 0) {
seen_head = true;
if (pkt_len >= 12) {
// uint8_t version = pkt_ptr[8];
// uint8_t channel_count = pkt_ptr[9];
if (pkt_len >= 16) {
sample_rate =
pkt_ptr[12] | (pkt_ptr[13] << 8) | (pkt_ptr[14] << 16) | (pkt_ptr[15] << 24);
}
}
}
continue;
}
if (!seen_tags) {
if (pkt_len >= 8 && std::memcmp(pkt_ptr, "OpusTags", 8) == 0) {
seen_tags = true;
}
continue;
}
// Audio packet (Opus)
std::vector<uint8_t> payload(pkt_ptr, pkt_ptr + pkt_len);
if (!DecodeAndPlay(payload, sample_rate)) {
ESP_LOGE(TAG, "Failed to decode and play packet");
}
}
offset = body_off + body_size;
}
} while (loop && !stop_requested_);
// 播放结束,不关闭 output,以免影响其他音频
is_playing_ = false;
task_handle_ = nullptr;
}
bool SimpleAudioPlayer::DecodeAndPlay(std::vector<uint8_t>& opus_payload, int sample_rate)
{
// 检查采样率是否变化
if (opus_decoder_->sample_rate() != sample_rate) {
opus_decoder_.reset();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, OPUS_FRAME_DURATION_MS);
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
output_resampler_->Configure(opus_decoder_->sample_rate(), codec_->output_sample_rate());
}
}
std::vector<int16_t> pcm;
if (opus_decoder_->Decode(std::move(opus_payload), pcm)) {
// Resample if needed
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
int target_size = output_resampler_->GetOutputSamples(pcm.size());
std::vector<int16_t> resampled(target_size);
output_resampler_->Process(pcm.data(), pcm.size(), resampled.data());
pcm = std::move(resampled);
}
codec_->OutputData(pcm);
return true;
}
return false;
}
@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class AudioCodec;
class OpusDecoderWrapper;
class OpusResampler;
class SimpleAudioPlayer {
public:
SimpleAudioPlayer();
~SimpleAudioPlayer();
// 播放 OGG 音频数据
// 注意:数据必须在播放期间保持有效(例如存储在 Flash 中的数据)
// 不会复制数据
// loop: 是否循环播放
bool Play(const uint8_t* data, size_t size, bool loop = false);
// 停止播放
void Stop();
// 是否正在播放
bool IsPlaying() const;
private:
void PlaybackTask(const uint8_t* data, size_t size, bool loop);
bool DecodeAndPlay(std::vector<uint8_t>& opus_payload, int sample_rate);
AudioCodec* codec_ = nullptr;
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
std::unique_ptr<OpusResampler> output_resampler_;
TaskHandle_t task_handle_ = nullptr;
volatile bool is_playing_ = false;
volatile bool stop_requested_ = false;
};
@@ -7,13 +7,7 @@
#define TAG "StackChanWifiStation"
#define WIFI_EVENT_CONNECTED BIT0
#define MAX_RECONNECT_COUNT 10
StackChanWifiStation& StackChanWifiStation::GetInstance()
{
static StackChanWifiStation instance;
return instance;
}
#define MAX_RECONNECT_COUNT 1
StackChanWifiStation::StackChanWifiStation()
{
@@ -95,7 +89,7 @@ void StackChanWifiStation::Start()
}
esp_netif_init();
// esp_event_loop_create_default(); // Assumed to be created by main app or previous init
esp_event_loop_create_default();
station_netif_ = esp_netif_create_default_wifi_sta();
@@ -10,9 +10,15 @@
#include <esp_netif.h>
#include <esp_wifi_types_generic.h>
/**
* @brief A simplied WiFi station for config verifyation and config store.
*
*/
class StackChanWifiStation {
public:
static StackChanWifiStation& GetInstance();
StackChanWifiStation();
~StackChanWifiStation();
void AddAuth(const std::string& ssid, const std::string& password);
void Start();
void Stop();
@@ -35,11 +41,6 @@ public:
void OnConnectFailed(std::function<void(const std::string& ssid)> on_connect_failed);
private:
StackChanWifiStation();
~StackChanWifiStation();
StackChanWifiStation(const StackChanWifiStation&) = delete;
StackChanWifiStation& operator=(const StackChanWifiStation&) = delete;
EventGroupHandle_t event_group_;
esp_event_handler_instance_t instance_any_id_ = nullptr;
esp_event_handler_instance_t instance_got_ip_ = nullptr;