An Arduino program for surfacing switches and knobs as a USB game controller.
Here’s a video demonstrating the features of Falconpanel:
https://www.youtube.com/watch?v=VwVLXjgCeJg
Alpha - all code subject to change. May cause tingling in the extremities.
I developed this in the process of building a control panel for my favorite flight simulator Falcon BMS. However, since this surfaces the Arduino as a regular USB game controller, there’s really no reason that it has to be used just with Falcon; it should work with any program that’s expecting DirectX buttons or axes.
The buttons and switches are at the left edge. The thing in the middle is a Logitech G13 that I use for the ICP/DED, and has nothing to do with Falconpanel, other than sitting on the same stand. The point of the picture is to show how I was able to make a pretty simple panel by drilling some holes in some 1/4” plywood and mounting various things in it. Here’s a shot of the back:
The hardware I used:
- An Arduino Leonardo
- An Adafruit Protoshield
- A tiny breadboard
- Momentary switches
- Two-position switches
- Three-position switches
- 10K potentiometer
- A 74LS151 3-to-8 multiplexer
But you can use any Arduino, you don’t need the protoshield nor the breadboard, and you can use whatever switches, knobs, and buttons you like. I used these.
If you want to compile and build exactly what I did (unlikely), you’ll need to install the Arduino tools and then - as the key part of all this - The HID Project libraries. NicoHood has done all the hard work here, and I will refer you to his documentation for installation.
The key idea in Falconpanel is that of Components. These map physical controls and other electronics to USB buttons and axes. You will need to map these to your particular setup by modifying the code in Falconpanel that looks like this:
// I've got a 74LS151 3-to-8 mux with its address pins connected to
// Arduino pins 2-4, and with its output pin connected to Arduino pin
// 5. We have to declare this outside the components array below
// because we reference it in there.
IC74LS151* mux1 = new IC74LS151(new DigitalOutputPin(2),
new DigitalOutputPin(3),
new DigitalOutputPin(4),
new DigitalInputPullupPin(5));
// Keep track of the button number so I don't have to keep looking at
// what I used.
int dxButton = 1;
Component* components[] = {
// List the mux here so its setup gets called
mux1,
// FACK
new PushButton(mux1->input(0), new DxButton(dxButton++)),
// Laser Arm
new OnOffSwitch(mux1->input(2),
new MomentaryButton(new DxButton(dxButton++)),
new MomentaryButton(new DxButton(dxButton++))),
// Master Arm
new OnOffOnSwitch(mux1->input(3),
mux1->input(4),
new MomentaryButton(new DxButton(dxButton++)),
new MomentaryButton(new DxButton(dxButton++)),
new MomentaryButton(new DxButton(dxButton++))),
// HMCS
new SwitchingRotary(new AnalogInputPin(0),
DxAxis::XRotation(),
new MomentaryButton(new DxButton(dxButton++)),
new MomentaryButton(new DxButton(dxButton++)),
0.05)
};
Falconpanel currently supports the following types of component
The simplest of the controls, this maps a Arduino input pin directly to a DirectX button. Intended to be connect to a momentary, pushbutton switch. Constructor:
PushButton(DigitalInput* in, DxButton* dxButton)
Watches the digital input in
and maps it to DirectX button
dxButton
(DirectX buttons are numbered from 1, with a max of 32).
The DirectX button stays pressed for as long as the physical button
does.
Maps a two-position switch to DirectX buttons for its up and down states. The DirectX button presses are momentary, even though the switch is not.
Constructor:
OnOffSwitch(DigitalInput* in, DxButton* dxButtonUp, DxButton* dxButtonDown, int duration)
Watches the digital input in
, and when it changes state,
presses DirectX button dxButtonUp
or dxButtonDown
(DirectX buttons
are numbered from 1, with a max of 32) depending on whether the switch
has been flipped up or down. The button stays pressed for duration
“ticks”, or until the switch state is changed. A tick is currently
about 150ms.
Note that one switch therefore generates two different DirectX button presses.
Maps a three-position switch to DirectX buttons for its up, middle, and down states. The DirectX button presses are momentary, even though the switch is not.
Constructor:
OnOffOnSwitch(DigitalInput* inUp, DigitalInput* inDown,
DxButton* dxButtonUp, DxButton* dxButtonMiddle, DxButton* dxButtonDown,
int duration)
Watches the digital inputs inUp
and inDown
, which should be
connected to the up and down leads of the physical switch, and when
the switch changes state, presses DirectX button dxButtonUp
,
dxButtonMiddle
, or dxButtonDown
(DirectX buttons are numbered from
1, with a max of 32) depending on which position the switch has been
flipped to. The button stays pressed for duration
“ticks”, or until
the switch state is changed. A tick is currently about 150ms.
Note that one switch therefore generates three different DirectX button presses.
Maps a potentiometer to a DirectX axis and two buttons - one for “switching on” and one for “switching off”. Note that there is no need to use a potentiometer with an actual switch - on/off state is tracked by watching whether the pot is below a configurable threshold.
Constructor:
SwitchingRotary(AnalogInput* in,
DxAxis* dxAxis, DxButton* dxButtonOn, DxButton* dxButtonOff,
int duration, float threshold)
Watches the analog input in
, which should be connected to the middle
lead of a potentiometer, ideally in the 10K Ohm range. When the pot is
below threshold
, reports the specified DirectX axis as being at its
minimum value. When above threshold
, reports values scaled between
the minimum and maximum DirectX axis values.
When the pot passes through the threshold value in the increasing
direction, sends a momentary press on DirectX button dxButtonOn
.
When the pot passes through the threshold value in the decreasing
direction, sends a momentary press on DirectX button dxButtonOff
.
Momentary presses are of duration duration
ticks, where a tick is
currently about 150ms.
Note that one pot therefore generates two different DirectX button presses and one DirectX axis.
DEPRECATED If you’re looking at this, it’s much more likely you want
to use RotaryEncoder
.
Maps a potentiometer to two buttons - one for motion in the direction of increasing input values (the “up” direction), and one for motion in the opposite direction (the “down” direction).
Constructor:
PulseRotary(AnalogInput* in, DxButton* dxButtonUp, DxButton* dxButtonDown, int divisions);
Watches the analog input in
, which should be connected to the middle
lead of a potentiometer, ideally in the 10K Ohm range. When the pot
moves more than 1/divisions of its range, the up or down button is
triggered, depending on the direction of motion. The position the pot
is in when this threshold is crossed is the new “home” position for
determining the next transition, so divisions
does not result in a
strict division of the pot range.
This control does take into account the possibility of the pot “wrapping around”, as can happen with a pot that was made or modified to have full 360 degree rotation, and will correctly calculate the direction.
The up or down button is pressed each time the threshold is crossed in
that direction, at which time the opposite button is released. It
maintains its own internal buffer of contiguous presses in one
direction, so it probably does not make sense to use this with buttons
wrapped in a MomentaryButton
.
Maps a rotary encoder onto two buttons: one for one direction, one for the other.
Constructor:
RotaryEncoder(DigitalInput* in1, DigitalInput in2,
Button* buttonForward, Button* buttonBackward,
int queueLimit);
Watches digital inputs in1
and in2
, which should be connected to
the non-ground leads of a rotary encoder. When the encoder is rotated
in each direction, a DirectX button press/release for “forward” or
“backward” is sent for each “click” of the encoder. Presses are
enqueued (up to queueLimit
), so if rotation of the physical control
can get ahead of the DirectX presses. queueLimit
should never be set
lower than one.
If your notion of forward and backward is the opposite of the DirectX events you’re seeing, just reverse the order of the digital inputs.
It probably doesn’t make any sense to use this with a
MomentaryButton
, as RotaryEncoder
is already inherently momentary.
Represents a 74LS151 3-to-8 mulitplexer (mux). These can be used to multiplex three input pins and one output pin on the Arduino to 8 input pins on the mux, effectively doubling the number of input wires you can have connected to a single Arduino.
IC74LS151
is a component mainly so it can be listed in the
components array and have its setup function called; the primary use
of it is via its input
method, which is an adapter that bridges from
an IC54LS151 mux instance to anything that’s expecting a digital
input, like the PushButton
class.
Constructor:
IC74LS151(DigitalOutput* dout0, DigitalOutput* dout1, DigitalOutput* dout2, DigitalInput* din)
Sets up a 74LS151 multiplexer with its address lines driven by dout0
(LSB), dout1
, and dout2
(MSB). Input will arrive on din
. Use the
input
method to connect the input pins of the mux to other controls,
as in this example:
// I've got a 74LS151 3-to-8 mux with its address pins connected to
// Arduino pins 2-4, and with its output pin connected to Arduino pin
// 5. We have to declare this outside the components array below
// because we reference it in there.
IC74LS151* mux1 = new IC74LS151(new DigitalOutputPin(2),
new DigitalOutputPin(3),
new DigitalOutputPin(4),
new DigitalInputPullupPin(5));
// A simple example with only one control connect to the mux.
// Ordinarily you would connect several, since saving Arduino pins is
// the point of the mux.
Component* components[] = {
// List the mux here so its setup gets called
mux1,
// Connect an On/Off switch to the mux input 3 (D3 on the data sheet)
new OnOffSwitch(mux1->input(3),
new DxButton(1),
new DxButton(2),
3)
};
To learn more about the mux, read the datasheet.
Each of these components can be hooked up to either Buttons, Axes or both, depending on the component. Currently, an axis is always a direct connect to a DirectX axis. Axis values are represented as floating point numbers in the range 0.0 to 1.0, inclusive.
Buttons outputs of components, however, can either be a direct connect
to a DirectX button on the virtual gamepad, or can go through a
MomentaryButton
adapter. MomentaryButton
turns a button press into
a press-and-release, where the release happens automatically a
configurable number of ticks later. A tick is currently about 150ms,
and the default delay is three ticks. This approach is useful in
having the button presses coming out of a component like an
OnOffSwitch
indicate changes in state rather than switch position.
This can help with mapping in a game, where holding buttons down may
cause problems.
Feel free to drop an issue here on the project or contact me at [email protected] if you have questions or feature requests. Hope you find it useful!