Asked — Edited

Windows 11 Controller Python Integration

Hello @Athena,

I am currently exploring the feasibility of using a Windows 11-compatible controller to execute a Python client script that transmits XInput data to a Python server script on the ARC Skill Control Board. My objective is to manage an H-Bridge with PWM speed control to facilitate tank track movement using the Synthiam ARC platform. Do you think this setup can be achieved?

I am employing the Anbernic Win600 controller, as depicted in the image below:

User-inserted image

At present, I am endeavoring to deploy a Python-coded server on my ARC-enabled PC to process control signals. I’ve included the Python script I’m using for this configuration below.

#!/usr/bin/env python3
# Async TCP receiver that prints controller events and maintains a latest_state snapshot.

import asyncio, json, signal, sys
from collections import defaultdict

HOST = "0.0.0.0"
PORT = 5555
PRINT_EVENTS = True

latest_state = {
    "buttons": defaultdict(lambda: False),
    "axes": defaultdict(lambda: 0.0),
    "hat": {"x": 0, "y": 0},
    "t": 0.0,
}

def apply_event(ev):
    et = ev.get("type")
    latest_state["t"] = ev.get("t", latest_state["t"])

    if et == "button":
        name = ev.get("name", f"BTN{ev.get('index', -1)}")
        pressed = bool(ev.get("pressed", False))
        latest_state["buttons"][name] = pressed
        if PRINT_EVENTS:
            print(f"[BTN] {name}: {'DOWN' if pressed else 'UP'}")

    elif et == "axis":
        name = ev.get("name", f"AXIS{ev.get('index', -1)}")
        val = float(ev.get("value", 0.0))
        latest_state["axes"][name] = val
        if PRINT_EVENTS:
            print(f"[AXIS] {name}: {val:+.3f}")

    elif et == "hat":
        v = ev.get("value", {"x": 0, "y": 0})
        latest_state["hat"] = {"x": int(v.get("x", 0)), "y": int(v.get("y", 0))}
        if PRINT_EVENTS:
            x, y = latest_state["hat"]["x"], latest_state["hat"]["y"]
            dirs = []
            if y == 1: dirs.append("Up")
            if y == -1: dirs.append("Down")
            if x == -1: dirs.append("Left")
            if x == 1: dirs.append("Right")
            print(f"[HAT] DPad: {'+'.join(dirs) if dirs else 'Neutral'}")

async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    peer = writer.get_extra_info("peername")
    print(f"[server] Client connected: {peer}")
    try:
        while True:
            line = await reader.readline()
            if not line:
                break
            try:
                ev = json.loads(line.decode("utf-8"))
            except json.JSONDecodeError:
                continue
            apply_event(ev)
    except asyncio.CancelledError:
        pass
    finally:
        print(f"[server] Client disconnected: {peer}")
        try:
            writer.close()
            await writer.wait_closed()
        except Exception:
            pass

async def status_printer():
    while True:
        await asyncio.sleep(0.5)
        lx = latest_state["axes"]["LX"]; ly = latest_state["axes"]["LY"]
        rx = latest_state["axes"]["RX"]; ry = latest_state["axes"]["RY"]
        lt = latest_state["axes"]["LT"]; rt = latest_state["axes"]["RT"]
        a  = latest_state["buttons"]["A"]; b = latest_state["buttons"]["B"]
        d  = latest_state["hat"]
        print(f"[state] LX:{lx:+.2f} LY:{ly:+.2f} RX:{rx:+.2f} RY:{ry:+.2f} LT:{lt:.2f} RT:{rt:.2f} A:{int(a)} B:{int(b)} D:{d}")

async def main():
    server = await asyncio.start_server(handle_client, HOST, PORT)
    print(f"[server] Listening on {HOST}:{PORT}")
    sp = asyncio.create_task(status_printer())

    # Graceful shutdown for Windows
    stop = asyncio.Future()
    def _sig():
        if not stop.done():
            stop.set_result(True)
    loop = asyncio.get_running_loop()
    try:
        loop.add_signal_handler(signal.SIGINT, _sig)   # may raise on Windows for some signals
        loop.add_signal_handler(signal.SIGTERM, _sig)
    except (NotImplementedError, AttributeError):
        # Fallback: rely on KeyboardInterrupt
        pass

    async with server:
        await asyncio.gather(server.serve_forever(), stop)

    sp.cancel()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        sys.exit(0)

I am eager to understand how to integrate this Python script to efficiently utilize the XInput data along with the ARC platform's capabilities for controlling tank tracks via the H-Bridge with PWM speed control. The H-Bridge interfaces with an Arduino Nano running the EZB firmware. Any insights or guidance on effectively achieving this setup would be greatly appreciated. Additionally, I aim to program other buttons for various ARC skills or script triggers.


Related Hardware Arduino Genuino Uno
Related Controls PWM Slider Joystick (XInput)

ARC Pro

Upgrade to ARC Pro

Your robot can be more than a simple automated machine with the power of ARC Pro!

PRO
Canada
#1  

Below is an overview of how you can integrate your Windows 11 controller (via your Python scripts) with Synthiam ARC to control your tank tracks using an H-Bridge (via PWM control) on your Arduino Nano running EZB Firmware.


Overview of the Approach

  1. Controller to Python Server Communication

    Your Python server script (running on your ARC-enabled PC) listens on a TCP port (in your case, port 5555) for XInput events from your Windows 11 controller.

    When a controller button is pressed or an analog axis moves, your server receives the event, updates an internal state (such as duty cycle values or directional flags), and prints/logs the information.

  2. Python Server to ARC Integration

    Once the controller events are processed, you want to forward these events (or trigger actions) inside ARC.

    There are two common ways to do this:

    • Option A: Update ARC Global Variables and Call ARC Commands in a Python Script Running Within ARC
      You can create an ARC Python script (or a robot skill that supports Python) that monitors variables (like $LeftMotorPWM and $RightMotorPWM) and then calls the built-in PWM.set() function to adjust the tank motors.

    • Option B: Write an ARC robot skill with a Built-in TCP Client
      Implement a TCP client within an ARC Python script that directly connects to your Python server. Once connected, when you receive the controller events, you immediately trigger ARC commands (for example, PWM.set(d0, dutyCycle)) to update motor speeds.

  3. Mapping Controller Events to H-Bridge Motor Control

    Depending on your tank drive setup, you’ll likely have two PWM channels (one for each track).

    For example, you might assign:

    • Left track PWM to a digital port (say, d0).
    • Right track PWM to a different digital port (say, d1).

    In your ARC Python script, you could map the analog joystick values (or the trigger values) to duty cycles (0-100) and then call:

    PWM.set(d0, leftDutyCycle);
    PWM.set(d1, rightDutyCycle);
    

    ARC’s built-in sleep() (which uses milliseconds) and global variable functions (getVar()/setVar()) can help you manage timing and share state between scripts.


Step-by-Step Integration

  1. Testing Your Controller Input

    First, verify that your Anbernic Win600 controller is correctly detected and that your Python server script (provided in your example) is receiving the XInput events.

    You can use the printed output (for example, verifying the values for LX, LY, triggers, and DPad events) to ensure the mapping is as you expect.

  2. Mapping Input to PWM Duty Cycle

    Once you have confirmed the controller input is received, decide how you wish to map the analog inputs (e.g., left joystick X and Y, triggers) to the motor speeds.

    For instance, a simple mapping might mean:

    // Pseudocode using the ARC PWM command:
    // Map analog values (e.g., from -1.0 to 1.0) to PWM duty cycle (0 to 100)
    var leftDutyCycle = (LX_value + 1) * 50;  // where LX_value from left joystick X axis
    var rightDutyCycle = (RX_value + 1) * 50; // using right joystick or complementary control
    PWM.set(d0, leftDutyCycle);
    PWM.set(d1, rightDutyCycle);
    

    You can adjust the mapping depending on whether you want differential steering (one motor increasing speed and the other decreasing).

  3. Integrating With ARC Python Scripting

    ARC allows you to run Python scripts that can call functions like PWM.set() and update global variables with setVar()/getVar().

    For example, create an ARC Python script (inside a Script Skill) with code similar to:

    import asyncio
    
    # Example function to update PWM using ARC's Python API
    def update_pwm(left_value, right_value):
        # Map your left_value and right_value (which could be between -1 and 1) to 0-100 range
        left_duty = int((left_value + 1) * 50)   # (0 to 100)
        right_duty = int((right_value + 1) * 50)   # (0 to 100)
    
        # Assuming d0 and d1 are the PWM ports for left and right motors
        PWM.set(d0, left_duty)
        PWM.set(d1, right_duty)
    
        # Optionally print the values for debugging:
        print("Left PWM:", left_duty, "Right PWM:", right_duty)
    
    # Example integration loop (you need to adapt this to your async setup)
    async def main_loop():
        while True:
            # For demonstration, here we would retrieve variables updated by your client
            # For example, assume getVar("$LeftStick") returns a value between -1.0 and 1.0
            left_value = float(getVar("$LeftStick", 0))
            right_value = float(getVar("$RightStick", 0))
            update_pwm(left_value, right_value)
            sleep(100)  # wait 100ms between updates
    
    # Run the loop
    asyncio.run(main_loop())
    

    Modify the above code to fit your mapping strategy. The basic idea is to update ARC with PWM commands based on the incoming controller data.

  4. Mapping Additional Buttons

    For extra functions (such as triggering other ARC skills or script events based on button presses), you can similarly use ARC’s commands.

    For example, if button A is pressed, you might call a command via:

    if getVar("$ButtonA") == "True":
        controlCommand(Camera, StartCamera)  # As an example command
    

    Define a mapping from your controller button names (as processed in your Python server) to global variables inside ARC, and then have your ARC Python script or Script Skill poll and react.

  5. Running the Integrated Setup

    Ensure that both the external Python server and your ARC Python integration are running on the same PC (or have a reliable network connection if on separate systems).

    Test the entire flow:

    • Verify the controller input.
    • Check that the Python server updates the state.
    • Confirm that ARC receives the state (either directly via a built-in TCP client or via shared global variables) and sends the correct PWM.set() commands to control your motors properly.

Additional Considerations

  • Direct ARC XInput Skill:
    Before building a completely custom solution, consider that ARC already includes a Joystick (XInput) Skill. This native skill may handle mapping of analog values and button presses, and you might be able to modify or extend it (or merely use some of its functionality) to suit your tank track control needs.

  • Creating Custom Robot Skills:
    If you need features not offered in default ARC skills, you can create your own robot skill following the guide here:
    Create robot skill Overview

  • Documentation and Resources:
    Synthiam ARC Downloads
    PWM Python API Documentation
    Python API Overview


Final Thoughts and Questions

Yes, it is feasible to use your Windows 11 controller with your Python server to handle XInput and then integrate with Synthiam ARC for controlling the tank tracks via PWM to an H-Bridge. The key points are:

  • Receiving and parsing your controller events correctly.
  • Mapping these events to PWM duty cycles.
  • Calling ARC Python functions (like PWM.set) to effect the changes on your motors.
  • Optionally, mapping additional button inputs to trigger other ARC skills.

Do you have any questions on any specific part of this setup? For example, would you like more detailed guidance on the TCP/client integration with ARC Python, or further help on mapping the controller's analog values to PWM duty cycle percentages? Feel free to ask!


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.