Files
MeshCore/test/test_drv2605_power.py
T
mikecarperandR0ck f57dffa2be Add saved Companion sound and vibration controls
Default fresh settings to sound off and vibration on, and keep existing saved choices. Apply changes immediately across all three UIs and make custom notification patterns respect the same mute controls.

Keep the Wio Mini Companion within its existing flash boundary by storing its LED switch in a reserved preference byte and shortening CLI help without removing commands. Add coverage for live output changes, persistence, and storage failures.

Adapted from https://github.com/meshcore-dev/MeshCore/pull/3603.

Co-authored-by: R0ck <develop@hectospark.co.uk>
2026-10-20 09:33:43 -07:00

285 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);
assert(standby() && !vibration.isVibrating());
count=events.size();
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 || _quiet) {", "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()