Add persistent T-Beam charge voltage controls and report Mesh Solar limits
This commit is contained in:
1 parent
3177ccfafa
commit
613d9d7c43
15 files changed
+1275
-9
No files matched your search
@@ -227,6 +227,8 @@ jobs:
|
||||
- name: Verify ESP32 static DRAM budget
|
||||
run: |
|
||||
python3 -B test/test_esp32_dram.py
|
||||
python3 -B test/test_battery_charge_cli.py -v
|
||||
python3 -B test/test_tbeam_charge_target.py -v
|
||||
python3 -B test/test_elrs_power.py
|
||||
python3 -B test/test_ota_heap_context.py
|
||||
python3 -B test/test_ota_self_serve_policy.py -v
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Battery charge voltage
|
||||
|
||||
The `charge.voltage` commands change the battery charger's constant-voltage
|
||||
target on supported boards. They are available through the shared text CLI in
|
||||
the companion, repeater, room-server, sensor, and secure-chat roles. They do not change the
|
||||
battery voltage display, low-voltage shutdown, battery chemistry, or charge
|
||||
current.
|
||||
|
||||
```text
|
||||
get charge.voltage
|
||||
get charge.voltage.options
|
||||
set charge.voltage 4.1
|
||||
```
|
||||
|
||||
Values are in volts. Query the options first: supported targets depend on the
|
||||
PMU fitted to the board. The firmware rejects values that the detected PMU
|
||||
cannot represent; it does not round them to a supported target.
|
||||
|
||||
On T-Beam boards, a successful change is saved in NVS and reapplied after a
|
||||
restart. The firmware reads the PMU setting back before reporting success.
|
||||
Boards without a saved setting retain their existing startup behavior: the full
|
||||
PMU driver sets 4.2 V, while the compact classic T-Beam observer driver leaves
|
||||
the charger setting untouched. A failed hardware write, readback, or preference
|
||||
save is reported as an error.
|
||||
|
||||
If the saved target cannot be read, is invalid, or cannot be restored, startup
|
||||
does not overwrite it with a default target. `get charge.voltage` reports the
|
||||
live charger voltage with `(boot restore failed)`. A successful saved change
|
||||
clears that warning. This cannot guarantee restoration while the PMU or storage
|
||||
is failing, but it avoids silently resetting a saved lower target to 4.2 V.
|
||||
|
||||
## Supported boards
|
||||
|
||||
| Board family | Detected PMU | Charge targets (V) |
|
||||
| --- | --- | --- |
|
||||
| LilyGo T-Beam SX1262 or SX1276 | AXP192 | 4.10, 4.15, 4.20, 4.36 |
|
||||
| LilyGo T-Beam SX1262 or SX1276 | AXP2101 | 4.00, 4.10, 4.20, 4.35, 4.40 |
|
||||
| LilyGo T-Beam S3 Supreme SX1262 | AXP2101 | 4.00, 4.10, 4.20, 4.35, 4.40 |
|
||||
|
||||
The classic T-Beam driver detects the fitted AXP192 or AXP2101. Radio type does
|
||||
not determine which PMU is present. T-Beam 1W uses a different power design and
|
||||
does not support this setting.
|
||||
|
||||
Neither AXP192 nor AXP2101 supports a 3.65 V target. The lowest supported target
|
||||
is 4.10 V for AXP192 and 4.00 V for AXP2101. These PMUs cannot be configured as
|
||||
LiFePO4 chargers by changing this setting.
|
||||
|
||||
## Heltec Mesh Solar
|
||||
|
||||
Mesh Solar reports `charge.voltage` as unsupported because its CN3795 charger's
|
||||
constant-voltage target is set by hardware. Heltec supplies separate Li-ion
|
||||
(`I`) and LiFePO4 (`F`) hardware versions, with 4.2 V/cell and 3.6 V/cell charging
|
||||
respectively. Cell-count soldering and switches must also match the battery
|
||||
configuration. A software battery-type setting does not convert one hardware
|
||||
version into the other. See [Heltec's usage guide](https://wiki.heltec.org/docs/devices/open-source-hardware/nrf52840-series/mesh-solar/usage-guide)
|
||||
and [MeshSolar datasheet, sections 4.1 and 4.4](https://resource.heltec.cn/download/MeshSolar/datasheet/MeshSolar_V1.0.0.pdf).
|
||||
|
||||
The BQ4050 on this board is a battery gauge and protection controller. Its
|
||||
`ChargingVoltage()` is a requested voltage for a compatible smart charger. The
|
||||
pinned Mesh Solar library calls that parameter `eoc`, but writing it does not
|
||||
change the CN3795's voltage-feedback circuit. Valid charge termination also
|
||||
requires the charging current to taper, so lowering `eoc` alone does not create
|
||||
a reliable lower-voltage charge cutoff. Charge-overvoltage protection is a
|
||||
separate safeguard, not the charger's normal constant-voltage target. See
|
||||
[TI's BQ4050 technical reference manual, sections 4.5, 4.6, and 4.11](https://www.ti.com/lit/pdf/SLUUAQ3).
|
||||
|
||||
These commands do not write Mesh Solar's BQ4050 data flash or change its battery
|
||||
type, protection limits, cell count, or charge-FET configuration.
|
||||
|
||||
## Battery profiles
|
||||
|
||||
The nRF52 `battery.profile`, `battery.full`, and `battery.empty` settings control
|
||||
voltage-based reporting and protection. They do not change any charger target.
|
||||
See [Battery profile commands](cli_commands.md#configure-nrf52-battery-protection).
|
||||
@@ -5111,6 +5111,22 @@ manufacturer images and the boards still requiring untouched factory dumps.
|
||||
|
||||
---
|
||||
|
||||
#### Configure battery charge voltage
|
||||
|
||||
T-Beam SX1262/SX1276 and T-Beam S3 Supreme support persistent charger-voltage
|
||||
selection through the fitted AXP192 or AXP2101 PMU:
|
||||
|
||||
```text
|
||||
get charge.voltage.options
|
||||
get charge.voltage
|
||||
set charge.voltage 4.1
|
||||
```
|
||||
|
||||
Values are exact supported targets in volts. Neither T-Beam PMU supports 3.65 V.
|
||||
Heltec Mesh Solar's CN3795 charge voltage is set by its Li-ion or LiFePO4 hardware
|
||||
version, so these commands report that limitation without changing BMS settings.
|
||||
See [charge-voltage behavior and supported targets](charge_voltage.md).
|
||||
|
||||
#### Configure nRF52 battery protection
|
||||
**Usage:** `get pwrmgt.bootlock`, `set pwrmgt.bootlock <2500-4200>`
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include "NotificationSettingsFile.h"
|
||||
#include "MyMesh.h"
|
||||
@@ -9854,6 +9855,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mesh::power::handleBatteryChargeCommand(board, command, reply,
|
||||
reply_capacity)) return true;
|
||||
|
||||
// Hook for future variant-specific commands not covered by the shared,
|
||||
// runtime-aware FEM handlers above.
|
||||
if (board.handleCommand(command, sender_timestamp, reply)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <helpers/RadioProfileCLI.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
@@ -468,6 +469,12 @@ public:
|
||||
}
|
||||
|
||||
void handleCommand(const char* command) {
|
||||
char charge_reply[160];
|
||||
if (mesh::power::handleBatteryChargeCommand(board, command, charge_reply,
|
||||
sizeof(charge_reply))) {
|
||||
Serial.println(charge_reply);
|
||||
return;
|
||||
}
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
char voltage_reply[160];
|
||||
if (mesh::power::handleVoltagePolicyCommand(command, voltage_reply,
|
||||
|
||||
@@ -239,6 +239,18 @@ public:
|
||||
// True while a board can identify an active battery-charging source.
|
||||
virtual bool isChargerActive() { return false; }
|
||||
|
||||
// Actual charger voltage, in millivolts. Battery reporting and protection
|
||||
// thresholds are separate capabilities. A successful setter must verify
|
||||
// hardware readback and persist the setting for the next boot.
|
||||
virtual bool getBatteryChargeTarget(uint16_t& millivolts) { return false; }
|
||||
virtual bool supportsBatteryChargeTarget(uint16_t millivolts) { return false; }
|
||||
virtual bool setBatteryChargeTarget(uint16_t millivolts) { return false; }
|
||||
virtual const char* getBatteryChargeTargetOptions() const { return nullptr; }
|
||||
virtual bool batteryChargeTargetRestoreFailed() const { return false; }
|
||||
virtual const char* getBatteryChargeTargetUnsupportedReason() const {
|
||||
return "charge.voltage is not supported on this board";
|
||||
}
|
||||
|
||||
// Optional, source-specific power detection. Boards that can distinguish a USB
|
||||
// supply from a solar charger override these; defaults keep every other board
|
||||
// unaffected (the generic CLI prints "n/a" when a capability is absent).
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace power {
|
||||
|
||||
// Parse volts without floating-point rounding, signs, exponents, or ignored
|
||||
// suffixes. Millivolt precision keeps unsupported targets from being rounded
|
||||
// into a supported charger setting.
|
||||
inline bool parseChargeVolts(const char* value, uint16_t& millivolts) {
|
||||
if (!value || *value < '0' || *value > '9') return false;
|
||||
uint32_t whole = 0, fraction = 0;
|
||||
while (*value >= '0' && *value <= '9') {
|
||||
whole = whole * 10 + (*value++ - '0');
|
||||
if (whole > 65) return false;
|
||||
}
|
||||
unsigned digits = 0;
|
||||
if (*value == '.') {
|
||||
++value;
|
||||
while (*value >= '0' && *value <= '9') {
|
||||
if (++digits > 3) return false;
|
||||
fraction = fraction * 10 + (*value++ - '0');
|
||||
}
|
||||
if (digits == 0) return false;
|
||||
}
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if (*value) return false;
|
||||
while (digits++ < 3) fraction *= 10;
|
||||
const uint32_t result = whole * 1000 + fraction;
|
||||
if (result > 65535) return false;
|
||||
millivolts = static_cast<uint16_t>(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool chargeCommandKey(const char* text, const char* key) {
|
||||
const size_t length = strlen(key);
|
||||
return strncmp(text, key, length) == 0
|
||||
&& (text[length] == 0 || text[length] == ' ' || text[length] == '\t');
|
||||
}
|
||||
|
||||
// Shared by infrastructure, Companion, and Terminal Chat. Board code owns
|
||||
// hardware validation and persistence; the CLI must not alter BMS protection
|
||||
// settings to simulate an unsupported charger voltage.
|
||||
template <typename Board>
|
||||
bool handleBatteryChargeCommand(Board& board, const char* command,
|
||||
char* reply, size_t reply_capacity) {
|
||||
if (!command || !reply || !reply_capacity) return false;
|
||||
while (*command == ' ' || *command == '\t') ++command;
|
||||
const bool get = strncmp(command, "get ", 4) == 0;
|
||||
const bool set = strncmp(command, "set ", 4) == 0;
|
||||
if (!get && !set) return false;
|
||||
const char* key = command + 4;
|
||||
const bool options = get && chargeCommandKey(key, "charge.voltage.options");
|
||||
if (!options && !chargeCommandKey(key, "charge.voltage")) return false;
|
||||
const char* value = key + strlen(options ? "charge.voltage.options" : "charge.voltage");
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if (get && *value) {
|
||||
snprintf(reply, reply_capacity, "Error: usage get charge.voltage[.options]");
|
||||
return true;
|
||||
}
|
||||
const char* targets = board.getBatteryChargeTargetOptions();
|
||||
if (!targets) {
|
||||
snprintf(reply, reply_capacity, "Error: %s", board.getBatteryChargeTargetUnsupportedReason());
|
||||
return true;
|
||||
}
|
||||
if (options) {
|
||||
snprintf(reply, reply_capacity, "> %s V", targets);
|
||||
return true;
|
||||
}
|
||||
uint16_t millivolts = 0;
|
||||
if (get) {
|
||||
if (!board.getBatteryChargeTarget(millivolts)) {
|
||||
snprintf(reply, reply_capacity, "Error: charger read failed");
|
||||
} else {
|
||||
snprintf(reply, reply_capacity, "> %u.%03u V%s",
|
||||
static_cast<unsigned>(millivolts / 1000),
|
||||
static_cast<unsigned>(millivolts % 1000),
|
||||
board.batteryChargeTargetRestoreFailed() ? " (boot restore failed)" : "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!parseChargeVolts(value, millivolts)) {
|
||||
snprintf(reply, reply_capacity, "Error: usage set charge.voltage <volts>");
|
||||
} else if (!board.supportsBatteryChargeTarget(millivolts)) {
|
||||
snprintf(reply, reply_capacity, "Error: unsupported target; allowed %s V", targets);
|
||||
} else if (!board.setBatteryChargeTarget(millivolts)) {
|
||||
snprintf(reply, reply_capacity, "Error: charge target not confirmed saved; check get charge.voltage");
|
||||
} else {
|
||||
snprintf(reply, reply_capacity, "OK - charge.voltage %u.%03u V (saved)",
|
||||
static_cast<unsigned>(millivolts / 1000),
|
||||
static_cast<unsigned>(millivolts % 1000));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace power
|
||||
} // namespace mesh
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UsbLoggingWatchdog.h"
|
||||
#include "HilStartupTrace.h"
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include "CLICommandUtils.h"
|
||||
#include "GpsPowerPolicy.h"
|
||||
#include "FloodAdvertCLI.h"
|
||||
@@ -2840,6 +2841,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply);
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return;
|
||||
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
|
||||
if (handleManagementCommand(command, reply)) return;
|
||||
if (mesh::wireless::control().handle(command, reply, 160, millis(),
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "TBeamBoard.h"
|
||||
#include "TBeamChargeTarget.h"
|
||||
#include <nvs.h>
|
||||
//#include <RadioLib.h>
|
||||
|
||||
uint32_t deviceOnline = 0x00;
|
||||
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER) || defined(TBEAM_SUPREME_SX1262)
|
||||
bool pmuInterrupt;
|
||||
static void setPmuFlag()
|
||||
{
|
||||
pmuInterrupt = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void TBeamBoard::begin() {
|
||||
|
||||
@@ -26,7 +30,9 @@ void TBeamBoard::begin() {
|
||||
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
|
||||
#endif
|
||||
|
||||
power_init();
|
||||
charge_pmu_ready = power_init();
|
||||
charge_target_restore_failed = !charge_pmu_ready;
|
||||
if (charge_pmu_ready) restoreBatteryChargeTarget();
|
||||
|
||||
//Configure user button
|
||||
pinMode(PIN_USER_BTN, INPUT);
|
||||
@@ -39,8 +45,8 @@ void TBeamBoard::begin() {
|
||||
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_DEEPSLEEP) {
|
||||
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
|
||||
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
|
||||
uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status();
|
||||
if (wakeup_source & (1ULL << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
|
||||
startup_reason = BD_STARTUP_RX_PACKET;
|
||||
}
|
||||
|
||||
@@ -139,13 +145,14 @@ void TBeamBoard::printPMU()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
static bool pmuRead(uint8_t reg, uint8_t& value) {
|
||||
PMU_WIRE_PORT.beginTransmission(I2C_PMU_ADD);
|
||||
PMU_WIRE_PORT.write(reg);
|
||||
if (PMU_WIRE_PORT.endTransmission(false) != 0 ||
|
||||
PMU_WIRE_PORT.requestFrom(I2C_PMU_ADD, 1) != 1) return false;
|
||||
value = PMU_WIRE_PORT.read();
|
||||
const int received = PMU_WIRE_PORT.read();
|
||||
if (received < 0) return false;
|
||||
value = static_cast<uint8_t>(received);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -156,6 +163,111 @@ static bool pmuWrite(uint8_t reg, uint8_t value) {
|
||||
return PMU_WIRE_PORT.endTransmission() == 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct ChargePmuIO {
|
||||
bool read(uint8_t reg, uint8_t& value) { return pmuRead(reg, value); }
|
||||
bool write(uint8_t reg, uint8_t value) { return pmuWrite(reg, value); }
|
||||
};
|
||||
|
||||
class ChargeTargetStore {
|
||||
nvs_handle_t handle = 0;
|
||||
bool opened = false;
|
||||
bool absent_namespace = false;
|
||||
static constexpr const char* key = "target_mv";
|
||||
public:
|
||||
ChargeTargetStore(const char* name, bool read_only) {
|
||||
if (!name) return;
|
||||
const esp_err_t result = nvs_open(name, read_only ? NVS_READONLY : NVS_READWRITE,
|
||||
&handle);
|
||||
opened = result == ESP_OK;
|
||||
absent_namespace = read_only && result == ESP_ERR_NVS_NOT_FOUND;
|
||||
}
|
||||
~ChargeTargetStore() { if (opened) nvs_close(handle); }
|
||||
bool read(uint16_t& value, bool& present) {
|
||||
if (absent_namespace) {
|
||||
value = 0;
|
||||
present = false;
|
||||
return true;
|
||||
}
|
||||
if (!opened) return false;
|
||||
const esp_err_t result = nvs_get_u16(handle, key, &value);
|
||||
if (result == ESP_ERR_NVS_NOT_FOUND) {
|
||||
value = 0;
|
||||
present = false;
|
||||
return true;
|
||||
}
|
||||
if (result != ESP_OK) return false;
|
||||
present = true;
|
||||
return true;
|
||||
}
|
||||
bool write(uint16_t value) {
|
||||
if (!opened || nvs_set_u16(handle, key, value) != ESP_OK ||
|
||||
nvs_commit(handle) != ESP_OK) return false;
|
||||
uint16_t actual = 0;
|
||||
bool present = false;
|
||||
return read(actual, present) && present && actual == value;
|
||||
}
|
||||
bool erase() {
|
||||
if (!opened) return false;
|
||||
const esp_err_t result = nvs_erase_key(handle, key);
|
||||
if (result != ESP_OK && result != ESP_ERR_NVS_NOT_FOUND) return false;
|
||||
if (result == ESP_OK && nvs_commit(handle) != ESP_OK) return false;
|
||||
uint16_t actual = 0;
|
||||
bool present = false;
|
||||
return read(actual, present) && !present;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
uint8_t TBeamBoard::getChargeTargetModel() const {
|
||||
if (!charge_pmu_ready) return 0;
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
return pmu_model;
|
||||
#else
|
||||
if (!PMU) return 0;
|
||||
if (PMU->getChipModel() == XPOWERS_AXP192) return mesh::tbeam_charge::AXP192;
|
||||
if (PMU->getChipModel() == XPOWERS_AXP2101) return mesh::tbeam_charge::AXP2101;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TBeamBoard::getBatteryChargeTarget(uint16_t& millivolts) {
|
||||
ChargePmuIO io;
|
||||
return mesh::tbeam_charge::read(io, getChargeTargetModel(), millivolts);
|
||||
}
|
||||
|
||||
bool TBeamBoard::supportsBatteryChargeTarget(uint16_t millivolts) {
|
||||
return mesh::tbeam_charge::supports(getChargeTargetModel(), millivolts);
|
||||
}
|
||||
|
||||
const char* TBeamBoard::getBatteryChargeTargetOptions() const {
|
||||
return mesh::tbeam_charge::options(getChargeTargetModel());
|
||||
}
|
||||
|
||||
bool TBeamBoard::setBatteryChargeTarget(uint16_t millivolts) {
|
||||
const uint8_t model = getChargeTargetModel();
|
||||
if (!mesh::tbeam_charge::supports(model, millivolts)) return false;
|
||||
ChargePmuIO io;
|
||||
ChargeTargetStore store(mesh::tbeam_charge::storeNamespace(model), false);
|
||||
const bool success = mesh::tbeam_charge::setPersistent(io, store, model, millivolts);
|
||||
if (success) charge_target_restore_failed = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
void TBeamBoard::restoreBatteryChargeTarget() {
|
||||
const uint8_t model = getChargeTargetModel();
|
||||
ChargePmuIO io;
|
||||
ChargeTargetStore store(mesh::tbeam_charge::storeNamespace(model), true);
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
const bool apply_fresh_default = false;
|
||||
#else
|
||||
const bool apply_fresh_default = true;
|
||||
#endif
|
||||
charge_target_restore_failed =
|
||||
!mesh::tbeam_charge::configureBoot(io, store, model, apply_fresh_default);
|
||||
}
|
||||
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
static bool pmuUpdate(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||
uint8_t value;
|
||||
return pmuRead(reg, value) && pmuWrite(reg, (value & ~clear_mask) | set_mask);
|
||||
@@ -163,7 +275,10 @@ static bool pmuUpdate(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||
|
||||
bool TBeamBoard::power_init() {
|
||||
PMU_WIRE_PORT.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
|
||||
pmu_model = 0;
|
||||
if (!pmuRead(0x03, pmu_model)) return false;
|
||||
if (pmu_model != mesh::tbeam_charge::AXP192 &&
|
||||
pmu_model != mesh::tbeam_charge::AXP2101) return false;
|
||||
|
||||
if (pmu_model == 0x4A) {
|
||||
// AXP2101: ALDO2 and ALDO3 at 3.3 V, plus battery voltage ADC.
|
||||
@@ -234,7 +349,6 @@ bool TBeamBoard::power_init()
|
||||
PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); //Disable PMU IRQ
|
||||
|
||||
PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); //Set battery charging current
|
||||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); //Set battery charge-stop voltage
|
||||
}
|
||||
else if(PMU->getChipModel() == XPOWERS_AXP2101){
|
||||
#ifdef TBEAM_SUPREME_SX1262
|
||||
@@ -315,7 +429,6 @@ bool TBeamBoard::power_init()
|
||||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); //Disable all PMU interrupts
|
||||
|
||||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); //Set battery charging current to 500mA
|
||||
PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); //Set battery charging cutoff voltage to 4.2V
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ enum {
|
||||
};
|
||||
|
||||
bool power_init();
|
||||
bool charge_pmu_ready = false;
|
||||
bool charge_target_restore_failed = false;
|
||||
uint8_t getChargeTargetModel() const;
|
||||
void restoreBatteryChargeTarget();
|
||||
//void radiotype_detect();
|
||||
|
||||
public:
|
||||
@@ -127,6 +131,13 @@ public:
|
||||
void scanDevices(TwoWire *w);
|
||||
#endif
|
||||
void begin();
|
||||
bool getBatteryChargeTarget(uint16_t& millivolts) override;
|
||||
bool supportsBatteryChargeTarget(uint16_t millivolts) override;
|
||||
bool setBatteryChargeTarget(uint16_t millivolts) override;
|
||||
const char* getBatteryChargeTargetOptions() const override;
|
||||
bool batteryChargeTargetRestoreFailed() const override {
|
||||
return charge_target_restore_failed;
|
||||
}
|
||||
|
||||
#ifndef TBEAM_SUPREME_SX1262
|
||||
void onBeforeTransmit() override{
|
||||
@@ -138,9 +149,9 @@ public:
|
||||
#endif
|
||||
|
||||
uint16_t getBattMilliVolts(){
|
||||
if (!charge_pmu_ready) return 0;
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
uint8_t high_reg = pmu_model == 0x4A ? 0x34 : 0x78;
|
||||
uint8_t low_reg = high_reg + 1;
|
||||
PMU_WIRE_PORT.beginTransmission(I2C_PMU_ADD);
|
||||
PMU_WIRE_PORT.write(high_reg);
|
||||
if (PMU_WIRE_PORT.endTransmission(false) != 0 ||
|
||||
@@ -150,7 +161,7 @@ public:
|
||||
if (pmu_model == 0x4A) return ((high & 0x1F) << 8) | low;
|
||||
return (uint16_t)((((high << 4) | (low & 0x0F)) * 11) / 10);
|
||||
#else
|
||||
return PMU->getBattVoltage();
|
||||
return PMU ? PMU->getBattVoltage() : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace tbeam_charge {
|
||||
|
||||
enum : uint8_t { AXP192 = 0x03, AXP2101 = 0x4A };
|
||||
|
||||
struct Setting {
|
||||
uint8_t reg;
|
||||
uint8_t mask;
|
||||
uint8_t bits;
|
||||
};
|
||||
|
||||
// These are the discrete hardware targets, not a voltage range. In particular,
|
||||
// neither PMU supports the 3.65 V termination voltage used by LiFePO4 cells.
|
||||
inline bool encode(uint8_t model, uint16_t millivolts, Setting& setting) {
|
||||
if (model == AXP192) {
|
||||
setting.reg = 0x33;
|
||||
setting.mask = 0x60;
|
||||
switch (millivolts) {
|
||||
case 4100: setting.bits = 0x00; return true;
|
||||
case 4150: setting.bits = 0x20; return true;
|
||||
case 4200: setting.bits = 0x40; return true;
|
||||
case 4360: setting.bits = 0x60; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
if (model == AXP2101) {
|
||||
setting.reg = 0x64;
|
||||
setting.mask = 0x07;
|
||||
switch (millivolts) {
|
||||
case 4000: setting.bits = 1; return true;
|
||||
case 4100: setting.bits = 2; return true;
|
||||
case 4200: setting.bits = 3; return true;
|
||||
case 4350: setting.bits = 4; return true;
|
||||
case 4400: setting.bits = 5; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool supports(uint8_t model, uint16_t millivolts) {
|
||||
Setting setting;
|
||||
return encode(model, millivolts, setting);
|
||||
}
|
||||
|
||||
inline const char* options(uint8_t model) {
|
||||
if (model == AXP192) return "4.10,4.15,4.20,4.36";
|
||||
if (model == AXP2101) return "4.00,4.10,4.20,4.35,4.40";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline const char* storeNamespace(uint8_t model) {
|
||||
if (model == AXP192) return "tbeam192_chg";
|
||||
if (model == AXP2101) return "tbeam2101_chg";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline bool decode(uint8_t model, uint8_t value, uint16_t& millivolts) {
|
||||
if (model == AXP192) {
|
||||
static const uint16_t targets[] = {4100, 4150, 4200, 4360};
|
||||
millivolts = targets[(value & 0x60) >> 5];
|
||||
return true;
|
||||
}
|
||||
if (model == AXP2101) {
|
||||
static const uint16_t targets[] = {4000, 4100, 4200, 4350, 4400};
|
||||
const uint8_t code = value & 0x07;
|
||||
if (code < 1 || code > 5) return false;
|
||||
millivolts = targets[code - 1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename IO>
|
||||
bool read(IO& io, uint8_t model, uint16_t& millivolts) {
|
||||
Setting setting;
|
||||
if (!encode(model, 4200, setting)) return false;
|
||||
uint8_t detected, value;
|
||||
if (!io.read(0x03, detected) || detected != model ||
|
||||
!io.read(setting.reg, value)) return false;
|
||||
return decode(model, value, millivolts);
|
||||
}
|
||||
|
||||
template<typename IO>
|
||||
bool write(IO& io, uint8_t model, uint16_t millivolts) {
|
||||
Setting setting;
|
||||
if (!encode(model, millivolts, setting)) return false;
|
||||
uint8_t detected, previous;
|
||||
if (!io.read(0x03, detected) || detected != model ||
|
||||
!io.read(setting.reg, previous)) return false;
|
||||
const uint8_t value = (previous & ~setting.mask) | setting.bits;
|
||||
if (value != previous && !io.write(setting.reg, value)) return false;
|
||||
uint8_t actual;
|
||||
return io.read(setting.reg, actual) && actual == value;
|
||||
}
|
||||
|
||||
// Store reads distinguish an absent key from an I/O failure; writes include
|
||||
// commit and readback. The caller must keep the store open for the transaction.
|
||||
template<typename IO, typename Store>
|
||||
bool setPersistent(IO& io, Store& store, uint8_t model, uint16_t millivolts) {
|
||||
if (!supports(model, millivolts)) return false;
|
||||
uint16_t previousHardware, previousStored = 0;
|
||||
bool existed = false;
|
||||
if (!read(io, model, previousHardware) ||
|
||||
!store.read(previousStored, existed)) return false;
|
||||
|
||||
if (!write(io, model, millivolts)) {
|
||||
// A failed readback may follow a successful physical write.
|
||||
write(io, model, previousHardware);
|
||||
return false;
|
||||
}
|
||||
if (existed && previousStored == millivolts) return true;
|
||||
if (store.write(millivolts)) return true;
|
||||
|
||||
// Restore persistence before restoring hardware, so a successful rollback
|
||||
// cannot leave the requested target to be silently reapplied on next boot.
|
||||
const bool restoredStore = existed ? store.write(previousStored) : store.erase();
|
||||
if (restoredStore) {
|
||||
write(io, model, previousHardware);
|
||||
} else {
|
||||
// A failed commit has an uncertain durable result. Match a readable saved
|
||||
// setting where possible; otherwise keep the last verified requested
|
||||
// hardware target and report failure, never pretend the state is unchanged.
|
||||
uint16_t stored = 0;
|
||||
bool present = false;
|
||||
if (store.read(stored, present)) {
|
||||
if (!present) write(io, model, previousHardware);
|
||||
else if (supports(model, stored)) write(io, model, stored);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename IO, typename Store>
|
||||
bool configureBoot(IO& io, Store& store, uint8_t model, bool apply_fresh_default) {
|
||||
uint16_t stored = 0;
|
||||
bool present = false;
|
||||
if (!store.read(stored, present)) return false;
|
||||
// A missing preference is the only condition that permits the existing full
|
||||
// driver's 4.2 V default. Unreadable/invalid saved settings must never raise
|
||||
// the charger target; preserve the PMU state and surface a boot failure.
|
||||
if (!present) return !apply_fresh_default || write(io, model, 4200);
|
||||
return supports(model, stored) && write(io, model, stored);
|
||||
}
|
||||
|
||||
} // namespace tbeam_charge
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the production charger CLI with hardware/storage failure fixtures."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class BatteryChargeCliTest(unittest.TestCase):
|
||||
def test_commands_failures_precision_and_bounded_replies(self):
|
||||
source = r'''
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
struct Board : mesh::MainBoard {
|
||||
bool capable = true, read_ok = true, save_ok = true, restore_failed = false;
|
||||
unsigned reads = 0, writes = 0;
|
||||
uint16_t target = 4200;
|
||||
uint16_t getBattMilliVolts() override { return 3700; }
|
||||
const char* getManufacturerName() const override { return "fixture"; }
|
||||
void reboot() override {}
|
||||
uint8_t getStartupReason() const override { return 0; }
|
||||
const char* getBatteryChargeTargetOptions() const override {
|
||||
return capable ? "4.10,4.15,4.20,4.36" : nullptr;
|
||||
}
|
||||
bool getBatteryChargeTarget(uint16_t& result) override {
|
||||
++reads;
|
||||
if (!read_ok) return false;
|
||||
result = target;
|
||||
return true;
|
||||
}
|
||||
bool batteryChargeTargetRestoreFailed() const override { return restore_failed; }
|
||||
bool supportsBatteryChargeTarget(uint16_t mv) override {
|
||||
return mv == 4100 || mv == 4150 || mv == 4200 || mv == 4360;
|
||||
}
|
||||
bool setBatteryChargeTarget(uint16_t mv) override {
|
||||
++writes;
|
||||
if (!save_ok) return false;
|
||||
target = mv;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
using mesh::power::handleBatteryChargeCommand;
|
||||
using mesh::power::parseChargeVolts;
|
||||
Board board;
|
||||
char reply[160];
|
||||
assert(handleBatteryChargeCommand(board, "get charge.voltage", reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> 4.200 V" && board.reads == 1);
|
||||
board.restore_failed = true;
|
||||
assert(handleBatteryChargeCommand(board, "get charge.voltage", reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> 4.200 V (boot restore failed)");
|
||||
board.restore_failed = false;
|
||||
assert(handleBatteryChargeCommand(board, "get charge.voltage.options", reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> 4.10,4.15,4.20,4.36 V");
|
||||
for (const char* value : {"4.1", "4.10", "4.100", "04.100", "4.10 \t"}) {
|
||||
std::string command = "set charge.voltage "; command += value;
|
||||
assert(handleBatteryChargeCommand(board, command.c_str(), reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "OK - charge.voltage 4.100 V (saved)");
|
||||
assert(board.target == 4100);
|
||||
}
|
||||
for (const char* value : {"", "4.", ".1", "4.1000", "4.1garbage", "+4.1", "-4.1",
|
||||
"nan", "inf", "4e0", "65536", "65.536", "999999999999999999999", "4 1"}) {
|
||||
const unsigned writes = board.writes;
|
||||
std::string command = "set charge.voltage "; command += value;
|
||||
assert(handleBatteryChargeCommand(board, command.c_str(), reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("Error: usage") == 0 && board.writes == writes);
|
||||
}
|
||||
for (const char* value : {"3.65", "3.650", "4", "4.099", "4.101", "0", "65.535"}) {
|
||||
const unsigned writes = board.writes;
|
||||
std::string command = "set charge.voltage "; command += value;
|
||||
assert(handleBatteryChargeCommand(board, command.c_str(), reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("Error: unsupported target") == 0);
|
||||
assert(board.writes == writes && board.target == 4100);
|
||||
}
|
||||
uint16_t parsed = 123;
|
||||
assert(parseChargeVolts("3.65", parsed) && parsed == 3650);
|
||||
assert(parseChargeVolts("65.535", parsed) && parsed == 65535);
|
||||
assert(!parseChargeVolts("65.536", parsed) && parsed == 65535);
|
||||
board.read_ok = false;
|
||||
assert(handleBatteryChargeCommand(board, "get charge.voltage", reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "Error: charger read failed");
|
||||
board.save_ok = false;
|
||||
assert(handleBatteryChargeCommand(board, "set charge.voltage 4.2", reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("Error: charge target not confirmed saved") == 0);
|
||||
assert(board.target == 4100);
|
||||
board.capable = false;
|
||||
unsigned writes = board.writes, reads = board.reads;
|
||||
assert(handleBatteryChargeCommand(board, "set charge.voltage 3.65", reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("not supported on this board") != std::string::npos);
|
||||
assert(board.writes == writes && board.reads == reads);
|
||||
board.capable = true;
|
||||
for (const char* command : {"get charge.voltage.bad", "set charge.voltage.options", "charge.voltage",
|
||||
"get battery.full", "set radio 910,62.5,7,5"}) {
|
||||
assert(!handleBatteryChargeCommand(board, command, reply, sizeof(reply)));
|
||||
}
|
||||
assert(handleBatteryChargeCommand(board, "get charge.voltage extra", reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("Error: usage") == 0);
|
||||
// Companion prefixes reserve three bytes; callers also use smaller buffers.
|
||||
for (size_t capacity = 1; capacity <= 157; ++capacity) {
|
||||
char bounded[164]; memset(bounded, '#', sizeof(bounded));
|
||||
memcpy(bounded, "ab|", 3);
|
||||
assert(handleBatteryChargeCommand(board, "set charge.voltage 3.65", bounded + 3, capacity));
|
||||
assert(memcmp(bounded, "ab|", 3) == 0);
|
||||
assert(bounded[3 + capacity] == '#' && memchr(bounded + 3, 0, capacity));
|
||||
}
|
||||
assert(!handleBatteryChargeCommand(board, nullptr, reply, sizeof(reply)));
|
||||
assert(!handleBatteryChargeCommand(board, "get charge.voltage", nullptr, sizeof(reply)));
|
||||
assert(!handleBatteryChargeCommand(board, "get charge.voltage", reply, 0));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp)
|
||||
(path / "test.cpp").write_text(source, encoding="ascii")
|
||||
binary = path / "test"
|
||||
subprocess.run(["c++", "-std=c++11", "-Wall", "-Wextra",
|
||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||
"-I", str(ROOT / "src"), str(path / "test.cpp"),
|
||||
"-o", str(binary)], check=True, capture_output=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
def test_shared_role_dispatch_and_meshsolar_hardware_limit(self):
|
||||
# Pin independent role entry points so a parser tested here cannot be
|
||||
# omitted from the Companion/BLE path while working on serial repeaters.
|
||||
for name in ("src/helpers/CommonCLI.cpp", "examples/companion_radio/MyMesh.cpp",
|
||||
"examples/simple_secure_chat/main.cpp"):
|
||||
source = (ROOT / name).read_text()
|
||||
self.assertIn("#include <helpers/BatteryChargeCLI.h>", source)
|
||||
self.assertIn("mesh::power::handleBatteryChargeCommand(", source)
|
||||
source = r'''
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include "MeshSolarBoard.h"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
int main() {
|
||||
MeshSolarBoard board;
|
||||
char reply[160];
|
||||
assert(!board.supportsBatteryChargeTarget(3650));
|
||||
assert(!board.setBatteryChargeTarget(3650));
|
||||
for (const char* cmd : {"get charge.voltage", "get charge.voltage.options", "set charge.voltage 3.65"}) {
|
||||
assert(mesh::power::handleBatteryChargeCommand(board, cmd, reply, sizeof(reply)));
|
||||
assert(std::string(reply).find("CN3795") != std::string::npos);
|
||||
assert(std::string(reply).find("hardware") != std::string::npos);
|
||||
}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp)
|
||||
(path / "helpers").mkdir()
|
||||
(path / "Arduino.h").write_text(
|
||||
"#pragma once\n#include <stdint.h>\ninline uint16_t meshSolarGetBattVoltage() {return 7400;}\n",
|
||||
encoding="ascii")
|
||||
(path / "helpers/NRF52Board.h").write_text(r'''
|
||||
#pragma once
|
||||
#include <MeshCore.h>
|
||||
class NRF52Board : public mesh::MainBoard {
|
||||
public:
|
||||
explicit NRF52Board(const char*) {}
|
||||
void reboot() override {}
|
||||
uint8_t getStartupReason() const override { return 0; }
|
||||
};
|
||||
using NRF52BoardDCDC = NRF52Board;
|
||||
''', encoding="ascii")
|
||||
(path / "test.cpp").write_text(source, encoding="ascii")
|
||||
binary = path / "test"
|
||||
subprocess.run(["c++", "-std=c++11", "-I", temp, "-I", str(ROOT / "src"),
|
||||
"-I", str(ROOT / "variants/heltec_mesh_solar"),
|
||||
str(path / "test.cpp"), "-o", str(binary)],
|
||||
check=True, capture_output=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -18,6 +18,7 @@ PREAMBLE = r'''
|
||||
#include <initializer_list>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/RadioProfileCommandUtils.h>
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#define ESP32 1
|
||||
#define WIFI_SSID "test"
|
||||
#define WITH_MQTT_BRIDGE 1
|
||||
@@ -84,6 +85,14 @@ struct Prefs {
|
||||
};
|
||||
struct Board {
|
||||
int power_offs=0;
|
||||
uint16_t charge_target=4200;
|
||||
unsigned charge_writes=0;
|
||||
const char* getBatteryChargeTargetOptions() const {return "4.10,4.20";}
|
||||
bool batteryChargeTargetRestoreFailed() const {return false;}
|
||||
const char* getBatteryChargeTargetUnsupportedReason() const {return "unsupported";}
|
||||
bool getBatteryChargeTarget(uint16_t& mv){mv=charge_target;return true;}
|
||||
bool supportsBatteryChargeTarget(uint16_t mv){return mv==4100 || mv==4200;}
|
||||
bool setBatteryChargeTarget(uint16_t mv){++charge_writes;charge_target=mv;return true;}
|
||||
bool rebootToUf2Bootloader(){return false;}
|
||||
bool handleCommand(const char*,uint32_t,char*){return false;}
|
||||
void powerOff(){++power_offs;}
|
||||
@@ -153,6 +162,17 @@ SCENARIOS = r'''
|
||||
int main() {
|
||||
MyMesh node;
|
||||
char reply[160] = {};
|
||||
assert(node.handleCommand("set charge.voltage 4.1",0,reply));
|
||||
assert(board.charge_target==4100 && strstr(reply,"saved"));
|
||||
for(uint32_t stamp:{0u,100u}) {
|
||||
node.onCLICommandRecv(ContactInfo{},nullptr,stamp,"A7|set charge.voltage 4.2",reply);
|
||||
assert(board.charge_target==4200 && !strcmp(reply,"A7|OK - charge.voltage 4.200 V (saved)"));
|
||||
const unsigned writes=board.charge_writes;
|
||||
node.onCLICommandRecv(ContactInfo{},nullptr,stamp,"A7|set charge.voltage 3.65",reply);
|
||||
assert(board.charge_writes==writes && !strncmp(reply,"A7|Error: unsupported target",27));
|
||||
node.onCLICommandRecv(ContactInfo{false},nullptr,stamp,"set charge.voltage 4.1",reply);
|
||||
assert(board.charge_writes==writes && board.charge_target==4200);
|
||||
}
|
||||
const char* local[] = {"get password", "get prv.key", "get wifi.pwd", "get mqtt1.password",
|
||||
"get mqtt1.token", "stats-core", "stats-radio", "stats-radio-diag", "stats-packets"};
|
||||
for(const char* command:local) {
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the real T-Beam charge driver against fault-injected I2C and NVS."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HELPER_TEST = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "helpers/esp32/TBeamChargeTarget.h"
|
||||
namespace charge = mesh::tbeam_charge;
|
||||
struct IO {
|
||||
uint8_t reg[256] = {};
|
||||
unsigned reads = 0, writes = 0, failRead = 0, failWrite = 0;
|
||||
unsigned corruptRead = 0;
|
||||
bool read(uint8_t address, uint8_t& value) {
|
||||
if (++reads == failRead) return false;
|
||||
value = reg[address];
|
||||
if (reads == corruptRead) value ^= 1;
|
||||
return true;
|
||||
}
|
||||
bool write(uint8_t address, uint8_t value) {
|
||||
if (++writes == failWrite) return false;
|
||||
reg[address] = value;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
struct Store {
|
||||
uint16_t value = 0;
|
||||
bool present = false;
|
||||
unsigned reads = 0, writes = 0, erases = 0;
|
||||
unsigned failRead = 0, failWrite = 0, failErase = 0;
|
||||
bool failedWriteCommits = false;
|
||||
bool read(uint16_t& out, bool& exists) {
|
||||
if (++reads == failRead) return false;
|
||||
out = value;
|
||||
exists = present;
|
||||
return true;
|
||||
}
|
||||
bool write(uint16_t requested) {
|
||||
const bool success = ++writes != failWrite;
|
||||
if (success || failedWriteCommits) { value = requested; present = true; }
|
||||
return success;
|
||||
}
|
||||
bool erase() {
|
||||
if (++erases == failErase) return false;
|
||||
present = false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
static IO initialized(uint8_t model) {
|
||||
IO io;
|
||||
io.reg[0x03] = model;
|
||||
io.reg[0x33] = 0xD9; // Charging enabled, 4.20 V, unrelated current/end bits.
|
||||
io.reg[0x64] = 0xBB; // 4.20 V, unrelated reserved/configuration bits.
|
||||
return io;
|
||||
}
|
||||
int main() {
|
||||
const uint16_t axp192[] = {4100,4150,4200,4360};
|
||||
const uint16_t axp2101[] = {4000,4100,4200,4350,4400};
|
||||
for (uint8_t model : {charge::AXP192, charge::AXP2101}) {
|
||||
const uint16_t* targets = model == charge::AXP192 ? axp192 : axp2101;
|
||||
const unsigned count = model == charge::AXP192 ? 4 : 5;
|
||||
for (unsigned index = 0; index < count; ++index) {
|
||||
IO io = initialized(model);
|
||||
const uint8_t address = model == charge::AXP192 ? 0x33 : 0x64;
|
||||
const uint8_t mask = model == charge::AXP192 ? 0x60 : 0x07;
|
||||
const uint8_t unrelated = io.reg[address] & ~mask;
|
||||
assert(charge::write(io, model, targets[index]));
|
||||
assert((io.reg[address] & ~mask) == unrelated);
|
||||
assert((io.reg[address] & mask) ==
|
||||
(model == charge::AXP192 ? index << 5 : index + 1));
|
||||
uint16_t actual = 7;
|
||||
assert(charge::read(io, model, actual) && actual == targets[index]);
|
||||
}
|
||||
for (uint16_t invalid : {uint16_t(0),uint16_t(3650),uint16_t(4099),
|
||||
uint16_t(4199),uint16_t(4201),uint16_t(65535)}) {
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
assert(!charge::setPersistent(io, store, model, invalid));
|
||||
assert(io.reads == 0 && io.writes == 0 && store.reads == 0);
|
||||
}
|
||||
const uint16_t target = targets[0];
|
||||
for (unsigned fail = 1; fail <= 5; ++fail) {
|
||||
IO io = initialized(model);
|
||||
io.failRead = fail;
|
||||
Store store;
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
assert(!store.present && store.writes == 0);
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == 4200);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
io.failWrite = 1;
|
||||
Store store;
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
assert(!store.present && store.writes == 0);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
io.corruptRead = 5; // Setter's register readback, after physical write.
|
||||
Store store;
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == 4200);
|
||||
assert(!store.present && store.writes == 0);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
store.failRead = 1;
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
assert(io.writes == 0 && store.writes == 0);
|
||||
}
|
||||
for (bool existed : {false, true}) {
|
||||
for (bool failedCommit : {false, true}) {
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
store.present = existed;
|
||||
store.value = 4200;
|
||||
store.failWrite = 1;
|
||||
store.failedWriteCommits = failedCommit;
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
assert(store.present == existed && (!existed || store.value == 4200));
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == 4200);
|
||||
}
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
store.failWrite = 1;
|
||||
store.failedWriteCommits = true;
|
||||
store.failErase = 1; // Persistent rollback fails after a partial commit.
|
||||
assert(!charge::setPersistent(io, store, model, target));
|
||||
assert(store.present && store.value == target);
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == target);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
assert(charge::setPersistent(io, store, model, target));
|
||||
assert(store.present && store.value == target && store.writes == 1);
|
||||
const unsigned writes = io.writes;
|
||||
assert(charge::setPersistent(io, store, model, target));
|
||||
assert(store.writes == 1 && io.writes == writes);
|
||||
IO rebooted = initialized(model);
|
||||
assert(charge::configureBoot(rebooted, store, model, false));
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(rebooted, model, actual) && actual == target);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
assert(charge::configureBoot(io, store, model, false));
|
||||
store.present = true;
|
||||
store.value = 3650;
|
||||
assert(!charge::configureBoot(io, store, model, true));
|
||||
assert(io.reads == 0 && io.writes == 0);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
assert(charge::write(io, model, targets[0]));
|
||||
store.failRead = 1;
|
||||
const unsigned previousWrites = io.writes;
|
||||
assert(!charge::configureBoot(io, store, model, true));
|
||||
assert(io.writes == previousWrites);
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == targets[0]);
|
||||
}
|
||||
{
|
||||
IO io = initialized(model);
|
||||
Store store;
|
||||
assert(charge::write(io, model, targets[0]));
|
||||
const unsigned previousWrites = io.writes;
|
||||
assert(charge::configureBoot(io, store, model, false));
|
||||
assert(io.writes == previousWrites);
|
||||
assert(charge::configureBoot(io, store, model, true));
|
||||
uint16_t actual = 0;
|
||||
assert(charge::read(io, model, actual) && actual == 4200);
|
||||
}
|
||||
}
|
||||
assert(std::strcmp(charge::storeNamespace(charge::AXP192),
|
||||
charge::storeNamespace(charge::AXP2101)) != 0);
|
||||
for (uint8_t code : {uint8_t(0),uint8_t(6),uint8_t(7)}) {
|
||||
IO io = initialized(charge::AXP2101);
|
||||
io.reg[0x64] = code;
|
||||
uint16_t target = 17;
|
||||
assert(!charge::read(io, charge::AXP2101, target) && target == 17);
|
||||
}
|
||||
{
|
||||
IO io = initialized(0xFF);
|
||||
Store store;
|
||||
assert(!charge::setPersistent(io, store, 0xFF, 4200));
|
||||
assert(io.reads == 0 && io.writes == 0 && store.reads == 0);
|
||||
assert(charge::options(0xFF) == nullptr);
|
||||
}
|
||||
{
|
||||
IO io = initialized(charge::AXP2101);
|
||||
uint16_t target = 17;
|
||||
assert(!charge::read(io, charge::AXP192, target) && target == 17);
|
||||
assert(!charge::write(io, charge::AXP192, 4100) && io.writes == 0);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
MOCK_SDK = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
constexpr int LOW = 0, HIGH = 1, INPUT = 2, INPUT_PULLUP = 3, FALLING = 4;
|
||||
constexpr int ESP_RST_DEEPSLEEP = 5, ESP_SLEEP_WAKEUP_UNDEFINED = 0;
|
||||
constexpr int BD_STARTUP_RX_PACKET = 1;
|
||||
using esp_reset_reason_t = int;
|
||||
using gpio_num_t = int;
|
||||
extern int reset_reason;
|
||||
extern uint64_t wakeup_mask;
|
||||
inline int esp_reset_reason() { return reset_reason; }
|
||||
inline int esp_sleep_get_wakeup_cause() { return 0; }
|
||||
inline uint64_t esp_sleep_get_ext1_wakeup_status() { return wakeup_mask; }
|
||||
inline void rtc_gpio_hold_dis(int) {}
|
||||
inline void rtc_gpio_deinit(int) {}
|
||||
inline void pinMode(int, int) {}
|
||||
inline void digitalWrite(int, int) {}
|
||||
inline void attachInterrupt(int, void(*)(), int) {}
|
||||
inline void delay(int) {}
|
||||
#define _BV(x) (1U << (x))
|
||||
#define MESH_DEBUG_PRINTLN(...) do {} while (0)
|
||||
struct TwoWire {
|
||||
uint8_t reg[256] = {};
|
||||
std::vector<uint8_t> tx;
|
||||
uint8_t address = 0;
|
||||
unsigned writes = 0;
|
||||
bool negative_read_once = false;
|
||||
bool target_write_fails_once = false;
|
||||
void begin(int, int) {}
|
||||
void beginTransmission(int) { tx.clear(); }
|
||||
void write(uint8_t value) { tx.push_back(value); }
|
||||
int endTransmission(bool = true) {
|
||||
if (tx.size() == 1) address = tx[0];
|
||||
if (tx.size() == 2) {
|
||||
if (target_write_fails_once && (tx[0] == 0x33 || tx[0] == 0x64)) {
|
||||
target_write_fails_once = false;
|
||||
return 1;
|
||||
}
|
||||
reg[tx[0]] = tx[1];
|
||||
++writes;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int requestFrom(int, int count) { return count; }
|
||||
int read() {
|
||||
if (negative_read_once) { negative_read_once = false; return -1; }
|
||||
return reg[address++];
|
||||
}
|
||||
};
|
||||
extern TwoWire Wire, Wire1;
|
||||
class ESP32Board {
|
||||
public:
|
||||
int startup_reason = 0;
|
||||
virtual ~ESP32Board() {}
|
||||
void begin() {}
|
||||
virtual void onBeforeTransmit() {}
|
||||
virtual void onAfterTransmit() {}
|
||||
virtual bool getBatteryChargeTarget(uint16_t&) { return false; }
|
||||
virtual bool supportsBatteryChargeTarget(uint16_t) { return false; }
|
||||
virtual bool setBatteryChargeTarget(uint16_t) { return false; }
|
||||
virtual const char* getBatteryChargeTargetOptions() const { return nullptr; }
|
||||
virtual bool batteryChargeTargetRestoreFailed() const { return false; }
|
||||
};
|
||||
constexpr int XPOWERS_AXP192 = 192, XPOWERS_AXP2101 = 2101;
|
||||
constexpr int XPOWERS_LDO2 = 0, XPOWERS_LDO3 = 0, XPOWERS_DCDC1 = 0;
|
||||
constexpr int XPOWERS_DCDC2 = 0, XPOWERS_DCDC3 = 0, XPOWERS_DCDC4 = 0;
|
||||
constexpr int XPOWERS_DCDC5 = 0, XPOWERS_ALDO1 = 0, XPOWERS_ALDO2 = 0;
|
||||
constexpr int XPOWERS_ALDO3 = 0, XPOWERS_ALDO4 = 0, XPOWERS_BLDO1 = 0;
|
||||
constexpr int XPOWERS_BLDO2 = 0, XPOWERS_VBACKUP = 0, XPOWERS_CHG_LED_CTRL_CHG = 0;
|
||||
constexpr int XPOWERS_DLDO1 = 0, XPOWERS_DLDO2 = 0;
|
||||
constexpr int XPOWERS_AXP2101_DCDC4_VOL2_MAX = 0, XPOWERS_AXP192_ALL_IRQ = 0;
|
||||
constexpr int XPOWERS_AXP2101_ALL_IRQ = 0, XPOWERS_AXP192_CHG_CUR_450MA = 0;
|
||||
constexpr int XPOWERS_AXP2101_CHG_CUR_500MA = 0, XPOWERS_POWEROFF_4S = 0;
|
||||
constexpr int XPOWERS_AXP192_CHG_VOL_4V2 = 2, XPOWERS_AXP2101_CHG_VOL_4V2 = 3;
|
||||
class XPowersLibInterface {
|
||||
TwoWire& bus;
|
||||
int model;
|
||||
public:
|
||||
XPowersLibInterface(TwoWire& wire, int type) : bus(wire), model(type) {}
|
||||
virtual ~XPowersLibInterface() {}
|
||||
bool init() { return bus.reg[0x03] == (model == XPOWERS_AXP192 ? 0x03 : 0x4A); }
|
||||
int getChipModel() const { return model; }
|
||||
uint16_t getBattVoltage() { return 4200; }
|
||||
void setChargingLedMode(int) {}
|
||||
void setPowerChannelVoltage(int, int) {}
|
||||
void enablePowerOutput(int) {}
|
||||
void disablePowerOutput(int) {}
|
||||
void setProtectedChannel(int) {}
|
||||
void disableIRQ(int) {}
|
||||
void setChargerConstantCurr(int) {}
|
||||
void setChargeTargetVoltage(uint8_t code) {
|
||||
if (model == XPOWERS_AXP192) bus.reg[0x33] = (bus.reg[0x33] & 0x9F) | (code << 5);
|
||||
else bus.reg[0x64] = (bus.reg[0x64] & 0xF8) | code;
|
||||
}
|
||||
void clearIrqStatus() {}
|
||||
void disableTSPinMeasure() {}
|
||||
void enableSystemVoltageMeasure() {}
|
||||
void enableVbusVoltageMeasure() {}
|
||||
void enableBattVoltageMeasure() {}
|
||||
void setPowerKeyPressOffTime(int) {}
|
||||
};
|
||||
struct XPowersAXP192 : XPowersLibInterface {
|
||||
XPowersAXP192(TwoWire& bus, int, int, int) : XPowersLibInterface(bus, XPOWERS_AXP192) {}
|
||||
};
|
||||
struct XPowersAXP2101 : XPowersLibInterface {
|
||||
XPowersAXP2101(TwoWire& bus, int, int, int) : XPowersLibInterface(bus, XPOWERS_AXP2101) {}
|
||||
};
|
||||
using esp_err_t = int;
|
||||
using nvs_handle_t = uint32_t;
|
||||
constexpr int ESP_OK = 0, ESP_FAIL = 1, ESP_ERR_NVS_NOT_FOUND = 2;
|
||||
constexpr int NVS_READONLY = 0, NVS_READWRITE = 1;
|
||||
extern std::map<std::string,uint16_t> saved;
|
||||
extern std::string opened_namespace;
|
||||
extern bool nvs_open_fails, nvs_commit_fails_once;
|
||||
extern bool nvs_set_fails_once, nvs_erase_fails_once;
|
||||
extern unsigned nvs_reads, nvs_fail_read_at;
|
||||
inline int nvs_open(const char* name, int mode, nvs_handle_t* handle) {
|
||||
if (nvs_open_fails) return ESP_FAIL;
|
||||
if (mode == NVS_READONLY && saved.find(name) == saved.end()) return ESP_ERR_NVS_NOT_FOUND;
|
||||
opened_namespace = name;
|
||||
*handle = 1;
|
||||
return ESP_OK;
|
||||
}
|
||||
inline void nvs_close(nvs_handle_t) {}
|
||||
inline int nvs_get_u16(nvs_handle_t, const char*, uint16_t* value) {
|
||||
if (++nvs_reads == nvs_fail_read_at) return ESP_FAIL;
|
||||
auto found = saved.find(opened_namespace);
|
||||
if (found == saved.end()) return ESP_ERR_NVS_NOT_FOUND;
|
||||
*value = found->second;
|
||||
return ESP_OK;
|
||||
}
|
||||
inline int nvs_set_u16(nvs_handle_t, const char*, uint16_t value) {
|
||||
if (nvs_set_fails_once) { nvs_set_fails_once = false; return ESP_FAIL; }
|
||||
saved[opened_namespace] = value;
|
||||
return ESP_OK;
|
||||
}
|
||||
inline int nvs_commit(nvs_handle_t) {
|
||||
if (nvs_commit_fails_once) { nvs_commit_fails_once = false; return ESP_FAIL; }
|
||||
return ESP_OK;
|
||||
}
|
||||
inline int nvs_erase_key(nvs_handle_t, const char*) {
|
||||
if (nvs_erase_fails_once) { nvs_erase_fails_once = false; return ESP_FAIL; }
|
||||
return saved.erase(opened_namespace) ? ESP_OK : ESP_ERR_NVS_NOT_FOUND;
|
||||
}
|
||||
'''
|
||||
|
||||
BOARD_TEST = r'''
|
||||
#include "helpers/esp32/TBeamBoard.h"
|
||||
#include <cstring>
|
||||
TwoWire Wire, Wire1;
|
||||
int reset_reason = 0;
|
||||
uint64_t wakeup_mask = 0;
|
||||
std::map<std::string,uint16_t> saved;
|
||||
std::string opened_namespace;
|
||||
bool nvs_open_fails = false, nvs_commit_fails_once = false;
|
||||
bool nvs_set_fails_once = false, nvs_erase_fails_once = false;
|
||||
unsigned nvs_reads = 0, nvs_fail_read_at = 0;
|
||||
#ifdef TBEAM_SUPREME_SX1262
|
||||
#define BUS Wire1
|
||||
#else
|
||||
#define BUS Wire
|
||||
#endif
|
||||
int main() {
|
||||
for (uint8_t model : {uint8_t(0x03),uint8_t(0x4A)}) {
|
||||
saved.clear();
|
||||
BUS.reg[0x03] = model;
|
||||
BUS.reg[0x33] = 0x99;
|
||||
BUS.reg[0x64] = 0xBA;
|
||||
const char* own_namespace = model == 0x03 ? "tbeam192_chg" : "tbeam2101_chg";
|
||||
const char* other_namespace = model == 0x03 ? "tbeam2101_chg" : "tbeam192_chg";
|
||||
TBeamBoard board;
|
||||
assert(board.getBattMilliVolts() == 0);
|
||||
board.begin();
|
||||
uint16_t actual = 0;
|
||||
assert(board.getBatteryChargeTarget(actual));
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
assert(actual == 4100); // Fresh compact boot leaves charger target alone.
|
||||
#else
|
||||
assert(actual == 4200); // Existing full-library default is preserved.
|
||||
#endif
|
||||
assert(saved.empty());
|
||||
assert(!board.batteryChargeTargetRestoreFailed());
|
||||
const unsigned previous_writes = BUS.writes;
|
||||
assert(!board.setBatteryChargeTarget(3650));
|
||||
assert(BUS.writes == previous_writes && saved.empty());
|
||||
assert(board.supportsBatteryChargeTarget(4100));
|
||||
assert(!board.supportsBatteryChargeTarget(3650));
|
||||
assert(std::strcmp(board.getBatteryChargeTargetOptions(), model == 0x03 ?
|
||||
"4.10,4.15,4.20,4.36" : "4.00,4.10,4.20,4.35,4.40") == 0);
|
||||
assert(board.setBatteryChargeTarget(4100));
|
||||
assert(saved.at(own_namespace) == 4100);
|
||||
assert(saved.find(other_namespace) == saved.end());
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
reset_reason = ESP_RST_DEEPSLEEP;
|
||||
wakeup_mask = 1ULL << P_LORA_DIO_1;
|
||||
TBeamBoard radio_wake;
|
||||
radio_wake.begin();
|
||||
assert(radio_wake.startup_reason == BD_STARTUP_RX_PACKET);
|
||||
reset_reason = 0;
|
||||
wakeup_mask = 0;
|
||||
nvs_commit_fails_once = true;
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
assert(saved.at(own_namespace) == 4100);
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
const unsigned saved_writes = BUS.writes;
|
||||
nvs_fail_read_at = nvs_reads + 1; // Initial saved-state snapshot fails.
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
assert(BUS.writes == saved_writes && saved.at(own_namespace) == 4100);
|
||||
nvs_fail_read_at = nvs_reads + 2; // Save commits, but verification read fails.
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
assert(saved.at(own_namespace) == 4100);
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
nvs_fail_read_at = 0;
|
||||
nvs_set_fails_once = true;
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
assert(saved.at(own_namespace) == 4100);
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
BUS.negative_read_once = true;
|
||||
actual = 17;
|
||||
assert(!board.getBatteryChargeTarget(actual) && actual == 17);
|
||||
saved.erase(own_namespace);
|
||||
nvs_commit_fails_once = true;
|
||||
nvs_erase_fails_once = true; // Save and rollback both have uncertain results.
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
assert(saved.at(own_namespace) == 4200);
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4200);
|
||||
assert(board.setBatteryChargeTarget(4100));
|
||||
nvs_open_fails = true;
|
||||
assert(!board.setBatteryChargeTarget(4200));
|
||||
nvs_open_fails = false;
|
||||
assert(board.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
TBeamBoard rebooted;
|
||||
rebooted.begin();
|
||||
assert(rebooted.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
assert(!rebooted.batteryChargeTargetRestoreFailed());
|
||||
saved[own_namespace] = 3650;
|
||||
BUS.reg[0x33] = 0x99;
|
||||
BUS.reg[0x64] = 0xBA;
|
||||
TBeamBoard invalid_saved;
|
||||
invalid_saved.begin();
|
||||
assert(invalid_saved.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
assert(invalid_saved.batteryChargeTargetRestoreFailed());
|
||||
assert(!invalid_saved.setBatteryChargeTarget(3650));
|
||||
assert(invalid_saved.batteryChargeTargetRestoreFailed());
|
||||
assert(invalid_saved.setBatteryChargeTarget(4100));
|
||||
assert(!invalid_saved.batteryChargeTargetRestoreFailed());
|
||||
saved[own_namespace] = 4100;
|
||||
nvs_open_fails = true;
|
||||
TBeamBoard unreadable_namespace;
|
||||
unreadable_namespace.begin();
|
||||
nvs_open_fails = false;
|
||||
assert(unreadable_namespace.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
assert(unreadable_namespace.batteryChargeTargetRestoreFailed());
|
||||
nvs_fail_read_at = nvs_reads + 1;
|
||||
TBeamBoard unreadable_entry;
|
||||
unreadable_entry.begin();
|
||||
nvs_fail_read_at = 0;
|
||||
assert(unreadable_entry.getBatteryChargeTarget(actual) && actual == 4100);
|
||||
assert(unreadable_entry.batteryChargeTargetRestoreFailed());
|
||||
// A validated lower target fails to write; preserve the existing lower
|
||||
// PMU target, never invoke the historical full-library 4.20 V setter.
|
||||
saved[own_namespace] = model == 0x03 ? 4100 : 4000;
|
||||
BUS.reg[0x33] = 0xB9; // 4.15 V.
|
||||
BUS.reg[0x64] = 0xBA; // 4.10 V.
|
||||
BUS.target_write_fails_once = true;
|
||||
TBeamBoard failed_restore;
|
||||
failed_restore.begin();
|
||||
assert(!BUS.target_write_fails_once);
|
||||
assert(failed_restore.getBatteryChargeTarget(actual));
|
||||
assert(actual == (model == 0x03 ? 4150 : 4100));
|
||||
assert(failed_restore.batteryChargeTargetRestoreFailed());
|
||||
assert(failed_restore.setBatteryChargeTarget(4100));
|
||||
assert(!failed_restore.batteryChargeTargetRestoreFailed());
|
||||
saved.erase(own_namespace);
|
||||
saved[other_namespace] = 4150;
|
||||
TBeamBoard wrong_model_saved;
|
||||
wrong_model_saved.begin();
|
||||
assert(wrong_model_saved.getBatteryChargeTarget(actual));
|
||||
#if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262)
|
||||
assert(actual == 4100);
|
||||
#else
|
||||
assert(actual == 4200);
|
||||
#endif
|
||||
assert(!wrong_model_saved.batteryChargeTargetRestoreFailed());
|
||||
}
|
||||
saved.clear();
|
||||
BUS.reg[0x03] = 0xFF;
|
||||
const unsigned writes = BUS.writes;
|
||||
TBeamBoard unknown;
|
||||
unknown.begin();
|
||||
assert(unknown.getBattMilliVolts() == 0);
|
||||
uint16_t actual = 17;
|
||||
assert(!unknown.getBatteryChargeTarget(actual) && actual == 17);
|
||||
assert(!unknown.supportsBatteryChargeTarget(4200));
|
||||
assert(!unknown.setBatteryChargeTarget(4200));
|
||||
assert(unknown.getBatteryChargeTargetOptions() == nullptr);
|
||||
assert(BUS.writes == writes && saved.empty());
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class TBeamChargeTargetTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.compiler = shutil.which("g++")
|
||||
if not self.compiler:
|
||||
self.skipTest("g++ is required")
|
||||
|
||||
def build_and_run(self, directory, source, extra=()):
|
||||
harness = directory / "harness.cpp"
|
||||
harness.write_text(source, encoding="ascii")
|
||||
binary = directory / "harness"
|
||||
command = [self.compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
||||
"-I", str(directory), "-I", str(ROOT / "src"), str(harness),
|
||||
*extra, "-o", str(binary)]
|
||||
built = subprocess.run(command, capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stderr)
|
||||
|
||||
def test_discrete_targets_and_persistent_transaction_faults(self):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-tbeam-charge-") as temporary:
|
||||
self.build_and_run(Path(temporary), "#include <initializer_list>\n" + HELPER_TEST)
|
||||
|
||||
def test_real_driver_full_compact_and_supreme_paths(self):
|
||||
for board, compact in (("TBEAM_SX1262", False), ("TBEAM_SX1276", False),
|
||||
("TBEAM_SUPREME_SX1262", False),
|
||||
("TBEAM_SX1262", True), ("TBEAM_SX1276", True)):
|
||||
with self.subTest(board=board, compact=compact):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-tbeam-driver-") as temporary:
|
||||
directory = Path(temporary)
|
||||
(directory / "MockSdk.h").write_text(MOCK_SDK, encoding="ascii")
|
||||
for name in ("Arduino.h", "Wire.h", "XPowersLib.h", "nvs.h",
|
||||
"helpers/ESP32Board.h"):
|
||||
stub = directory / name
|
||||
stub.parent.mkdir(parents=True, exist_ok=True)
|
||||
stub.write_text('#include "MockSdk.h"\n', encoding="ascii")
|
||||
definitions = ["-D" + board, "-DPIN_BOARD_SDA=21", "-DPIN_BOARD_SCL=22",
|
||||
"-DPIN_USER_BTN=38", "-DP_LORA_TX_LED=4",
|
||||
"-DP_LORA_DIO_1=33", "-DP_LORA_NSS=18"]
|
||||
if board == "TBEAM_SUPREME_SX1262":
|
||||
definitions = definitions[:-2] # The real header supplies these pins.
|
||||
if compact:
|
||||
definitions.append("-DPORTABLE_MQTT_OBSERVER")
|
||||
self.build_and_run(directory, BOARD_TEST, [*definitions,
|
||||
str(ROOT / "src/helpers/esp32/TBeamBoard.cpp")])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -29,6 +29,12 @@ public:
|
||||
return meshSolarGetBattVoltage();
|
||||
}
|
||||
|
||||
const char* getBatteryChargeTargetUnsupportedReason() const override {
|
||||
// The I/F hardware versions use different CN3795 charger voltages. BQ4050
|
||||
// charge-algorithm requests and protection thresholds do not set that CV.
|
||||
return "Mesh Solar CN3795 charge voltage is hardware-set; BQ4050 settings do not change charger voltage";
|
||||
}
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "Heltec Mesh Solar";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user