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 #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_ #endif // _BOARD_CONFIG_H_

View File

@@ -25,12 +25,64 @@
#include <iot_knob.h> #include <iot_knob.h>
#include <esp_io_expander_tca95xx_16bit.h> #include <esp_io_expander_tca95xx_16bit.h>
#include <esp_sleep.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" #define TAG "sensecap_watcher"
LV_FONT_DECLARE(font_puhui_30_4); 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 { class SensecapWatcher : public WifiBoard {
private: private:
@@ -42,7 +94,7 @@ private:
PowerSaveTimer* power_save_timer_; PowerSaveTimer* power_save_timer_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr; esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr; esp_lcd_panel_handle_t panel_ = nullptr;
uint32_t long_press_cnt_;
void InitializePowerSaveTimer() { void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300); power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() { power_save_timer_->OnEnterSleepMode([this]() {
@@ -144,7 +196,7 @@ private:
void OnKnobRotate(bool clockwise) { void OnKnobRotate(bool clockwise) {
auto codec = GetAudioCodec(); auto codec = GetAudioCodec();
int current_volume = codec->output_volume(); 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) { if (new_volume > 100) {
@@ -163,7 +215,7 @@ private:
ESP_LOGE(TAG, "Failed to set volume! Expected:%d Actual:%d", ESP_LOGE(TAG, "Failed to set volume! Expected:%d Actual:%d",
new_volume, codec->output_volume()); 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(); power_save_timer_->WakeUp();
} }
@@ -193,7 +245,7 @@ private:
}, },
}; };
//watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击 // watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击
ESP_LOGI(TAG, "waiting for knob button release"); ESP_LOGI(TAG, "waiting for knob button release");
while(IoExpanderGetLevel(BSP_KNOB_BTN) == 0) { while(IoExpanderGetLevel(BSP_KNOB_BTN) == 0) {
vTaskDelay(50 / portTICK_PERIOD_MS); 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) { iot_button_register_cb(btns, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data); auto self = static_cast<SensecapWatcher*>(usr_data);
bool is_charging = (self->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0); bool is_charging = (self->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
self->long_press_cnt_ = 0;
if (is_charging) { if (is_charging) {
ESP_LOGI(TAG, "charging"); ESP_LOGI(TAG, "charging");
} else { } else {
@@ -219,6 +272,18 @@ private:
self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0); self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
} }
}, this); }, 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() { void InitializeSpi() {
@@ -270,23 +335,18 @@ private:
esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel_, true); esp_lcd_panel_disp_on_off(panel_, true);
display_ = new SpiLcdDisplay(panel_io_, panel_, 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, 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(),
});
// 使每次刷新的起始列数索引是4的倍数且列数总数是4的倍数以满足SPD2010的要求 // 使每次刷新的起始列数索引是4的倍数且列数总数是4的倍数以满足SPD2010的要求
lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t *e) { 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); lv_area_t *area = (lv_area_t *)lv_event_get_param(e);
uint16_t x1 = area->x1; uint16_t x1 = area->x1;
uint16_t x2 = area->x2; uint16_t x2 = area->x2;
// round the start of area down to the nearest 4N number // round the start of area down to the nearest 4N number
area->x1 = (x1 >> 2) << 2; area->x1 = (x1 >> 2) << 2;
// round the end of area up to the nearest 4M+3 number // round the end of area up to the nearest 4M+3 number
area->x2 = ((x2 >> 2) << 2) + 3; area->x2 = ((x2 >> 2) << 2) + 3;
}, LV_EVENT_INVALIDATE_AREA, NULL); }, LV_EVENT_INVALIDATE_AREA, NULL);
} }
@@ -296,15 +356,159 @@ private:
auto& thing_manager = iot::ThingManager::GetInstance(); auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker")); thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Screen")); 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: public:
SensecapWatcher(){ SensecapWatcher() {
ESP_LOGI(TAG, "Initialize Sensecap Watcher"); ESP_LOGI(TAG, "Initialize Sensecap Watcher");
InitializePowerSaveTimer(); InitializePowerSaveTimer();
InitializeI2c(); InitializeI2c();
InitializeSpi(); InitializeSpi();
InitializeExpander(); InitializeExpander();
InitializeCmd(); //工厂生产测试使用
InitializeButton(); InitializeButton();
InitializeKnob(); InitializeKnob();
Initializespd2010Display(); Initializespd2010Display();
@@ -352,6 +556,23 @@ public:
} }
WifiBoard::SetPowerSaveMode(enabled); 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); DECLARE_BOARD(SensecapWatcher);

View File

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

View File

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

View File

@@ -363,10 +363,10 @@ void LcdDisplay::SetupUI() {
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0); 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_bg_color(low_battery_popup_, current_theme.low_battery, 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0); lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); 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_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label); lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); 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_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_bg_color(low_battery_popup_, current_theme.low_battery, 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0); lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); 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_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label); lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
} }
#endif #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_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_bg_color(low_battery_popup_, lv_color_black(), 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0); lv_obj_set_style_radius(low_battery_popup_, 10, 0);
lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); low_battery_label_ = lv_label_create(low_battery_popup_);
lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); 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_set_style_text_color(low_battery_label_, lv_color_white(), 0);
lv_obj_center(low_battery_label); lv_obj_center(low_battery_label_);
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
} }