use std::path::Path;
use crate::runner::ElfInfo;
#[derive(Debug, Clone)]
pub struct MatrixTestKey {
pub row: u8,
pub col: u8,
pub expected_hid_usage: u8,
}
#[derive(Debug, Clone)]
pub struct MatrixTestPlan {
pub row_gpios: Vec<u32>,
pub col_gpios: Vec<u32>,
pub keys: Vec<MatrixTestKey>,
}
impl MatrixTestPlan {
#[must_use]
pub fn generate_sio_peripheral(&self) -> String {
let row_list = join(&self.row_gpios);
let col_list = join(&self.col_gpios);
format!(
r#"# Auto-generated RP2040 SIO peripheral for matrix HIL testing.
# Simulates GPIO row-strobe / column-read for a {rows}x{cols} matrix.
ROW_GPIOS = [{row_list}]
COL_GPIOS = [{col_list}]
SPINLOCK_BASE = 0x100
SPINLOCK_COUNT = 32
gpio_out = 0xFFFFFFFF # all pins high (idle)
pressed_row = -1
pressed_col = -1
locks = [False] * SPINLOCK_COUNT
def read_word(offset):
global gpio_out, pressed_row, pressed_col
# GPIO_IN at offset 0x04
if offset == 0x04:
result = 0xFFFFFFFF # all high (no key pressed)
if pressed_row >= 0 and pressed_col >= 0:
row_gpio = ROW_GPIOS[pressed_row] if pressed_row < len(ROW_GPIOS) else -1
if row_gpio >= 0 and (gpio_out & (1 << row_gpio)) == 0:
col_gpio = COL_GPIOS[pressed_col] if pressed_col < len(COL_GPIOS) else -1
if col_gpio >= 0:
result &= ~(1 << col_gpio) # pull column low
return result
# GPIO_OUT at offset 0x10
if offset == 0x10:
return gpio_out
# Magic test control register at offset 0xF00
if offset == 0xF00:
return (pressed_row << 8) | (pressed_col & 0xFF) if pressed_row >= 0 else 0xFFFF
# Spinlock range
if SPINLOCK_BASE <= offset < SPINLOCK_BASE + SPINLOCK_COUNT * 4:
idx = (offset - SPINLOCK_BASE) // 4
if locks[idx]:
return 0
locks[idx] = True
return 1
return 0
def write_word(offset, value):
global gpio_out, pressed_row, pressed_col
# GPIO_OUT at offset 0x10
if offset == 0x10:
gpio_out = value
return
# GPIO_OUT_SET at offset 0x14
if offset == 0x14:
gpio_out |= value
return
# GPIO_OUT_CLR at offset 0x18
if offset == 0x18:
gpio_out &= ~value
return
# GPIO_OUT_XOR at offset 0x1C
if offset == 0x1C:
gpio_out ^= value
return
# Magic test control register at offset 0xF00
if offset == 0xF00:
if value == 0xFFFF:
pressed_row = -1
pressed_col = -1
else:
pressed_row = (value >> 8) & 0xFF
pressed_col = value & 0xFF
return
# Spinlock range
if SPINLOCK_BASE <= offset < SPINLOCK_BASE + SPINLOCK_COUNT * 4:
idx = (offset - SPINLOCK_BASE) // 4
locks[idx] = False
def read_byte(offset):
return read_word(offset) & 0xFF
def write_byte(offset, value):
write_word(offset, value)
if 'self' in dir():
self.read_byte = read_byte
self.write_byte = write_byte
self.read_word = read_word
self.write_word = write_word
"#,
rows = self.row_gpios.len(),
cols = self.col_gpios.len(),
)
}
#[must_use]
pub fn generate_matrix_repl(&self, sio_script_path: &str) -> String {
format!(
r#"flash: Memory.MappedMemory @ sysbus 0x10000000
size: 0x200000
sram: Memory.MappedMemory @ sysbus 0x20000000
size: 0x42000
psm: Memory.MappedMemory @ sysbus 0x40010000
size: 0x4000
sio: Python.PythonPeripheral @ sysbus 0xD0000000
size: 0x1000
initable: true
filename: "{sio_script_path}"
nvic: IRQControllers.NVIC @ sysbus 0xe000e000
systickFrequency: 125000000
-> cpu@0
cpu: CPU.CortexM @ sysbus
cpuType: "cortex-m0+"
nvic: nvic
"#
)
}
}
#[must_use]
pub fn build_test_plan(
row_gpios: Vec<u32>,
col_gpios: Vec<u32>,
base_layer_keys: &[u8],
cols: usize,
) -> MatrixTestPlan {
let mut keys = Vec::new();
for (i, &usage_id) in base_layer_keys.iter().enumerate() {
if usage_id == 0 {
continue;
}
keys.push(MatrixTestKey {
row: (i / cols) as u8,
col: (i % cols) as u8,
expected_hid_usage: usage_id,
});
}
MatrixTestPlan {
row_gpios,
col_gpios,
keys,
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn generate_matrix_test_resc(
firmware_elf: &Path,
repl_path: &str,
report_marker_addr: u32,
hil_marker_addr: u32,
report_buf_addr: u32,
report_count_addr: u32,
info: &ElfInfo,
timeout_secs: u32,
tapdance_marker_addr: Option<u32>,
tap_result_addr: Option<u32>,
hold_layer_result_addr: Option<u32>,
) -> String {
let tapdance_hook = match (tapdance_marker_addr, tap_result_addr, hold_layer_result_addr) {
(Some(td), Some(tap), Some(hold)) => format!(
"cpu AddHook {td:#010X} \"tap = self.Bus.ReadByte({tap}); hold = self.Bus.ReadByte({hold}); self.Log(LogLevel.Warning, 'TAPDANCE:' + hex(tap) + ',' + hex(hold))\"",
),
_ => String::new(),
};
format!(
r#"mach create "hil"
machine LoadPlatformDescription @{repl_path}
sysbus LoadELF @{firmware}
cpu VectorTableOffset {vtor:#010X}
cpu SP {sp:#010X}
cpu PC {pc:#010X}
cpu AddHook {report_marker:#010X} "idx = self.Bus.ReadDoubleWord({count_addr}); addr = {buf_addr} + (idx - 1) * 4; val = self.Bus.ReadDoubleWord(addr); self.Log(LogLevel.Warning, 'KEY:' + str(val & 0xFF) + ',' + str((val >> 8) & 0xFF) + ',' + hex((val >> 16) & 0xFF) + ',' + hex((val >> 24) & 0xFF))"
cpu AddHook {hil_marker:#010X} "self.Log(LogLevel.Warning, 'HIL OK')"
{tapdance_hook}
start
emulation RunFor "{run_for}"
quit
"#,
firmware = firmware_elf.display(),
vtor = info.vtor,
sp = info.sp,
pc = info.pc,
report_marker = report_marker_addr,
hil_marker = hil_marker_addr,
count_addr = report_count_addr,
buf_addr = report_buf_addr,
run_for = crate::runner::format_run_for(timeout_secs),
)
}
#[must_use]
pub fn parse_key_log(output: &str) -> Vec<(u8, u8, u8, u8)> {
let mut results = Vec::new();
for line in output.lines() {
let Some(payload) = extract_after(line, "KEY:") else {
continue;
};
let parts: Vec<&str> = payload.split(',').collect();
if parts.len() == 4 {
if let (Some(r), Some(c), Some(m), Some(k)) = (
parts[0].trim().parse().ok(),
parts[1].trim().parse().ok(),
parse_hex_or_dec(parts[2]),
parse_hex_or_dec(parts[3]),
) {
results.push((r, c, m, k));
}
}
}
results
}
#[must_use]
pub fn parse_tapdance_log(output: &str) -> Option<(u8, u8)> {
for line in output.lines() {
let Some(payload) = extract_after(line, "TAPDANCE:") else {
continue;
};
let parts: Vec<&str> = payload.split(',').collect();
if parts.len() == 2 {
return Some((parse_hex_or_dec(parts[0])?, parse_hex_or_dec(parts[1])?));
}
}
None
}
fn parse_hex_or_dec(s: &str) -> Option<u8> {
let s = s.trim().trim_end_matches(['L', 'l']);
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u8::from_str_radix(hex, 16).ok()
} else {
u8::from_str_radix(s, 16).ok().or_else(|| s.parse().ok())
}
}
fn extract_after<'a>(line: &'a str, marker: &str) -> Option<&'a str> {
let idx = line.find(marker)?;
let rest = line.get(idx + marker.len()..)?;
Some(rest.trim().trim_end_matches('\''))
}
fn join(values: &[u32]) -> String {
values
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_plan_skips_zero_keys_and_maps_indices() {
let plan = build_test_plan(vec![0, 1], vec![2, 3, 4], &[0x04, 0, 0x05, 0, 0, 0x06], 3);
assert_eq!(plan.keys.len(), 3);
assert_eq!((plan.keys[0].row, plan.keys[0].col), (0, 0));
assert_eq!((plan.keys[1].row, plan.keys[1].col), (0, 2));
assert_eq!((plan.keys[2].row, plan.keys[2].col), (1, 2));
}
#[test]
fn sio_peripheral_embeds_gpio_lists() {
let plan = build_test_plan(vec![0, 1, 2], vec![4, 5], &[], 2);
let py = plan.generate_sio_peripheral();
assert!(py.contains("ROW_GPIOS = [0, 1, 2]"));
assert!(py.contains("COL_GPIOS = [4, 5]"));
assert!(py.contains("3x2 matrix"));
}
#[test]
fn matrix_repl_references_python_peripheral() {
let plan = build_test_plan(vec![0], vec![1], &[], 1);
let repl = plan.generate_matrix_repl("/hil/sio.py");
assert!(repl.contains("Python.PythonPeripheral"));
assert!(repl.contains("filename: \"/hil/sio.py\""));
}
#[test]
fn parse_key_log_reads_dec_and_hex_fields() {
let log = "warn KEY:1,2,0x00,0x04\nnoise\nKEY:0,11,0xE0,0x2a'";
let keys = parse_key_log(log);
assert_eq!(keys, vec![(1, 2, 0x00, 0x04), (0, 11, 0xE0, 0x2A)]);
}
#[test]
fn parse_tapdance_log_reads_pair() {
assert_eq!(parse_tapdance_log("TAPDANCE:0x2c,0x1e"), Some((0x2C, 0x1E)));
assert_eq!(parse_tapdance_log("nothing here"), None);
}
#[test]
fn matrix_resc_includes_hooks_and_optional_tapdance() {
let info = ElfInfo {
vtor: 0x1000_0000,
sp: 0x2004_2000,
pc: 0x1000_0100,
hook_addr: None,
fail_addr: None,
panic_addr: None,
};
let resc = generate_matrix_test_resc(
Path::new("/abs/fw.elf"),
"/hil/platform.repl",
0x1000_0300,
0x1000_0400,
0x2000_0000,
0x2000_0010,
&info,
10,
Some(0x1000_0500),
Some(0x2000_0020),
Some(0x2000_0024),
);
assert!(resc.contains("'KEY:'"));
assert!(resc.contains("'HIL OK'"));
assert!(resc.contains("'TAPDANCE:'"));
assert!(resc.contains("RunFor \"00:00:10\""));
let no_td = generate_matrix_test_resc(
Path::new("/abs/fw.elf"),
"/hil/platform.repl",
0x1000_0300,
0x1000_0400,
0x2000_0000,
0x2000_0010,
&info,
10,
None,
None,
None,
);
assert!(!no_td.contains("TAPDANCE"));
}
}