Skip to content

Control Types

A PanelGroup sketch is mostly a list of control objects — one per physical switch, knob, LED, or gauge. Inputs read hardware and fire CAN events; outputs receive DCS state and drive hardware. This page is the catalogue, with honest status: every input and output the A-4E-C needs is implemented; two more input classes land before the first release.

Wiring a control for the first time?

Each control has a beginner page — the part it's for, the line to type, wiring diagrams and troubleshooting — under Getting Started → Controls.

What's built and what isn't

Implemented today: Switch2Pos, Switch3Pos, SwitchMultiPos, AnalogMultiPos, AnalogInput, RotaryEncoder, RotaryAcceleratedEncoder and ActionButton (inputs); LED, DrumDisplay, NeedleGauge, Dimmer and IntegerOutput (outputs, the last two over the AnalogOutput family base); plus the PinRef abstraction. All are hardware-verified; ActionButton's hardware run, RotaryAcceleratedEncoder's live-DCS feel check and Dimmer's live backlight check are part of the v1.0 soak test. Rows marked Planned below are specified but not written yet — don't expect them to compile today.

PinRef — the hardware abstraction (implemented)

Every input and output takes a PinRef, not a raw pin number. One interface over three hardware backends:

Backend Constructor Notes
STM32 GPIO PinRef(pin) direct digitalRead/Write, analogRead/Write
MCP23017 PinRef(chip, PORT_A\|PORT_B, bit) digital I/O expander
ADS1115 PinRef(adc, channel) analog input, channels 0–3

Only direct STM32 GPIO can do PWM or servo output — isGpio() reports the backend type. A class that needs one checks its PinRef at startup: Dimmer refuses anything but a GPIO on a timer channel, and says so on the diagnostic serial port instead of silently not dimming.

Routing is by controlId, not by class. The same input class drives a DCS-BIOS control or a HID button depending on the controlId you give it:

// DCS-BIOS route — controlId in 0x8000–0x86FF
OpenSkyhawk::Switch2Pos masterArm(DCSIN_ARM_MASTER, PinRef(PB5));

// HID route — controlId < 0x8000
OpenSkyhawk::Switch2Pos trigger(CTRL_TRIGGER, PinRef(PA6));

See DCS-BIOS vs HID for which to use.

Input classes

Class Status What it is
Switch2Pos Implemented Debounced 2-position switch (20 ms). value 0/1
Switch3Pos Implemented (hardware-verified) 3-position (ON-OFF-ON). value 0/1/2
SwitchMultiPos Implemented (hardware-verified) N-pin rotary, one active. value = index
AnalogMultiPos Implemented (hardware-verified) Resistor-ladder selector on one analog pin
ActionButton Implemented (#116) Momentary button driving a control that latches in the sim — one TOGGLE per press, nothing on release. See the note below
RotaryEncoder Implemented (dual-mode REL/DIR, #147) Quadrature encoder, relative. REL → ±step (continuous knobs); DIR → ±1 (no-indicator selectors)
RotaryAcceleratedEncoder Implemented (hardware-verified) RotaryEncoder subclass: DCS-BIOS's momentum filter for noisy encoders, plus a bigger step when spun fast
RotarySwitch Not planned Use RotaryEncoder in DIR mode — see the note below
AnalogInput Implemented (hardware-verified) Continuous analog, normalised to 16-bit (EWMA + hysteresis)
AngleSensorInput Implemented (hardware-verified) AnalogInput subclass reading a magnetic angle sensor (AS5600/MT6701) — absolute knobs such as gunsight elevation
SwitchWithCover2Pos Implemented (hardware-verified) One switch pin driving a guarded sim control: opens the sim's cover before the switch and closes it after (DCS-BIOS behaviour)

All inputs normalise analog sources to 16-bit (0–65535) before sending. Inputs self-register at global scope; PanelGroup::loop() polls them and batches events into CAN frames — EVT_n for an absolute value, EVT_REL_n and EVT_DIR_n for RotaryEncoder's two modes, and EVT_ACTION_n for ActionButton. See CAN Bus for the frame IDs and why the event families are split.

ActionButton lets a momentary button drive a switch that latches in the sim

Switch2Pos sends the switch's absolute position, so it needs a physical part that latches. ActionButton instead sends one TOGGLE per press: DCS-BIOS reads the control's current value, flips it, and writes it back. Each press toggles and the state persists — so a plain momentary button can drive a two-position cockpit switch.

A second benefit falls out of that. Because the sim supplies the current value, the panel cannot desync. A physical latching switch can end up disagreeing with the sim after a cold start, a keyboard binding, or a mission script, and then it physically lies about the state until you cycle it. A momentary button has no position to be wrong about.

The trade-off is you lose at-a-glance state — the panel can't show you what's on without a separate indicator. OpenSkyhawk's own panels use real latching switches for that reason; ActionButton is here for builders who'd rather not source them.

Only for controls DCS-BIOS declares with defineToggleSwitch — ones that latch in the sim. Pointing it at a definePushButton control is wrong: the sim tracks those by physical position, so a TOGGLE latches them on until the next press. A lamp-test button wired this way would stay lit. Use Switch2Pos for those.

Classes come in families, named after DCS-BIOS

A class that only changes where an existing one reads from, or what it drives, is a subclass of it — RotaryAcceleratedEncoder extends RotaryEncoder, AngleSensorInput extends AnalogInput, Dimmer and IntegerOutput extend AnalogOutput. Names follow dcs-bios-arduino-library wherever it has an equivalent, so a DCS-BIOS sketch translates class for class.

The one deliberate gap is RotarySwitch. The DCS-BIOS version assumes position 0 at power-up and sends absolute positions, so the first click after a cold start can jump the sim away from a mission preset. RotaryEncoder in DIR mode sends "one up / one down" instead: the sim keeps the position and stops at the ends, and the cockpit readout shows the result.

Output classes

Class Status What it is
LED Implemented GPIO pin driven from one bit of a DCS value
DrumDisplay Implemented (hardware-verified) OLED rolling-drum readout — multi-digit gauges (speed, lat/lon, frequency, range) + optional 2-state flag. Own library; pulls U8g2
NeedleGauge Implemented (hardware-verified against live DCS, #137) Pointer/needle gauge — maps a DCS value to a motor angle over a swappable driver backend (linear or calibrated curve). Supersedes SwitecX25Output / AccelStepperOutput / ServoOutput
AnalogOutput Implemented (hardware-verified) Family base for outputs driven by one DCS value: matching, decoding and change detection
Dimmer Implemented (hardware-verified) PWM duty on a GPIO timer pin — backlight zones driven by the sim's LIGHTS_* intensities
IntegerOutput Implemented (hardware-verified) The decoded value to your own callback, for custom displays (DCS-BIOS IntegerBuffer)

Outputs use DCS-BIOS output addresses from the generated A4EC headers — the address constant plus its bitmask. Example for the implemented LED:

OpenSkyhawk::LED gearLight(A_4E_C_GEAR_LIGHT, A_4E_C_GEAR_LIGHT_AM, PinRef(PB0));

Note the naming: A_4E_C_<NAME> for the address and A_4E_C_<NAME>_AM for the mask — not the old _A suffix. See DCS-BIOS Integration.

NeedleGauge drives gauge motors through a swappable backend

NeedleGauge does only the value→angle mapping. The drive lives in a reusable motor-driver layer (Firmware/Libraries/PanelGroup/Drivers/): a MotorDriver base with a StepperMotor backend today — non-blocking, driving four coils through PinRef: native GPIO, an MCP23017 expander (all four on one port, so the coil pattern changes in one transaction), or a 74HC595 shift register. One air-core profile covers the X27.589 / VID-29 / BKA-30 family; homing is either a mechanical hard-stop or a digital home sensor (switch / reed / hall / opto). Drive the X27 at 5 V through a DRV8833. A ServoMotor backend is planned (#132).

DrumDisplay is a separate, opt-in library

DrumDisplay lives in Firmware/Libraries/DrumDisplay/ (not PanelGroup) so the U8g2 OLED driver only lands on nodes that actually use a display — add DrumDisplay and olikraus/U8g2@^2.35 to a sketch's lib_deps to use it. Each readout (its digit sources, geometry, and optional flag) is described by a DrumReadout defined in the sketch, like the PinRef wiring map. Each panel takes one transport argument: an I²C bus, or an I2cMux when several same-address OLEDs share a TCA9548A. Omit it and the default bus is used. A bare OLED may not share a trunk with panels behind a mux — it is shadowed whenever a channel is open — so put the mux on one trunk and a bare panel on the other.

Each source also declares its band — how many positions the gauge divides its range into, and what each one displays. DCS-BIOS normalises a gauge's declared range to 16 bits and leaves the meaning to the client, and the A-4E-C gauges genuinely differ: an ordinary digit drum exports digit/10, the ARC-51 selectors export a position index, and the altimeter's inHg group runs over {29, 30}. A plain digit drum needs no extra fields.

Wiring map convention

PinRef bit positions and mask values must be named constants, never inline literals. Define a wiring-map block at the top of each sketch — one named PinRef per physical connection, matching the schematic net label — so every pin appears exactly once and traces back to the schematic.