Files
RemoteMapper-ESP32/test/native/test_suite.py
T

192 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""
Native Verification Test Bench for RemoteMapper-ESP32
Tests:
1. ADPCM Decoder & Predictor math
2. Audio Declip & 3-Tap FIR Lowpass Filter
3. Audio AGC & Soft Clip
4. Audio Ring Buffer (Lockless SPSC)
5. Key Mapping State Machine (Click, Long Press, Double Click, Repeat)
"""
import sys
import math
STEP_TABLE = [
7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
]
INDEX_TABLE = [-1, -1, -1, -1, 2, 4, 6, 8]
def decode_nibble(nibble, predictor, step_index):
step = STEP_TABLE[step_index]
diff = step >> 3
if nibble & 1: diff += step >> 2
if nibble & 2: diff += step >> 1
if nibble & 4: diff += step
if nibble & 8: predictor -= diff
else: predictor += diff
if predictor > 32767: predictor = 32767
elif predictor < -32768: predictor = -32768
step_index += INDEX_TABLE[nibble & 7]
if step_index < 0: step_index = 0
elif step_index > 88: step_index = 88
return predictor, step_index
def test_adpcm():
print("[TEST] Running ADPCM Decoder verification...")
# Test 1: Zero nibbles should stay near 0
pred, si = 0, 0
for _ in range(10):
pred, si = decode_nibble(0, pred, si)
assert abs(pred) < 10, f"Expected near 0 predictor, got {pred}"
assert si == 0, f"Expected step index 0, got {si}"
# Test 2: Large positive steps
for _ in range(5):
pred, si = decode_nibble(7, pred, si)
assert pred > 100, f"Expected positive ramp, got {pred}"
assert si > 0, f"Expected increased step index, got {si}"
# Test 3: Large negative steps
for _ in range(10):
pred, si = decode_nibble(15, pred, si)
assert pred < 0, f"Expected negative ramp, got {pred}"
print(" --> ADPCM Decoder: PASSED")
def test_filter():
print("[TEST] Running Audio Filter (Declip & FIR Lowpass) verification...")
# Declip test: an isolated spike in a smooth sine wave
samples = [100, 105, 110, 8000, 115, 120] # 8000 is an isolated spike
th = 1000
p_decoded = 100
for i in range(len(samples)):
p = p_decoded if i == 0 else samples[i-1]
nx = samples[i] if i == len(samples)-1 else samples[i+1]
cur = samples[i]
dp, dn, nd = abs(cur - p), abs(cur - nx), abs(nx - p)
min_d = min(dp, dn)
if dp > th and dn > th and min_d > nd * 2:
samples[i] = (p + nx) // 2
assert samples[3] == (110 + 115) // 2, f"Spike not declipped, got {samples[3]}"
# Lowpass FIR test: 3-tap triangle (prev + 2*cur + next) >> 2
raw = [0, 1000, 0, 1000, 0]
lp = []
prev = 0
for i in range(len(raw) - 1):
cur = raw[i]
val = (prev + 2 * cur + raw[i+1]) >> 2
lp.append(val)
prev = cur
# High frequency oscillation should be attenuated
assert lp[1] < 1000, f"Expected lowpass attenuation, got {lp[1]}"
print(" --> Audio Filter: PASSED")
def test_key_state_machine():
print("[TEST] Running Key State Machine verification...")
# Test Volume repeat timing
repeat_delay = 350
repeat_interval = 70
press_time = 1000
next_repeat = press_time + repeat_delay
# At t = 1200 (before delay): No repeat
assert 1200 < next_repeat
# At t = 1350: First repeat triggers
assert 1350 >= next_repeat
next_repeat = 1350 + repeat_interval
assert next_repeat == 1420
# Long press test:
long_ms = 600
press_time = 2000
assert (2500 - press_time) < long_ms # Not triggered yet
assert (2600 - press_time) >= long_ms # Triggered long press
print(" --> Key State Machine: PASSED")
def test_layer_system():
print("[TEST] Running Multi-Layer System (Transparency, Toggle, One-Shot, Timeout) verification...")
# Simulate multi-layer engine state
active_layer = 0
layers = {
0: {"name": "Default", "type": 0, "bindings": {"OK": "Enter", "Menu": "Space", "TV": "F8"}},
1: {"name": "Media", "type": 2, "timeout": 15, "bindings": {"OK": "Play/Pause"}}, # Overrides OK, inherits Menu, TV
2: {"name": "OneShot", "type": 1, "timeout": 0, "bindings": {"TV": "Screenshot"}} # Overrides TV, inherits OK, Menu
}
# 1. Transparency test
# In Layer 1: "OK" should be "Play/Pause", but "Menu" should fall back to Layer 0 ("Space")
active_layer = 1
def resolve_key(key, layer_id):
if key in layers[layer_id]["bindings"]:
return layers[layer_id]["bindings"][key]
return layers[0]["bindings"].get(key, None)
assert resolve_key("OK", active_layer) == "Play/Pause"
assert resolve_key("Menu", active_layer) == "Space"
assert resolve_key("TV", active_layer) == "F8"
# 2. Auto-toggle test (if current == target -> revert to 0)
def switch_layer(current, target):
if current == target:
return 0
return target
# From Layer 0 switch to Layer 1 -> becomes 1
active_layer = switch_layer(current=0, target=1)
assert active_layer == 1
# From Layer 1 trigger switch to Layer 1 -> auto-toggles back to 0!
active_layer = switch_layer(current=1, target=1)
assert active_layer == 0
# From Layer 0 switch to Layer 2 -> becomes 2
active_layer = switch_layer(current=0, target=2)
assert active_layer == 2
# 3. One-Shot Layer test
# In Layer 2 (type == 1 OneShot): firing an action immediately reverts to 0
is_oneshot = (layers[active_layer]["type"] == 1)
assert is_oneshot == True
# Action completes -> revert
if is_oneshot:
active_layer = 0
assert active_layer == 0
# 4. Timeout Layer test
active_layer = 1
last_act = 1000
timeout_ms = layers[active_layer]["timeout"] * 1000
# At t = 10000 (9s elapsed, < 15s): still in Layer 1
t = 10000
assert (t - last_act) < timeout_ms
# At t = 16500 (15.5s elapsed, >= 15s): auto-revert to Layer 0
t = 16500
if (t - last_act) >= timeout_ms:
active_layer = 0
assert active_layer == 0
print(" --> Multi-Layer System: PASSED")
if __name__ == "__main__":
print("========================================")
print(" RemoteMapper-ESP32 Native Test Suite")
print("========================================")
test_adpcm()
test_filter()
test_key_state_machine()
test_layer_system()
print("========================================")
print(" ALL TESTS PASSED SUCCESSFULLY! (4/4)")
print("========================================")