Files
MeshCore/test/test_heltec_r8_external_power.py
T

214 lines
7.1 KiB
Python

"""Run the real R8 board's power detector with simulated USB, ADC and time."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
ARDUINO = r'''
#pragma once
#include <cstdint>
#define PIN_VEXT_EN 40
#define PIN_VEXT_EN_ACTIVE 0
#define PIN_VBAT_READ 1
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_TX_LED 46
#define P_LORA_PA_POWER 7
#define HIGH 1
#define LOW 0
#define BD_STARTUP_RX_PACKET 1
inline uint32_t test_now = 0;
inline uint32_t millis() { return test_now; }
inline void digitalWrite(int, int) {}
inline void analogReadResolution(int) {}
inline uint32_t analogReadMilliVolts(int) { return 830; }
'''
BASE_BOARD = r'''
#pragma once
#include <Arduino.h>
class ESP32Board {
protected:
uint8_t startup_reason = 0;
public:
void begin() {}
virtual ~ESP32Board() = default;
virtual void onBeforeTransmit() {}
virtual void onAfterTransmit() {}
virtual void powerOff() {}
virtual void shutdownPeripherals() {}
virtual bool setLoRaFemLnaEnabled(bool) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
virtual uint16_t getBattMilliVolts() { return 0; }
virtual bool isExternalPowered() { return false; }
virtual bool isUsbHostConnected() { return false; }
virtual bool setAdcMultiplier(float) { return false; }
virtual float getAdcMultiplier() const { return 0; }
virtual const char* getManufacturerName() const { return "test"; }
};
'''
TEST = r'''
#include <cassert>
#include "HeltecV4R8Board.h"
#include <helpers/ui/DisplayPowerPolicy.h>
void LoRaFEMControl::init() {}
void LoRaFEMControl::setSleepModeEnable() {}
void LoRaFEMControl::setTxModeEnable() {}
void LoRaFEMControl::setRxModeEnable() {}
void LoRaFEMControl::setLNAEnable(bool enabled) { lna_enabled = enabled; }
class TestBoard : public HeltecV4R8Board {
public:
bool host = false;
uint16_t voltage = 3700;
unsigned reads = 0;
bool isUsbHostConnected() override { return host; }
uint16_t getBattMilliVolts() override { ++reads; return voltage; }
};
int main() {
TestBoard board;
assert(!board.isExternalPowered());
assert(board.reads == 1); // Sample immediately, including at millis() == 0.
board.voltage = 4230;
test_now = 4999;
assert(!board.isExternalPowered());
assert(board.reads == 1); // Frequent UI polling must not repeatedly use ADC.
test_now = 5000;
assert(board.isExternalPowered());
assert(board.reads == 2);
board.voltage = 4205;
test_now = 10000;
assert(board.isExternalPowered()); // Noise inside the margin retains state.
board.voltage = 4200;
test_now = 15000;
assert(!board.isExternalPowered());
board.voltage = 4210;
test_now = 20000;
assert(!board.isExternalPowered()); // Exactly 4.21 V is not above threshold.
board.voltage = 4211;
test_now = 25000;
assert(board.isExternalPowered());
board.voltage = 0;
test_now = 30000;
assert(!board.isExternalPowered()); // An absent/invalid low reading clears it.
// Host detection overrides a cached negative voltage result immediately,
// including an empty battery; removing the host must not latch external power.
const unsigned reads = board.reads;
board.host = true;
assert(board.isExternalPowered());
assert(board.reads == reads);
board.host = false;
assert(!board.isExternalPowered());
// A long host connection must not preserve stale high battery voltage.
board.voltage = 4250;
test_now = 35000;
assert(board.isExternalPowered());
board.host = true;
board.voltage = 3700;
test_now = 50000;
assert(board.isExternalPowered());
board.host = false;
assert(!board.isExternalPowered());
TestBoard wrapped;
test_now = UINT32_MAX - 1000;
assert(!wrapped.isExternalPowered());
wrapped.voltage = 4230;
test_now = 3998;
assert(!wrapped.isExternalPowered());
test_now = 3999;
assert(wrapped.isExternalPowered()); // Five-second sampling survives wrap.
// Both sources select the actual USB display profile, returning to battery
// when neither source is detected. No additional display setting is needed.
mesh::ui::DisplayPowerPrefs prefs;
prefs.battery.mode = mesh::ui::DisplayMode::Off;
prefs.usb.mode = mesh::ui::DisplayMode::On;
mesh::ui::DisplayPowerPolicy display;
TestBoard source;
test_now = 0;
const auto update = [&]() {
display.update(prefs, source.isExternalPowered() || source.isUsbHostConnected(),
false, false, test_now);
};
update(); assert(!display.on());
source.host = true;
update(); assert(display.on());
source.host = false;
update(); assert(!display.on());
source.voltage = 4230;
test_now = 5000;
update(); assert(display.on());
source.voltage = 4100;
test_now = 10000;
update(); assert(!display.on());
// The estimate uses the real calibrated ADC path, including user calibration.
HeltecV4R8Board calibrated;
calibrated.setAdcMultiplier(5.1f);
assert(calibrated.isExternalPowered()); // 830 mV * 5.1 = 4233 mV.
calibrated.setAdcMultiplier(5.0f);
test_now += 5000;
assert(!calibrated.isExternalPowered()); // Same raw sample becomes 4150 mV.
}
'''
class R8ExternalPowerTest(unittest.TestCase):
def test_host_voltage_and_display_profile(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "host C++ compiler required")
with tempfile.TemporaryDirectory(prefix="mesh-r8-power-") as directory:
work = Path(directory)
files = {
"Arduino.h": ARDUINO,
"helpers/ESP32Board.h": BASE_BOARD,
"helpers/RefCountedDigitalPin.h": """#pragma once
class RefCountedDigitalPin {
public:
RefCountedDigitalPin(int, int) {}
void begin() {}
void claim() {}
void release() {}
};
""",
"driver/rtc_io.h": """#pragma once
using gpio_num_t = int;
using esp_reset_reason_t = int;
constexpr int ESP_RST_DEEPSLEEP = 1;
inline int esp_reset_reason() { return 0; }
inline long esp_sleep_get_ext1_wakeup_status() { return 0; }
inline void rtc_gpio_hold_dis(int) {}
inline void rtc_gpio_hold_en(int) {}
inline void rtc_gpio_deinit(int) {}
""",
"test.cpp": TEST,
}
for path, content in files.items():
target = work / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
executable = work / "r8-power.exe"
variant = ROOT / "variants/heltec_v4_r8"
result = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I" + str(work), "-I" + str(variant), "-I" + str(ROOT / "src"),
str(work / "test.cpp"), str(variant / "HeltecV4R8Board.cpp"),
"-o", str(executable),
], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()