Files
MeshCore/test/test_drv2605_power.py
T

283 lines
12 KiB
Python

"""Exercise the real haptic driver lifecycle with inert I2C/register mocks.
No PlatformIO, device access or board-current measurement. Negative controls
prove the checks reject idle-awake and premature-playback-stop regressions.
"""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
DRIVER = ROOT / "src/helpers/ui/DRV2605Vibration.cpp"
ARDUINO = r'''
#pragma once
#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
#define OUTPUT 1
#define LOW 0
#define HIGH 1
struct Event { std::string name; unsigned value; };
extern std::vector<Event> events;
extern uint32_t now_ms;
extern int enable_level;
inline uint32_t millis() { return now_ms; }
inline void pinMode(int pin,int mode) {
assert(pin==37 && mode==OUTPUT);events.push_back({"enable-mode",1});
}
inline void digitalWrite(int pin,int value) {
assert(pin==37);enable_level=value;
events.push_back({"enable",static_cast<unsigned>(value)});
}
inline void delay(unsigned ms) {
assert(ms==10);events.push_back({"delay",ms});
}
'''
WIRE = "#pragma once\nstruct TwoWire {};\nextern TwoWire Wire;\n"
ADAFRUIT = r'''
#pragma once
#include <Arduino.h>
#include <Wire.h>
#define DRV2605_MODE_INTTRIG 0x00
#define DRV2605_MODE_REALTIME 0x05
#define DRV2605_REG_MODE 0x01
#define DRV2605_REG_FEEDBACK 0x1a
#define DRV2605_REG_GO 0x0c
struct DriverState {
bool found=true;
unsigned begins=0,reads=0;
uint8_t registers[256]={};
};
extern DriverState hardware;
class Adafruit_DRV2605 {
public:
bool begin(TwoWire* wire) {
assert(wire==&Wire);
#ifdef PIN_DRV_EN
assert(enable_level==HIGH);
#endif
++hardware.begins;events.push_back({"begin",0});
if(!hardware.found) return false;
// The dependency takes the chip out of standby during initialization.
writeRegister8(DRV2605_REG_MODE,0);
return true;
}
void writeRegister8(uint8_t reg,uint8_t value) {
hardware.registers[reg]=value;
events.push_back({"write",static_cast<unsigned>((reg<<8)|value)});
}
uint8_t readRegister8(uint8_t reg) {
assert(reg==DRV2605_REG_GO);++hardware.reads;
events.push_back({"read",reg});return hardware.registers[reg];
}
void selectLibrary(uint8_t library) { writeRegister8(3,library); }
void setMode(uint8_t mode) { writeRegister8(DRV2605_REG_MODE,mode); }
void setWaveform(uint8_t slot,uint8_t effect) {
assert(slot<8);writeRegister8(4+slot,effect);
}
void setRealtimeValue(uint8_t value) { writeRegister8(2,value); }
void go() {
assert(hardware.registers[DRV2605_REG_MODE]==DRV2605_MODE_INTTRIG);
writeRegister8(DRV2605_REG_GO,1);
}
void stop() { writeRegister8(DRV2605_REG_GO,0); }
};
'''
HARNESS = r'''
#include <helpers/ui/DRV2605Vibration.h>
std::vector<Event> events;
uint32_t now_ms=100;
int enable_level=LOW;
TwoWire Wire;
DriverState hardware;
bool standby() { return hardware.registers[DRV2605_REG_MODE]==0x40; }
void idleLoop(DRV2605Vibration& vibration) {
const auto count=events.size();
const auto reads=hardware.reads;
for(unsigned i=0;i<10;++i) { now_ms+=1000;vibration.loop(); }
assert(events.size()==count && hardware.reads==reads);
}
void checkSettings() {
assert(hardware.begins==1); // wake is not a reset/reinitialization
assert(hardware.registers[3]==1 && hardware.registers[0x16]==0x55);
#ifdef DRV2605_USE_LRA
assert(hardware.registers[DRV2605_REG_FEEDBACK]==0xb6);
#else
assert(hardware.registers[DRV2605_REG_FEEDBACK]==0);
#endif
}
void checkEffect(DRV2605Vibration& vibration) {
assert(vibration.isVibrating() && !standby());
assert(hardware.registers[DRV2605_REG_MODE]==DRV2605_MODE_INTTRIG);
assert(hardware.registers[DRV2605_REG_GO]==1);
assert(hardware.registers[4]==DRV2605_EFFECT && hardware.registers[5]==0);
assert(hardware.registers[6]==DRV2605_EFFECT && hardware.registers[7]==0);
checkSettings();
}
int main() {
DRV2605Vibration vibration;
// Inert before initialization, including stop/off callbacks.
vibration.loop();vibration.stop();vibration.pulse(false);
vibration.pulse(true);vibration.trigger(true);
assert(events.empty() && !vibration.isVibrating());
vibration.begin();
assert(standby() && !vibration.isVibrating());
#ifdef PIN_DRV_EN
assert(enable_level==HIGH);
#endif
hardware.registers[0x16]=0x55; // representative retained actuator setting
checkSettings();idleLoop(vibration);
vibration.trigger();checkEffect(vibration);
const auto reads=hardware.reads;
now_ms+=19;vibration.loop();assert(hardware.reads==reads);
++now_ms;vibration.loop();
assert(hardware.reads==reads+1);checkEffect(vibration);
// Still active after a long effect: do not guess a fixed playback duration.
now_ms+=10000;vibration.loop();checkEffect(vibration);
hardware.registers[DRV2605_REG_GO]=0; // hardware finishes the entire sequence
now_ms+=20;vibration.loop();
assert(standby() && !vibration.isVibrating());
checkSettings();idleLoop(vibration);
vibration.trigger(true);checkEffect(vibration);
auto count=events.size();
vibration.trigger();assert(events.size()==count); // cooldown suppresses wake
vibration.quiet(true);
vibration.trigger(true);assert(events.size()==count);
vibration.stop();assert(standby() && !vibration.isVibrating());
assert(hardware.registers[DRV2605_REG_GO]==0 && hardware.registers[2]==0);
vibration.quiet(false);checkSettings();idleLoop(vibration);
vibration.pulse(true);
assert(vibration.isVibrating());
assert(hardware.registers[DRV2605_REG_MODE]==DRV2605_MODE_REALTIME);
assert(hardware.registers[2]==100 && hardware.registers[DRV2605_REG_GO]==0);
// GO does not reflect RTP playback. Never let the effect poll stop a pulse.
idleLoop(vibration);assert(vibration.isVibrating() && !standby());
vibration.pulse(false);
assert(standby() && !vibration.isVibrating() && hardware.registers[2]==0);
idleLoop(vibration);checkSettings();
// Switching between RTP and an effect must replace tracked state, not let an
// old effect completion turn off an active pulse.
vibration.trigger(true);checkEffect(vibration);
vibration.pulse(true);hardware.registers[DRV2605_REG_GO]=0;
idleLoop(vibration);assert(vibration.isVibrating() && !standby());
vibration.trigger(true);checkEffect(vibration);
hardware.registers[DRV2605_REG_GO]=0;now_ms+=20;vibration.loop();
assert(standby() && !vibration.isVibrating());
// Poll scheduling must survive the nRF millis() rollover.
now_ms=UINT32_MAX-10;vibration.trigger(true);checkEffect(vibration);
count=hardware.reads;
now_ms+=19;vibration.loop();assert(hardware.reads==count);
++now_ms;vibration.loop();assert(hardware.reads==count+1);
checkEffect(vibration);
hardware.registers[DRV2605_REG_GO]=0;now_ms+=20;vibration.loop();
assert(standby() && !vibration.isVibrating());
checkSettings();idleLoop(vibration);
// No rail toggling during successful standby/wake or register resets.
#ifdef PIN_DRV_EN
unsigned enables=0;
for(const auto& event:events) if(event.name=="enable") {
assert(event.value==HIGH);++enables;
}
assert(enables==1 && enable_level==HIGH);
#endif
// Reinitialization failure must discard active state and remain inert.
vibration.pulse(true);hardware.found=false;vibration.begin();
assert(!vibration.isVibrating());
#ifdef PIN_DRV_EN
assert(enable_level==LOW);
#endif
count=events.size();
vibration.loop();vibration.trigger(true);vibration.stop();
vibration.pulse(true);vibration.pulse(false);
assert(events.size()==count && !vibration.isVibrating());
}
'''
class Drv2605PowerTests(unittest.TestCase):
def run_driver(self, *, enable=True, lra=True, mutation=None, expected_success=True):
source = DRIVER.read_text()
if mutation == "no-initial-standby":
source = source.replace("drv.setMode(Standby);", "// negative control", 1)
elif mutation == "no-completion-standby":
original = extract_braced(source, "void DRV2605Vibration::loop()")
source = source.replace(original, "void DRV2605Vibration::loop() {}")
elif mutation == "premature-stop":
source = source.replace("(drv.readRegister8(DRV2605_REG_GO) & 1) == 0",
"(drv.readRegister8(DRV2605_REG_GO) & 1) != 0")
elif mutation == "no-pulse-off":
source = source.replace("if (!on) {", "if (false && !on) {")
elif mutation == "poll-idle":
source = source.replace("if (!_ready || !_effect_active) return;",
"if (!_ready) return;")
elif mutation == "failed-begin-rail-on":
source = source.replace("digitalWrite(PIN_DRV_EN, LOW);", "// negative control")
# Compile the actual implementation and header, replacing dependencies
# only. The include path still resolves the production driver header.
source = source.replace('#include "DRV2605Vibration.h"',
'#include <helpers/ui/DRV2605Vibration.h>')
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++11 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-drv2605-power-") as folder:
work = Path(folder)
(work/"Arduino.h").write_text(ARDUINO, encoding="utf-8")
(work/"Wire.h").write_text(WIRE, encoding="utf-8")
(work/"Adafruit_DRV2605.h").write_text(ADAFRUIT, encoding="utf-8")
cpp = work/"case.cpp"
cpp.write_text(source+"\n"+HARNESS, encoding="utf-8")
binary = work/("case.exe" if os.name=="nt" else "case")
flags = ["-DHAS_DRV2605=1"]
if enable:
flags.append("-DPIN_DRV_EN=37")
if lra:
flags.append("-DDRV2605_USE_LRA=1")
sanitize = [] if os.name=="nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
*sanitize, *flags, "-I", str(work), "-I", str(ROOT/"src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(built.returncode, 0, built.stdout+built.stderr)
run = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
if expected_success:
self.assertEqual(run.returncode, 0, run.stdout+run.stderr)
else:
self.assertNotEqual(run.returncode, 0, "negative control unexpectedly passed")
def test_lra_effect_completion_rtp_retention_cooldown_and_failed_begin(self):
self.run_driver()
def test_no_optional_enable_pin_or_lra_mode(self):
self.run_driver(enable=False, lra=False)
def test_initial_idle_awake_negative_control(self):
self.run_driver(mutation="no-initial-standby", expected_success=False)
def test_effect_completion_awake_negative_control(self):
self.run_driver(mutation="no-completion-standby", expected_success=False)
def test_stop_before_go_finishes_negative_control(self):
self.run_driver(mutation="premature-stop", expected_success=False)
def test_pulse_off_awake_negative_control(self):
self.run_driver(mutation="no-pulse-off", expected_success=False)
def test_idle_i2c_polling_negative_control(self):
self.run_driver(mutation="poll-idle", expected_success=False)
def test_failed_begin_enable_leak_negative_control(self):
self.run_driver(mutation="failed-begin-rail-on", expected_success=False)
if __name__ == "__main__":
unittest.main()