189 lines
8.1 KiB
Python
189 lines
8.1 KiB
Python
"""Compile the actual Indicator HAL against an error-injectable I2C device."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class IndicatorRadioHalTest(unittest.TestCase):
|
|
def test_port_zero_transactions_fresh_busy_and_shared_irq_errors(self):
|
|
arduino = r'''
|
|
#pragma once
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
constexpr uint32_t RADIOLIB_NC = 0xffffffff;
|
|
constexpr int LOW=0, HIGH=1, INPUT_PULLUP=2;
|
|
constexpr int PIN_BOARD_SDA=39, PIN_BOARD_SCL=40, P_LORA_BUSY=66;
|
|
extern int nativeLevel, nativeWrites, nativeModes;
|
|
inline void pinMode(uint32_t, uint32_t) { ++nativeModes; }
|
|
inline int digitalRead(uint32_t) { return nativeLevel; }
|
|
'''
|
|
buffered = r'''
|
|
#pragma once
|
|
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
struct ArduinoHal {
|
|
enum { GpioModeOutput=1, GpioModeInput=0, GpioLevelLow=0, GpioLevelHigh=1,
|
|
GpioInterruptRising=2, GpioInterruptFalling=3 };
|
|
virtual ~ArduinoHal() = default;
|
|
virtual void pinMode(uint32_t, uint32_t) { ++nativeModes; }
|
|
virtual void digitalWrite(uint32_t, uint32_t) { ++nativeWrites; }
|
|
virtual uint32_t digitalRead(uint32_t) { return nativeLevel; }
|
|
virtual void attachInterrupt(uint32_t, void (*)(), uint32_t) {}
|
|
virtual void detachInterrupt(uint32_t) {}
|
|
virtual uint32_t pinToInterrupt(uint32_t pin) { return pin + 100; }
|
|
};
|
|
struct ESP32BufferedRadioHal : ArduinoHal {
|
|
explicit ESP32BufferedRadioHal(SPIClass&) {}
|
|
};
|
|
constexpr uint32_t RISING = ArduinoHal::GpioInterruptRising;
|
|
'''
|
|
gfx = r'''
|
|
#pragma once
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
namespace lgfx { namespace i2c {
|
|
struct Result { bool ok; bool has_value() const { return ok; } };
|
|
Result readRegister(uint8_t, uint8_t, uint8_t, uint8_t*, size_t, uint32_t);
|
|
Result transactionWrite(uint8_t, uint8_t, const uint8_t*, size_t, uint32_t);
|
|
Result init(uint8_t, uint8_t, uint8_t);
|
|
} }
|
|
'''
|
|
fixture = r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include "@ROOT@/variants/sensecap_indicator-espnow/IndicatorRadioHal.cpp"
|
|
int nativeLevel=HIGH, nativeWrites=0, nativeModes=0;
|
|
uint8_t registers[8] = {0,0,0xff,0xff,0,0,0xff,0xff};
|
|
struct Transfer { bool write; uint8_t reg; size_t size; };
|
|
std::vector<Transfer> transfers;
|
|
bool failRead=false, failWrite=false, failInit=false;
|
|
unsigned callbacks=0, inits=0;
|
|
void irq() { ++callbacks; }
|
|
namespace lgfx { namespace i2c {
|
|
Result readRegister(uint8_t port,uint8_t address,uint8_t reg,uint8_t* data,
|
|
size_t size,uint32_t frequency) {
|
|
assert(port==0 && address==0x20 && frequency==400000);
|
|
assert((size==1 || size==2) && reg+size<=8);
|
|
transfers.push_back({false,reg,size});
|
|
if(failRead) return {false};
|
|
std::copy(registers+reg,registers+reg+size,data);return {true};
|
|
}
|
|
Result transactionWrite(uint8_t port,uint8_t address,const uint8_t* data,
|
|
size_t size,uint32_t frequency) {
|
|
assert(port==0 && address==0x20 && frequency==400000);
|
|
assert((size==2 || size==3) && data[0]+size-1<=8);
|
|
transfers.push_back({true,data[0],size});
|
|
if(failWrite) return {false};
|
|
std::copy(data+1,data+size,registers+data[0]);return {true};
|
|
}
|
|
Result init(uint8_t port,uint8_t sda,uint8_t scl) {
|
|
assert(port==0 && sda==39 && scl==40);++inits;return {!failInit};
|
|
}
|
|
} }
|
|
void last(bool write,uint8_t reg,size_t size) {
|
|
assert(!transfers.empty());auto t=transfers.back();
|
|
assert(t.write==write && t.reg==reg && t.size==size);
|
|
}
|
|
int main() {
|
|
SPIClass spi;IndicatorRadioHal hal(spi);
|
|
assert(hal.beginExpander() && transfers.size()==3);
|
|
for(auto t:transfers) assert(!t.write && t.size==2);
|
|
assert(hal.beginExpander() && transfers.size()==3);
|
|
|
|
// Low-bank pin updates send only that bank, leaving unrelated Port 1 alone.
|
|
registers[7]=0x5a;
|
|
hal.pinMode(64,ArduinoHal::GpioModeOutput);last(true,6,2);
|
|
assert(registers[6]==0xfe && registers[7]==0x5a);
|
|
registers[3]=0x5a;
|
|
hal.digitalWrite(64,ArduinoHal::GpioLevelLow);last(true,2,2);
|
|
assert(registers[2]==0xfe && registers[3]==0x5a);
|
|
size_t count=transfers.size();hal.digitalWrite(64,0);
|
|
assert(transfers.size()==count); // unchanged latch still needs no write
|
|
|
|
// Failed writes must not poison the software shadow or suppress the retry.
|
|
failWrite=true;hal.digitalWrite(64,1);last(true,2,2);
|
|
assert(registers[2]==0xfe);failWrite=false;
|
|
hal.digitalWrite(64,1);assert(registers[2]==0xff);
|
|
failWrite=true;hal.pinMode(65,ArduinoHal::GpioModeOutput);
|
|
assert(registers[6]==0xfe);failWrite=false;
|
|
hal.pinMode(65,ArduinoHal::GpioModeOutput);assert(registers[6]==0xfc);
|
|
|
|
// Preserve generic Port 1 support and its original whole-word transactions.
|
|
hal.digitalWrite(72,0);last(true,2,3);assert(registers[3]==0xfe);
|
|
hal.pinMode(72,ArduinoHal::GpioModeOutput);last(true,6,3);
|
|
registers[1]=1;assert(hal.digitalRead(72)==1);last(false,0,2);
|
|
|
|
// BUSY must be read over I2C every time, even with expander INT inactive.
|
|
nativeLevel=HIGH;registers[0]=4;
|
|
assert(hal.digitalRead(66)==1);last(false,0,1);
|
|
count=transfers.size();registers[0]=0;
|
|
assert(hal.digitalRead(66)==0 && transfers.size()==count+1);last(false,0,1);
|
|
#ifdef MC_SX1262_HIL
|
|
// Only HIL can restore the old two-bank bus-width control in this image.
|
|
hal.hilUsePort0Only=false;
|
|
assert(hal.digitalRead(66)==0);last(false,0,2);
|
|
hal.digitalWrite(64,0);last(true,2,3);
|
|
hal.pinMode(65,ArduinoHal::GpioModeInput);last(true,6,3);
|
|
hal.hilUsePort0Only=true;
|
|
assert(hal.digitalRead(66)==0);last(false,0,1);
|
|
hal.digitalWrite(64,1);last(true,2,2);
|
|
hal.pinMode(65,ArduinoHal::GpioModeOutput);last(true,6,2);
|
|
#endif
|
|
failRead=true;assert(hal.digitalRead(66)==1 && hal.digitalRead(67)==0);
|
|
failRead=false;
|
|
|
|
// IRQ service still reads BOTH banks, clearing all shared expander sources.
|
|
hal.attachInterrupt(67,irq,ArduinoHal::GpioInterruptRising);last(false,0,2);
|
|
registers[0]=8;nativeLevel=LOW;
|
|
hal.serviceInterrupt();last(false,0,2);assert(callbacks==1);
|
|
hal.serviceInterrupt();assert(callbacks==1);
|
|
registers[0]=0;assert(hal.digitalRead(66)==0);assert(callbacks==1);
|
|
failRead=true;registers[0]=8;hal.serviceInterrupt();assert(callbacks==1);
|
|
failRead=false;hal.serviceInterrupt();assert(callbacks==2);
|
|
hal.detachInterrupt(67);count=transfers.size();hal.serviceInterrupt();
|
|
assert(count==transfers.size());
|
|
|
|
// Native and invalid pins do not accidentally address the expander.
|
|
count=transfers.size();hal.digitalWrite(5,1);hal.pinMode(5,1);
|
|
assert(hal.digitalRead(5)==LOW && nativeWrites==1);
|
|
assert(hal.pinToInterrupt(5)==105 && hal.pinToInterrupt(67)==67);
|
|
assert(hal.digitalRead(80)==0);hal.digitalWrite(80,1);hal.pinMode(80,1);
|
|
assert(count==transfers.size());
|
|
|
|
// Initial bus recovery remains whole-word and fails closed when unavailable.
|
|
IndicatorRadioHal failed(spi);failRead=true;
|
|
assert(!failed.beginExpander() && inits==1);
|
|
assert(failed.digitalRead(66)==1);failRead=false;
|
|
assert(failed.beginExpander());
|
|
}
|
|
'''.replace("@ROOT@", str(ROOT))
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
base = Path(folder)
|
|
(base / "helpers/radiolib").mkdir(parents=True)
|
|
(base / "Arduino.h").write_text(arduino)
|
|
(base / "SPI.h").write_text("#pragma once\nstruct SPIClass {};\n")
|
|
(base / "LovyanGFX.hpp").write_text(gfx)
|
|
(base / "helpers/radiolib/ESP32BufferedRadioHal.h").write_text(buffered)
|
|
source, binary = base / "fixture.cpp", base / "fixture"
|
|
source.write_text(fixture)
|
|
for definitions in ([], ["-DMC_SX1262_HIL"]):
|
|
with self.subTest(definitions=definitions):
|
|
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
|
"-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined",
|
|
"-fno-omit-frame-pointer", "-no-pie", "-DSENSECAP_INDICATOR_LORA",
|
|
*definitions, "-I", str(base), str(source), "-o", str(binary)],
|
|
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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|