add dora-joy node publishing gamepad axes and buttons
validate / validate (pull_request) Successful in 58m42s
validate / test (pull_request) Successful in 58m47s
validate / markdownlint (pull_request) Successful in 58m40s

This commit is contained in:
2026-09-16 05:10:33 -05:00
parent 9f56aa8f75
commit 3624c63ead
24 changed files with 1478 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
"""The dora event loop: one reading published per tick.
Every tick, changed or not. That is deliberate — the stream is also a heartbeat,
and it is what lets a consumer tell "the pad is being held still" from "this node
died". A consumer driving anything that moves needs both answers, and only one of
them can be inferred from silence.
Nothing here decides what a reading means. The pad is read in
:mod:`dora_joy.gamepad` and packed in :mod:`dora_joy.message`, and that is the
whole node.
"""
import argparse
from dora import Node
from dora_joy import message
from dora_joy.config import Config
from dora_joy.gamepad import Gamepad
from dora_joy.log import log
def main() -> None:
"""Start reading the pad and publish it until the tick closes."""
args = _parse_args()
config = Config.from_env()
# Passing the name lets the node also run standalone as a dynamic node; when
# dora spawns it, the id from the dataflow wins.
node = Node(args.name)
gamepad = Gamepad(config.joystick_index)
gamepad.start()
log(f"publishing joystick {config.joystick_index} on every tick")
present = None
try:
for event in node:
if event["type"] == "ERROR":
raise RuntimeError(event["error"])
if event["type"] == "INPUT_CLOSED" and event.get("id") == "tick":
break
if event["type"] != "INPUT" or event["id"] != "tick":
continue
sample = gamepad.read()
# A pad arriving or going away is worth one line each; the readings
# themselves are on the wire for anyone who wants them.
if sample.present != present:
present = sample.present
log("gamepad connected" if present else "no gamepad")
node.send_output("joy", message.pack(sample))
finally:
gamepad.shutdown()
def _parse_args() -> argparse.Namespace:
"""Read the node name, for when dora did not spawn us."""
parser = argparse.ArgumentParser(description="Publish a gamepad's axes and buttons into a dataflow.")
parser.add_argument(
"--name",
default="joy",
help="The name of the node in the dataflow, used when running as a dynamic node.",
)
return parser.parse_args()
if __name__ == "__main__":
main()