mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-27 19:12:40 +00:00
update firmware source code to v0.18 (#9)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ®, 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, ®, 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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user