Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
05241c15dThis file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python
"""Live readout of what the Pi sees from each Joy-Con.
For every connected Joy-Con it prints, on each button edge, the raw button index,
its physical name, AND the mapped teleop function from gamepad_profile.json
(e.g. "DPAD_RIGHT (btn10) -> grip_open"). Sticks print the mapped xy_x/xy_y while
deflected. Ctrl-C to stop.
Run:
.venv/bin/python -m tomctl.input.joycon_monitor
(SDL env for the no-udev pip SDL is set automatically.)
"""
import json
import os
import time
from pathlib import Path
os.environ.setdefault("SDL_JOYSTICK_HIDAPI", "0")
os.environ.setdefault("SDL_JOYSTICK_DEVICE", "/dev/input/js0:/dev/input/js1")
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
import pygame # noqa: E402
# Physical button names per shell (decoded from the kernel; SDL = ascending keycode order).
NAMES_L = {0: "Capture", 1: "SL", 2: "SR", 3: "ZL", 4: "ZR", 5: "minus",
6: "stick_click", 7: "DPAD_UP", 8: "DPAD_DOWN", 9: "DPAD_LEFT", 10: "DPAD_RIGHT"}
NAMES_R = {0: "B", 1: "A", 2: "X", 3: "Y", 4: "SL", 5: "SR", 6: "ZR", 7: "ZL",
8: "plus", 9: "home", 10: "stick_click"}
from tomctl import ASSETS # noqa: E402
PROFILE = ASSETS / "gamepad_profile.json"
def load_bindings_for(name, profile):
"""Match a connected controller name to a profile entry (by name substring, like the runtime)."""
for role, spec in profile.get("controllers", {}).items():
if (spec.get("name") or "").lower() in name.lower():
return role, spec.get("bindings", {})
return None, {}
def main():
profile = json.loads(PROFILE.read_text()) if PROFILE.exists() else {"controllers": {}, "motion": {}}
deadzone = float(profile.get("motion", {}).get("deadzone", 0.15))
pygame.init()
pygame.joystick.init()
n = pygame.joystick.get_count()
if n == 0:
print("No controllers detected. Run: bash reconnect_joycons.sh")
return
pads = []
print(f"{n} controller(s):")
for i in range(n):
js = pygame.joystick.Joystick(i)
js.init()
name = js.get_name()
role, bindings = load_bindings_for(name, profile)
names = NAMES_L if "(L)" in name else NAMES_R
btn2fn = {b["index"]: fn for fn, b in bindings.items() if b.get("type") == "button"}
pads.append({"js": js, "name": name, "role": role, "bindings": bindings,
"names": names, "btn2fn": btn2fn, "prev": {}, "last_stick": 0.0})
print(f" [{i}] {name} -> profile role: {role or '(unmapped!)'}")
print("\nPress buttons / move sticks. Ctrl-C to stop.\n")
def axis_val(js, b):
if not b or b.get("type") != "axis":
return 0.0
v = js.get_axis(b["index"])
return -v if b.get("invert") else v
try:
while True:
pygame.event.pump()
now = time.time()
for p in pads:
js, name, names = p["js"], p["name"], p["names"]
tag = f"{name:12}"
# button edges
for b in range(js.get_numbuttons()):
v = js.get_button(b)
if v != p["prev"].get(b, 0):
p["prev"][b] = v
phys = names.get(b, f"btn{b}")
fn = p["btn2fn"].get(b)
mapped = f"-> {fn}" if fn else "(unmapped)"
state = "DOWN" if v else " UP "
print(f"[{tag}] {state} {phys:12} (btn{b:>2}) {mapped}")
# stick (mapped, throttled while deflected)
x = axis_val(js, p["bindings"].get("xy_x"))
y = axis_val(js, p["bindings"].get("xy_y"))
if max(abs(x), abs(y)) > deadzone and now - p["last_stick"] > 0.25:
p["last_stick"] = now
print(f"[{tag}] STICK xy_x={x:+.2f} xy_y={y:+.2f}")
time.sleep(0.02)
except KeyboardInterrupt:
print("\nStopped.")
if __name__ == "__main__":
main()