diff --git a/src/battery/CMakeLists.txt b/src/battery/CMakeLists.txt new file mode 100644 index 00000000..313a3731 --- /dev/null +++ b/src/battery/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright 2023 jacqueline +# +# SPDX-License-Identifier: GPL-3.0-only + +idf_component_register( + SRCS "battery.cpp" + INCLUDE_DIRS "include" + REQUIRES "drivers" "events") + +target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/battery/battery.cpp b/src/battery/battery.cpp new file mode 100644 index 00000000..d73f4f29 --- /dev/null +++ b/src/battery/battery.cpp @@ -0,0 +1,101 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "battery.hpp" + +#include + +#include "adc.hpp" +#include "event_queue.hpp" +#include "freertos/portmacro.h" +#include "samd.hpp" +#include "system_events.hpp" + +namespace battery { + +static const TickType_t kBatteryCheckPeriod = pdMS_TO_TICKS(60 * 1000); + +/* + * Battery voltage, in millivolts, at which the battery charger IC will stop + * charging. + */ +static const uint32_t kFullChargeMilliVolts = 4200; + +/* + * Battery voltage, in millivolts, at which *we* will consider the battery to + * be completely discharged. This is intentionally higher than the charger IC + * cut-off and the protection on the battery itself; we want to make sure we + * finish up and have everything unmounted and snoozing before the BMS cuts us + * off. + */ +static const uint32_t kEmptyChargeMilliVolts = 3200; // BMS limit is 3100. + +using ChargeStatus = drivers::Samd::ChargeStatus; + +void check_voltage_cb(TimerHandle_t timer) { + Battery* instance = reinterpret_cast(pvTimerGetTimerID(timer)); + instance->Update(); +} + +Battery::Battery(drivers::Samd* samd, drivers::AdcBattery* adc) + : samd_(samd), adc_(adc) { + timer_ = xTimerCreate("BATTERY", kBatteryCheckPeriod, true, this, + check_voltage_cb); + xTimerStart(timer_, portMAX_DELAY); + Update(); +} + +Battery::~Battery() { + xTimerStop(timer_, portMAX_DELAY); + xTimerDelete(timer_, portMAX_DELAY); +} + +auto Battery::Update() -> void { + std::lock_guard lock{state_mutex_}; + + auto charge_state = samd_->GetChargeStatus(); + if (!charge_state || *charge_state == ChargeStatus::kNoBattery) { + if (state_) { + EmitEvent(); + } + state_.reset(); + return; + } + // FIXME: So what we *should* do here is measure the actual real-life + // time from full battery -> empty battery, store it in NVS, then rely on + // that. If someone could please do this, it would be lovely. Thanks! + uint32_t mV = std::max(adc_->Millivolts(), kEmptyChargeMilliVolts); + uint_fast8_t percent = static_cast(std::min( + std::max(0.0, mV - kEmptyChargeMilliVolts) / + (kFullChargeMilliVolts - kEmptyChargeMilliVolts) * 100.0, + 100.0)); + + bool is_charging = *charge_state == ChargeStatus::kChargingRegular || + *charge_state == ChargeStatus::kChargingFast || + *charge_state == ChargeStatus::kFullCharge; + + if (!state_ || state_->is_charging != is_charging || + state_->percent != percent) { + EmitEvent(); + } + + state_ = BatteryState{ + .percent = percent, + .is_charging = is_charging, + }; +} + +auto Battery::State() -> std::optional { + std::lock_guard lock{state_mutex_}; + return state_; +} + +auto Battery::EmitEvent() -> void { + events::System().Dispatch(system_fsm::BatteryStateChanged{}); + events::Ui().Dispatch(system_fsm::BatteryStateChanged{}); +} + +} // namespace battery diff --git a/src/battery/include/battery.hpp b/src/battery/include/battery.hpp new file mode 100644 index 00000000..dcb9b4ea --- /dev/null +++ b/src/battery/include/battery.hpp @@ -0,0 +1,44 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" + +#include "adc.hpp" +#include "samd.hpp" + +namespace battery { + +class Battery { + public: + Battery(drivers::Samd* samd, drivers::AdcBattery* adc); + ~Battery(); + + auto Update() -> void; + + struct BatteryState { + uint_fast8_t percent; + bool is_charging; + }; + + auto State() -> std::optional; + + private: + auto EmitEvent() -> void; + + drivers::Samd* samd_; + drivers::AdcBattery* adc_; + + TimerHandle_t timer_; + std::mutex state_mutex_; + std::optional state_; +}; + +} // namespace battery diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index a64495f0..1df58f72 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" + SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" "i2c.cpp" "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "bluetooth.cpp" INCLUDE_DIRS "include" diff --git a/src/drivers/battery.cpp b/src/drivers/adc.cpp similarity index 58% rename from src/drivers/battery.cpp rename to src/drivers/adc.cpp index 1755fd64..56d2cbb4 100644 --- a/src/drivers/battery.cpp +++ b/src/drivers/adc.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -#include "battery.hpp" +#include "adc.hpp" #include #include "esp_adc/adc_cali.h" @@ -14,21 +14,6 @@ namespace drivers { -/* - * Battery voltage, in millivolts, at which the battery charger IC will stop - * charging. - */ -static const uint32_t kFullChargeMilliVolts = 4200; - -/* - * Battery voltage, in millivolts, at which *we* will consider the battery to - * be completely discharged. This is intentionally higher than the charger IC - * cut-off and the protection on the battery itself; we want to make sure we - * finish up and have everything unmounted and snoozing before the BMS cuts us - * off. - */ -static const uint32_t kEmptyChargeMilliVolts = 3200; // BMS limit is 3100. - static const adc_bitwidth_t kAdcBitWidth = ADC_BITWIDTH_12; static const adc_unit_t kAdcUnit = ADC_UNIT_1; // Max battery voltage should be a little over 2V due to our divider, so we need @@ -37,7 +22,7 @@ static const adc_atten_t kAdcAttenuation = ADC_ATTEN_DB_11; // Corresponds to SENSOR_VP. static const adc_channel_t kAdcChannel = ADC_CHANNEL_0; -Battery::Battery() { +AdcBattery::AdcBattery() { adc_oneshot_unit_init_cfg_t unit_config = { .unit_id = kAdcUnit, }; @@ -59,16 +44,14 @@ Battery::Battery() { }; ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting( &calibration_config, &adc_calibration_handle_)); - - UpdatePercent(); } -Battery::~Battery() { +AdcBattery::~AdcBattery() { adc_cali_delete_scheme_line_fitting(adc_calibration_handle_); ESP_ERROR_CHECK(adc_oneshot_del_unit(adc_handle_)); } -auto Battery::Millivolts() -> uint32_t { +auto AdcBattery::Millivolts() -> uint32_t { // GPIO 34 int raw = 0; ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, kAdcChannel, &raw)); @@ -81,17 +64,4 @@ auto Battery::Millivolts() -> uint32_t { return voltage * 2; } -auto Battery::UpdatePercent() -> bool { - auto old_percent = percent_; - // FIXME: So what we *should* do here is measure the actual real-life - // time from full battery -> empty battery, store it in NVS, then rely on - // that. If someone could please do this, it would be lovely. Thanks! - uint32_t mV = std::max(Millivolts(), kEmptyChargeMilliVolts); - percent_ = static_cast(std::min( - std::max(0.0, mV - kEmptyChargeMilliVolts) / - (kFullChargeMilliVolts - kEmptyChargeMilliVolts) * 100.0, - 100.0)); - return old_percent != percent_; -} - } // namespace drivers diff --git a/src/drivers/include/battery.hpp b/src/drivers/include/adc.hpp similarity index 69% rename from src/drivers/include/battery.hpp rename to src/drivers/include/adc.hpp index 64a00135..3e94a9ee 100644 --- a/src/drivers/include/battery.hpp +++ b/src/drivers/include/adc.hpp @@ -15,25 +15,23 @@ namespace drivers { -class Battery { +/* + * Handles measuring the battery's current voltage. + */ +class AdcBattery { public: - static auto Create() -> Battery* { return new Battery(); } - Battery(); - ~Battery(); + static auto Create() -> AdcBattery* { return new AdcBattery(); } + AdcBattery(); + ~AdcBattery(); /** * Returns the current battery level in millivolts. */ auto Millivolts() -> uint32_t; - auto UpdatePercent() -> bool; - auto Percent() -> uint_fast8_t { return percent_; } - private: adc_oneshot_unit_handle_t adc_handle_; adc_cali_handle_t adc_calibration_handle_; - - uint_fast8_t percent_; }; } // namespace drivers diff --git a/src/system_fsm/CMakeLists.txt b/src/system_fsm/CMakeLists.txt index 037e72c7..fced4093 100644 --- a/src/system_fsm/CMakeLists.txt +++ b/src/system_fsm/CMakeLists.txt @@ -5,5 +5,5 @@ idf_component_register( SRCS "system_fsm.cpp" "running.cpp" "booting.cpp" "idle.cpp" INCLUDE_DIRS "include" - REQUIRES "tinyfsm" "drivers" "database" "ui" "result" "events" "audio" "app_console") + REQUIRES "tinyfsm" "drivers" "database" "ui" "result" "events" "audio" "app_console" "battery") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 33ed39d1..f33d1679 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -38,12 +38,6 @@ namespace states { static const char kTag[] = "BOOT"; -static const TickType_t kBatteryCheckPeriod = pdMS_TO_TICKS(60 * 1000); - -static void battery_timer_cb(TimerHandle_t timer) { - events::System().Dispatch(internal::BatteryTimerFired{}); -} - auto Booting::entry() -> void { ESP_LOGI(kTag, "beginning tangara boot"); ESP_LOGI(kTag, "installing early drivers"); @@ -53,33 +47,31 @@ auto Booting::entry() -> void { ESP_ERROR_CHECK(drivers::init_spi()); sGpios.reset(drivers::Gpios::Create()); + sSamd.reset(drivers::Samd::Create()); + sAdc.reset(drivers::AdcBattery::Create()); + assert(sSamd.get() && sAdc.get()); + + sBattery.reset(new battery::Battery(sSamd.get(), sAdc.get())); + // Start bringing up LVGL now, since we have all of its prerequisites. sTrackQueue.reset(new audio::TrackQueue()); ESP_LOGI(kTag, "starting ui"); - if (!ui::UiState::Init(sGpios.get(), sTrackQueue.get())) { + if (!ui::UiState::Init(sGpios.get(), sTrackQueue.get(), sBattery)) { events::System().Dispatch(FatalError{}); return; } // Install everything else that is certain to be needed. ESP_LOGI(kTag, "installing remaining drivers"); - sSamd.reset(drivers::Samd::Create()); - sBattery.reset(drivers::Battery::Create()); sNvs.reset(drivers::NvsStorage::Open()); sTagParser.reset(new database::TagParserImpl()); - if (!sSamd || !sBattery || !sNvs) { + if (!sNvs) { events::System().Dispatch(FatalError{}); events::Ui().Dispatch(FatalError{}); return; } - ESP_LOGI(kTag, "battery is at %u%% (= %lu mV)", sBattery->Percent(), - sBattery->Millivolts()); - TimerHandle_t battery_timer = xTimerCreate("battery", kBatteryCheckPeriod, - true, NULL, battery_timer_cb); - xTimerStart(battery_timer, portMAX_DELAY); - // ESP_LOGI(kTag, "starting bluetooth"); // sBluetooth.reset(new drivers::Bluetooth(sNvs.get())); // sBluetooth->Enable(); diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp index 8a3ba5ec..64cbd393 100644 --- a/src/system_fsm/include/system_events.hpp +++ b/src/system_fsm/include/system_events.hpp @@ -53,7 +53,7 @@ struct HasPhonesChanged : tinyfsm::Event { }; struct ChargingStatusChanged : tinyfsm::Event {}; -struct BatteryPercentChanged : tinyfsm::Event {}; +struct BatteryStateChanged : tinyfsm::Event {}; namespace internal { @@ -62,8 +62,6 @@ struct SamdInterrupt : tinyfsm::Event {}; struct IdleTimeout : tinyfsm::Event {}; -struct BatteryTimerFired : tinyfsm::Event {}; - } // namespace internal } // namespace system_fsm diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index a556be9e..371e5527 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -48,7 +48,6 @@ class SystemState : public tinyfsm::Fsm { void react(const FatalError&); void react(const internal::GpioInterrupt&); void react(const internal::SamdInterrupt&); - void react(const internal::BatteryTimerFired&); virtual void react(const DisplayReady&) {} virtual void react(const BootComplete&) {} @@ -67,7 +66,8 @@ class SystemState : public tinyfsm::Fsm { static std::shared_ptr sTouch; static std::shared_ptr sRelativeTouch; - static std::shared_ptr sBattery; + static std::shared_ptr sAdc; + static std::shared_ptr sBattery; static std::shared_ptr sStorage; static std::shared_ptr sDisplay; static std::shared_ptr sBluetooth; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 9ad89c7a..d21e8bcb 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -23,7 +23,8 @@ std::shared_ptr SystemState::sNvs; std::shared_ptr SystemState::sTouch; std::shared_ptr SystemState::sRelativeTouch; -std::shared_ptr SystemState::sBattery; +std::shared_ptr SystemState::sAdc; +std::shared_ptr SystemState::sBattery; std::shared_ptr SystemState::sStorage; std::shared_ptr SystemState::sDisplay; std::shared_ptr SystemState::sBluetooth; @@ -95,16 +96,6 @@ void SystemState::react(const internal::SamdInterrupt&) { } } -void SystemState::react(const internal::BatteryTimerFired&) { - ESP_LOGI(kTag, "checking battery"); - if (sBattery->UpdatePercent()) { - ESP_LOGI(kTag, "battery now at %u%%", sBattery->Percent()); - BatteryPercentChanged ev{}; - events::Ui().Dispatch(ev); - events::System().Dispatch(ev); - } -} - auto SystemState::IdleCondition() -> bool { return !sGpios->Get(drivers::IGpios::Pin::kKeyLock) && audio::AudioState::is_in_state(); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index fa3f2bc1..6b5c393a 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -9,5 +9,5 @@ idf_component_register( "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "splash.c" "font_fusion.c" "font_symbols.c" INCLUDE_DIRS "include" - REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/include/screen_settings.hpp b/src/ui/include/screen_settings.hpp index ebc5bf7d..66124164 100644 --- a/src/ui/include/screen_settings.hpp +++ b/src/ui/include/screen_settings.hpp @@ -21,14 +21,15 @@ class Settings : public MenuScreen { public: Settings(); ~Settings(); - private: - std::shared_ptr bluetooth_; - std::shared_ptr headphones_; - std::shared_ptr appearance_; - std::shared_ptr input_method_; - std::shared_ptr storage_; - std::shared_ptr firmware_update_; - std::shared_ptr about_; + + private: + std::shared_ptr bluetooth_; + std::shared_ptr headphones_; + std::shared_ptr appearance_; + std::shared_ptr input_method_; + std::shared_ptr storage_; + std::shared_ptr firmware_update_; + std::shared_ptr about_; }; class Bluetooth : public MenuScreen { @@ -36,32 +37,32 @@ class Bluetooth : public MenuScreen { Bluetooth(); }; -class Headphones : public MenuScreen { +class Headphones : public MenuScreen { public: Headphones(); }; -class Appearance : public MenuScreen { +class Appearance : public MenuScreen { public: Appearance(); }; -class InputMethod : public MenuScreen { +class InputMethod : public MenuScreen { public: InputMethod(); }; -class Storage : public MenuScreen { +class Storage : public MenuScreen { public: Storage(); }; -class FirmwareUpdate : public MenuScreen { +class FirmwareUpdate : public MenuScreen { public: FirmwareUpdate(); }; -class About : public MenuScreen { +class About : public MenuScreen { public: About(); }; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 80c01c68..8f1aa1bc 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -10,6 +10,7 @@ #include #include "audio_events.hpp" +#include "battery.hpp" #include "relative_wheel.hpp" #include "screen_playing.hpp" #include "tinyfsm.hpp" @@ -27,7 +28,9 @@ namespace ui { class UiState : public tinyfsm::Fsm { public: - static auto Init(drivers::IGpios*, audio::TrackQueue*) -> bool; + static auto Init(drivers::IGpios*, + audio::TrackQueue*, + std::shared_ptr) -> bool; virtual ~UiState() {} @@ -41,7 +44,7 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - void react(const system_fsm::BatteryPercentChanged&); + void react(const system_fsm::BatteryStateChanged&); virtual void react(const audio::PlaybackStarted&) {} virtual void react(const audio::PlaybackUpdate&) {} @@ -76,6 +79,7 @@ class UiState : public tinyfsm::Fsm { static std::shared_ptr sTouchWheel; static std::shared_ptr sRelativeWheel; static std::shared_ptr sDisplay; + static std::shared_ptr sBattery; static std::weak_ptr sDb; static std::stack> sScreens; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 508fb740..32032978 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -8,6 +8,7 @@ #include +#include "battery.hpp" #include "core/lv_obj.h" #include "misc/lv_gc.h" @@ -43,16 +44,19 @@ audio::TrackQueue* UiState::sQueue; std::shared_ptr UiState::sTouchWheel; std::shared_ptr UiState::sRelativeWheel; std::shared_ptr UiState::sDisplay; +std::shared_ptr UiState::sBattery; std::weak_ptr UiState::sDb; std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; std::shared_ptr UiState::sCurrentModal; -auto UiState::Init(drivers::IGpios* gpio_expander, audio::TrackQueue* queue) - -> bool { +auto UiState::Init(drivers::IGpios* gpio_expander, + audio::TrackQueue* queue, + std::shared_ptr battery) -> bool { sIGpios = gpio_expander; sQueue = queue; + sBattery = battery; lv_init(); sDisplay.reset( @@ -100,15 +104,17 @@ void UiState::react(const system_fsm::KeyLockChanged& ev) { sRelativeWheel->SetEnabled(ev.falling); } -void UiState::react(const system_fsm::BatteryPercentChanged&) { +void UiState::react(const system_fsm::BatteryStateChanged&) { UpdateTopBar(); } void UiState::UpdateTopBar() { + auto battery_state = sBattery->State(); widgets::TopBar::State state{ .playback_state = widgets::TopBar::PlaybackState::kIdle, - .battery_percent = 50, - .is_charging = true, + .battery_percent = static_cast( + battery_state.has_value() ? battery_state->percent : 100), + .is_charging = !battery_state.has_value() || battery_state->is_charging, }; if (sCurrentScreen) { sCurrentScreen->UpdateTopBar(state); diff --git a/src/ui/widget_top_bar.cpp b/src/ui/widget_top_bar.cpp index 7d4ef98c..851b617f 100644 --- a/src/ui/widget_top_bar.cpp +++ b/src/ui/widget_top_bar.cpp @@ -65,17 +65,18 @@ auto TopBar::Update(const State& state) -> void { break; } - if (state.battery_percent >= 95) { - lv_label_set_text(battery_, "100"); - } else if (state.battery_percent >= 70) { - lv_label_set_text(battery_, ">70"); - } else if (state.battery_percent >= 40) { - lv_label_set_text(battery_, ">40"); - } else if (state.battery_percent >= 10) { - lv_label_set_text(battery_, ">10"); - } else { - lv_label_set_text(battery_, "0"); - } + lv_label_set_text(battery_, std::to_string(state.battery_percent).c_str()); + // if (state.battery_percent >= 95) { + // lv_label_set_text(battery_, "100"); + // } else if (state.battery_percent >= 70) { + // lv_label_set_text(battery_, ">70"); + // } else if (state.battery_percent >= 40) { + // lv_label_set_text(battery_, ">40"); + // } else if (state.battery_percent >= 10) { + // lv_label_set_text(battery_, ">10"); + // } else { + // lv_label_set_text(battery_, "0"); + // } } } // namespace widgets