Skip to content

Python: basic pybind11 interface to the microcontroller - #1612

Open
pa-devbot wants to merge 54 commits into
mainfrom
python-pybind-basic
Open

pa-devbot wants to merge 54 commits into
mainfrom
python-pybind-basic

Conversation

@pa-devbot

@pa-devbot pa-devbot commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Basic Python interface using pybind11, so a Python developer can send simple commands to the microcontroller. Used PR #1519 as a reference, but kept this to the small, well-understood part of it (no MCP server, video or OCR).

What's in it

  • _pa_core (new, SerialPrograms/Source/PythonBindings/PythonBindings_Module.cpp): a pybind11 module that wraps the existing CoreLib class NintendoSwitch::PybindSwitchProController — a thin 1:1 binding:
    • Controller(port) + wait_for_ready(), is_ready(), current_status(), controller_name()
    • Queued commands: push_button(), push_dpad(), push_left_joystick(), push_right_joystick(), controller_state(), wait(), plus wait_for_all_requests() (flush) and cancel_all_and_wait(timeout_ms) (cancel + wait for the device to confirm the neutral state, returns a bool)
    • BUTTONS / DPAD_POSITIONS constants generated from the C++ enums
    • Logging controls (set_log_stderr(), set_log_file(), log(), recent_logs()) — nothing goes to stdout
    • C++ PokemonAutomation::Exception -> Python RuntimeError; blocking calls release the GIL
  • pokemon_automation Python package on top of it:
    • SwitchController with button names instead of bitfields: sw.press("L+R"), sw.press("up", repeat=2), sw.stick("left", "up", duration_ms=1500), sw.hold("B", left="up"), sw.flush(), sw.cancel_all_and_wait(); close() cancels with confirmation first
    • buttons.py (name parsing, verified against the C++ enums), devices.list_serial_ports(), fake.FakeController for tests without hardware, python -m pokemon_automation.selftest [port] for a quick hardware check
    • 26 pytest tests; pip install -e SerialPrograms/Source/PythonBindings works
  • CMake: new PA_PYTHON_BINDINGS option (OFF by default, so existing builds/CI are unaffected), wired into both the full and PA_CORE_ONLY builds. pybind11 is picked up from a system package or pip install pybind11, with a pinned FetchContent fallback.
  • Build fix for the miniz warnings: the third-party 3rdParty-Core/miniz-3.1.1/miniz.c is compiled into every target that uses CORE_LIBRARY_SOURCES — CoreLib in both build modes, and SerialProgramsLib in the full build. It now gets a per-file warning suppression (/W0 on MSVC, -w on GCC/Clang) set in the base CMakeLists.txt before the PA_CORE_ONLY early return (the old copy from File Unzip using miniz library #1098 sat below it, so core-only builds never got it; source properties are global, so one setting covers all targets). It includes its own header from its own directory, so no SYSTEM include directory can shield it, and if(MSVC) / COMPILE_OPTIONS replace the old if(MSVC)-with-COMPILE_FLAGS form.

Example

from pokemon_automation import SwitchController

with SwitchController("/dev/ttyACM0") as sw:
    print(sw.status())
    sw.press("A")
    sw.press("L+R", hold_ms=200)
    sw.stick("left", "up", duration_ms=1500)
    sw.flush()

Verified (aarch64 Linux)

  • Fresh core-only configure: miniz.c's compile line ends in -w; module builds clean
  • Fresh full-build configure: both CoreLib's and SerialProgramsLib's miniz.c compile lines carry the suppression
  • Python suite: 26 passed (including module tests; skipped gracefully when _pa_core isn't built)
  • (Previous round: full GUI build 1600/1600 and all 5,546 C++ command-line tests passed)

Notes / follow-ups

  • A CI workflow for this (core-only build + pytest on Ubuntu) is ready locally but the bot's token can't push .github/workflows/ files (no workflow scope) — a maintainer can add it:

    # .github/workflows/cpp-ci-python-bindings.yml
    - cmake .. -DPA_CORE_ONLY=ON -DPA_PYTHON_BINDINGS=ON
    - python3 -m pytest -q   # in SerialPrograms/Source/PythonBindings
  • PR Agent mcp server #1519 extends this with the full package (MCP server, video, OCR) — this PR is meant to land as the understood foundation for that.

@jw098
jw098 marked this pull request as draft October 19, 2026 04:42
Adds _pa_core, a pybind11 module (new PA_PYTHON_BINDINGS cmake option, OFF
by default) that wraps the existing CoreLib class
NintendoSwitch::PybindSwitchProController so Python can act as a Nintendo
Switch controller through a PABotBase2 serial device: status, queued button /
d-pad / stick commands, flush and cancel, button/d-pad constants, and
log-to-stderr/file controls. C++ exceptions surface as RuntimeError;
blocking calls release the GIL.

The pokemon_automation Python package sits on top of it:
SwitchController (button names instead of bitfields), button parsing,
list_serial_ports(), a hardware-free FakeController, a hardware self-test,
and a pytest suite. See SerialPrograms/Source/PythonBindings/README.md.

A new CI workflow builds the GUI-free core plus the module on Ubuntu and
runs the pytest suite. PR #1519 is used as a reference.
@jw098
jw098 force-pushed the python-pybind-basic branch from 23b3635 to 871232d Compare October 19, 2026 05:37
…ng CoreLib

The rebased branch picked up AbstractController::cancel_all_and_wait()
(cancel + wait for the device to acknowledge the neutral state, returns a
bool), which replaced the old fire-and-forget cancel_all_commands(). Update
the _pa_core binding, the pokemon_automation API (cancel_all_and_wait() now
returns whether the device confirmed; close() uses it to guarantee a neutral
state), the fake controller, the tests, and the docs.

Also compile the third-party 3rdParty-Core/miniz-3.1.1/miniz.c with warnings
off (/W0 on MSVC, -w on GCC/Clang): it is compiled straight into CoreLib, and
newer compilers emit warnings in it that -Werror / /WX turn into build errors.
It includes miniz.h from its own directory, so a SYSTEM include directory
cannot shield it; suppressing the file itself is the portable fix.
…ties()

The old copy in CMakeLists.txt only runs in the full build: with
PA_CORE_ONLY the configure returns before reaching it, so miniz.c was still
compiled with -Werror / /WX in core-only builds. Setting the source property
from pa_apply_base_target_properties() (cmake/CoreLib.cmake) covers both build
modes and every target that uses it; the two now-redundant copies are removed.
Use if(MSVC) instead of if(WIN32) - the flag choice follows the compiler's
command-line dialect (true for cl and for clang-cl), and COMPILE_OPTIONS
instead of the deprecated COMPILE_FLAGS.
…e PA_CORE_ONLY

miniz.c is compiled into every target that uses CORE_LIBRARY_SOURCES -
CoreLib in both build modes, and SerialProgramsLib in the full build (which
compiles its own copy of the core sources), so the fix belongs in the base
CMakeLists.txt, not CoreLib.cmake. It must also run before the PA_CORE_ONLY
early return, which is where the old copy at the bottom of the file (from
#1098) was, and why core-only builds still failed. Source properties are
global, so one set_source_files_properties() covers all targets; verified
that in a full build both CoreLib's and SerialProgramsLib's miniz.c compile
lines carry the suppression.
@jw098
jw098 marked this pull request as ready for review October 19, 2026 17:43
@jw098
jw098 marked this pull request as draft October 19, 2026 17:43
jw098 and others added 4 commits October 19, 2026 12:20
- 'simulatenously' -> 'simultaneously'
- selftest code block: [port] is not shell syntax; show the optional
  argument as a real example instead
- rename 'Running the test file' to 'Hardware self-test' (the pytest suite
  lives in tests/) and add the missing 'Running the unit tests' section,
  which the Layout section advertises but nothing in the file explained
- add wait() to the method list (it is used in the example)
- drop trailing whitespace
@jw098
jw098 marked this pull request as ready for review October 19, 2026 20:00
@jw098
jw098 marked this pull request as draft October 19, 2026 20:29
jw098 and others added 14 commits October 19, 2026 16:22
Add comments naming the C++ source each Python definition mirrors or
depends on (repo-relative paths): the BUTTON_BITS/DPAD_POSITIONS constants
and check_against_core() vs the enums in
NintendoSwitch_ControllerButtons.h; the ControllerBackend protocol,
status/flush/cancel_all_and_wait and press() vs
PybindSwitchProController / AbstractController; the fake and the _core
loader vs the module source and the wrapped class; and the test headers.
Also note that DEFAULT_HOLD_MS/DEFAULT_RELEASE_MS are Python-side defaults
(the C++ issue_buttons() takes explicit durations).
The command methods of SwitchController now match the names of the
_p a_core.Controller methods they wrap (and thus the C++
PybindSwitchProController methods):

- press()       -> push_button() / push_dpad() (mixed combos now go to
                   controller_state(); each method rejects the other kind
                   with a pointer to the right method)
- hold()        -> controller_state() (duration_ms first, like the C++
                   signature; no implicit release wait)
- stick(side)   -> push_left_joystick() / push_right_joystick()
- flush()       -> wait_for_all_requests()
- status()      -> current_status()
- name()        -> controller_name()
- wait(), cancel_all_and_wait(), is_ready() and close() already matched.

Button-name parsing is unchanged; _pa_core and FakeController are
untouched. Updated examples in the package docstrings, README,
switch_connection_test.py and the tests (31 tests now, including
rejection of mixed inputs in push_button()/push_dpad()).
Each call now queues exactly one tap, matching the single-command
semantics of the C++ layer; repeated presses are just repeated calls
(or a controller_state() with a longer hold).
jw098 and others added 13 commits October 21, 2026 17:06
Drop the FetchContent fallback. If pybind11 is not found (system
package or pip-installed for the target interpreter), configure now
fails with a FATAL_ERROR that names the interpreter and gives the
install commands, instead of silently git-cloning a pinned pybind11
release during configure. Builds no longer reach the network on their
own. README updated to match.
- controller.py: cancel_all_and_wait() docstring cited the old C++\n  parameter name (timeout_millis -> timeout_ms).\n- README: 'insid' -> 'inside', 'run the all tests' -> 'run all the tests',\n  queue examples used a nonexistent duration kwarg (now hold_ms, which is\n  what push_left_joystick() actually takes), and mkdir bin -> mkdir -p bin\n  so the instructions work on a re-run.\n- test_push_right_joystick now actually passes a [x, y] list, matching its\n  docstring (lists are accepted via parse_stick_pair).
close() no longer lets a command slip in after the cancel: the closed
flag is set under the command lock, and every command method re-checks it
while holding the lock before sending, so commands starting while the
cancel is in flight are rejected.
@jw098
jw098 force-pushed the python-pybind-basic branch from a9db077 to b1698ea Compare October 22, 2026 22:04
jw098 added 2 commits October 22, 2026 15:26
…eturns

nullptr for an empty name, leaving m_connection null and crashing
current_status(). The constructor, destructor, controller() and
current_status() now tolerate a null connection ("No connection."), and
SwitchController rejects an empty port up front with a ValueError.
#1 Destruction race (PybindSwitchController.cpp): the connection thread is
joined only when m_connection is destroyed, i.e. after m_controller, m_cv
and m_lock already were. Destroying the controller while a connection is
still establishing (e.g. SwitchController's timeout path) let the connect
thread write to destroyed members. The destructor now resets m_connection
first, so the thread is joined before any member is destroyed.

#2 Empty port name: SerialPABotBase2_Descriptor::open_connection() returns
nullptr for an empty name, leaving m_connection null and crashing
current_status(). The constructor, destructor, controller() and
current_status() now tolerate a null connection ("No connection."), and
SwitchController rejects an empty port up front with a ValueError.
@jw098
jw098 force-pushed the python-pybind-basic branch from 1b6d376 to 3d248db Compare October 22, 2026 22:57
@jw098
jw098 marked this pull request as ready for review October 23, 2026 01:09
@jw098
jw098 requested review from Gin890 and Mysticial October 23, 2026 01:10
@Mysticial

Copy link
Copy Markdown
Collaborator

I've looked through about half the files so far. Still working on the rest.

I'm wondering if we should rename SwitchController -> SwitchProController to leave room for the joycons should we ever decide to expose that to Python as well.

pa-devbot and others added 3 commits October 24, 2026 17:56
Matches the C++ class it wraps (PybindSwitchProController) and the
device's controller type (the Pro Controller / wired controller).
No backwards-compat alias: the package is pre-release (0.1.0).
START/SELECT/L3/LS/R3/RS and the "+"/"-" spellings no longer map to
PLUS/MINUS/LCLICK/RCLICK. "+" remains a separator (a lone "+" parses to
no input), and "-" and the former alias names are now unknown buttons
with the usual ValueError. Removed the now-dead "+"/"-" special cases
in _split()/parse_buttons(), the __init__ export, and the docs; the alias
test now pins the new behavior.

@Mysticial Mysticial left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at a high level. Rubber stamping most of the Python details. API seems ok.

We'll want to follow up later with a Python TurboA or something just to feel it out before we commit to the API. But at least we're not a in a hurry, gen10 is a year away.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants