From 076d907abf155c25f348006390e120f73a10ab1d Mon Sep 17 00:00:00 2001 From: virgil Date: Sat, 12 Apr 2025 09:33:07 +0800 Subject: [PATCH] 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 --- main/boards/sensecap-watcher/config.h | 5 + .../sensecap-watcher/sensecap_watcher.cc | 261 ++++++++++++++++-- main/display/display.cc | 4 +- main/display/display.h | 3 +- main/display/lcd_display.cc | 16 +- main/display/oled_display.cc | 8 +- 6 files changed, 263 insertions(+), 34 deletions(-) diff --git a/main/boards/sensecap-watcher/config.h b/main/boards/sensecap-watcher/config.h index 7375af09..b03c9557 100644 --- a/main/boards/sensecap-watcher/config.h +++ b/main/boards/sensecap-watcher/config.h @@ -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_ diff --git a/main/boards/sensecap-watcher/sensecap_watcher.cc b/main/boards/sensecap-watcher/sensecap_watcher.cc index 8d4756a7..76710555 100644 --- a/main/boards/sensecap-watcher/sensecap_watcher.cc +++ b/main/boards/sensecap-watcher/sensecap_watcher.cc @@ -25,12 +25,64 @@ #include #include #include +#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(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(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(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(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(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); diff --git a/main/display/display.cc b/main/display/display.cc index 2744bc03..633b67a2 100644 --- a/main/display/display.cc +++ b/main/display/display.cc @@ -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_); } diff --git a/main/display/display.h b/main/display/display.h index a5102407..8342a32a 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -46,7 +46,8 @@ 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; bool muted_ = false; diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index d60aadd2..da41cf65 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -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 diff --git a/main/display/oled_display.cc b/main/display/oled_display.cc index 3b1b905a..e0900599 100644 --- a/main/display/oled_display.cc +++ b/main/display/oled_display.cc @@ -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); }