sensecap watcher manufacture (#469)

* feat: add shutdown and battery cmd.

* fix: fixed the issue that the LCD does not light up when some devices are turned on.

* fix:  fix task sys_evt  stack overflow.

* feat: Optimize UI display for circles; add Added factory reset function.

* feat:  "low_battery_label_" obj  configurable

* feat: add read_mac cmd

* fix: fix "low_battery_label_" obj  redefine

* style: modify Google C++ Style.

* Update sensecap_watcher.cc

Remove extra spaces

---------

Co-authored-by: Xiaoxia <terrence@tenclass.com>
This commit is contained in:
virgil
2025-04-12 09:33:07 +08:00
committed by GitHub
parent 04c0da059f
commit 076d907abf
6 changed files with 263 additions and 34 deletions

View File

@@ -98,4 +98,9 @@
#define CONFIG_BSP_LCD_SPI_DMA_SIZE_DIV 16
/* ADC */
#define BSP_BAT_ADC_CHAN (ADC_CHANNEL_2) // GPIO3
#define BSP_BAT_ADC_ATTEN (ADC_ATTEN_DB_2_5) // 0 ~ 1100 mV
#define BSP_BAT_VOL_RATIO ((62 + 20) / 20)
#endif // _BOARD_CONFIG_H_

View File

@@ -25,12 +25,64 @@
#include <iot_knob.h>
#include <esp_io_expander_tca95xx_16bit.h>
#include <esp_sleep.h>
#include "esp_console.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "assets/lang_config.h"
#define TAG "sensecap_watcher"
LV_FONT_DECLARE(font_puhui_30_4);
LV_FONT_DECLARE(font_awesome_30_4);
LV_FONT_DECLARE(font_awesome_20_4);
class CustomLcdDisplay : public SpiLcdDisplay {
public:
CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle,
esp_lcd_panel_handle_t panel_handle,
int width,
int height,
int offset_x,
int offset_y,
bool mirror_x,
bool mirror_y,
bool swap_xy)
: SpiLcdDisplay(io_handle, panel_handle, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy,
{
.text_font = &font_puhui_30_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
}) {
DisplayLockGuard lock(this);
lv_obj_set_size(status_bar_, LV_HOR_RES, fonts_.text_font->line_height * 2 + 10);
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0);
lv_obj_set_style_pad_top(status_bar_, 10, 0);
lv_obj_set_style_pad_bottom(status_bar_, 1, 0);
// 针对圆形屏幕调整位置
// network battery mute //
// status //
lv_obj_align(battery_label_, LV_ALIGN_TOP_MID, -2.5*fonts_.icon_font->line_height, 0);
lv_obj_align(network_label_, LV_ALIGN_TOP_MID, -0.5*fonts_.icon_font->line_height, 0);
lv_obj_align(mute_label_, LV_ALIGN_TOP_MID, 1.5*fonts_.icon_font->line_height, 0);
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_flex_grow(status_label_, 0);
lv_obj_set_width(status_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(notification_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(notification_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_set_style_bg_color(low_battery_popup_, lv_color_hex(0xFF0000), 0);
lv_obj_set_width(low_battery_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(low_battery_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
}
};
class SensecapWatcher : public WifiBoard {
private:
@@ -42,7 +94,7 @@ private:
PowerSaveTimer* power_save_timer_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
uint32_t long_press_cnt_;
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
@@ -144,7 +196,7 @@ private:
void OnKnobRotate(bool clockwise) {
auto codec = GetAudioCodec();
int current_volume = codec->output_volume();
int new_volume = current_volume + (clockwise ? 5 : -5);
int new_volume = current_volume + (clockwise ? -5 : 5);
// 确保音量在有效范围内
if (new_volume > 100) {
@@ -163,7 +215,7 @@ private:
ESP_LOGE(TAG, "Failed to set volume! Expected:%d Actual:%d",
new_volume, codec->output_volume());
}
GetDisplay()->ShowNotification("音量: " + std::to_string(codec->output_volume()));
GetDisplay()->ShowNotification(std::string(Lang::Strings::VOLUME) + ": "+std::to_string(codec->output_volume()));
power_save_timer_->WakeUp();
}
@@ -193,7 +245,7 @@ private:
},
};
//watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击
// watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击
ESP_LOGI(TAG, "waiting for knob button release");
while(IoExpanderGetLevel(BSP_KNOB_BTN) == 0) {
vTaskDelay(50 / portTICK_PERIOD_MS);
@@ -212,6 +264,7 @@ private:
iot_button_register_cb(btns, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data);
bool is_charging = (self->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
self->long_press_cnt_ = 0;
if (is_charging) {
ESP_LOGI(TAG, "charging");
} else {
@@ -219,6 +272,18 @@ private:
self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
}
}, this);
iot_button_register_cb(btns, BUTTON_LONG_PRESS_HOLD, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data);
self->long_press_cnt_++; // 每隔20ms加一
// 长按10s 恢复出厂设置: 2+0.02*400 = 10
if (self->long_press_cnt_ > 400) {
ESP_LOGI(TAG, "Factory reset");
nvs_flash_erase();
esp_restart();
}
}, this);
}
void InitializeSpi() {
@@ -270,23 +335,18 @@ private:
esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel_, true);
display_ = new SpiLcdDisplay(panel_io_, panel_,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_30_4,
.icon_font = &font_awesome_30_4,
.emoji_font = font_emoji_64_init(),
});
display_ = new CustomLcdDisplay(panel_io_, panel_,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
// 使每次刷新的起始列数索引是4的倍数且列数总数是4的倍数以满足SPD2010的要求
lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t *e) {
lv_area_t *area = (lv_area_t *)lv_event_get_param(e);
uint16_t x1 = area->x1;
uint16_t x2 = area->x2;
// round the start of area down to the nearest 4N number
area->x1 = (x1 >> 2) << 2;
// round the end of area up to the nearest 4M+3 number
area->x2 = ((x2 >> 2) << 2) + 3;
lv_area_t *area = (lv_area_t *)lv_event_get_param(e);
uint16_t x1 = area->x1;
uint16_t x2 = area->x2;
// round the start of area down to the nearest 4N number
area->x1 = (x1 >> 2) << 2;
// round the end of area up to the nearest 4M+3 number
area->x2 = ((x2 >> 2) << 2) + 3;
}, LV_EVENT_INVALIDATE_AREA, NULL);
}
@@ -296,15 +356,159 @@ private:
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Screen"));
thing_manager.AddThing(iot::CreateThing("Battery"));
}
uint16_t BatterygetVoltage(void) {
static bool initialized = false;
static adc_oneshot_unit_handle_t adc_handle;
static adc_cali_handle_t cali_handle = NULL;
if (!initialized) {
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_config, &adc_handle);
adc_oneshot_chan_cfg_t ch_config = {
.atten = BSP_BAT_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
adc_oneshot_config_channel(adc_handle, BSP_BAT_ADC_CHAN, &ch_config);
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.chan = BSP_BAT_ADC_CHAN,
.atten = BSP_BAT_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
if (adc_cali_create_scheme_curve_fitting(&cali_config, &cali_handle) == ESP_OK) {
initialized = true;
}
}
if (initialized) {
int raw_value = 0;
int voltage = 0; // mV
adc_oneshot_read(adc_handle, BSP_BAT_ADC_CHAN, &raw_value);
adc_cali_raw_to_voltage(cali_handle, raw_value, &voltage);
voltage = voltage * 82 / 20;
// ESP_LOGI(TAG, "voltage: %dmV", voltage);
return (uint16_t)voltage;
}
return 0;
}
uint8_t BatterygetPercent(bool print = false) {
int voltage = 0;
for (uint8_t i = 0; i < 10; i++) {
voltage += BatterygetVoltage();
}
voltage /= 10;
int percent = (-1 * voltage * voltage + 9016 * voltage - 19189000) / 10000;
percent = (percent > 100) ? 100 : (percent < 0) ? 0 : percent;
if (print) {
printf("voltage: %dmV, percentage: %d%%\r\n", voltage, percent);
}
return (uint8_t)percent;
}
void InitializeCmd() {
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.max_cmdline_length = 1024;
repl_config.prompt = "SenseCAP>";
const esp_console_cmd_t cmd1 = {
.command = "reboot",
.help = "reboot the device",
.hint = nullptr,
.func = [](int argc, char** argv) -> int {
esp_restart();
return 0;
},
.argtable = nullptr
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd1));
const esp_console_cmd_t cmd2 = {
.command = "shutdown",
.help = "shutdown the device",
.hint = nullptr,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
auto self = static_cast<SensecapWatcher*>(context);
self->GetBacklight()->SetBrightness(0);
self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd2));
const esp_console_cmd_t cmd3 = {
.command = "battery",
.help = "get battery percent",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
auto self = static_cast<SensecapWatcher*>(context);
self->BatterygetPercent(true);
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd3));
const esp_console_cmd_t cmd4 = {
.command = "factory_reset",
.help = "factory reset and reboot the device",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
auto self = static_cast<SensecapWatcher*>(context);
nvs_flash_erase();
esp_restart();
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd4));
const esp_console_cmd_t cmd5 = {
.command = "read_mac",
.help = "Read mac address",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
printf("wifi_sta_mac: " MACSTR "\n", MAC2STR(mac));
esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
printf("wifi_softap_mac: " MACSTR "\n", MAC2STR(mac));
esp_read_mac(mac, ESP_MAC_BT);
printf("bt_mac: " MACSTR "\n", MAC2STR(mac));
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd5));
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
ESP_ERROR_CHECK(esp_console_start_repl(repl));
}
public:
SensecapWatcher(){
SensecapWatcher() {
ESP_LOGI(TAG, "Initialize Sensecap Watcher");
InitializePowerSaveTimer();
InitializeI2c();
InitializeSpi();
InitializeExpander();
InitializeCmd(); //工厂生产测试使用
InitializeButton();
InitializeKnob();
Initializespd2010Display();
@@ -352,6 +556,23 @@ public:
}
WifiBoard::SetPowerSaveMode(enabled);
}
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
static bool last_discharging = false;
charging = (IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
discharging = !charging;
level = (int)BatterygetPercent(false);
if (discharging != last_discharging) {
power_save_timer_->SetEnabled(discharging);
last_discharging = discharging;
}
if (level <= 1 && discharging) {
ESP_LOGI(TAG, "Battery level is low, shutting down");
IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
}
return true;
}
};
DECLARE_BOARD(SensecapWatcher);

View File

@@ -75,7 +75,9 @@ Display::~Display() {
lv_obj_del(battery_label_);
lv_obj_del(emotion_label_);
}
if( low_battery_popup_ != nullptr ) {
lv_obj_del(low_battery_popup_);
}
if (pm_lock_ != nullptr) {
esp_pm_lock_delete(pm_lock_);
}

View File

@@ -46,6 +46,7 @@ protected:
lv_obj_t *battery_label_ = nullptr;
lv_obj_t* chat_message_label_ = nullptr;
lv_obj_t* low_battery_popup_ = nullptr;
lv_obj_t* low_battery_label_ = nullptr;
const char* battery_icon_ = nullptr;
const char* network_icon_ = nullptr;

View File

@@ -363,10 +363,10 @@ void LcdDisplay::SetupUI() {
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(low_battery_popup_, current_theme.low_battery, 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0);
lv_obj_center(low_battery_label);
low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
}
@@ -628,10 +628,10 @@ void LcdDisplay::SetupUI() {
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(low_battery_popup_, current_theme.low_battery, 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0);
lv_obj_center(low_battery_label);
low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
}
#endif

View File

@@ -218,10 +218,10 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(low_battery_popup_, lv_color_black(), 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0);
lv_obj_center(low_battery_label);
low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
}