Repository: ArduPilot/MAVProxy
Pull request #1515: mavproxy_joystick: add support for toggle buttons
Author: login: heeplr
Created: 2025-02-28T11:36:17Z | Merged: 2025-03-04T10:59:10Z

Since there usually are no persistent switches on gamepads but just momentary switches, I added the "toggle" control to simulate persistent switches.
This allows to e.g. arm/disarm using a button.

`toggle` takes a list of values that are cycled through on every button press/release.

By using a list for `values`, not only on/off switches can be simulated but also multi-position or rotary switches as well.


Diff:
diff --git a/MAVProxy/modules/mavproxy_joystick/controls.py b/MAVProxy/modules/mavproxy_joystick/controls.py
index 146ff5984f..fa7f677288 100644
--- a/MAVProxy/modules/mavproxy_joystick/controls.py
+++ b/MAVProxy/modules/mavproxy_joystick/controls.py
@@ -16,12 +16,14 @@ class Control (object):
     '''Base class for all controls'''
     def __init__(self, joystick,
                  inlow=-1, inhigh=1,
-                 outlow=1000, outhigh=2000):
+                 outlow=1000, outhigh=2000,
+                 values=[]):
         self.joystick = joystick
         self.inlow = inlow
         self.inhigh = inhigh
         self.outlow = outlow
         self.outhigh = outhigh
+        self.values = values
 
 
 class Button (Control):
@@ -42,6 +44,35 @@ def value(self):
             return self.outlow
 
 
+class ToggleButton (Control):
+    '''A ToggleButton acts like a toggle or rotary switch.
+    Initially, the corresponding channel value is set to the first value in `values`;
+    when button `id` is pressed consecutively, the values are iterated on each press.
+    Iteration is looped by starting from the beginning when last value is reached.'''
+
+    def __init__(self, joystick, id, **kwargs):
+        super(ToggleButton, self).__init__(joystick, **kwargs)
+        self.id = id
+        # index of value to set next
+        self._current_value_index = 0
+
+    @property
+    def value(self):
+        state = self.joystick.get_button(self.id)
+        value = self.values[self._current_value_index]
+        # button was pressed
+        if state:
+            # choose new value for next press
+            if self._current_value_index >= len(self.values)-1:
+                # start over with first value
+                self._current_value_index = 0
+            else:
+                # choose next value
+                self._current_value_index += 1
+
+        return value
+
+
 class MultiButton (Control):
     '''A MultiButton maps multiple buttons to the same channel like a
     multiple-position switch.  When a button is pressed, the channel
@@ -128,6 +159,13 @@ def __init__(self, joystick, controls):
 
                 handler = Button(self.joystick, control['id'], **kwargs)
 
+            elif control['type'] == 'toggle':
+                kwargs = {k: control[k]
+                          for k in control.keys()
+                          if k in ['values']}
+
+                handler = ToggleButton(self.joystick, control['id'], **kwargs)
+                      
             elif control['type'] == 'axis':
                 kwargs = {k: control[k]
                           for k in control.keys()
diff --git a/docs/JOYSTICKS.md b/docs/JOYSTICKS.md
index 62218ee3c8..01b8698edc 100644
--- a/docs/JOYSTICKS.md
+++ b/docs/JOYSTICKS.md
@@ -127,6 +127,49 @@ Example:
       type: button
       id: 1
 
+### toggle
+
+A `toggle` button acts like a toggle or rotary switch.
+Initially, the value of the corresponding channel is 
+set to the first value in the `values` list;
+when the push button `id` is pressed consecutively, 
+the values of this list are cycled 
+with each press/release.
+After the last value, the iteration will start over
+and the first value is set again.
+
+Example:
+
+    - channel: 1
+      type: toggle
+      id: 2
+      values:
+        - 1000
+        - 2000
+
+This example will set the value of channel 1 to 1000 initially. 
+When button 2 is pressed, it will be set to 2000.
+When button 2 is pressed again, 1000 will be set again (and so on).
+
+Example 2:
+
+    - channel: 1
+      type: toggle
+      id: 2
+      values:
+        - 1000
+        - 1500
+        - 2000
+
+This example will set the value of channel 1 to 1000 initially.
+After each button press, value will be set to
+* 1500
+* 2000
+* 1000
+* 1500
+* ...
+
+
 ### multibutton
 
 A `multibutton` maps multiple buttons to the same channel like a


Reviews:
user: login: peterbarker
body: This looks really good.

But I do think we need to re-name this variable, sorry!

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T05:50:40Z
user: login: heeplr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:27:28Z

Review comments:
diff_hunk: @@ -42,6 +44,35 @@ def value(self):
             return self.outlow
 
 
+class ToggleButton (Control):
+    '''A ToggleButton acts like a toggle or rotary switch.
+    Initially, the corresponding channel value is set to the first value in `values`;
+    when button `id` is pressed consecutively, the values are iterated on each press.
+    Iteration is looped by starting from the beginning when last value is reached.'''
+
+    def __init__(self, joystick, id, **kwargs):
+        super(ToggleButton, self).__init__(joystick, **kwargs)
+        self.id = id
+        # index of value to set next
+        self._current_value = 0
path: MAVProxy/modules/mavproxy_joystick/controls.py
user: login: peterbarker
body: ```suggestion
        self._current_value_index = 0
```
created_at: 2025-03-04T05:48:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +44,35 @@ def value(self):
             return self.outlow
 
 
+class ToggleButton (Control):
+    '''A ToggleButton acts like a toggle or rotary switch.
+    Initially, the corresponding channel value is set to the first value in `values`;
+    when button `id` is pressed consecutively, the values are iterated on each press.
+    Iteration is looped by starting from the beginning when last value is reached.'''
+
+    def __init__(self, joystick, id, **kwargs):
+        super(ToggleButton, self).__init__(joystick, **kwargs)
+        self.id = id
+        # index of value to set next
+        self._current_value = 0
path: MAVProxy/modules/mavproxy_joystick/controls.py
user: login: heeplr
body: Right. Changed it.
created_at: 2025-03-04T07:27:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: heeplr
created_at: 2025-03-03T10:37:09Z
body: I can't make a lot of the Windows build fail. If anyone sees why it's failing, please notify me if it's related to the patch.
author_association: CONTRIBUTOR
user: login: peterbarker
created_at: 2025-03-04T05:41:58Z
body: > I can't make a lot of the Windows build fail. If anyone sees why it's failing, please notify me if it's related to the patch.

This isn't your bug :-)

author_association: CONTRIBUTOR
user: login: peterbarker
created_at: 2025-03-04T10:59:16Z
body: Merged, thanks!

author_association: CONTRIBUTOR