Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Xbox controller support to Thunderscope #3064

Merged

Conversation

Bvasilchikov
Copy link
Contributor

@Bvasilchikov Bvasilchikov commented Nov 26, 2023

Description

This PR adds XBox controller support to Thunderscope.

Work Done

  • Added a handler class for managing the controller USB connection, through the builtin linux evdev API using a python package
  • Updated logic inside RobotCommunication to handle using GUI or controller input values and passing that through to the robots
  • Bumped PyQtGraph major version
  • Added button to load Xbox controller and a status view for the controller
  • Removed geneva UI slider

Testing (Needed To Be) Done

  • Test XBox controller only works when MANUAL control is set and Xbox is toggled in diagnostics.
  • Test multiple robots move when using MANUAL and Xbox
  • Test that XBox can still control 1 or more robots, when AI is running.

Resolved Issues

Review Checklist

It is the reviewers responsibility to also make sure every item here has been covered

  • Function & Class comments: All function definitions (usually in the .h file) should have a javadoc style comment at the start of them. For examples, see the functions defined in thunderbots/software/geom. Similarly, all classes should have an associated Javadoc comment explaining the purpose of the class.
  • Remove all commented out code
  • Remove extra print statements: for example, those just used for testing
  • Resolve all TODO's: All TODO (or similar) statements should either be completed or associated with a github issue

Bvasilchikov and others added 27 commits April 1, 2023 10:10
…into boris/shoot_or_pass_play_fsm_guards

� Conflicts:
�	src/software/jetson_nano/redis/redis_client_test.cpp
…most of the manager and overall structure done
@itsarune
Copy link
Contributor

itsarune commented Dec 2, 2023

you can tag issue #2562


def __init__(
self,
logger: Logger,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it makes much sense to pass logger as a parameter into a class. HandheldDeviceManager shouldn't depend on logger.

This is what people usually do instead:

https://stackoverflow.com/questions/18052778/should-a-python-logger-be-passed-as-parameter#:~:text=Not%20usually%3B%20it%20is%20typically,is%20different%20for%20each%20module.

Are you trying to log to the same file as DiagnosticsWidget?

Copy link
Contributor

@Mr-Anyone Mr-Anyone Sep 26, 2024

Choose a reason for hiding this comment

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

People tend to make logger a global variable I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we need to completely redo our logging in python. Boris added these logging changes and I didn't take them out. I'm thinking we should use https://github.com/Delgan/loguru

"""Loop that continuously reads and processes events from the connected handheld device."""
while True:
with self.lock:
self.__read_and_process_event()
Copy link
Contributor

@Mr-Anyone Mr-Anyone Sep 26, 2024

Choose a reason for hiding this comment

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

I am not sure if this is going to be memory safe or what not.

Why not put self.lock in the __read_and_processe_event() instead of here? I think it would yield the same result. Are you trying to make the code look prettier so it doesn't have mutiple indent?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah it's just for readability so we don't have an extra indent in __read_and_process_event


for device in list_devices():
handheld_device = InputDevice(device)
if (
Copy link
Contributor

@Mr-Anyone Mr-Anyone Sep 26, 2024

Choose a reason for hiding this comment

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

During the test with Arun, we encountered an issue where handheld_device.name was not present in device_config_map. This led to some confusion among users regarding the status of the controller.

To improve clarity, could we add a debug log message indicating that a device is found but is not supported? This would help users understand the situation better.

Copy link
Contributor

Choose a reason for hiding this comment

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

alternatively, we could strip whitespace and match lowercase controller names to stop these kinds of issues

)
)

def __event_loop(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Just some thoughts you could ignore and mark as resolve:

I've been reflecting on our code structure for a while. We have several threads, each running its own event loop. The problem is that these threads don’t yield control to one another because we are running a while True: loop. This design doesn’t scale well with many asynchronous tasks since we only need to call read_and_process_event when there's an actual event to process. As a result, we waste resources and compute time by constantly checking for read events.

We might consider using asyncio or a similar approach to handle these events more efficiently. This way, we can reduce the resources spent on checking for read conditions. Although, this might be a good idea, I feel likle it might but too much work for us to rewrite the full IO stack. The performance improvement may not justifiy the need.

Copy link
Contributor

Choose a reason for hiding this comment

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

Async would work well in this case. But for Thunderscope in general, I'm not sure there will be a significant performance improvement from switching to async to avoid busy waiting (it actually may add overhead and slow things down) since most of our tasks are either CPU bound or just don't sit around spinning.

e.g. in ProtoPlayer the playback worker thread probably spends way more time unzipping the replay chunks vs. waiting on file I/O.

In RobotCommunication the thread that sends primitives pretty much always has new primitives to process, so that thread is always working (except for a bit of waiting on network I/O).

So converting the work these threads perform into async tasks would just move a bunch of heavy processing to the Qt thread and end up blocking the UI.

Also, right now we don't have enough tasks to warrant using async for scalability benefits (we only have a single thread in RobotCommunication that sends primitives, unlike a web server that might handle hundreds of concurrent network requests).

@Mr-Anyone
Copy link
Contributor

I will take a look of the rest of thing later. Need to work on mini project for CPEN 221.

HANDHELD_DEVICE_NAME_CONFIG_MAP = {
"Microsoft Xbox One X pad": XboxConfig,
"Microsoft X-Box One S pad": XboxConfig,
"Microsoft Xbox 360 pad": XboxConfig,
Copy link
Contributor

Choose a reason for hiding this comment

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

Add the following line: Microsoft Xbox One S pad": XboxConfig. This was there before, and I am pretty sure this is a supported device that Boris tested.

Copy link
Contributor

Choose a reason for hiding this comment

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

nah I accidently changed it from Microsoft X-Box One S pad to Microsoft Xbox One S pad because I didn't realize the actual device names were listed in this map (i thought it was just a typo)

Copy link
Contributor

@Mr-Anyone Mr-Anyone Sep 26, 2024

Choose a reason for hiding this comment

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

I see. what a circular issues I have traced with Arun.

dribbler_enable=DeviceAbsEvent(event_code=5, max_value=1023.0),
)

HANDHELD_DEVICE_NAME_CONFIG_MAP = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that I think about it, does it make sense to even make sense to have this CONFIG_MAP. The only configruation we currently have is XboxConfig.

We could literally just have a list and call it supported_deviced and initialize this XboxConfig if the device we are looking at is in this list.

Are we even going to support different controller in the future?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably a YAGNI moment. But I'll leave the config map as is bc it's already implemented

Copy link
Contributor

Choose a reason for hiding this comment

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

I imagine at some point we might want to add a generic controller or a play station controller or something, so may as well

time.sleep(DiagnosticsConstants.EVENT_LOOP_SLEEP_DURATION)

def __read_and_process_event(self) -> None:
"""Try reading an event from the connected handheld device and process it."""
Copy link
Contributor

Choose a reason for hiding this comment

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

this function should be documented as not being thread-safe (requires mutex locking)

@williamckha
Copy link
Contributor

Just completed a major refactor of the code

  • HandheldController is the new class responsible for reading input events from the controller

    • Switched to using InputDevice.read_loop which uses the select syscall, instead of endlessly calling InputDevice.read_one in a loop which wastes CPU time busy waiting

    • HandheldController has no public methods that mutate its state so it should be thread safe. No need for mutex

  • Removed DiagnosticsInputWidget for toggling between diagnostics/xbox control (too much inter-widget communication with signals). Replaced with a "Enable Input" checkbox in the HandheldControllerWidget

  • DriveAndDribblerWidget and ChickerWidget are now the only classes responsible for sending out MotorControl/PowerControl protos. The handheld controller inputs are converted into MotorControl/PowerControl values in HandheldControllerWidget and those values are forwarded to DriveAndDribblerWidget/ChickerWidget to be sent out. This prevents duplication of some logic and safety code (e.g. chicker timeout)

  • Fixed bugs where kick/chip buttons are enabled/disabled at the wrong time or chicker timeout is ignored

Copy link
Contributor

@itsarune itsarune left a comment

Choose a reason for hiding this comment

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

looks good, pending merge conflicts

Copy link
Contributor

@itsarune itsarune left a comment

Choose a reason for hiding this comment

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

Awesome, nice work @williamckha @Bvasilchikov

@williamckha williamckha dismissed sauravbanna’s stale review November 13, 2024 21:01

We have enough approvals

@williamckha williamckha merged commit 78556e6 into UBC-Thunderbots:master Nov 13, 2024
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants