192 lines
6.7 KiB
Python
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("========================================")
|
|
|