diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index 56b09917b8e..fb2e9480d97 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -42,36 +42,43 @@ compiler = cpbuild.Compiler(srcdir, builddir, cmake_args) ALWAYS_ON_MODULES = ["sys", "collections"] +# Keep in alphabetical order. DEFAULT_MODULES = [ "__future__", - "time", - "os", - "microcontroller", - "struct", + "adafruit_bus_device", + "aesio", "array", - "json", - "random", - "digitalio", + "asyncio", + "atexit", + "binascii", "bitbangio", - "rotaryio", - "rainbowio", - "traceback", - "warnings", - "supervisor", + "codeop", + "digitalio", "errno", + "getpass", + "hashlib", "io", + "json", + "keypad", + "keypad_demux", + "locale", "math", + "microcontroller", "msgpack", - "aesio", - "hashlib", - "zlib", - "adafruit_bus_device", - "getpass", - "storage", - "binascii", + "os", + "rainbowio", + "random", "re", - "asyncio", + "rotaryio", "select", + "storage", + "struct", + "supervisor", + "time", + "touchio", + "traceback", + "warnings", + "zlib", ] # Flags that don't match with with a *bindings module. Some used by adafruit_requests MPCONFIG_FLAGS = ["array", "errno", "io", "json", "math", "binascii"] @@ -117,8 +124,10 @@ EXTRA_FLAGS = { "audiobusio": {"AUDIOBUSIO_I2SOUT": 1, "AUDIOBUSIO_PDMIN": 0}, "busio": {"BUSIO_SPI": 1, "BUSIO_I2C": 1, "BUSIO_UART": 1}, + "keypad": {"KEYPAD_KEYMATRIX": 1, "KEYPAD_KEYS": 1, "KEYPAD_SHIFTREGISTERKEYS": 1}, "rotaryio": {"ROTARYIO_SOFTENCODER": 1}, "synthio": {"SYNTHIO_MAX_CHANNELS": 12}, + "touchio": {"TOUCHIO_USE_NATIVE": 0}, } # Library sources. Will be globbed from the top level directory diff --git a/ports/zephyr-cp/tests/test_keypad.py b/ports/zephyr-cp/tests/test_keypad.py new file mode 100644 index 00000000000..8deb182fd6a --- /dev/null +++ b/ports/zephyr-cp/tests/test_keypad.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2026 Dan Halbert for Adafruit Industries LLC +# SPDX-License-Identifier: MIT + +"""Test keypad functionality on native_sim.""" + +import re + +import pytest + + +KEYS_CODE = """\ +import time +import keypad +import microcontroller + +keys = keypad.Keys( + (microcontroller.pin.P_01, microcontroller.pin.P_02), + value_when_pressed={value_when_pressed}, + pull=True, +) +event = keypad.Event() +print(f"t_abs={{time.monotonic():.3f}} start") + +while time.monotonic() < 7.0: + if keys.events.get_into(event): + print( + f"t_abs={{time.monotonic():.3f}} key={{event.key_number}} " + f"pressed={{event.pressed}} ts={{event.timestamp}}" + ) + time.sleep(0.01) + +print(f"t_abs={{time.monotonic():.3f}} done") +""" + + +def keys_trace(pressed: int) -> dict: + """Press and release key 0 at 4.0-4.5 s, then key 1 at 5.0-5.5 s.""" + released = 1 - pressed + return { + "gpio_emul.01": [ + (4_000_000_000, pressed), + (4_500_000_000, released), + ], + "gpio_emul.02": [ + (5_000_000_000, pressed), + (5_500_000_000, released), + ], + } + + +# (key_number, pressed, trace time in seconds) +EXPECTED_EVENTS = [ + (0, True, 4.0), + (0, False, 4.5), + (1, True, 5.0), + (1, False, 5.5), +] + +EVENT_RE = re.compile(r"t_abs=([0-9.]+) key=(\d+) pressed=(True|False) ts=(\d+)") + + +def check_keys_events(output: str) -> None: + start_match = re.search(r"t_abs=([0-9.]+) start", output) + assert start_match is not None + # The trace only reaches pins that are already inputs. + assert float(start_match.group(1)) < 4.0 + + events = [ + (float(t_abs), int(key), pressed == "True", int(ts)) + for t_abs, key, pressed, ts in EVENT_RE.findall(output) + ] + assert [(key, pressed) for _, key, pressed, _ in events] == [ + (key, pressed) for key, pressed, _ in EXPECTED_EVENTS + ] + + # Seen within one scan interval plus one poll of the trace edge. + for (t_abs, _, _, _), (_, _, trace_time) in zip(events, EXPECTED_EVENTS): + assert trace_time <= t_abs <= trace_time + 0.1 + + # Event timestamps are in ms; each key is held for 500 ms. + for press, release in ((events[0], events[1]), (events[2], events[3])): + assert abs((release[3] - press[3]) - 500) <= 50 + + assert "done" in output + + +@pytest.mark.circuitpy_drive({"code.py": KEYS_CODE.format(value_when_pressed=False)}) +@pytest.mark.input_trace(keys_trace(pressed=0)) +def test_keypad_keys_active_low(circuitpython): + """Keys with pull-ups reports presses when a trace pulls the pins low.""" + circuitpython.wait_until_done() + check_keys_events(circuitpython.serial.all_output) + + +@pytest.mark.circuitpy_drive({"code.py": KEYS_CODE.format(value_when_pressed=True)}) +@pytest.mark.input_trace(keys_trace(pressed=1)) +def test_keypad_keys_active_high(circuitpython): + """Keys with pull-downs reports presses when a trace drives the pins high.""" + circuitpython.wait_until_done() + check_keys_events(circuitpython.serial.all_output) diff --git a/ports/zephyr-cp/tests/test_touchio.py b/ports/zephyr-cp/tests/test_touchio.py new file mode 100644 index 00000000000..cd2ec0a62b8 --- /dev/null +++ b/ports/zephyr-cp/tests/test_touchio.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2026 Dan Halbert for Adafruit Industries LLC +# SPDX-License-Identifier: MIT + +"""Test touchio functionality on native_sim. + +touchio charges the pin, switches it to an input, and counts until it reads +back discharged. gpio_emul has no RC model: an input reads whatever value was +last set, so the count is either 0 or the 10000-tick timeout. A pin held at its +charge level by an input trace stands in for a touch. +""" + +import re + +import pytest + + +TOUCH_CODE = """\ +import time +import microcontroller +import touchio + +touch = touchio.TouchIn(microcontroller.pin.P_01) +print(f"t_abs={time.monotonic():.3f} threshold={touch.threshold}") + + +def report(): + print(f"t_abs={time.monotonic():.3f} raw={touch.raw_value} value={touch.value}") + + +report() +# Read only between trace edges, so each edge lands while the pin is an input. +for until in (4.5, 5.5): + while time.monotonic() < until: + time.sleep(0.05) + report() + +print("done") +""" + +TOUCH_TRACE = { + "gpio_emul.01": [ + (4_000_000_000, 1), # 4.0s: touched + (5_000_000_000, 0), # 5.0s: released + ], +} + +REPORT_RE = re.compile(r"t_abs=([0-9.]+) raw=(\d+) value=(True|False)") + + +@pytest.mark.circuitpy_drive({"code.py": TOUCH_CODE}) +@pytest.mark.input_trace(TOUCH_TRACE) +def test_touchio_touch_and_release(circuitpython): + """TouchIn reads touched while a trace holds the pin high.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + threshold_match = re.search(r"t_abs=([0-9.]+) threshold=(\d+)", output) + assert threshold_match is not None + # The trace only reaches pins that are already inputs. + assert float(threshold_match.group(1)) < 4.0 + # Untouched raw reading of 0 gives a threshold of 0 * 1.05 + 100. + assert int(threshold_match.group(2)) == 100 + + reports = [(int(raw), value == "True") for _, raw, value in REPORT_RE.findall(output)] + assert reports == [(0, False), (10000, True), (0, False)] + assert "done" in output + + +NO_PULL_CODE = """\ +import digitalio +import microcontroller +import touchio + +# Nothing pulls P_01 up, so it never charges low. +try: + touchio.TouchIn(microcontroller.pin.P_01, pull=digitalio.Pull.UP) +except ValueError as e: + print(f"pull up: {e}") + +# Leave P_02 high, as if nothing pulled it down. +with digitalio.DigitalInOut(microcontroller.pin.P_02) as pin: + pin.switch_to_input(pull=digitalio.Pull.UP) +try: + touchio.TouchIn(microcontroller.pin.P_02) +except ValueError as e: + print(f"pull down: {e}") + +print("done") +""" + + +@pytest.mark.circuitpy_drive({"code.py": NO_PULL_CODE}) +def test_touchio_missing_pull_resistor(circuitpython): + """TouchIn raises ValueError when the pin never discharges.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "pull up: No pullup on pin; 1Mohm recommended" in output + assert "pull down: No pulldown on pin; 1Mohm recommended" in output + assert "done" in output