Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 29 additions & 20 deletions ports/zephyr-cp/cptools/build_circuitpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions ports/zephyr-cp/tests/test_keypad.py
Original file line number Diff line number Diff line change
@@ -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)
100 changes: 100 additions & 0 deletions ports/zephyr-cp/tests/test_touchio.py
Original file line number Diff line number Diff line change
@@ -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
Loading