diff --git a/lua/settings.lua b/lua/settings.lua index 49a826af..79572ee9 100644 --- a/lua/settings.lua +++ b/lua/settings.lua @@ -485,6 +485,59 @@ local DatabaseSettings = SettingsScreen:new { end } +local PowerSettings = SettingsScreen:new { + title = "Power", + createUi = function(self) + SettingsScreen.createUi(self) + local power = require("power") + + local charge_pct = widgets.Row(self.content, "Charge").right + local charge_volts = widgets.Row(self.content, "Voltage").right + local charge_state = widgets.Row(self.content, "Status").right + + self.bindings = self.bindings + { + power.battery_pct:bind(function(pct) + charge_pct:set { text = string.format("%d%%", pct) } + end), + power.battery_millivolts:bind(function(mv) + charge_volts:set { text = string.format("%.2fV", mv / 1000) } + end), + power.charge_state:bind(function(state) + charge_state:set { text = state } + end), + } + + local fast_charge_container = self.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "center", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_bottom = 4, + } + fast_charge_container:add_style(styles.list_item) + fast_charge_container:Label { text = "Fast Charging", flex_grow = 1 } + local fast_charge_sw = fast_charge_container:Switch {} + + fast_charge_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() + power.fast_charge:set(fast_charge_sw:enabled()) + end) + + self.bindings = self.bindings + { + power.fast_charge:bind(function(en) + if en then + fast_charge_sw:add_state(lvgl.STATE.CHECKED) + else + fast_charge_sw:clear_state(lvgl.STATE.CHECKED) + end + end), + } + end +} + local SamdConfirmation = SettingsScreen:new { title = "Are you sure?", createUi = function(self) @@ -696,6 +749,9 @@ return widgets.MenuScreen:new { section("System") submenu("Database", DatabaseSettings) + submenu("Power", PowerSettings) + + section("About") submenu("Firmware", FirmwareSettings) submenu("Licenses", LicensesScreen) submenu("Regulatory", RegulatoryScreen) diff --git a/lua/widgets.lua b/lua/widgets.lua index 78d53a57..f830390f 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -58,7 +58,7 @@ widgets.MenuScreen = screen:new { end } -function widgets.Row(parent, left, right) +function widgets.Row(parent, left_text, right_text) local container = parent:Object { flex = { flex_direction = "row", @@ -70,12 +70,16 @@ function widgets.Row(parent, left, right) h = lvgl.SIZE_CONTENT } container:add_style(styles.list_item) - container:Label { - text = left, - flex_grow = 1 + local left = container:Label { + text = left_text, + flex_grow = 1, + } + local right = container:Label { + text = right_text or "", } - container:Label { - text = right + return { + left = left, + right = right, } end diff --git a/luals-stubs/power.lua b/luals-stubs/power.lua index ac7f15bb..d28cd196 100644 --- a/luals-stubs/power.lua +++ b/luals-stubs/power.lua @@ -6,6 +6,8 @@ --- @field battery_pct Property The battery's current charge, as a percentage of the maximum charge. --- @field battery_millivolts Property The battery's current voltage, in millivolts. --- @field plugged_in Property Whether or not the device is currently receiving external power. +--- @field fast_charge Property Whether or not fast charging is enabled. Fast charging can fully recharge the battery up to two times faster than regular charging, but will have a small negative impact on the lifetime of the battery. +--- @field charge_state Property a string property describing the current charging state. May be one of "no_battery", "critical", "discharging", "charge_regular", "charge_fast", "full_charge", "fault", or "unknown". local power = {} return power diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index 8eb28cc9..e298ffc3 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -90,6 +90,9 @@ class NvsStorage { auto LraCalibration() -> std::optional; auto LraCalibration(const LraData&) -> void; + auto FastCharge() -> bool; + auto FastCharge(bool) -> void; + auto PreferredBluetoothDevice() -> std::optional; auto PreferredBluetoothDevice(std::optional) -> void; @@ -150,6 +153,7 @@ class NvsStorage { Setting display_rows_; Setting haptic_motor_type_; Setting lra_calibration_; + Setting fast_charge_; Setting brightness_; Setting sensitivity_; diff --git a/src/drivers/include/drivers/samd.hpp b/src/drivers/include/drivers/samd.hpp index 897e78d6..ff479225 100644 --- a/src/drivers/include/drivers/samd.hpp +++ b/src/drivers/include/drivers/samd.hpp @@ -10,6 +10,7 @@ #include #include +#include "drivers/nvs.hpp" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -17,9 +18,7 @@ namespace drivers { class Samd { public: - static auto Create() -> Samd* { return new Samd(); } - - Samd(); + Samd(NvsStorage& nvs); ~Samd(); auto Version() -> std::string; @@ -37,8 +36,14 @@ class Samd { kChargingFast, // The battery is full charged, and we are still plugged in. kFullCharge, + // Charging failed. + kFault, + // The battery status returned isn't a known enum value. + kUnknown, }; + static auto chargeStatusToString(ChargeStatus) -> std::string; + auto GetChargeStatus() -> std::optional; auto UpdateChargeStatus() -> void; @@ -68,6 +73,8 @@ class Samd { Samd& operator=(const Samd&) = delete; private: + NvsStorage& nvs_; + uint8_t version_; std::optional charge_status_; UsbStatus usb_status_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index e3c4aa06..6fac8c61 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -40,6 +40,7 @@ static constexpr char kKeyDisplayRows[] = "disprows"; static constexpr char kKeyHapticMotorType[] = "hapticmtype"; static constexpr char kKeyLraCalibration[] = "lra_cali"; static constexpr char kKeyDbAutoIndex[] = "dbautoindex"; +static constexpr char kKeyFastCharge[] = "fastchg"; static auto nvs_get_string(nvs_handle_t nvs, const char* key) -> std::optional { @@ -239,6 +240,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) display_rows_(kKeyDisplayRows), haptic_motor_type_(kKeyHapticMotorType), lra_calibration_(kKeyLraCalibration), + fast_charge_(kKeyFastCharge), brightness_(kKeyBrightness), sensitivity_(kKeyScrollSensitivity), amp_max_vol_(kKeyAmpMaxVolume), @@ -444,6 +446,16 @@ auto NvsStorage::OutputMode(Output out) -> void { nvs_commit(handle_); } +auto NvsStorage::FastCharge() -> bool { + std::lock_guard lock{mutex_}; + return fast_charge_.get().value_or(true); +} + +auto NvsStorage::FastCharge(bool en) -> void { + std::lock_guard lock{mutex_}; + fast_charge_.set(en); +} + auto NvsStorage::ScreenBrightness() -> uint_fast8_t { std::lock_guard lock{mutex_}; return std::clamp(brightness_.get().value_or(50), 0, 100); diff --git a/src/drivers/samd.cpp b/src/drivers/samd.cpp index e4aa73ad..c2308760 100644 --- a/src/drivers/samd.cpp +++ b/src/drivers/samd.cpp @@ -5,11 +5,13 @@ */ #include "drivers/samd.hpp" +#include #include #include #include +#include "drivers/nvs.hpp" #include "esp_err.h" #include "esp_log.h" #include "hal/gpio_types.h" @@ -32,7 +34,29 @@ namespace drivers { static constexpr gpio_num_t kIntPin = GPIO_NUM_35; -Samd::Samd() { +auto Samd::chargeStatusToString(ChargeStatus status) -> std::string { + switch (status) { + case ChargeStatus::kNoBattery: + return "no_battery"; + case ChargeStatus::kBatteryCritical: + return "critical"; + case ChargeStatus::kDischarging: + return "discharging"; + case ChargeStatus::kChargingRegular: + return "charge_regular"; + case ChargeStatus::kChargingFast: + return "charge_fast"; + case ChargeStatus::kFullCharge: + return "full_charge"; + case ChargeStatus::kFault: + return "fault"; + case ChargeStatus::kUnknown: + default: + return "unknown"; + } +} + +Samd::Samd(NvsStorage& nvs) : nvs_(nvs) { gpio_set_direction(kIntPin, GPIO_MODE_INPUT); // Being able to interface with the SAMD properly is critical. To ensure we @@ -51,7 +75,7 @@ Samd::Samd() { UpdateChargeStatus(); UpdateUsbStatus(); - SetFastChargeEnabled(true); + SetFastChargeEnabled(nvs.FastCharge()); } Samd::~Samd() {} @@ -78,16 +102,38 @@ auto Samd::UpdateChargeStatus() -> void { return; } - // FIXME: Ideally we should be using the three 'charge status' bits to work - // out whether we're actually charging, or if we've got a full charge, - // critically low charge, etc. + // Lower two bits are the usb power status, next three are the BMS status. + // See 'gpio.c' in the SAMD21 firmware for how these bits get packed. + uint8_t charge_state = (raw_res & 0b11100) >> 2; uint8_t usb_state = raw_res & 0b11; - if (usb_state == 0) { - charge_status_ = ChargeStatus::kDischarging; - } else if (usb_state == 1) { - charge_status_ = ChargeStatus::kChargingRegular; - } else { - charge_status_ = ChargeStatus::kChargingFast; + switch (charge_state) { + case 0b000: + charge_status_ = ChargeStatus::kNoBattery; + break; + case 0b001: + // BMS says we're charging; work out how fast we're charging. + if (usb_state >= 0b10 && nvs_.FastCharge()) { + charge_status_ = ChargeStatus::kChargingFast; + } else { + charge_status_ = ChargeStatus::kChargingRegular; + } + break; + case 0b010: + charge_status_ = ChargeStatus::kFullCharge; + break; + case 0b011: + charge_status_ = ChargeStatus::kFault; + break; + case 0b100: + charge_status_ = ChargeStatus::kBatteryCritical; + break; + case 0b101: + charge_status_ = ChargeStatus::kDischarging; + break; + case 0b110: + case 0b111: + charge_status_ = ChargeStatus::kUnknown; + break; } } @@ -127,9 +173,15 @@ auto Samd::ResetToFlashSamd() -> void { } auto Samd::SetFastChargeEnabled(bool en) -> void { + // Always update NVS, so that the setting is right after the SAMD firmware is + // updated. + nvs_.FastCharge(en); + if (version_ < 4) { return; } + ESP_LOGI(kTag, "set fast charge %u", en); + I2CTransaction transaction; transaction.start() .write_addr(kAddress, I2C_MASTER_WRITE) diff --git a/src/tangara/app_console/app_console.cpp b/src/tangara/app_console/app_console.cpp index 11862143..21dec56a 100644 --- a/src/tangara/app_console/app_console.cpp +++ b/src/tangara/app_console/app_console.cpp @@ -465,26 +465,7 @@ int CmdSamd(int argc, char** argv) { } else if (cmd == "charge") { auto res = samd.GetChargeStatus(); if (res) { - switch (res.value()) { - case drivers::Samd::ChargeStatus::kNoBattery: - std::cout << "kNoBattery" << std::endl; - break; - case drivers::Samd::ChargeStatus::kBatteryCritical: - std::cout << "kBatteryCritical" << std::endl; - break; - case drivers::Samd::ChargeStatus::kDischarging: - std::cout << "kDischarging" << std::endl; - break; - case drivers::Samd::ChargeStatus::kChargingRegular: - std::cout << "kChargingRegular" << std::endl; - break; - case drivers::Samd::ChargeStatus::kChargingFast: - std::cout << "kChargingFast" << std::endl; - break; - case drivers::Samd::ChargeStatus::kFullCharge: - std::cout << "kFullCharge" << std::endl; - break; - } + std::cout << drivers::Samd::chargeStatusToString(*res) << std::endl; } else { std::cout << "unknown" << std::endl; } diff --git a/src/tangara/battery/battery.cpp b/src/tangara/battery/battery.cpp index 3cfdb20c..9bde86a8 100644 --- a/src/tangara/battery/battery.cpp +++ b/src/tangara/battery/battery.cpp @@ -93,6 +93,8 @@ auto Battery::Update() -> void { .percent = percent, .millivolts = mV, .is_charging = is_charging, + .raw_status = + charge_state.value_or(drivers::Samd::ChargeStatus::kUnknown), }; EmitEvent(); } diff --git a/src/tangara/battery/battery.hpp b/src/tangara/battery/battery.hpp index 80b0f2d2..c4f631e0 100644 --- a/src/tangara/battery/battery.hpp +++ b/src/tangara/battery/battery.hpp @@ -27,6 +27,7 @@ class Battery { uint_fast8_t percent; uint32_t millivolts; bool is_charging; + drivers::Samd::ChargeStatus raw_status; bool operator==(const BatteryState& other) const { return percent == other.percent && is_charging == other.is_charging; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index a3fed9fa..1f99e3ab 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -87,7 +87,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); drivers::spiffs_mount(); - sServices->samd(std::unique_ptr(drivers::Samd::Create())); + sServices->samd(std::make_unique(sServices->nvs())); sServices->touchwheel( std::unique_ptr{drivers::TouchWheel::Create()}); sServices->haptics(std::make_unique(sServices->nvs())); @@ -96,8 +96,8 @@ auto Booting::entry() -> void { sServices->battery(std::make_unique( sServices->samd(), std::unique_ptr(adc))); - sServices->track_queue( - std::make_unique(sServices->bg_worker(), sServices->database())); + sServices->track_queue(std::make_unique( + sServices->bg_worker(), sServices->database())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); sServices->tts(std::make_unique()); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index cd39dc9c..38e9b8e1 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -101,6 +101,15 @@ static auto lvgl_delay_cb(uint32_t ms) -> void { lua::Property UiState::sBatteryPct{0}; lua::Property UiState::sBatteryMv{0}; lua::Property UiState::sBatteryCharging{false}; +lua::Property UiState::sPowerChargeState{"unknown"}; +lua::Property UiState::sPowerFastChargeEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + sServices->samd().SetFastChargeEnabled(std::get(val)); + return true; + }}; lua::Property UiState::sBluetoothEnabled{ false, [](const lua::LuaValue& val) { @@ -406,6 +415,13 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) { sBatteryPct.setDirect(static_cast(ev.new_state.percent)); sBatteryMv.setDirect(static_cast(ev.new_state.millivolts)); sBatteryCharging.setDirect(ev.new_state.is_charging); + sPowerChargeState.setDirect( + drivers::Samd::chargeStatusToString(ev.new_state.raw_status)); + + // FIXME: Avoid calling these event handlers before boot. + if (sServices) { + sPowerFastChargeEnabled.setDirect(sServices->nvs().FastCharge()); + } } void UiState::react(const audio::QueueUpdate&) { @@ -414,7 +430,8 @@ void UiState::react(const audio::QueueUpdate&) { sQueueSize.setDirect(static_cast(queue_size)); int current_pos = queue.currentPosition(); - // If there is nothing in the queue, the position should be 0, otherwise, add one because lua + // If there is nothing in the queue, the position should be 0, otherwise, add + // one because lua if (queue_size > 0) { current_pos++; } @@ -564,11 +581,14 @@ void Lua::entry() { auto& registry = lua::Registry::instance(*sServices); sLua = registry.uiThread(); - registry.AddPropertyModule("power", { - {"battery_pct", &sBatteryPct}, - {"battery_millivolts", &sBatteryMv}, - {"plugged_in", &sBatteryCharging}, - }); + registry.AddPropertyModule("power", + { + {"battery_pct", &sBatteryPct}, + {"battery_millivolts", &sBatteryMv}, + {"plugged_in", &sBatteryCharging}, + {"charge_state", &sPowerChargeState}, + {"fast_charge", &sPowerFastChargeEnabled}, + }); registry.AddPropertyModule( "bluetooth", { {"enabled", &sBluetoothEnabled}, @@ -663,6 +683,8 @@ void Lua::entry() { } sBluetoothKnownDevices.setDirect(bt.knownDevices()); + sPowerFastChargeEnabled.setDirect(sServices->nvs().FastCharge()); + if (sServices->sd() == drivers::SdState::kMounted) { sLua->RunScript("/sdcard/config.lua"); } diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index cef9a13a..41f0db3a 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -101,6 +101,8 @@ class UiState : public tinyfsm::Fsm { static lua::Property sBatteryPct; static lua::Property sBatteryMv; static lua::Property sBatteryCharging; + static lua::Property sPowerChargeState; + static lua::Property sPowerFastChargeEnabled; static lua::Property sBluetoothEnabled; static lua::Property sBluetoothConnecting;