Anyone had luck reading from a gamepad?

I thought I might play around a little and see if I could read in a gamepad such as
a PS3 or PS4 or XBox One... I thought I would start off with USB connection.

I did some searching about how interface to them using Python, and it looked
like pygame might be the answer?

Using Xbox One Controller with Pygame in Python 3 – DNMTechs – Sharing and Storing Technology Knowledge

So I thought I would try setting up to use Python without ... And followed @ptillish
instructions.

I hacked up the python code that was just doing blink.

pyproject.toml  python/
arduino@Uno-Q4:~/gamepad_test$ cat python/main.py
# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
#
# SPDX-License-Identifier: MPL-2.0

from arduino.app_utils import *
import time
import pygame
import inputs

led_state = False

pygame.init()
pygame.joystick.init()
joystick_count = pygame.joystick.get_count()
print(joystick_count)
joystick = pygame.joystick.Joystick(0)
joystick.init()

def loop():
    global led_state

    for event in pygame.event.get() :
        if event.type == pygame.JOYAXISMOTION :
            if event.axis == 0 :
                print("Left stick X: {}".format(event.value))
            elif event.axis == 1:
                print("Left stick y: {}".format(event.value))
        elif event.type == pygame.JOYBUTTONDOWN :
            print("button down: {}".format(event.button))

    #time.sleep(1)
    #led_state = not led_state
    #Bridge.call("set_led_state", led_state)

App.run(user_loop=loop)

When I tried it with a XBox one controller, it failed in that count of joysticks was 0.
With a PS3, it goes through there, but I am not getting any events.

But then again Python is not my native language.
Will also check with a few different controllers.

Suggestions?

EDIT: I am getting some input from PS4 controller so will start there

I got an Xbox One controller working via Bluetooth with the Uno Q running in SBC mode.

One quick check is to open the browser (Chromium?) to Hardware Tester. Press buttons and stick to see if the web app recognizes the gamepad.

Another quick test is to install jstest-gtk as in sudo apt install jstest-gtk.
Run it to get a joystick/gamepad tester that works without an Internet connectio
n.

I did not try to read the gamepad from inside docker.

Thanks, I started up the SBC again this morning, and then used the bluetooth app to link to the xbox one, I then started up the python
uv run python/main.py
And sure enough it was able to then talk to the XBox one and was able to see the one sticks movements and buttons pressed... now to play with passing them along

(post deleted by author)

I decided to have some fun with it again. Wondered about getting the joystick data up to the Arduino side, so I got that working today... So far just for testing...

If anyone wants to play along, current PIP is up at:
Arduino_UNO_Q/gamepad_test at main · KurtE/Arduino_UNO_Q

Note: Readme could use a bit of work, so far just put in the command lines to remind me how to build and install the sketch and how to start up the python with the uv run command...

Here is a snapshot of it:
Python:

# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
#
# SPDX-License-Identifier: MPL-2.0

from arduino.app_utils import *
import time
import pygame
import inputs

led_state = False

# This dict can be left as-is, since pygame will generate a
# pygame.JOYDEVICEADDED event for every joystick connected
# at the start of the program.
joysticks = {}

pygame.init()
pygame.joystick.init()
joystick_count = pygame.joystick.get_count()
print(joystick_count)
#joystick = pygame.joystick.Joystick(0)
#joystick.init()

def loop():
    global led_state
    event_processed = False
    #axis_motions: list[int] = [65536, 65536, 65536, 65536, 65536, 65536]
    axis_motion_list: list[int] = []

    for event in pygame.event.get() :
        event_processed = True
        if event.type == pygame.JOYAXISMOTION :
            axis_motion_list.append(event.axis)
            axis_motion_list.append(int(event.value * 32767))
            #axis_motions[event.axis] = event.value
            if event.axis == 0 :
                print("Left stick X: {}".format(event.value))
            elif event.axis == 1:
                print("Left stick y: {}".format(event.value))
            elif event.axis == 2:
                print("Right stick x: {}".format(event.value))
            elif event.axis == 3:
                print("Right stick y: {}".format(event.value))
            else :
            	print("Other axis {}: {}".format(event.axis,event.value))
        elif event.type == pygame.JOYHATMOTION :
            Bridge.notify("joy_hat_motion", event.value[0], event.value[1])
            print("Hat {}: {}".format(event.hat, event.value))
        elif event.type == pygame.JOYBUTTONDOWN :
            Bridge.notify("joy_button_down", event.button)
            print("button down: {}".format(event.button))
        elif event.type == pygame.JOYBUTTONUP :
            Bridge.notify("joy_button_up", event.button)
            print("button up: {}".format(event.button))
        # Handle hotplugging
        elif event.type == pygame.JOYDEVICEADDED:
            # This event will be generated when the program starts for every
            # joystick, filling up the list without needing to create them manually.
            Bridge.notify("joy_device_added", event.device_index)
            joy = pygame.joystick.Joystick(event.device_index)
            joysticks[joy.get_instance_id()] = joy
            print(f"Joystick {joy.get_instance_id()} connencted")
        
        elif event.type == pygame.JOYDEVICEREMOVED:
            Bridge.notify("joy_device_removed", event.instance_id)
            del joysticks[event.instance_id]
            print(f"Joystick {event.instance_id} disconnected")

        else :
            print("Other Event: {}".format(event))
    if event_processed :
        if len(axis_motion_list) :
            Bridge.notify("joy_axis_motion", axis_motion_list)
        print("---")
    #time.sleep(1)
    #led_state = not led_state
    #Bridge.call("set_led_state", led_state)

App.run(user_loop=loop)

And the sketch:

// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
//
// SPDX-License-Identifier: MPL-2.0

#include "Arduino_RouterBridge.h"
#include <vector>

uint32_t last_led_update_time = 0;
#define BLINK_TIME 500

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);

    Bridge.begin();
    Monitor.begin();
    Bridge.provide("joy_button_down", joy_button_down);
    Bridge.provide("joy_button_up", joy_button_up);
    Bridge.provide("joy_axis_motion", joy_axis_motion);
    Bridge.provide("joy_hat_motion", joy_hat_motion);
    Bridge.provide("joy_device_added", joy_device_added);
    Bridge.provide("joy_device_removed", joy_device_removed);
}

void loop() {
    if ((millis() - last_led_update_time) >= BLINK_TIME) {
        digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
        last_led_update_time = millis();
    }
}

void joy_button_down(int btn) {
    printk("BDN: %d\n", btn);
}

void joy_button_up(int btn) {
    printk("BUP: %d\n", btn);
}

void joy_axis_motion(std::vector<int> motions) {
    for (int i = 0; i < motions.size(); i += 2) {
        printk("%d:%d ", motions[i], motions[i+1]);
    }
    printk("\n");
}

 void joy_hat_motion(int x, int y) {
    printk("Hat: %d %d\n", x, y);
 }

void joy_device_added (int index) {
    printk("\nJoystick device added: %d\n", index);
}
void joy_device_removed (int index) {
    printk("\nJoystick device removed: %d\n", index);
}

Some python output from run:

arduino@Uno-q2:~/gamepad_test$ uv run python/main.py
pygame 2.6.1 (SDL 2.28.4, Python 3.13.5)
Hello from the pygame community. https://www.pygame.org/contribute.html
0
======== App is starting ============================
2026-04-19 20:11:50.615 INFO - [MainThread] App:  App started
Other Event: <Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 0})>
---
Joystick 0 connencted
---
button down: 12
---
button up: 12
---
button down: 4
---
button up: 4
---
button down: 3
---
button up: 3
---
button down: 1
---
button up: 1
---
Hat 0: (0, 1)
---
Hat 0: (0, 0)
---
Left stick y: -0.005554368724631489
Left stick y: 0.027802362132633443
---
Left stick y: 0.032654805139317
---
Left stick y: 0.03451643421735282
---
Left stick y: 0.029389324625385297
---
Left stick y: 0.027436140018921477
---
Left stick y: 0.023194067201757866
---
Left stick y: 0.021454512161626027
---
Left stick y: 0.017120883816034424
---
Left stick y: 0.015381328775902585
---
Left stick y: 0.03488265633106479
---
Left stick X: 0.022339548936429945
Left stick X: 0.044709616382335886
---
Left stick X: 0.04934842982268746
---
Left stick X: 0.053529465620899074
Left stick y: 0.03088473158970916
---
Left stick X: 0.05526902066103091
Left stick y: 0.02359080782494583
---
Left stick y: -0.010162663655507066
---
Left stick y: -0.015900143436994536
---
Left stick y: -0.032227546006653035
---
Left stick y: -0.03842280343028046
---
Left stick y: -0.05694753868221076
---
Left stick X: 0.059297463911862545
Left stick y: -0.11700796533097324
---
Left stick X: 0.06457716605121006
Left stick y: -0.1567430646687216
---
Left stick X: 0.0913113803521836
Left stick y: -0.18939786980803858
---
Left stick X: 0.10901211584826197
Left stick y: -0.2141178624835963
---
Left stick X: 0.1141392254402295
Left stick y: -0.24414807580797754
---
Left stick X: 0.1882381664479507
Left stick y: -0.3395184179204688
---
Left stick X: 0.2087466048158208
Left stick y: -0.36423841059602646
---
Left stick X: 0.23483993041779838
Left stick y: -0.42429883724478895
---
Left stick X: 0.2599871822260201
Left stick y: -0.48432874538407544
---
Left stick X: 0.26557206946012757
Left stick y: -0.56556901760918
---
Left stick X: 0.28235724967192605
Left stick y: -0.6750694296090579
---
Left stick X: 0.3075350199896237
Left stick y: -0.73863948484756
---
Left stick X: 0.3447981200598163
Left stick y: -0.7942747276223029
---
Left stick X: 0.36997589037751394
Left stick y: -0.8313547166356395
---
Left stick X: 0.40537736136967073
Left stick y: -0.8613849299600208
---
Left stick X: 0.45011749626148256
Left stick y: -0.9072847682119205
---
Left stick X: 0.45570238349559006
---
Left stick X: 0.46128727072969755
---
Left stick y: -0.8825647755363628
---
Left stick X: 0.46687215796380505
Left stick y: -0.8128299813837092
---
Left stick X: 0.47105319376201665
Left stick y: -0.7651295510727256
---
Left stick y: -0.6944792016357921
---
Left stick y: -0.620319223609119
---
Left stick y: -0.5682241279335917
---
Left stick y: -0.3845637379070406
---
Left stick y: -0.32715842158268993
---
Left stick y: -0.22296823023163548
---
Left stick y: -0.12936796166875209
---
Left stick y: -0.02868739890743736
---
Left stick y: 0.21326334421826837
---
Left stick y: 0.3280434583574938
---
Left stick y: 0.42429883724478895
---
Left stick y: 0.5152439954832606
---
Left stick y: 0.5850093081453902
---
Left stick X: 0.47233497116000855
Left stick y: 0.7183446760460219
---
Left stick X: 0.46586504715109717
Left stick y: 0.7536545915097507
---
Left stick X: 0.4174932096316416
---
Left stick X: 0.37183751945554977
Left stick y: 0.7086092715231788
---
Left stick X: 0.2786339915158544
Left stick y: 0.5267189550462356
---
Left stick X: 0.026062807092501604
Left stick y: 0.0004272591326639607
---
Left stick X: 0.024201178014465773
Left stick y: -0.0011291848506118961
---
Left stick y: -0.0029602954191717277
---
Left stick y: -0.007446516312143315
---
Left stick y: -0.00924710837122715
---
Left stick y: -0.013245033112582781
---
Left stick X: -0.01120029297769097
Right stick x: 0.021698660237434005
Right stick x: 0.04031495101779229
---
Left stick X: -0.017273476363414413
Right stick x: 0.06338694418164617
---
Left stick X: -0.1295815912350841
Right stick x: 0.1369975890377514
---
Left stick X: -0.23114719077120274
Left stick y: -0.01834162419507431
Right stick x: 0.22214423047578355
---
Left stick X: -0.3532517471846675
Left stick y: -0.02264473403118992
Right stick x: 0.325907162694174
---
Left stick X: -0.4641560106204413
Left stick y: -0.02655110324411756
Right stick x: 0.42701498458815274
---
Left stick X: -0.6020996734519486
Right stick x: 0.5494247260963775
Right stick y: 0.03247169408246101
Right stick y: 0.007019257179479354
---
Left stick X: -0.859309671315653
Right stick x: 0.7844477675710319
Right stick y: -0.0684835352641377
---
Left stick X: -1.000030518509476
Right stick x: 0.869594409009064
Right stick y: -0.10623493148594623
---
Right stick x: 0.9937742240668965
Right stick y: -0.1554002502517777
---
Right stick x: 0.9953306680501725
Right stick y: -0.2054506057924131
---
Right stick y: -0.20676290169988099
---
Right stick y: -0.23880733664967804
---
Right stick y: -0.24628437147129734
---
Left stick X: -0.9988708151493881
---
Left stick X: -0.695303201391644
Left stick y: -0.03250221259193701
---
Left stick X: -0.01120029297769097
Left stick y: -0.03811761833552049
Right stick x: 0.8953215124973296
---
Left stick X: 0.00836207159642323
Left stick y: -0.002197332682271798
Right stick x: -0.0696432386242256
Right stick y: 0.016693624683370465
---
Left stick X: 0.01022370067445906
Right stick x: 0.003967406231879635
Right stick y: 0.021057771538438064
---
button down: 12
---
button up: 12
---
Left stick X: 0.022339548936429945
Left stick y: -0.005554368724631489
Right stick x: 0.021698660237434005
Right stick y: 0.03247169408246101
Joystick 0 disconnected
---

And output on Serial1 from Arduino sketch (printk)

Joystick device added: 0
BDN: 12
BUP: 12
BDN: 4
BUP: 4
BDN: 3
BUP: 3
BDN: 1
BUP: 1
Hat: 0 1
Hat: 0 0
1:-182 1:911
1:1070
1:1131
1:963
1:899
1:760
1:703
1:561
1:504
1:1143

Next up, hook up Robotis Dynamixel shield and maybe put on Turtle Burger and see if I can drive the wheels with the joystick input...

Mainly just having some fun.

Thanks @KurtE - will have to give it a try tomorrow. Will probably take me a while to think about it :slight_smile:

Thanks @Merlin513,

As per question on other input, I was able to get it to build directly
within Applab, but so far I have not been able to get any input from
the remote...

Wondering if the dock stuff is not allowing input to the joystick...
@ptillisch any ideas on this?

I was wondering about if the main function was being called so thought about maybe blinking one of the LEDS, that is connected to the Linux side.
Not sure how to do it.

I did find a library for some of this:
misaz/unoqgpio: Python library allowing controlling digital ports of Arduino Uno Q from python script without need to communicate between MPU and MCU.

But again it looks like from the Readme that it won't work within Applab...

You should be able to do it via the Leds class of the arduino.app_utils Python package, which is preinstalled in the container of the Arduino App's Python script. There is an demo App script here:

https://docs.arduino.cc/tutorials/uno-q/user-manual/#rgb-leds:~:text=2%20in%20red-,Remember%20to%20create%20a%20new%20App%20inside%20Arduino%20App%20Lab%20and%20then%20copy%20and%20paste%20the%20script%20below%20in%20the%20python%20section%20of%20your%20App%3A,-1

Thanks, that worked, so I verified that the main loop is being called.
But it is not retrieving events... Probably a different event loop in the main
app code is seeing or discarding them...

Although I do see one event when Stop the app in app lab:

after pygame.init
after jostick.init
0
======== App is starting ============================
2026-04-20 13:57:54.003 INFO - [MainThread] App:  App started
Other Event: <Event(256-Quit {})>
---

I am looking around and wondering if there is a document or the like that descibes what all is in the arduino.app_utils.

Also wondering things like does bluetooth work within App lab stuff.

Or if I need to move around some of the init stuff until after the code
that shows "App is starting" is printed... So wondering if there is a
logical setup function that can be defined and/or maybe I will try it
off of main ...

Edit: Found another thread for part of this:
App.run() documentation - Development Tools / App Lab - Arduino Forum

Which points you to the github project:
app-bricks-py/src/arduino/app_utils/app.py at 14d2de61aaf36227e9cd5d58754922809dbd7ebf · arduino/app-bricks-py

Thanks again

The approach you are already using is fine:

If you want to package the setup code in a function, you can do that too. For example:

--- a/gamepad_test/python/main.py
+++ b/gamepad_test/python/main.py
@@ -14,10 +14,12 @@ led_state = False
 # at the start of the program.
 joysticks = {}

-pygame.init()
-pygame.joystick.init()
-joystick_count = pygame.joystick.get_count()
-print(joystick_count)
+def setup():
+    pygame.init()
+    pygame.joystick.init()
+    joystick_count = pygame.joystick.get_count()
+    print(joystick_count)
+
 #joystick = pygame.joystick.Joystick(0)
 #joystick.init()

@@ -76,4 +78,6 @@ def loop():
     #led_state = not led_state
     #Bridge.call("set_led_state", led_state)

+setup()
+
 App.run(user_loop=loop)

Thanks,

I packaged up the setup stuff, as I wondered if maybe calling some of the Bluetooth stuff after the call:
App.run(user_loop=loop)
Would maybe help to get the Bluetooth Gamepad to work.

# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
#
# SPDX-License-Identifier: MPL-2.0

from arduino.app_utils import *
import time
import pygame

#import inputs
led_state = False

#import os
#os.environ['SDL_AUDIODRIVER'] = 'dsp'

# This dict can be left as-is, since pygame will generate a
# pygame.JOYDEVICEADDED event for every joystick connected
# at the start of the program.
print("Python start")
joysticks = {}

#joystick = pygame.joystick.Joystick(0)
#joystick.init()

last_time_led = time.time()
last_led_value = int(0)
first_loop_call = True

def setup():
    print ("My setup called")
    pygame.init()
    print("after pygame.init");
    pygame.joystick.init()
    print("after jostick.init");
    joystick_count = pygame.joystick.get_count()
    print(joystick_count)
    

def loop():
    global last_time_led, last_led_value, first_loop_call

    if (first_loop_call) :
        setup()
        first_loop_call = False
        
    loop_time = time.time()
    if ((loop_time - last_time_led) > 0.5) :
        last_led_value = last_led_value ^ 1
        last_time_led = time.time()
        Leds.set_led1_color(0,last_led_value,0) # LED 1 in Green
        
    global led_state
    event_processed = False
    #axis_motions: list[int] = [65536, 65536, 65536, 65536, 65536, 65536]
    axis_motion_list: list[int] = []

    for event in pygame.event.get() :
        event_processed = True
        if event.type == pygame.JOYAXISMOTION :
            axis_motion_list.append(event.axis)
            axis_motion_list.append(int(event.value * 32767))
            #axis_motions[event.axis] = event.value
            if event.axis == 0 :
                print("Left stick X: {}".format(event.value))
            elif event.axis == 1:
                print("Left stick y: {}".format(event.value))
            elif event.axis == 2:
                print("Right stick x: {}".format(event.value))
            elif event.axis == 3:
                print("Right stick y: {}".format(event.value))
            else :
            	print("Other axis {}: {}".format(event.axis,event.value))
        elif event.type == pygame.JOYHATMOTION :
            Bridge.notify("joy_hat_motion", event.value[0], event.value[1])
            print("Hat {}: {}".format(event.hat, event.value))
        elif event.type == pygame.JOYBUTTONDOWN :
            Bridge.notify("joy_button_down", event.button)
            print("button down: {}".format(event.button))
        elif event.type == pygame.JOYBUTTONUP :
            Bridge.notify("joy_button_up", event.button)
            print("button up: {}".format(event.button))
        # Handle hotplugging
        elif event.type == pygame.JOYDEVICEADDED:
            # This event will be generated when the program starts for every
            # joystick, filling up the list without needing to create them manually.
            Bridge.notify("joy_device_added", event.device_index)
            joy = pygame.joystick.Joystick(event.device_index)
            joysticks[joy.get_instance_id()] = joy
            print(f"Joystick {joy.get_instance_id()} connencted")
        
        elif event.type == pygame.JOYDEVICEREMOVED:
            Bridge.notify("joy_device_removed", event.instance_id)
            del joysticks[event.instance_id]
            print(f"Joystick {event.instance_id} disconnected")

        else :
            print("Other Event: {}".format(event))
    if event_processed :
        if len(axis_motion_list) :
            Bridge.notify("joy_axis_motion", axis_motion_list)
        print("---")
    #time.sleep(1)
    #led_state = not led_state
    #Bridge.call("set_led_state", led_state)


Leds.set_led1_color(1,0,0) # LED 1 in red
time.sleep(1)
Leds.set_led1_color(0,1,0) # LED 1 in Green
time.sleep(1)
Leds.set_led1_color(0,0,1) # LED 1 in Blue
time.sleep(1)
Leds.set_led1_color(0,0,0) # LED 1 off

App.run(user_loop=loop)
print("After App.run")

But does not appear to help. My guess looking through some other threads, like those who were trying to get Bluetooth speakers to work under applab failed. Some ended up running some external app to handle the speaker or the like.

For now I will probably just play with the version that works where I have external uv setup with pygame included... which is working

@ptillish and all, Currently I still do not have any luck talking to the gamepad through applab.
However it is working fine with the uv/sketch setup.

So now I thought I would try it with then using the DynamixelShield... Yep it does cause the RX pin of the Serial port to get a +5v PU resistor from it... But that works as +5v tolerant.

I did quick setup on the makeshift Turtlebot Burger with it.


I have the two servos that are the wheels plugged into it. +12v 5Amp connected to shield.
I don't have the jumper in place yet for it to supply the VIN. I first wanted to verify that VIN to the Q could be 12v. Looks like it can go to 24, so will try that. Also need to secure the board.

The sketch looks for button 3 (X) to be pressed and then sends a request for a broadcast ping and prints out the results using printk...

Joystick device added: 0
BDN: 3
▒▒▒▒1BDetected Dynamixel :    1, Model:1060, Ver:42
    2, Model:1060, Ver:42
BUP: 3
Joystick device removed: 0
Joystick device added: 0
Joystick device removed: 1

I pushed both version up to github
The uv version is at the root level (gamepad_test) and the Applab version is under
ArduinoApps/gamepad_test

Next up see if I can make the wheels spin, using one of the sticks. WIll probably first try running one or both wheels at same speed forward/back, then figure out what formulas to use to control both wheels depending on angles...

Side note: I decided to move the stuff off of using printk as this goes to Serial1 as does the Dynamixels... So used Monitor.

When I use Monitor using the IDE, it also goes to Serial1, so was curious. Nothing came out.

Then I remembered with Applab it comes up in it's own window pane...

So looked, and opened up another remote command window to the Q2 and
typed: arduino-app-cli monitor
And I now getting the output. :smiley:

I added some simple stuff to maybe turn the wheels...
Probably these lines:

      dxl.setGoalVelocity(LEFT_WHEEL_ID, cur_wheel_velocity);
      dxl.setGoalVelocity(RIGHT_WHEEL_ID, cur_wheel_velocity);

Whole sketch:

// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
//
// SPDX-License-Identifier: MPL-2.0

#include "Arduino_RouterBridge.h"
#include <vector>
#include <Dynamixel2Arduino.h>

uint32_t last_led_update_time = 0;
#define BLINK_TIME 500
#define LEFT_WHEEL_ID 1
#define RIGHT_WHEEL_ID 2

enum { JOY_LEFT_X = 0,
       JOY_LEFT_Y,
       JOY_RIGHT_X,
       JOY_RIGHT_Y,
       JOY_LEFT_TRIGGER,
       JOY_RIGHT_TRIGGER };

const int DXL_DIR_PIN = 2;  // DYNAMIXEL Shield DIR PIN
const uint8_t DXL_ID = 1;
const float DXL_PROTOCOL_VERSION = 2.0;
#define DEBUG_SERIAL Monitor
#define DXL_SERIAL Serial

Dynamixel2Arduino dxl(DXL_SERIAL, DXL_DIR_PIN);

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);

  Bridge.begin();
  Monitor.begin();
  Bridge.provide("joy_button_down", joy_button_down);
  Bridge.provide("joy_button_up", joy_button_up);
  Bridge.provide("joy_axis_motion", joy_axis_motion);
  Bridge.provide("joy_hat_motion", joy_hat_motion);
  Bridge.provide("joy_device_added", joy_device_added);
  Bridge.provide("joy_device_removed", joy_device_removed);


  // Set Port baudrate to 57600bps. This has to match with DYNAMIXEL baudrate.
  dxl.begin(1000000);
  Monitor.print("After dxl.begin\n");
  // Set Port Protocol Version. This has to match with DYNAMIXEL protocol version.
  dxl.setPortProtocolVersion(DXL_PROTOCOL_VERSION);
  Monitor.println("Set protocol");

  FindServos();

  // Set both servos into Velocity mode
  // Turn off torque when configuring items in EEPROM area
  dxl.torqueOff(LEFT_WHEEL_ID);
  dxl.setOperatingMode(LEFT_WHEEL_ID, OP_VELOCITY);
  dxl.torqueOn(LEFT_WHEEL_ID);
  dxl.torqueOff(RIGHT_WHEEL_ID);
  dxl.setOperatingMode(RIGHT_WHEEL_ID, OP_VELOCITY);
  dxl.torqueOn(RIGHT_WHEEL_ID);

}


char buffer[128];
DYNAMIXEL::InfoFromPing_t ping_info[32];

void FindServos(void) {
  DEBUG_SERIAL.println("  Try Protocol 2 - broadcast ping: ");
  DEBUG_SERIAL.flush();  // flush it as ping may take awhile...

  if (uint8_t count_pinged = dxl.ping(DXL_BROADCAST_ID, ping_info,
                                      sizeof(ping_info) / sizeof(ping_info[0]))) {
    //DEBUG_SERIAL.print("Detected Dynamixel : \n");
    Monitor.print("Detected Dynamixel :");
    for (int i = 0; i < count_pinged; i++) {
      sprintf(buffer, "    %u, Model:%d, Ver:%d\n", ping_info[i].id, ping_info[i].model_number, ping_info[i].firmware_version);
      DEBUG_SERIAL.print(buffer);
      //printk("    %u, Model:%d, Ver:%d\n", ping_info[i].id, ping_info[i].model_number, ping_info[i].firmware_version);
      //g_servo_protocol[i] = 2;
    }
  } else {
    DEBUG_SERIAL.print("Broadcast returned no items : ");
    DEBUG_SERIAL.println(dxl.getLastLibErrCode());
    //printk("Broadcast returned no items: %d\n", dxl.getLastLibErrCode());
  }
}

int16_t cur_wheel_velocity = 0;
volatile int16_t new_wheel_velocity = 0;

void loop() {
  Bridge.update();
  if ((millis() - last_led_update_time) >= BLINK_TIME) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    last_led_update_time = millis();
  }

  if (new_wheel_velocity != cur_wheel_velocity)
      Monitor.println("change Velocity");
      cur_wheel_velocity = new_wheel_velocity;
      dxl.setGoalVelocity(LEFT_WHEEL_ID, cur_wheel_velocity);
      dxl.setGoalVelocity(RIGHT_WHEEL_ID, cur_wheel_velocity);
}

void joy_button_down(int btn) {
  sprintf(buffer, "BDN: %d\n", btn);
  Monitor.print(buffer);
  if (btn == 3) {
    FindServos();
  }
}

void joy_button_up(int btn) {
  sprintf(buffer, "BUP: %d\n", btn);
  Monitor.print(buffer);
}

void joy_axis_motion(std::vector<int> motions) {
  for (int i = 0; i < motions.size(); i += 2) {
    sprintf(buffer, "%d:%d ", motions[i], motions[i + 1]);
    Monitor.print(buffer);
    if (motions[i] == JOY_LEFT_Y) {
      new_wheel_velocity = motions[i+1];
    }
  }
  Monitor.print("\n");
}

void joy_hat_motion(int x, int y) {
  sprintf(buffer, "Hat: %d %d\n", x, y);
  Monitor.print(buffer);
}

void joy_device_added(int index) {
  sprintf(buffer, "\nJoystick device added: %d\n", index);
  Monitor.print(buffer);
}
void joy_device_removed(int index) {
  sprintf(buffer, "\nJoystick device removed: %d\n", index);
  Monitor.print(buffer);
}

Everything stopped working. So to make sure anything worked, I programmed "blink" and that ran. I then redid the bootloader...

Then did again: arduino-cli ... of the sketch and the logical Serial (soon Serial1) output from kernel showed:

*** Booting Zephyr OS build v4.2.0-44-g7cfc53a6f3fc ***
[00:00:00.219,000] <err> elf: sym '__aeabi_dadd': relocation out of range (0x2000912e -> 0x8000299)

[00:00:00.229,000] <err> llext: Failed to link, ret -8
Failed to load sketch, rc -8

So another one of those... Probably need to create another PR or at least an issue.
Or maybe try on current sources

EDIT: Already is:
Arduino Uno Q: Motor Shield V2 motor test fails to upload with latest changes. · Issue #378 · arduino/ArduinoCore-zephyr

@ptillisch @Merlin513 and all,

Sorry, maybe it has been answered or in some document, but is there more information about using custom bricks and in particular about turning on requires_container?

In particular, with my experiments with gamepad, I was able to talk to the joystick using an external container, using your instructions up on the thread:

But the same python code run within app-lab does not work, I believe it is not seeing the joystick. So again wondered about if it makes sense to move the joystick handling code into a brick? And if to do so would require container...

So I tried modifying your CustomBrickHelloWorld app, to add a container like:

services:
  joystick:
    image: debian:bookworm-slim
    devices:
      - /dev/input/js0
    command: >
      sh -c "
      apt update &&
      apt install -y python3 mpg123 alsa-utils procps curl ca-certificates &&
      exec python3 /webradio/radio_service.py
      "

And if I don't have the joystick paired it errors out, but if I do have it paired, the
rest of the app runs...

So can the services command be setup like:
command: uv run joystick_code.py

Do I need to setup the uv using your steps in the first link? Where I add in the
required dependencies? like: uv add numpy ... ?

Or is the requires_container: true
Already doing some of this for me already?

Assuming any of this works, then next step will be to figure out how to propagate the
input data from the joystick, through the brick to the python code up to the Arduino code...

EDIT: If the python within your container does something like:
print("Right stick y: {}".format(event.value))
Where (if anywhere) does that output go? Or rephrased can I view these outputs?

Communication between docker containers some stuff up at:
Docker Networking: Enabling Communication Between Containers | by Mehmet Turgut Gezgin | Insider One Engineering | Medium

Any of this make sense?

A different approach can side step the problem of accessing joysticks. Suppose you want to control an SBC-based robot, like the Uno Q, from another computer. In this setup, the gamepad is connected to the computer rather than the robot. The process starts by the computer by opening a web browser to <uno q ip>:7000, where the Uno Q serves a page containing HTML, CSS, and JavaScript. Using the HTML5 Gamepad API, the JavaScript program captures input from any USB or Bluetooth gamepad attached to the computer and sends those events to the Uno Q via WebSockets. From there, main.py can use those input events to send commands to the STM32 to control motors, etc.

@KurtE

I just tried working with one of your test sketches:

#!/usr/bin/env python3
import os
import struct
import sys
import time

JOYSTICK_PATH = "/dev/input/js0"

# Joystick event format:
# struct js_event {
#     uint32_t time;     // event timestamp in milliseconds
#     int16_t  value;    // value
#     uint8_t  type;     // event type
#     uint8_t  number;   // axis/button number
# };
EVENT_FORMAT = "IhBB"     # matches the C structure
EVENT_SIZE = struct.calcsize(EVENT_FORMAT)

# Event type bit flags
JS_EVENT_BUTTON = 0x01
JS_EVENT_AXIS   = 0x02
JS_EVENT_INIT   = 0x80  # Initial state events at startup

def open_joystick(path="/dev/input/js0"):
    """Open the joystick device safely."""
    if not os.path.exists(path):
        print(f"Error: joystick device {path} not found.")
        sys.exit(1)
    try:
        return open(path, "rb")
    except PermissionError:
        print(f"Permission denied opening {path}. Try running with correct permissions.")
        sys.exit(1)
    except OSError as e:
        print(f"Failed to open joystick device: {e}")
        sys.exit(1)

def main():
    keyboard_abort = False;
    while True:
        # lets wait for joystick to connect
        if not os.path.exists(JOYSTICK_PATH):
            print("Waiting for Joystick")

            while True:
                if os.path.exists(JOYSTICK_PATH):
                    break
                time.sleep(0.25)
    
        js = open_joystick()

        print("Reading joystick events. Press Ctrl+C to exit.")
        try:
            while True:
                try:
                    data = js.read(EVENT_SIZE)
                except Exception as e:
                    # Handle unexpected errors safely
                    print("An error occurred:", str(e))
                    print("Breaking out of loop due to exception.")
                    break

                if len(data) != EVENT_SIZE:
                    print("Incomplete event read. Device may have disconnected.")
                    break

                js_time, value, etype, number = struct.unpack(EVENT_FORMAT, data)

                # Filter out initialization events unless needed
                is_init = bool(etype & JS_EVENT_INIT)
                etype = etype & ~JS_EVENT_INIT

                if etype == JS_EVENT_BUTTON:
                    state = "pressed" if value else "released"
                    print(f"[{js_time} ms] Button {number} {state} (init={is_init})")

                elif etype == JS_EVENT_AXIS:
                    print(f"[{js_time} ms] Axis {number} value={value} (init={is_init})")

                # Unknown event type (rare)
                else:
                    print(f"[{js_time} ms] Unknown event type {etype}")

        except KeyboardInterrupt:
            print("\nExiting...")
            keyboard_abort = True
        finally:
            js.close()

        if keyboard_abort:
            break;

if __name__ == "__main__":
    main()

Running it from applab you receive:

======== App is starting ============================
Permission denied opening /dev/input/js0. Try running with correct permissions.
exited with code 1

Running it as a straight python app python3 main.py works no issue. So sounds like a Docker issue. Going to try a docker compose and see that happens with spi3bridge

@Merlin513 @ptillisch - My WAG is it will have to be in a different docker, maybe same one as SPIDEV. Where we can simply loop waiting for Joystick to be connected..
Then for each 6 byte joystick packet we package it up to send them over SPIDEV to the
STM32... Where we process them, at least for the EVENT_BUTTON and EVENT_AXIS,
and probably create an EVENT for CONNECT and DISCONNECT.

But again this is a total WAG!

After looking back in my notes looks like may have to do similar to what was done in the audio play brick

Not sure how to interface directly with low level linux kernel commands from a brick.

But this is just a guess.

EDIT: Might be able to adapt this for a joystick as toplevel interface to /dev/input/jsx

Another idea is to generate a seperate joystick brick the uses the spi3bridge image by specifying using the image mjs513/spibridge-spi3bridge. But this is already giving me a headache :)

I posted demo code for Uno Q joystick/gamepad input. This depends on the HTML5 gamepad API available in web browsers. This has the advantage of bypassing docker for input events. It is possible to also get keyboard, mouse, and touch screen input using Javascript APIs then pass the events over a websocket back to python/main.py.