From b81cf802743a32b747179f204ebe2c021246d94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 09:55:31 +0100 Subject: [PATCH 01/23] tests: Extend test framework for driverless board qualification scenarios. --- tests/conftest.py | 1 + tests/runner/mpremote_bridge.py | 15 +++++++++++++++ tests/test_scenarios.py | 32 ++++++++++++++++++++------------ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 11d0fd92..a4e2f40f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ def pytest_configure(config): config.addinivalue_line("markers", "mock: tests that run with FakeI2C (no hardware)") config.addinivalue_line("markers", "hardware: tests that require a real board") config.addinivalue_line("markers", "manual: tests that require human validation") + config.addinivalue_line("markers", "board: board qualification tests (no driver, hardware only)") def pytest_collection_modifyitems(config, items): diff --git a/tests/runner/mpremote_bridge.py b/tests/runner/mpremote_bridge.py index b7323256..11bba3b9 100644 --- a/tests/runner/mpremote_bridge.py +++ b/tests/runner/mpremote_bridge.py @@ -142,6 +142,21 @@ def run_script( last_line = output.strip().rsplit("\n", 1)[-1] return json.loads(last_line) + def run_raw_script(self, script): + """Run a raw MicroPython script on the board without driver context. + + The script must set a ``result`` variable. Returns the + JSON-decoded value of ``result``. + """ + code = ( + f"import json\n" + f"{script}\n" + f"print(json.dumps(result))" + ) + output = self._run(code) + last_line = output.strip().rsplit("\n", 1)[-1] + return json.loads(last_line) + def scan_bus(self, i2c_config): """Scan I2C bus and return list of addresses.""" i2c_init = _i2c_init_code(i2c_config) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 4744e9b0..9c94a838 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -36,12 +36,17 @@ def iter_scenario_tests(): with open(yaml_file, encoding="utf-8") as f: scenario = yaml.safe_load(f) - driver = scenario["driver"] + is_board = scenario.get("type") == "board" + name = scenario.get("name", scenario.get("driver", yaml_file.stem)) for test in scenario.get("tests", []): modes = test.get("mode", ["mock", "hardware"]) + if is_board: + # Board scenarios are hardware-only + modes = [m for m in modes if m == "hardware"] for mode in modes: - test_id = f"{driver}/{test['name']}/{mode}" - yield pytest.param(scenario, test, mode, id=test_id) + test_id = f"{name}/{test['name']}/{mode}" + marks = [pytest.mark.board] if is_board else [] + yield pytest.param(scenario, test, mode, id=test_id, marks=marks) def make_mock_instance(scenario): @@ -154,15 +159,18 @@ def test_scenario(scenario, test, mode, port): input(f" [INTERACTIVE] {pre_prompt} ") else: print(f" [INTERACTIVE] {pre_prompt}") - result = bridge.run_script( - scenario["driver"], - scenario["driver_class"], - scenario["i2c"], - test["script"], - module_name=scenario.get("module_name"), - hardware_init=scenario.get("hardware_init"), - i2c_address=scenario.get("i2c_address"), - ) + if scenario.get("type") == "board": + result = bridge.run_raw_script(test["script"]) + else: + result = bridge.run_script( + scenario["driver"], + scenario["driver_class"], + scenario["i2c"], + test["script"], + module_name=scenario.get("module_name"), + hardware_init=scenario.get("hardware_init"), + i2c_address=scenario.get("i2c_address"), + ) else: pytest.fail(f"Unknown action: {action}") From bc70f05915f448b59f3a997f4295842888d96257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:13:18 +0100 Subject: [PATCH 02/23] tests: Add hardware marker and guard board scenarios against driver actions. --- tests/test_scenarios.py | 105 ++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 9c94a838..a7c470af 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -45,7 +45,7 @@ def iter_scenario_tests(): modes = [m for m in modes if m == "hardware"] for mode in modes: test_id = f"{name}/{test['name']}/{mode}" - marks = [pytest.mark.board] if is_board else [] + marks = [pytest.mark.board, pytest.mark.hardware] if is_board else [] yield pytest.param(scenario, test, mode, id=test_id, marks=marks) @@ -89,6 +89,8 @@ def test_scenario(scenario, test, mode, port): bridge = MpremoteBridge(port=port) action = test["action"] + is_board = scenario.get("type") == "board" + if action == "manual": # Skip manual tests when stdin is not available (no -s flag) import sys @@ -96,59 +98,66 @@ def test_scenario(scenario, test, mode, port): pytest.skip("manual test requires interactive mode (-s)") # Display values before prompting if 'display' is defined - for display in test.get("display", []): - value = bridge.call_method( + # (board scenarios have no driver, so display is not supported) + if not is_board: + for display in test.get("display", []): + value = bridge.call_method( + scenario["driver"], + scenario["driver_class"], + scenario["i2c"], + display["method"], + display.get("args"), + module_name=scenario.get("module_name"), + hardware_init=scenario.get("hardware_init"), + i2c_address=scenario.get("i2c_address"), + ) + label = display.get("label", display["method"]) + unit = display.get("unit", "") + if isinstance(value, float): + print(f" {label}: {value:.2f} {unit}") + else: + print(f" {label}: {value} {unit}") + prompt = test.get("prompt", "Manual check") + result = prompt_yes_no(prompt) + elif action in ("call", "read_register", "interactive"): + if is_board: + pytest.fail( + f"Board scenarios do not support '{action}' action; " + f"use 'hardware_script' or 'manual' instead" + ) + if action == "call": + result = bridge.call_method( scenario["driver"], scenario["driver_class"], scenario["i2c"], - display["method"], - display.get("args"), + test["method"], + test.get("args"), + module_name=scenario.get("module_name"), + hardware_init=scenario.get("hardware_init"), + i2c_address=scenario.get("i2c_address"), + ) + elif action == "read_register": + result = bridge.read_register( + scenario["i2c"], + scenario["i2c_address"], + test["register"], + ) + else: # interactive + import sys + if not sys.stdin.isatty(): + pytest.skip("interactive test requires interactive mode (-s)") + pre_prompt = test.get("pre_prompt", "Perform action then press Enter") + input(f" [INTERACTIVE] {pre_prompt} ") + result = bridge.call_method( + scenario["driver"], + scenario["driver_class"], + scenario["i2c"], + test["method"], + test.get("args"), module_name=scenario.get("module_name"), hardware_init=scenario.get("hardware_init"), i2c_address=scenario.get("i2c_address"), ) - label = display.get("label", display["method"]) - unit = display.get("unit", "") - if isinstance(value, float): - print(f" {label}: {value:.2f} {unit}") - else: - print(f" {label}: {value} {unit}") - prompt = test.get("prompt", "Manual check") - result = prompt_yes_no(prompt) - elif action == "call": - result = bridge.call_method( - scenario["driver"], - scenario["driver_class"], - scenario["i2c"], - test["method"], - test.get("args"), - module_name=scenario.get("module_name"), - hardware_init=scenario.get("hardware_init"), - i2c_address=scenario.get("i2c_address"), - ) - elif action == "read_register": - result = bridge.read_register( - scenario["i2c"], - scenario["i2c_address"], - test["register"], - ) - elif action == "interactive": - # Prompt user first, then call method - import sys - if not sys.stdin.isatty(): - pytest.skip("interactive test requires interactive mode (-s)") - pre_prompt = test.get("pre_prompt", "Perform action then press Enter") - input(f" [INTERACTIVE] {pre_prompt} ") - result = bridge.call_method( - scenario["driver"], - scenario["driver_class"], - scenario["i2c"], - test["method"], - test.get("args"), - module_name=scenario.get("module_name"), - hardware_init=scenario.get("hardware_init"), - i2c_address=scenario.get("i2c_address"), - ) elif action == "hardware_script": import sys if not sys.stdin.isatty(): @@ -159,7 +168,7 @@ def test_scenario(scenario, test, mode, port): input(f" [INTERACTIVE] {pre_prompt} ") else: print(f" [INTERACTIVE] {pre_prompt}") - if scenario.get("type") == "board": + if is_board: result = bridge.run_raw_script(test["script"]) else: result = bridge.run_script( From 222efc2dcfb756f2a8293f3870141ebd10df51fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:08:13 +0100 Subject: [PATCH 03/23] tests: Add board qualification tests for user pins. --- tests/scenarios/board_pins.yaml | 126 ++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/scenarios/board_pins.yaml diff --git a/tests/scenarios/board_pins.yaml b/tests/scenarios/board_pins.yaml new file mode 100644 index 00000000..ae401ae8 --- /dev/null +++ b/tests/scenarios/board_pins.yaml @@ -0,0 +1,126 @@ +type: board +name: "board_pins" + +# User pin tests for board qualification via test bench. +# These tests verify electrical continuity and GPIO/ADC functionality +# of each user-accessible pin on the edge connector and croc pads. +# +# Note: Full pin testing requires a physical test bench (jig) that +# connects each pin to a verification circuit. Tests below cover +# basic GPIO read-back; more comprehensive tests will be added +# once the test bench hardware is designed. + +tests: + - name: "Pin P0 (PC4) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P0")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P1 (PA5) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P1")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P2 (PC5) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P2")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P3 (PA2) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P3")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P4 (PA4) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P4")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P10 (PA6) analog read" + action: hardware_script + script: | + from machine import ADC, Pin + adc = ADC(Pin("P10")) + raw = adc.read_u16() + result = 0 <= raw <= 65535 + expect_true: true + mode: [hardware] + + - name: "Pin P6 (PC3) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P6", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] + + - name: "Pin P7 (PA9) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P7", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] + + - name: "Pin P8 (PA15) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P8", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] + + - name: "Pin P9 (PC2) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P9", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] + + - name: "Pin P12 (PC6) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P12", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] + + - name: "Pin P16 (PE4) digital pull-up read" + action: hardware_script + script: | + from machine import Pin + p = Pin("P16", Pin.IN, Pin.PULL_UP) + result = p.value() == 1 + expect_true: true + mode: [hardware] From 9679596afed58627b6fb509044c6b3d4cb295957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:25:56 +0100 Subject: [PATCH 04/23] tests: Clarify pin tests as smoke tests and simplify ADC assertions. --- tests/scenarios/board_pins.yaml | 37 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/scenarios/board_pins.yaml b/tests/scenarios/board_pins.yaml index ae401ae8..36d7ce04 100644 --- a/tests/scenarios/board_pins.yaml +++ b/tests/scenarios/board_pins.yaml @@ -1,14 +1,13 @@ type: board name: "board_pins" -# User pin tests for board qualification via test bench. -# These tests verify electrical continuity and GPIO/ADC functionality -# of each user-accessible pin on the edge connector and croc pads. +# Smoke tests for user-accessible pins on the edge connector and croc pads. +# ADC tests verify that the pin can be configured and read without error. +# Digital tests verify the internal pull-up reads high when the pin is floating. # -# Note: Full pin testing requires a physical test bench (jig) that -# connects each pin to a verification circuit. Tests below cover -# basic GPIO read-back; more comprehensive tests will be added -# once the test bench hardware is designed. +# Note: These are basic configuration/readback smoke tests. Full electrical +# continuity testing requires a physical test bench (jig) with known voltages +# and will be added once the test bench hardware is designed. tests: - name: "Pin P0 (PC4) analog read" @@ -16,8 +15,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P0")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] @@ -26,8 +25,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P1")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] @@ -36,8 +35,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P2")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] @@ -46,8 +45,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P3")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] @@ -56,8 +55,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P4")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] @@ -66,8 +65,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P10")) - raw = adc.read_u16() - result = 0 <= raw <= 65535 + adc.read_u16() + result = True expect_true: true mode: [hardware] From 6ad9b3a58c093b48a7b607cab215306d1e95fe28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:45:16 +0100 Subject: [PATCH 05/23] tests: Skip printing bare boolean results in test output. --- tests/test_scenarios.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index a7c470af..d17fccd1 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -20,6 +20,9 @@ def _print_result(result, test): """Print the measured value for report capture.""" + if isinstance(result, bool): + # Boolean smoke tests (True/False) — nothing useful to print + return if isinstance(result, float): print(f"{result:.2f}") elif isinstance(result, int) and "expect" in test and isinstance(test["expect"], int) and test["expect"] > 0xFF: From 4922d579de0c61d9faeb3fef38adec1523563ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:45:22 +0100 Subject: [PATCH 06/23] tests: Return raw values in pin tests for visual control. --- tests/scenarios/board_pins.yaml | 56 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/tests/scenarios/board_pins.yaml b/tests/scenarios/board_pins.yaml index 36d7ce04..6e5572b6 100644 --- a/tests/scenarios/board_pins.yaml +++ b/tests/scenarios/board_pins.yaml @@ -2,7 +2,7 @@ type: board name: "board_pins" # Smoke tests for user-accessible pins on the edge connector and croc pads. -# ADC tests verify that the pin can be configured and read without error. +# ADC tests verify that the pin can be configured and return a raw reading. # Digital tests verify the internal pull-up reads high when the pin is floating. # # Note: These are basic configuration/readback smoke tests. Full electrical @@ -15,9 +15,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P0")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P1 (PA5) analog read" @@ -25,9 +24,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P1")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P2 (PC5) analog read" @@ -35,9 +33,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P2")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P3 (PA2) analog read" @@ -45,9 +42,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P3")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P4 (PA4) analog read" @@ -55,9 +51,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P4")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P10 (PA6) analog read" @@ -65,9 +60,8 @@ tests: script: | from machine import ADC, Pin adc = ADC(Pin("P10")) - adc.read_u16() - result = True - expect_true: true + result = adc.read_u16() + expect_range: [0, 65535] mode: [hardware] - name: "Pin P6 (PC3) digital pull-up read" @@ -75,8 +69,8 @@ tests: script: | from machine import Pin p = Pin("P6", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] - name: "Pin P7 (PA9) digital pull-up read" @@ -84,8 +78,8 @@ tests: script: | from machine import Pin p = Pin("P7", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] - name: "Pin P8 (PA15) digital pull-up read" @@ -93,8 +87,8 @@ tests: script: | from machine import Pin p = Pin("P8", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] - name: "Pin P9 (PC2) digital pull-up read" @@ -102,8 +96,8 @@ tests: script: | from machine import Pin p = Pin("P9", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] - name: "Pin P12 (PC6) digital pull-up read" @@ -111,8 +105,8 @@ tests: script: | from machine import Pin p = Pin("P12", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] - name: "Pin P16 (PE4) digital pull-up read" @@ -120,6 +114,6 @@ tests: script: | from machine import Pin p = Pin("P16", Pin.IN, Pin.PULL_UP) - result = p.value() == 1 - expect_true: true + result = p.value() + expect: 1 mode: [hardware] From bd312202becc529b0bd26518a030822dc1d886b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:07:32 +0100 Subject: [PATCH 07/23] tests: Add board qualification tests for buzzer. --- tests/scenarios/board_buzzer.yaml | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/scenarios/board_buzzer.yaml diff --git a/tests/scenarios/board_buzzer.yaml b/tests/scenarios/board_buzzer.yaml new file mode 100644 index 00000000..efffd6fc --- /dev/null +++ b/tests/scenarios/board_buzzer.yaml @@ -0,0 +1,41 @@ +type: board +name: "board_buzzer" + +tests: + - name: "Buzzer tone 440Hz" + action: hardware_script + script: | + from machine import Pin, PWM + from time import sleep_ms + pwm = PWM(Pin("SPEAKER"), freq=440, duty_u16=32768) + sleep_ms(500) + pwm.deinit() + result = True + expect_true: true + mode: [hardware] + + - name: "Buzzer 440Hz audible" + action: manual + prompt: "Avez-vous entendu un bip (La 440Hz) ?" + expect_true: true + mode: [hardware] + + - name: "Buzzer frequency sweep" + action: hardware_script + script: | + from machine import Pin, PWM + from time import sleep_ms + pwm = PWM(Pin("SPEAKER"), freq=200, duty_u16=32768) + for f in range(200, 2001, 100): + pwm.freq(f) + sleep_ms(50) + pwm.deinit() + result = True + expect_true: true + mode: [hardware] + + - name: "Buzzer sweep audible" + action: manual + prompt: "Avez-vous entendu un balayage de fréquence (grave vers aigu) ?" + expect_true: true + mode: [hardware] From 88d4474f1f41f08e45db948fbacb35cf94f0b2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:51:59 +0100 Subject: [PATCH 08/23] tests: Fix buzzer tests to use pyb.Timer instead of machine.PWM. --- tests/scenarios/board_buzzer.yaml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/scenarios/board_buzzer.yaml b/tests/scenarios/board_buzzer.yaml index efffd6fc..543bbcf7 100644 --- a/tests/scenarios/board_buzzer.yaml +++ b/tests/scenarios/board_buzzer.yaml @@ -5,11 +5,14 @@ tests: - name: "Buzzer tone 440Hz" action: hardware_script script: | - from machine import Pin, PWM + import pyb from time import sleep_ms - pwm = PWM(Pin("SPEAKER"), freq=440, duty_u16=32768) + timer = pyb.Timer(1, freq=440) + ch = timer.channel(4, pyb.Timer.PWM, pin=pyb.Pin("SPEAKER")) + ch.pulse_width_percent(50) sleep_ms(500) - pwm.deinit() + ch.pulse_width_percent(0) + timer.deinit() result = True expect_true: true mode: [hardware] @@ -23,13 +26,16 @@ tests: - name: "Buzzer frequency sweep" action: hardware_script script: | - from machine import Pin, PWM + import pyb from time import sleep_ms - pwm = PWM(Pin("SPEAKER"), freq=200, duty_u16=32768) + timer = pyb.Timer(1, freq=200) + ch = timer.channel(4, pyb.Timer.PWM, pin=pyb.Pin("SPEAKER")) + ch.pulse_width_percent(50) for f in range(200, 2001, 100): - pwm.freq(f) + timer.freq(f) sleep_ms(50) - pwm.deinit() + ch.pulse_width_percent(0) + timer.deinit() result = True expect_true: true mode: [hardware] From 7223472d8c3b1161d29dd0f59bc459df9ae77aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:55:23 +0100 Subject: [PATCH 09/23] tests: Fix buzzer sweep timing and remove double initial note. --- tests/scenarios/board_buzzer.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/scenarios/board_buzzer.yaml b/tests/scenarios/board_buzzer.yaml index 543bbcf7..f1d22f93 100644 --- a/tests/scenarios/board_buzzer.yaml +++ b/tests/scenarios/board_buzzer.yaml @@ -30,10 +30,10 @@ tests: from time import sleep_ms timer = pyb.Timer(1, freq=200) ch = timer.channel(4, pyb.Timer.PWM, pin=pyb.Pin("SPEAKER")) - ch.pulse_width_percent(50) for f in range(200, 2001, 100): timer.freq(f) - sleep_ms(50) + ch.pulse_width_percent(50) + sleep_ms(200) ch.pulse_width_percent(0) timer.deinit() result = True From 0569351080d4a1c826b16a5b0eb610a14826c006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:58:51 +0100 Subject: [PATCH 10/23] tests: Add Ode to Joy melody test for buzzer qualification. --- tests/scenarios/board_buzzer.yaml | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/scenarios/board_buzzer.yaml b/tests/scenarios/board_buzzer.yaml index f1d22f93..2f5550e5 100644 --- a/tests/scenarios/board_buzzer.yaml +++ b/tests/scenarios/board_buzzer.yaml @@ -45,3 +45,39 @@ tests: prompt: "Avez-vous entendu un balayage de fréquence (grave vers aigu) ?" expect_true: true mode: [hardware] + + - name: "Buzzer Ode to Joy" + action: hardware_script + script: | + import pyb + from time import sleep_ms + timer = pyb.Timer(1, freq=440) + ch = timer.channel(4, pyb.Timer.PWM, pin=pyb.Pin("SPEAKER")) + # Ode to Joy (Beethoven) - first phrase + # (frequency Hz, duration ms) + melody = [ + (330, 300), (330, 300), (349, 300), (392, 300), + (392, 300), (349, 300), (330, 300), (294, 300), + (262, 300), (262, 300), (294, 300), (330, 300), + (330, 450), (294, 150), (294, 600), + (330, 300), (330, 300), (349, 300), (392, 300), + (392, 300), (349, 300), (330, 300), (294, 300), + (262, 300), (262, 300), (294, 300), (330, 300), + (294, 450), (262, 150), (262, 600), + ] + for freq, dur in melody: + timer.freq(freq) + ch.pulse_width_percent(50) + sleep_ms(dur) + ch.pulse_width_percent(0) + sleep_ms(30) + timer.deinit() + result = True + expect_true: true + mode: [hardware] + + - name: "Buzzer Ode to Joy audible" + action: manual + prompt: "Avez-vous reconnu l'Hymne à la joie (Beethoven) ?" + expect_true: true + mode: [hardware] From c3c45785a28b240549f0a353a5a6f14f5e2f1cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:03:07 +0100 Subject: [PATCH 11/23] tests: Add board qualification tests for A/B/Menu buttons. --- tests/scenarios/board_buttons.yaml | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/scenarios/board_buttons.yaml diff --git a/tests/scenarios/board_buttons.yaml b/tests/scenarios/board_buttons.yaml new file mode 100644 index 00000000..e76d2a7d --- /dev/null +++ b/tests/scenarios/board_buttons.yaml @@ -0,0 +1,66 @@ +type: board +name: "board_buttons" + +tests: + - name: "Button A press detection" + action: hardware_script + pre_prompt: "Appuyez sur le bouton A quand la LED verte s'allume (5s)." + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if btn.value() == 0: + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "Button B press detection" + action: hardware_script + pre_prompt: "Appuyez sur le bouton B quand la LED verte s'allume (5s)." + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("B_BUTTON", Pin.IN, Pin.PULL_UP) + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if btn.value() == 0: + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "Button MENU press detection" + action: hardware_script + pre_prompt: "Appuyez sur le bouton MENU quand la LED verte s'allume (5s)." + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if btn.value() == 0: + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] From 84c12e4b551cc7fffebcc43e5216c99763e7ceb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:24:41 +0100 Subject: [PATCH 12/23] tests: Add released-state check and use wait false for button tests. --- tests/scenarios/board_buttons.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/scenarios/board_buttons.yaml b/tests/scenarios/board_buttons.yaml index e76d2a7d..ef6ef721 100644 --- a/tests/scenarios/board_buttons.yaml +++ b/tests/scenarios/board_buttons.yaml @@ -4,12 +4,16 @@ name: "board_buttons" tests: - name: "Button A press detection" action: hardware_script - pre_prompt: "Appuyez sur le bouton A quand la LED verte s'allume (5s)." + pre_prompt: "Test bouton A : appuyez sur le bouton A quand la LED verte s'allume (5s)." + wait: false script: | from machine import Pin from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) + # Wait for button to be released before starting + while btn.value() == 0: + sleep_ms(20) led.on() detected = False t0 = ticks_ms() @@ -25,12 +29,15 @@ tests: - name: "Button B press detection" action: hardware_script - pre_prompt: "Appuyez sur le bouton B quand la LED verte s'allume (5s)." + pre_prompt: "Bouton B" + wait: false script: | from machine import Pin from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("B_BUTTON", Pin.IN, Pin.PULL_UP) + while btn.value() == 0: + sleep_ms(20) led.on() detected = False t0 = ticks_ms() @@ -46,12 +53,15 @@ tests: - name: "Button MENU press detection" action: hardware_script - pre_prompt: "Appuyez sur le bouton MENU quand la LED verte s'allume (5s)." + pre_prompt: "Bouton MENU" + wait: false script: | from machine import Pin from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) + while btn.value() == 0: + sleep_ms(20) led.on() detected = False t0 = ticks_ms() From 2d80e7b514e7089ae5028867f1714042d4381cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 11:05:22 +0100 Subject: [PATCH 13/23] tests: Add interrupt tests and rename existing to polling for buttons. --- tests/scenarios/board_buttons.yaml | 99 ++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/tests/scenarios/board_buttons.yaml b/tests/scenarios/board_buttons.yaml index ef6ef721..af81aff1 100644 --- a/tests/scenarios/board_buttons.yaml +++ b/tests/scenarios/board_buttons.yaml @@ -1,17 +1,19 @@ type: board name: "board_buttons" +# --- Polling tests (one per button) --- +# LED_GREEN signals when the board is ready to read the button. + tests: - - name: "Button A press detection" + - name: "Button A press (polling)" action: hardware_script - pre_prompt: "Test bouton A : appuyez sur le bouton A quand la LED verte s'allume (5s)." + pre_prompt: "Test polling boutons : appuyez sur chaque bouton quand la LED verte s'allume (5s). Commençons par A." wait: false script: | from machine import Pin from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) - # Wait for button to be released before starting while btn.value() == 0: sleep_ms(20) led.on() @@ -27,7 +29,7 @@ tests: expect_true: true mode: [hardware] - - name: "Button B press detection" + - name: "Button B press (polling)" action: hardware_script pre_prompt: "Bouton B" wait: false @@ -51,7 +53,7 @@ tests: expect_true: true mode: [hardware] - - name: "Button MENU press detection" + - name: "Button MENU press (polling)" action: hardware_script pre_prompt: "Bouton MENU" wait: false @@ -74,3 +76,90 @@ tests: result = detected expect_true: true mode: [hardware] + + # --- Interrupt tests (one per button) --- + # LED_GREEN signals when the board is ready; uses Pin.irq() to detect press. + + - name: "Button A press (interrupt)" + action: hardware_script + pre_prompt: "Test interrupt boutons : appuyez brièvement sur chaque bouton quand la LED verte s'allume (5s). Commençons par A." + wait: false + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) + while btn.value() == 0: + sleep_ms(20) + detected = False + def on_press(pin): + global detected + detected = True + btn.irq(trigger=Pin.IRQ_FALLING, handler=on_press) + led.on() + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if detected: + break + sleep_ms(20) + btn.irq(handler=None) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "Button B press (interrupt)" + action: hardware_script + pre_prompt: "Bouton B" + wait: false + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("B_BUTTON", Pin.IN, Pin.PULL_UP) + while btn.value() == 0: + sleep_ms(20) + detected = False + def on_press(pin): + global detected + detected = True + btn.irq(trigger=Pin.IRQ_FALLING, handler=on_press) + led.on() + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if detected: + break + sleep_ms(20) + btn.irq(handler=None) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "Button MENU press (interrupt)" + action: hardware_script + pre_prompt: "Bouton MENU" + wait: false + script: | + from machine import Pin + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + btn = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) + while btn.value() == 0: + sleep_ms(20) + detected = False + def on_press(pin): + global detected + detected = True + btn.irq(trigger=Pin.IRQ_FALLING, handler=on_press) + led.on() + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + if detected: + break + sleep_ms(20) + btn.irq(handler=None) + led.off() + result = detected + expect_true: true + mode: [hardware] From 0aaac6ba7ee819061ffedd454bd14efa50969b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 11:13:30 +0100 Subject: [PATCH 14/23] tests: Add D-PAD buttons and timeout on release-wait loops. --- tests/scenarios/board_buttons.yaml | 120 ++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/tests/scenarios/board_buttons.yaml b/tests/scenarios/board_buttons.yaml index af81aff1..c6b6fbba 100644 --- a/tests/scenarios/board_buttons.yaml +++ b/tests/scenarios/board_buttons.yaml @@ -14,7 +14,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) led.on() detected = False @@ -38,7 +41,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("B_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) led.on() detected = False @@ -62,7 +68,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) led.on() detected = False @@ -77,7 +86,107 @@ tests: expect_true: true mode: [hardware] - # --- Interrupt tests (one per button) --- + # --- D-PAD buttons via MCP23009E (I2C 0x20, GPIO register 0x09) --- + # Read directly via I2C without importing the driver. + # Bit mapping: UP=bit7, DOWN=bit5, LEFT=bit6, RIGHT=bit4. + + - name: "D-PAD UP press (polling)" + action: hardware_script + pre_prompt: "Test polling D-PAD : appuyez sur chaque bouton quand la LED verte s'allume (5s). Commençons par UP." + wait: false + script: | + from machine import Pin, I2C + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + i2c = I2C(1) + bit = 7 + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + gpio = i2c.readfrom_mem(0x20, 0x09, 1)[0] + if not (gpio & (1 << bit)): + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "D-PAD DOWN press (polling)" + action: hardware_script + pre_prompt: "Bouton DOWN" + wait: false + script: | + from machine import Pin, I2C + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + i2c = I2C(1) + bit = 5 + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + gpio = i2c.readfrom_mem(0x20, 0x09, 1)[0] + if not (gpio & (1 << bit)): + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "D-PAD LEFT press (polling)" + action: hardware_script + pre_prompt: "Bouton LEFT" + wait: false + script: | + from machine import Pin, I2C + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + i2c = I2C(1) + bit = 6 + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + gpio = i2c.readfrom_mem(0x20, 0x09, 1)[0] + if not (gpio & (1 << bit)): + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + - name: "D-PAD RIGHT press (polling)" + action: hardware_script + pre_prompt: "Bouton RIGHT" + wait: false + script: | + from machine import Pin, I2C + from time import sleep_ms, ticks_ms, ticks_diff + led = Pin("LED_GREEN", Pin.OUT) + i2c = I2C(1) + bit = 4 + led.on() + detected = False + t0 = ticks_ms() + while ticks_diff(ticks_ms(), t0) < 5000: + gpio = i2c.readfrom_mem(0x20, 0x09, 1)[0] + if not (gpio & (1 << bit)): + detected = True + break + sleep_ms(20) + led.off() + result = detected + expect_true: true + mode: [hardware] + + # --- Interrupt tests (GPIO direct buttons) --- # LED_GREEN signals when the board is ready; uses Pin.irq() to detect press. - name: "Button A press (interrupt)" @@ -89,7 +198,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("A_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) detected = False def on_press(pin): @@ -117,7 +229,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("B_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) detected = False def on_press(pin): @@ -145,7 +260,10 @@ tests: from time import sleep_ms, ticks_ms, ticks_diff led = Pin("LED_GREEN", Pin.OUT) btn = Pin("MENU_BUTTON", Pin.IN, Pin.PULL_UP) + t_rel = ticks_ms() while btn.value() == 0: + if ticks_diff(ticks_ms(), t_rel) > 3000: + break sleep_ms(20) detected = False def on_press(pin): From 273e2bb66a7534a6d0f1f3b8d4748bf7db7c495c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:02:08 +0100 Subject: [PATCH 15/23] tests: Add board qualification tests for user LEDs. --- tests/scenarios/board_leds.yaml | 105 ++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/scenarios/board_leds.yaml diff --git a/tests/scenarios/board_leds.yaml b/tests/scenarios/board_leds.yaml new file mode 100644 index 00000000..c665c333 --- /dev/null +++ b/tests/scenarios/board_leds.yaml @@ -0,0 +1,105 @@ +type: board +name: "board_leds" + +tests: + - name: "LED RED on/off" + action: hardware_script + script: | + from machine import Pin + from time import sleep_ms + led = Pin("LED_RED", Pin.OUT) + led.on() + sleep_ms(1000) + led.off() + result = True + expect_true: true + mode: [hardware] + + - name: "LED RED visible" + action: manual + prompt: "La LED rouge s'est-elle allumée puis éteinte ?" + expect_true: true + mode: [hardware] + + - name: "LED GREEN on/off" + action: hardware_script + script: | + from machine import Pin + from time import sleep_ms + led = Pin("LED_GREEN", Pin.OUT) + led.on() + sleep_ms(1000) + led.off() + result = True + expect_true: true + mode: [hardware] + + - name: "LED GREEN visible" + action: manual + prompt: "La LED verte s'est-elle allumée puis éteinte ?" + expect_true: true + mode: [hardware] + + - name: "LED BLUE on/off" + action: hardware_script + script: | + from machine import Pin + from time import sleep_ms + led = Pin("LED_BLUE", Pin.OUT) + led.on() + sleep_ms(1000) + led.off() + result = True + expect_true: true + mode: [hardware] + + - name: "LED BLUE visible" + action: manual + prompt: "La LED bleue s'est-elle allumée puis éteinte ?" + expect_true: true + mode: [hardware] + + - name: "RGB LED full cycle" + action: hardware_script + script: | + from machine import Pin + from time import sleep_ms + r = Pin("LED_RED", Pin.OUT) + g = Pin("LED_GREEN", Pin.OUT) + b = Pin("LED_BLUE", Pin.OUT) + for led in [r, g, b]: + led.on() + sleep_ms(500) + led.off() + # White = all on + r.on(); g.on(); b.on() + sleep_ms(1000) + r.off(); g.off(); b.off() + result = True + expect_true: true + mode: [hardware] + + - name: "RGB LED full cycle visible" + action: manual + prompt: "La LED a-t-elle affiché rouge, vert, bleu, puis blanc ?" + expect_true: true + mode: [hardware] + + - name: "LED BLE on/off" + action: hardware_script + script: | + from machine import Pin + from time import sleep_ms + led = Pin("LED_BLE", Pin.OUT) + led.on() + sleep_ms(1000) + led.off() + result = True + expect_true: true + mode: [hardware] + + - name: "LED BLE visible" + action: manual + prompt: "La LED BLE (bleue) s'est-elle allumée puis éteinte ?" + expect_true: true + mode: [hardware] From 6cc9cf90c5c599e98d24fd5c730af330f9591df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:22:49 +0100 Subject: [PATCH 16/23] tests: Add pre_prompt to LED tests and align white duration. --- tests/scenarios/board_leds.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/scenarios/board_leds.yaml b/tests/scenarios/board_leds.yaml index c665c333..24691cdb 100644 --- a/tests/scenarios/board_leds.yaml +++ b/tests/scenarios/board_leds.yaml @@ -4,6 +4,7 @@ name: "board_leds" tests: - name: "LED RED on/off" action: hardware_script + pre_prompt: "Observez la LED rouge sur la carte, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -23,6 +24,7 @@ tests: - name: "LED GREEN on/off" action: hardware_script + pre_prompt: "Observez la LED verte, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -42,6 +44,7 @@ tests: - name: "LED BLUE on/off" action: hardware_script + pre_prompt: "Observez la LED bleue, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -61,6 +64,7 @@ tests: - name: "RGB LED full cycle" action: hardware_script + pre_prompt: "Observez la LED RGB (séquence R/G/B/blanc), puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -73,7 +77,7 @@ tests: led.off() # White = all on r.on(); g.on(); b.on() - sleep_ms(1000) + sleep_ms(500) r.off(); g.off(); b.off() result = True expect_true: true @@ -87,6 +91,7 @@ tests: - name: "LED BLE on/off" action: hardware_script + pre_prompt: "Observez la LED BLE, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms From d57c6781fc692024774771a371b34ae4b9748880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 11:41:29 +0100 Subject: [PATCH 17/23] tests: Remove unnecessary pre_prompt pauses between LED tests. --- tests/scenarios/board_leds.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/scenarios/board_leds.yaml b/tests/scenarios/board_leds.yaml index 24691cdb..5b50e55e 100644 --- a/tests/scenarios/board_leds.yaml +++ b/tests/scenarios/board_leds.yaml @@ -4,7 +4,7 @@ name: "board_leds" tests: - name: "LED RED on/off" action: hardware_script - pre_prompt: "Observez la LED rouge sur la carte, puis appuyez sur Entrée." + pre_prompt: "Observez la LED RGB sur la carte, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -24,7 +24,6 @@ tests: - name: "LED GREEN on/off" action: hardware_script - pre_prompt: "Observez la LED verte, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -44,7 +43,6 @@ tests: - name: "LED BLUE on/off" action: hardware_script - pre_prompt: "Observez la LED bleue, puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms @@ -64,7 +62,6 @@ tests: - name: "RGB LED full cycle" action: hardware_script - pre_prompt: "Observez la LED RGB (séquence R/G/B/blanc), puis appuyez sur Entrée." script: | from machine import Pin from time import sleep_ms From 2f1d10794ef1449ada63c135518c3bed9f839e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 11:43:27 +0100 Subject: [PATCH 18/23] tests: Rephrase pre_prompt to clarify Enter starts the test. --- tests/scenarios/board_leds.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/scenarios/board_leds.yaml b/tests/scenarios/board_leds.yaml index 5b50e55e..71e06f33 100644 --- a/tests/scenarios/board_leds.yaml +++ b/tests/scenarios/board_leds.yaml @@ -4,7 +4,7 @@ name: "board_leds" tests: - name: "LED RED on/off" action: hardware_script - pre_prompt: "Observez la LED RGB sur la carte, puis appuyez sur Entrée." + pre_prompt: "Appuyez sur Entrée pour lancer le test LED RGB, puis observez la carte." script: | from machine import Pin from time import sleep_ms @@ -88,7 +88,7 @@ tests: - name: "LED BLE on/off" action: hardware_script - pre_prompt: "Observez la LED BLE, puis appuyez sur Entrée." + pre_prompt: "Appuyez sur Entrée pour lancer le test LED BLE, puis observez la carte." script: | from machine import Pin from time import sleep_ms From 89cb6122f2f3c8ea25f3ca254d8b403853be9eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 09:59:04 +0100 Subject: [PATCH 19/23] tests: Add board qualification test for I2C bus scan. --- tests/scenarios/board_i2c_scan.yaml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/scenarios/board_i2c_scan.yaml diff --git a/tests/scenarios/board_i2c_scan.yaml b/tests/scenarios/board_i2c_scan.yaml new file mode 100644 index 00000000..42fec5d8 --- /dev/null +++ b/tests/scenarios/board_i2c_scan.yaml @@ -0,0 +1,30 @@ +type: board +name: "board_i2c_scan" + +tests: + - name: "I2C bus scan finds expected devices" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + found = i2c.scan() + # Expected I2C addresses on STeaMi board: + # 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960 + # 0x55=BQ27441, 0x5C=WSEN-HIDS, 0x5D=WSEN-PADS, 0x5F=HTS221 + expected = [0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] + missing = [hex(a) for a in expected if a not in found] + result = len(missing) == 0 + expect_true: true + mode: [hardware] + + - name: "No unexpected I2C devices" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + found = i2c.scan() + known = [0x1E, 0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] + unexpected = [hex(a) for a in found if a not in known] + result = len(unexpected) == 0 + expect_true: true + mode: [hardware] From f38ec8c460c73dd3cd0960fad9176701f9e99dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 10:17:08 +0100 Subject: [PATCH 20/23] tests: Return missing/unexpected addresses for better diagnostics. --- tests/scenarios/board_i2c_scan.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/scenarios/board_i2c_scan.yaml b/tests/scenarios/board_i2c_scan.yaml index 42fec5d8..511152bc 100644 --- a/tests/scenarios/board_i2c_scan.yaml +++ b/tests/scenarios/board_i2c_scan.yaml @@ -12,9 +12,9 @@ tests: # 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960 # 0x55=BQ27441, 0x5C=WSEN-HIDS, 0x5D=WSEN-PADS, 0x5F=HTS221 expected = [0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] - missing = [hex(a) for a in expected if a not in found] - result = len(missing) == 0 - expect_true: true + missing = sorted([hex(a) for a in expected if a not in found]) + result = missing + expect: [] mode: [hardware] - name: "No unexpected I2C devices" @@ -24,7 +24,7 @@ tests: i2c = I2C(1) found = i2c.scan() known = [0x1E, 0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] - unexpected = [hex(a) for a in found if a not in known] - result = len(unexpected) == 0 - expect_true: true + unexpected = sorted([hex(a) for a in found if a not in known]) + result = unexpected + expect: [] mode: [hardware] From fbc26430c91783ab9d9a77f6228280c070b8c669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 12:00:07 +0100 Subject: [PATCH 21/23] tests: Fix I2C scan addresses and add WHO_AM_I checks per device. --- tests/scenarios/board_i2c_scan.yaml | 95 ++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/tests/scenarios/board_i2c_scan.yaml b/tests/scenarios/board_i2c_scan.yaml index 511152bc..a0b97509 100644 --- a/tests/scenarios/board_i2c_scan.yaml +++ b/tests/scenarios/board_i2c_scan.yaml @@ -5,13 +5,18 @@ tests: - name: "I2C bus scan finds expected devices" action: hardware_script script: | - from machine import I2C + from machine import I2C, Pin + # Release MCP23009E from reset so it responds to scan + rst = Pin("RST_EXPANDER", Pin.OUT) + rst.on() + from time import sleep_ms + sleep_ms(10) i2c = I2C(1) found = i2c.scan() - # Expected I2C addresses on STeaMi board: - # 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960 - # 0x55=BQ27441, 0x5C=WSEN-HIDS, 0x5D=WSEN-PADS, 0x5F=HTS221 - expected = [0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] + # Expected I2C addresses on STeaMi board (7-bit): + # 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960, 0x3B=STM32F103CB + # 0x55=BQ27441, 0x5D=WSEN-PADS, 0x5F=HTS221/WSEN-HIDS, 0x6B=ISM330DL + expected = [0x20, 0x29, 0x39, 0x3B, 0x55, 0x5D, 0x5F, 0x6B] missing = sorted([hex(a) for a in expected if a not in found]) result = missing expect: [] @@ -20,11 +25,87 @@ tests: - name: "No unexpected I2C devices" action: hardware_script script: | - from machine import I2C + from machine import I2C, Pin + rst = Pin("RST_EXPANDER", Pin.OUT) + rst.on() + from time import sleep_ms + sleep_ms(10) i2c = I2C(1) found = i2c.scan() - known = [0x1E, 0x20, 0x29, 0x39, 0x55, 0x5C, 0x5D, 0x5F] + known = [0x1E, 0x20, 0x29, 0x39, 0x3B, 0x55, 0x5D, 0x5F, 0x6B] unexpected = sorted([hex(a) for a in found if a not in known]) result = unexpected expect: [] mode: [hardware] + + # --- WHO_AM_I / Device ID checks --- + # Each test reads the identification register of one component. + + - name: "LIS2MDL WHO_AM_I (0x1E)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + result = i2c.readfrom_mem(0x1E, 0x4F, 1)[0] + expect: 0x40 + mode: [hardware] + + - name: "MCP23009E IODIR default (0x20)" + action: hardware_script + script: | + from machine import I2C, Pin + rst = Pin("RST_EXPANDER", Pin.OUT) + rst.on() + from time import sleep_ms + sleep_ms(10) + i2c = I2C(1) + # IODIR register default value after reset = 0xFF (all inputs) + result = i2c.readfrom_mem(0x20, 0x00, 1)[0] + expect: 0xFF + mode: [hardware] + + - name: "VL53L1X model ID (0x29)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + data = i2c.readfrom_mem(0x29, 0x010F, 2, addrsize=16) + result = (data[0] << 8) | data[1] + expect: 0xEACC + mode: [hardware] + + - name: "APDS9960 device ID (0x39)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + result = i2c.readfrom_mem(0x39, 0x92, 1)[0] + expect: 0xAB + mode: [hardware] + + - name: "WSEN-PADS device ID (0x5D)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + result = i2c.readfrom_mem(0x5D, 0x0F, 1)[0] + expect: 0xB3 + mode: [hardware] + + - name: "HTS221/WSEN-HIDS device ID (0x5F)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + result = i2c.readfrom_mem(0x5F, 0x0F, 1)[0] + expect: 0xBC + mode: [hardware] + + - name: "ISM330DL WHO_AM_I (0x6B)" + action: hardware_script + script: | + from machine import I2C + i2c = I2C(1) + result = i2c.readfrom_mem(0x6B, 0x0F, 1)[0] + expect: 0x6A + mode: [hardware] From b0699ff310c34a7b02cf25318990411455467703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 12:09:03 +0100 Subject: [PATCH 22/23] tests: Add LIS2MDL to expected I2C addresses list. --- tests/scenarios/board_i2c_scan.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/scenarios/board_i2c_scan.yaml b/tests/scenarios/board_i2c_scan.yaml index a0b97509..8f51352c 100644 --- a/tests/scenarios/board_i2c_scan.yaml +++ b/tests/scenarios/board_i2c_scan.yaml @@ -14,9 +14,10 @@ tests: i2c = I2C(1) found = i2c.scan() # Expected I2C addresses on STeaMi board (7-bit): - # 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960, 0x3B=STM32F103CB - # 0x55=BQ27441, 0x5D=WSEN-PADS, 0x5F=HTS221/WSEN-HIDS, 0x6B=ISM330DL - expected = [0x20, 0x29, 0x39, 0x3B, 0x55, 0x5D, 0x5F, 0x6B] + # 0x1E=LIS2MDL, 0x20=MCP23009E, 0x29=VL53L1X, 0x39=APDS9960 + # 0x3B=STM32F103CB, 0x55=BQ27441, 0x5D=WSEN-PADS + # 0x5F=HTS221/WSEN-HIDS, 0x6B=ISM330DL + expected = [0x1E, 0x20, 0x29, 0x39, 0x3B, 0x55, 0x5D, 0x5F, 0x6B] missing = sorted([hex(a) for a in expected if a not in found]) result = missing expect: [] From dcd9d6700863fc073cb00cc27fa47d762a80ca9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Fri, 13 Mar 2026 12:21:26 +0100 Subject: [PATCH 23/23] tests: Release MCP23009E from reset before D-PAD button tests. --- tests/scenarios/board_buttons.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/scenarios/board_buttons.yaml b/tests/scenarios/board_buttons.yaml index c6b6fbba..f23f62bf 100644 --- a/tests/scenarios/board_buttons.yaml +++ b/tests/scenarios/board_buttons.yaml @@ -97,6 +97,10 @@ tests: script: | from machine import Pin, I2C from time import sleep_ms, ticks_ms, ticks_diff + # Release MCP23009E from reset + rst = Pin("RST_EXPANDER", Pin.OUT) + rst.on() + sleep_ms(10) led = Pin("LED_GREEN", Pin.OUT) i2c = I2C(1) bit = 7