-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Refactor game controller system #1097
Comments
@Ezward - this is a very thorough and comprehensive description of the problem and the possible solutions. I had to prototype a solution already in order to get the generic vehicle template approach to work, where we need to rely on all parts to communicate through the Without spending too much time on a strategic solution, I directly implemented a solution where the controller would simply spit out a map of the buttons that are pressed or released. This map is returned from the Note, I don't think these variables should get cleared at the end of the vehicle loop. The reason is that sometimes the parts topology in the car can become complicated, e.g. for some reason you would want the AI pilot to run before your controller and for some other reason you want to run it afterwards, usually because you have some cyclic dependencies, so part one's ( As you say, the mapping of physical buttons to button names might be platform dependent, so it should be taken care of in the controller itself, I would think. The mapping of buttons, say |
@DocGarbanzo can you give me a link to the branch where you are developing generic vehicle template approach? |
Looking at current Joystick and desired kinds of events, I think the layers could be
It is possible for parts that implement behavior to use the outputs of the ControllerEvents part or the AxisButtonEvents part directly. However, it may be better to have another layer that turns these device input events into high level events. For instance, rather than a part that looks in the list of button events for a click event on "button_1" to toggle recording, it would be better to have a part that maps a "event/click/button_1" event to a "toggle-recording" event (button->behavior-event) and have a separate part that handles toggling the recording; that separates those concerns and makes everything more re-usable. In that way we can have other parts toggle recording, not just those that listen for button events. I think we don't want to have to have one part per button->behavior-event translation. Ideally we can have one part that uses data to translate any number of button/axis events into high level behavior events. We could use the order of the list of inputs and outputs as the association; so the first input would map to the first output, etc. We could get the list of inputs and outputs from configuration. For example;
Note in the above example that AxisButtonEvents take V.mem as a constructor argument with the notion that it would have a variable number of outputs, so it can't declare them ahead of time. Note that BehaviorEvents' inputs are the only thing that needs to change to create a custom mapping (and to handle driver and OS differences). For that purpose we can have a lookup that takes the controller and os as arguments, looks up the button-event->behavior-event as a Python dictionary, then turns that dictionary into a list of inputs and outputs. We include a version of the dictionary called "custom". If the user wants a custom mapping then they edit the custom dictionary and choose "custom" as their controller type.
|
@Ezward - this is here: https://github.com/DocGarbanzo/donkeycar/tree/part_factory |
I have one more comment to the Event System. I think if we add one-shot events to the vehicle memory, these events must be cleared by the part that generates those. Say a one-click event would be registered, only if the button is pressed through at least 2 vehicle loops and or a double click requires at at least 2 vehicle loops between the single clicks. Such an event would be issued when registered from the controller and would be cleared in the next loop. This will allow the parts that react to that event to be placed either before or after the controller in the vehicle loop. I think it is important to have that flexibility, and we already have parts which are issuing data into the memory that will be picked up by other parts only in the next loop, for example the current tub index is read by the webcontroller only in the next iteration. The causal topology structure in the car is not a DAG. For your questions above, how to correctly recognise short or long or double clicks, I am already using some of this for my channel 3 controller, so I have some code for it. The controller part will issue a different message when pressed long (for me this issues a Ctrl-C like event to stop the vehicle) vs pressing short (which will clear the last 1s of data from the tub). |
@DocGarbanzo can you share your code with me? |
Proof of ConceptA proof of concept is available on branch 1097-refactor-game-controller-system. I've also opened a draft pull request to make it easier to see the changes and to comment on the code; that PR is not intended to ever be merged. Please address comments regarding the approach to replacing the legacy joystick code in this issue rather than the PR. Please make comments that are specific to the code in the PR. This lengthy comment is a discussion of the approach taken to refactoring the joystick code as embodied in the branch. Running the POC
One-shot EventsThe current Donkeycar game controller code tightly binds many behaviors to specific button/axes, like toggling the pilot mode with a button press or setting throttle with an joystick (axis) movement. That legacy design is a problem because we can't remap buttons/axes to different behaviors. That is a drag for two reasons; first it makes supporting new templates or new template behavior more difficult and second it makes supporting Dirk's declarative vehicle assembly basically impossible. This POC shows how we can eliminate the The first thing we need to do is surface input control changes as one-shot events. The POC implements a one-shot event as values in memory that exist for a single complete pass through the vehicle loop and are then removed. Contrast this to 'normal' memory state, which is persistent for the entire life of the vehicle loop. We want events to be one-shot so that they can trigger code once in the loop when they are emitted and not over and over again on subsequent passes through the loop. In addition to their one-shot nature, button and axis events must be named in a regular, predictable way so that using them as inputs or run_conditions is easy. Refactoring
|
Problem Statement
Our current system hard-codes behavior in the joystick parts. This has unwanted side-effects;
We know that XBox joystick has different mapping under different SBCs and even within different releases of the OS on a given SBC. We have split the joystick parts into two pieces;
Together these two parts effectively map a button code to a function. The problem is that different drivers for the same controller (different across drive versions or OS versions of SBC versions) output different codes. So the 'X' button on an Xbox controller may be 0x05 on one driver and 0x23 on another. The breaks everything because the mapping of code to name in the Joystick() may be completely missing or may map to the wrong actually axis or button. The canonical example is the difference between the Xbox controller on RaspberryPi and Jetson Nano.
The other issue is that every JoystickController subclass must implement the behaviors itself.
Goals:
Proposed Direction:
Replace the hard-coded behavior we have in our controllers with an event system where the controller emits a event with a name and value when an axis/button changes, then an event handler (a part) can handle the event to implement the behavior.
Using a two level mapping creates more understandable low-level button names, like 'button-x', We would have configuration that maps from the button code (like 0x0f) to this more understandable name. We would have predefined configuration for supported controllers, which we would use by default based on the controller type selected in configuration. We would also allow the user to create their own controller mapping (perhaps we make a version of joystick creator that just outputs such a map) and point to it in configuration.
So then we would map these low level names to high-level events in the second mapping. It may be possible here to have a single mapping from low-level to high level provided that controllers have orthogonal names for their buttons (so 'button-x' maps to 'toggle-recording' to handle Xbox and a second mapping of 'button-triangle' also maps to 'toggle-recording' to handle Sony). We will want a set of lookup tables that handle all of our current functions and controllers. We want separate tables for each template, since they have different functionality.
We would still be left with the issue that the button/axis names might differ between drivers. We can handle differences between drivers by including extra tables. So we might have 'xbox' lookup table to handle the xbox controller on Raspberry Pi and an 'xbox-jetson' lookup table to handle the same controller on the Jetson because the xbox driver on Jetson outputs a different set of button-#/axis-# names than it does on Raspberry Pi.
Beyond the mapping of low-level codes to high-level events (be that in a single-stage to two-stage system) we need a system that delivers the event to a part that implements the necessary behavior. So we would have a part that handles toggling the recording state and it would run when we get a 'toggle-recording' event.
Note 1: The user has some customization available to the them now; we have the JoystickWizard that creates a custom joystick part that maps low-level code to a low level name. So technically users can remap buttons/axes to existing functions; but they cannot assign new functions. This is also complex; the Joystick Wizard ends up generating a Python class that it writes the the root of the my_car folder. If the user changes their joystick preference to "custom", then add_user_controller() looks for my_joystick.py in the my_car folder and imports it dynamically. It would be much better if the wizard just wrote out a lookup table (a standard csv file) and the custom joystick always used that as it's lookup table. Then we would not have to had the weird code that dynamically load the 'my_joystick.py' part. The user could easily remap things by editing the file. We could also have the system read configuration files based on the controller names in the configuration. So we might have a 'controllers' folder and in that folder we have CSV files to map low level codes to button names
Tasks
Create an event system
Add a new type of state to handle one-shot events. This might simply be a dictionary we insert into memory, or it might be separate dictionary. Parts can add named events with values to the event dictionary. The event dictionary is cleared the next time the part that inserted it runs. Note that behavior; this is to guarantee that an event goes through the event loop one full time. For instance, we may have parts that run before the joystick controller part that want to handle joystick events; if we clear the events at the end of a loop, they will never see the events. Alternatively we might want to clear the event when it is handled; when a part pulls it from the event dictionary. It would then be incumbent on the part to set some other state, perhaps a run_condition, so that other parts could respond. In this case we probably would still want to clear the event after it has been all the way through the loop. Perhaps we mark each event at the loop start and those events that are marked will get automatically cleared on the next loop start. More generally, we could have the event have a counter that is supplied when the event is emitted (written to the dictionary); that counter would get tested at the start of the loop; when it is zero the event is removed, otherwise it is decremented. That would allow the event designer to specify how long the event lives. The default value would be 0, so only parts that run after the part that emitted the event will see it. See the Feb 18 comment for more details.
Load mappings at startup.
Modify the templates to load the correct CSV files for the mappings from the
controllers/
folder based on the joystick configuration and to pass them to the Joystick() to handle button name mappings and an instance of the event mapping part to handle mapping button names to high level event names.modify the joystick system to produce button events
We modify the Joystick() part to take a lookup table in it's constructor. That table is loaded from a csv file in the
controllers/
folder based on joystick configuration. The joystick part uses that table to map controller codes to button/axis names. When a button is pushed or an axis is changed then the Joystick controller inserts an event into the event system that corresponds to the button name. See the Feb 18 comment for more details.Create a part that maps one event to another event.
This part would take a mapping in it's constructor. For this discussion, this would be the map from button name to high level behavior name, like 'button-x' to 'toggle-recording'. See the Feb 18 comment for more details.
modify templates so joystick-controllable behaviors are event driven parts
We modify some functionality in the templates that is called directly be the JoystickController() part so that is triggered by events instead. So the part will have an run_event filter, much like the run_condition filter. This works in conjunction with the run_condition filter, so events are only processed if the run_condition is set. If the run_event filter passes, then the part runs, otherwise it does not run.
Related Issues
The text was updated successfully, but these errors were encountered: