diff --git a/Boards/LilygoTdeck/CMakeLists.txt b/Boards/LilygoTdeck/CMakeLists.txt index 087beb3a..05ae1a26 100644 --- a/Boards/LilygoTdeck/CMakeLists.txt +++ b/Boards/LilygoTdeck/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRC_DIRS "Source" "Source/hal" INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd esp_lcd_touch_gt911 driver vfs fatfs + REQUIRES Tactility esp_lvgl_port esp_lcd esp_lcd_touch_gt911 driver vfs fatfs esp_adc ) diff --git a/Boards/LilygoTdeck/Source/InitHardware.cpp b/Boards/LilygoTdeck/Source/InitHardware.cpp index 76be2798..292f114e 100644 --- a/Boards/LilygoTdeck/Source/InitHardware.cpp +++ b/Boards/LilygoTdeck/Source/InitHardware.cpp @@ -2,7 +2,6 @@ #include "hal/TdeckDisplayConstants.h" #include #include -#include #define TAG "tdeck" @@ -19,7 +18,6 @@ #define TDECK_LCD_BACKLIGHT_LEDC_DUTY_RES LEDC_TIMER_8_BIT #define TDECK_LCD_BACKLIGHT_LEDC_FREQUENCY (4000) - static bool init_spi() { spi_bus_config_t bus_config = { .mosi_io_num = TDECK_SPI_PIN_MOSI, diff --git a/Boards/LilygoTdeck/Source/LilygoTdeck.cpp b/Boards/LilygoTdeck/Source/LilygoTdeck.cpp index 3a2a710f..7d8297f2 100644 --- a/Boards/LilygoTdeck/Source/LilygoTdeck.cpp +++ b/Boards/LilygoTdeck/Source/LilygoTdeck.cpp @@ -1,6 +1,7 @@ #include "hal/Configuration.h" #include "hal/TdeckDisplay.h" #include "hal/TdeckKeyboard.h" +#include "hal/TdeckPower.h" #include "hal/sdcard/Sdcard.h" bool tdeck_init_power(); @@ -16,7 +17,7 @@ extern const tt::hal::Configuration lilygo_tdeck = { .createDisplay = createDisplay, .createKeyboard = createKeyboard, .sdcard = &tdeck_sdcard, - .power = nullptr, + .power = tdeck_get_power, .i2c = { tt::hal::i2c::Configuration { .name = "Internal", diff --git a/Boards/LilygoTdeck/Source/hal/TdeckPower.cpp b/Boards/LilygoTdeck/Source/hal/TdeckPower.cpp new file mode 100644 index 00000000..ef8a0bba --- /dev/null +++ b/Boards/LilygoTdeck/Source/hal/TdeckPower.cpp @@ -0,0 +1,135 @@ +#include "TdeckPower.h" + +#include "Log.h" +#include "CoreDefines.h" + +#define TAG "power" + +/** + * The ratio of the voltage divider is supposedly 2.0, but when we set that, the ADC reports a bit over 4.45V + * when charging the device. + * There was also supposedly a +0.11 sag compensation related to "display under-voltage" according to Meshtastic firmware. + * Either Meshtastic implemented it incorrectly OR there is simply a 5-10% deviation in accuracy. + * The latter is feasible as the selected resistors for the voltage divider might not have been matched appropriately. + */ +#define ADC_MULTIPLIER 1.89f + +#define ADC_REF_VOLTAGE 3.3f +#define BATTERY_VOLTAGE_MIN 3.2f +#define BATTERY_VOLTAGE_MAX 4.2f + +static adc_oneshot_unit_init_cfg_t adcConfig = { + .unit_id = ADC_UNIT_1, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, +}; + +static adc_oneshot_chan_cfg_t adcChannelConfig = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, +}; + +static uint8_t estimateChargeLevelFromVoltage(uint32_t milliVolt) { + float volts = TT_MIN((float)milliVolt / 1000.f, BATTERY_VOLTAGE_MAX); + float voltage_percentage = (volts - BATTERY_VOLTAGE_MIN) / (BATTERY_VOLTAGE_MAX - BATTERY_VOLTAGE_MIN); + float voltage_factor = TT_MIN(1.0f, voltage_percentage); + auto charge_level = (uint8_t) (voltage_factor * 100.f); + TT_LOG_V(TAG, "mV = %lu, scaled = %.2f, factor = %.2f, result = %d", milliVolt, volts, voltage_factor, charge_level); + return charge_level; +} + +TdeckPower::TdeckPower() { + if (adc_oneshot_new_unit(&adcConfig, &adcHandle) != ESP_OK) { + TT_LOG_E(TAG, "ADC config failed"); + return; + } + + if (adc_oneshot_config_channel(adcHandle, ADC_CHANNEL_3, &adcChannelConfig) != ESP_OK) { + TT_LOG_E(TAG, "ADC channel config failed"); + + adc_oneshot_del_unit(adcHandle); + return; + } +} + +TdeckPower::~TdeckPower() { + if (adcHandle) { + adc_oneshot_del_unit(adcHandle); + } +} + +bool TdeckPower::supportsMetric(MetricType type) const { + switch (type) { + case BATTERY_VOLTAGE: + case CHARGE_LEVEL: + return true; + case IS_CHARGING: + case CURRENT: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TdeckPower::getMetric(Power::MetricType type, Power::MetricData& data) { + switch (type) { + case BATTERY_VOLTAGE: + return readBatteryVoltageSampled(data.valueAsUint32); + case CHARGE_LEVEL: + if (readBatteryVoltageSampled(data.valueAsUint32)) { + data.valueAsUint32 = estimateChargeLevelFromVoltage(data.valueAsUint32); + return true; + } else { + return false; + } + case IS_CHARGING: + case CURRENT: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TdeckPower::readBatteryVoltageOnce(uint32_t& output) { + int raw; + if (adc_oneshot_read(adcHandle, ADC_CHANNEL_3, &raw) == ESP_OK) { + output = ADC_MULTIPLIER * ((1000.f * ADC_REF_VOLTAGE) / 4096.f) * (float)raw; + TT_LOG_V(TAG, "Raw = %d, voltage = %lu", raw, output); + return true; + } else { + TT_LOG_E(TAG, "Read failed"); + return false; + } +} + +#define MAX_VOLTAGE_SAMPLES 15 + +bool TdeckPower::readBatteryVoltageSampled(uint32_t& output) { + size_t samples_read = 0; + uint32_t sample_accumulator = 0; + uint32_t sample_read_buffer; + + for (size_t i = 0; i < MAX_VOLTAGE_SAMPLES; ++i) { + if (readBatteryVoltageOnce(sample_read_buffer)) { + sample_accumulator += sample_read_buffer; + samples_read++; + } + } + + if (samples_read > 0) { + output = sample_accumulator / samples_read; + return true; + } else { + return false; + } +} + +static std::shared_ptr power; + +std::shared_ptr tdeck_get_power() { + if (power == nullptr) { + power = std::make_shared(); + } + return power; +} + diff --git a/Boards/LilygoTdeck/Source/hal/TdeckPower.h b/Boards/LilygoTdeck/Source/hal/TdeckPower.h new file mode 100644 index 00000000..fd6be1b9 --- /dev/null +++ b/Boards/LilygoTdeck/Source/hal/TdeckPower.h @@ -0,0 +1,27 @@ +#pragma once + +#include "hal/Power.h" +#include +#include + +using namespace tt::hal; + +class TdeckPower : public Power { + + adc_oneshot_unit_handle_t adcHandle = nullptr; + +public: + + TdeckPower(); + ~TdeckPower(); + + bool supportsMetric(MetricType type) const override; + bool getMetric(Power::MetricType type, Power::MetricData& data) override; + +private: + + bool readBatteryVoltageSampled(uint32_t& output); + bool readBatteryVoltageOnce(uint32_t& output); +}; + +std::shared_ptr tdeck_get_power(); diff --git a/Boards/M5stackCore2/Source/M5stackCore2.cpp b/Boards/M5stackCore2/Source/M5stackCore2.cpp index 2bf3b1b4..5a289e61 100644 --- a/Boards/M5stackCore2/Source/M5stackCore2.cpp +++ b/Boards/M5stackCore2/Source/M5stackCore2.cpp @@ -1,12 +1,13 @@ #include "M5stackCore2.h" #include "M5stackShared.h" +#include "hal/M5stackPower.h" extern const tt::hal::Configuration m5stack_core2 = { - .initBoot = &m5stack_bootstrap, - .initLvgl = &m5stack_lvgl_init, + .initBoot = m5stack_bootstrap, + .initLvgl = m5stack_lvgl_init, .createDisplay = createDisplay, .sdcard = &m5stack_sdcard, - .power = &m5stack_power, + .power = m5stack_get_power, .i2c = { tt::hal::i2c::Configuration { .name = "Internal", diff --git a/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp b/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp index 7a7cf0ff..5f3115a2 100644 --- a/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp +++ b/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp @@ -1,12 +1,13 @@ #include "M5stackCoreS3.h" #include "M5stackShared.h" +#include "hal/M5stackPower.h" const tt::hal::Configuration m5stack_cores3 = { - .initBoot = &m5stack_bootstrap, - .initLvgl = &m5stack_lvgl_init, + .initBoot = m5stack_bootstrap, + .initLvgl = m5stack_lvgl_init, .createDisplay = createDisplay, .sdcard = &m5stack_sdcard, - .power = &m5stack_power, + .power = m5stack_get_power, .i2c = { tt::hal::i2c::Configuration { .name = "Internal", diff --git a/Boards/M5stackShared/Source/M5stackShared.h b/Boards/M5stackShared/Source/M5stackShared.h index 63ff27f8..6ef643dd 100644 --- a/Boards/M5stackShared/Source/M5stackShared.h +++ b/Boards/M5stackShared/Source/M5stackShared.h @@ -8,5 +8,4 @@ extern bool m5stack_bootstrap(); extern bool m5stack_lvgl_init(); -extern const tt::hal::Power m5stack_power; extern const tt::hal::sdcard::SdCard m5stack_sdcard; diff --git a/Boards/M5stackShared/Source/Power.cpp b/Boards/M5stackShared/Source/Power.cpp deleted file mode 100644 index 3d80d0cf..00000000 --- a/Boards/M5stackShared/Source/Power.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "hal/Power.h" -#include "M5Unified.hpp" - -/** - * M5.Power by default doesn't have a check to see if charging is enabled. - * However, it's always enabled by default after boot, so we cover that here: - */ -static bool charging_enabled = true; - -static bool is_charging() { - return M5.Power.isCharging() == m5::Power_Class::is_charging; -} - -static bool is_charging_enabled() { - return charging_enabled; -} - -static void set_charging_enabled(bool enabled) { - charging_enabled = enabled; // Local shadow copy because M5 API doesn't provide a function for it - M5.Power.setBatteryCharge(enabled); -} - -static uint8_t get_charge_level() { - uint16_t scaled = (uint16_t)M5.Power.getBatteryLevel() * 255 / 100; - return (uint8_t)scaled; -} - -static int32_t get_current() { - return M5.Power.getBatteryCurrent(); -} - -extern const tt::hal::Power m5stack_power = { - .isCharging = &is_charging, - .isChargingEnabled = &is_charging_enabled, - .setChargingEnabled = &set_charging_enabled, - .getChargeLevel = &get_charge_level, - .getCurrent = &get_current -}; diff --git a/Boards/M5stackShared/Source/hal/M5stackPower.cpp b/Boards/M5stackShared/Source/hal/M5stackPower.cpp new file mode 100644 index 00000000..6902c7fe --- /dev/null +++ b/Boards/M5stackShared/Source/hal/M5stackPower.cpp @@ -0,0 +1,50 @@ +#include "M5stackPower.h" + +#include "M5Unified.h" + +#define TAG "m5stack_power" + +bool M5stackPower::supportsMetric(MetricType type) const { + switch (type) { + case IS_CHARGING: + case CURRENT: + case BATTERY_VOLTAGE: + case CHARGE_LEVEL: + return true; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool M5stackPower::getMetric(Power::MetricType type, Power::MetricData& data) { + switch (type) { + case IS_CHARGING: + data.valueAsBool = M5.Power.isCharging(); + return true; + case CURRENT: + data.valueAsInt32 = M5.Power.getBatteryCurrent(); + return true; + case BATTERY_VOLTAGE: + data.valueAsUint32 = M5.Power.getBatteryVoltage(); + return true; + case CHARGE_LEVEL: + data.valueAsUint8 = M5.Power.getBatteryLevel(); + return true; + } + + return false; // Safety guard for when new enum values are introduced +} + +void M5stackPower::setAllowedToCharge(bool canCharge) { + M5.Power.setBatteryCharge(canCharge); +} + +static std::shared_ptr power; + +std::shared_ptr m5stack_get_power() { + if (power == nullptr) { + power = std::make_shared(); + } + return power; +} + diff --git a/Boards/M5stackShared/Source/hal/M5stackPower.h b/Boards/M5stackShared/Source/hal/M5stackPower.h new file mode 100644 index 00000000..5bc018a8 --- /dev/null +++ b/Boards/M5stackShared/Source/hal/M5stackPower.h @@ -0,0 +1,23 @@ +#pragma once + +#include "hal/Power.h" +#include + +using namespace tt::hal; + +class M5stackPower : public Power { + +public: + + M5stackPower() {} + ~M5stackPower() {} + + bool supportsMetric(MetricType type) const override; + bool getMetric(Power::MetricType type, Power::MetricData& data) override; + + bool supportsChargeControl() const { return true; } + bool isAllowedToCharge() const { return true; } /** We can call setChargingAllowed() but the actual value is unknown as it resets when re-plugging USB */ + void setAllowedToCharge(bool canCharge); +}; + +std::shared_ptr m5stack_get_power(); diff --git a/Documentation/ideas.md b/Documentation/ideas.md index d0403d7b..af563708 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -23,6 +23,8 @@ - Wifi bug: when pressing disconnect while between `WIFI_EVENT_STA_START` and `IP_EVENT_STA_GOT_IP`, then auto-connect becomes activate again. - T-Deck Plus: Create separate board config - External app loading: Check version of Tactility and check ESP target hardware, to check for compatibility. +- hal::Configuration: Replace CreateX fields with plain instances +- T-Deck Power: capacity estimation uses linear voltage curve, but it should use some sort of battery discharge curve. # Core Ideas - Support for displays with different DPI. Consider the layer-based system like on Android. diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index 4acb7dc3..5ce9bfe2 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -13,12 +13,13 @@ namespace tt::app::power { #define TAG "power" extern const AppManifest manifest; -static void on_timer(TT_UNUSED std::shared_ptr context); +static void onTimer(TT_UNUSED std::shared_ptr context); struct Data { - Timer update_timer = Timer(Timer::TypePeriodic, &on_timer, nullptr); - const hal::Power* power = getConfiguration()->hardware->power; + Timer update_timer = Timer(Timer::TypePeriodic, &onTimer, nullptr); + std::shared_ptr power = getConfiguration()->hardware->power(); lv_obj_t* enable_switch = nullptr; + lv_obj_t* battery_voltage = nullptr; lv_obj_t* charge_state = nullptr; lv_obj_t* charge_level = nullptr; lv_obj_t* current = nullptr; @@ -35,25 +36,71 @@ std::shared_ptr _Nullable optData() { } static void updateUi(std::shared_ptr data) { - bool charging_enabled = data->power->isChargingEnabled(); - const char* charge_state = data->power->isCharging() ? "yes" : "no"; - uint8_t charge_level = data->power->getChargeLevel(); - uint16_t charge_level_scaled = (int16_t)charge_level * 100 / 255; - int32_t current = data->power->getCurrent(); + const char* charge_state; + hal::Power::MetricData metric_data; + if (data->power->getMetric(hal::Power::MetricType::IS_CHARGING, metric_data)) { + charge_state = metric_data.valueAsBool ? "yes" : "no"; + } else { + charge_state = "N/A"; + } + + uint8_t charge_level; + bool charge_level_scaled_set = false; + if (data->power->getMetric(hal::Power::MetricType::CHARGE_LEVEL, metric_data)) { + charge_level = metric_data.valueAsUint8; + charge_level_scaled_set = true; + } + + bool charging_enabled_set = data->power->supportsChargeControl(); + bool charging_enabled = data->power->supportsChargeControl() && data->power->isAllowedToCharge(); + + int32_t current; + bool current_set = false; + if (data->power->getMetric(hal::Power::MetricType::CURRENT, metric_data)) { + current = metric_data.valueAsInt32; + current_set = true; + } + + uint32_t battery_voltage; + bool battery_voltage_set = false; + if (data->power->getMetric(hal::Power::MetricType::BATTERY_VOLTAGE, metric_data)) { + battery_voltage = metric_data.valueAsUint32; + battery_voltage_set = true; + } lvgl::lock(kernel::millisToTicks(1000)); - lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled); + + if (charging_enabled_set) { + lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled); + lv_obj_remove_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); + } + lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state); - lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level_scaled); -#ifdef ESP_PLATFORM - lv_label_set_text_fmt(data->current, "Current: %ld mAh", current); -#else - lv_label_set_text_fmt(data->current, "Current: %d mAh", current); -#endif + + if (battery_voltage_set) { + lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: %lu mV", battery_voltage); + } else { + lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: N/A"); + } + + if (charge_level_scaled_set) { + lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level); + } else { + lv_label_set_text_fmt(data->charge_level, "Charge level: N/A"); + } + + if (current_set) { + lv_label_set_text_fmt(data->current, "Current: %ld mAh", current); + } else { + lv_label_set_text_fmt(data->current, "Current: N/A"); + } + lvgl::unlock(); } -static void on_timer(TT_UNUSED std::shared_ptr context) { +static void onTimer(TT_UNUSED std::shared_ptr context) { auto data = optData(); if (data != nullptr) { updateUi(data); @@ -68,8 +115,8 @@ static void onPowerEnabledChanged(lv_event_t* event) { auto data = optData(); if (data != nullptr) { - if (data->power->isChargingEnabled() != is_on) { - data->power->setChargingEnabled(is_on); + if (data->power->isAllowedToCharge() != is_on) { + data->power->setAllowedToCharge(is_on); updateUi(data); } } @@ -107,6 +154,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) { data->enable_switch = enable_switch; data->charge_state = lv_label_create(wrapper); data->charge_level = lv_label_create(wrapper); + data->battery_voltage = lv_label_create(wrapper); data->current = lv_label_create(wrapper); updateUi(data); @@ -119,7 +167,7 @@ static void onHide(TT_UNUSED AppContext& app) { } static void onStart(AppContext& app) { - auto data = std::shared_ptr(); + auto data = std::make_shared(); app.setData(data); assert(data->power != nullptr); // The Power app only shows up on supported devices } diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 15ed97da..6ef25467 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -118,23 +118,29 @@ static void update_sdcard_icon(std::shared_ptr data) { // region power static _Nullable const char* power_get_status_icon() { - _Nullable const hal::Power* power = getConfiguration()->hardware->power; - if (power != nullptr) { - uint8_t charge = power->getChargeLevel(); - if (charge >= 230) { - return TT_ASSETS_ICON_POWER_100; - } else if (charge >= 161) { - return TT_ASSETS_ICON_POWER_080; - } else if (charge >= 127) { - return TT_ASSETS_ICON_POWER_060; - } else if (charge >= 76) { - return TT_ASSETS_ICON_POWER_040; - } else { - return TT_ASSETS_ICON_POWER_020; - } - } else { + const std::shared_ptr power = getConfiguration()->hardware->power(); + if (power == nullptr) { return nullptr; } + + hal::Power::MetricData charge_level; + if (!power->getMetric(hal::Power::MetricType::CHARGE_LEVEL, charge_level)) { + return nullptr; + } + + uint8_t charge = charge_level.valueAsUint8; + + if (charge >= 90) { + return TT_ASSETS_ICON_POWER_100; + } else if (charge >= 70) { + return TT_ASSETS_ICON_POWER_080; + } else if (charge >= 50) { + return TT_ASSETS_ICON_POWER_060; + } else if (charge >= 30) { + return TT_ASSETS_ICON_POWER_040; + } else { + return TT_ASSETS_ICON_POWER_020; + } } static void update_power_icon(std::shared_ptr data) { diff --git a/TactilityHeadless/Source/hal/Configuration.h b/TactilityHeadless/Source/hal/Configuration.h index c2fce663..240ea1db 100644 --- a/TactilityHeadless/Source/hal/Configuration.h +++ b/TactilityHeadless/Source/hal/Configuration.h @@ -16,6 +16,7 @@ class Display; class Keyboard; typedef Display* (*CreateDisplay)(); typedef Keyboard* (*CreateKeyboard)(); +typedef std::shared_ptr (*CreatePower)(); struct Configuration { /** @@ -53,7 +54,7 @@ struct Configuration { /** * An optional power interface for battery or other power delivery. */ - const Power* _Nullable power = nullptr; + const CreatePower _Nullable power = nullptr; /** * A list of i2c devices (can be empty, but preferably accurately represents the device capabilities) diff --git a/TactilityHeadless/Source/hal/Power.h b/TactilityHeadless/Source/hal/Power.h index 89404eb2..4a834bec 100644 --- a/TactilityHeadless/Source/hal/Power.h +++ b/TactilityHeadless/Source/hal/Power.h @@ -4,18 +4,38 @@ namespace tt::hal { -typedef bool (*PowerIsCharging)(); -typedef bool (*PowerIsChargingEnabled)(); -typedef void (*PowerSetChargingEnabled)(bool enabled); -typedef uint8_t (*PowerGetBatteryCharge)(); // Power value [0, 255] which maps to 0-100% charge -typedef int32_t (*PowerGetCurrent)(); // Consumption or charge current in mAh +class Power{ -typedef struct { - PowerIsCharging isCharging; - PowerIsChargingEnabled isChargingEnabled; - PowerSetChargingEnabled setChargingEnabled; - PowerGetBatteryCharge getChargeLevel; - PowerGetCurrent getCurrent; -} Power; +public: + + Power() = default; + virtual ~Power() = default; + + enum MetricType { + IS_CHARGING, // bool + CURRENT, // int32_t, mAh + BATTERY_VOLTAGE, // uint32_t, mV + CHARGE_LEVEL, // uint8_t [0, 100] + }; + + union MetricData { + int32_t valueAsInt32 = 0; + uint32_t valueAsUint32; + uint8_t valueAsUint8; + float valueAsFloat; + bool valueAsBool; + }; + + virtual bool supportsMetric(MetricType type) const = 0; + + /** + * @return false when metric is not supported or (temporarily) not available. + */ + virtual bool getMetric(Power::MetricType type, MetricData& data) = 0; + + virtual bool supportsChargeControl() const { return false; } + virtual bool isAllowedToCharge() const { return false; } + virtual void setAllowedToCharge(bool canCharge) { /* NO-OP*/ } +}; } // namespace tt