HID Controller Setup

ATLAS supports HID controllers (gamepads, joysticks) for hands-on map control in the DEF and BMS functional areas. The controller gateway publishes raw input over DDS, and the HMI maps axes and buttons to screen actions via JSON configuration.

Architecture

flowchart LR Controller["🎮 USB/BT Controller
(Xbox, PlayStation, etc.)"] Gateway["gva-gateway-joystick
SDL2 → DDS"] HMI["gva-hmi
HidInputManager"] DEF["DEF Radar
Zoom · Threats"] BMS["BMS Map
Zoom · Pan"] Controller --> Gateway Gateway -->|"DDS
Displays_And_Controls"| HMI HMI --> DEF HMI --> BMS

The system uses two processes:

Process Role
gva-gateway-joystick Reads the physical controller via SDL2 and publishes raw axes/buttons on DDS Displays_And_Controls topics. No application logic — just raw input.
gva-hmi Subscribes to the DDS topics, resolves each component's role descriptor, and maps inputs to screen-specific actions based on the JSON config.

This separation means any SDL2-compatible controller works — Xbox, PlayStation, Switch Pro, flight sticks, or third-party pads — without changing the HMI.

Supported Controllers

Any controller recognised by SDL2's SDL_GameController API works out of the box:

  • Xbox Elite / Xbox Series — recommended, all features mapped
  • Xbox 360 — wired and wireless (with adapter)
  • PlayStation DualSense / DualShock 4 — via SDL2's mapping database
  • Nintendo Switch Pro — via SDL2's mapping database
  • Generic USB gamepads — if listed in SDL2's controller database

Tip

Run gva-gateway-joystick --verbose to see a live ASCII readout of all axes and buttons — useful for verifying your controller is recognised.

Quick Start

1. Connect the controller

Plug in a USB controller or pair a Bluetooth controller. Verify Linux sees it:

# List game controllers
ls /dev/input/js*

# Or check SDL2 directly
gva-gateway-joystick --verbose
# Output: SDL sees 1 joystick(s)
#   [0] gamepad  Xbox Elite Wireless Controller

2. Start the joystick gateway

# On the same DDS domain as the HMI (default: 0)
gva-gateway-joystick --domain=0

The gateway publishes three types of DDS samples:

DDS Topic Controller Input Published When
C_Input_Control_2D Left stick, right stick, D-Pad Axis value changes
C_Input_Control_1D Left trigger, right trigger Trigger value changes
C_Button_Control A/B/X/Y, LB/RB, Back/Start, stick clicks Button press/release

Idle input generates zero DDS traffic — samples are only published on change.

3. Start the HMI

gva-hmi --domain=0

Navigate to the DEF screen (key 3). Trigger zoom and bumper threat-cycling work immediately with default bindings.

4. Verify with DDS discovery

astutedds-discovery-dump -d 0

Look for the gateway's participants and matched writer/reader pairs on the Displays_And_Controls__* topics.

Gateway Command-Line Options

gva-gateway-joystick [options]
Option Default Description
-d, --domain <N> 0 DDS domain ID (must match the HMI)
-i, --id <N> 0 Gateway LDM resource ID
-j, --joystick <N> 0 SDL2 controller index (0 = first detected)
-p, --publish-rate <ms> 20 Poll interval in milliseconds (50 Hz default)
-z, --deadzone <0..1> 0.05 Normalised stick/trigger deadzone at the gateway level
-V, --verbose off Print live ASCII gamepad state to stdout

JSON Configuration

Controller bindings are defined in the "controller" section of gva-hmi-config.json. The HMI reads this at startup — no recompilation needed.

Configuration File Location

The HMI searches for gva-hmi-config.json in order:

  1. ./gva-hmi-config.json (current directory)
  2. ./etc/gva-hmi-config.json
  3. <app-dir>/etc/gva-hmi-config.json
  4. /etc/gva/hmi/gva-hmi-config.json

Controller Section

{
    "controller": {
        "enabled": true,
        "resourceId": -1,
        "instanceId": -1,
        "deadzone": 0.15,
        "triggerZoomSpeed": 1.5,
        "tickIntervalMs": 30,
        "axisBindings": {
            "LEFT_STICK": { "action": "pan", "invertY": false },
            "RIGHT_STICK": { "action": "rotateTilt", "invertY": false },
            "DPAD": { "action": "pan", "invertY": false },
            "LEFT_TRIGGER": { "action": "zoomOut" },
            "RIGHT_TRIGGER": { "action": "zoomIn" }
        },
        "buttonBindings": {
            "BUTTON_A": "select",
            "BUTTON_B": "back",
            "BUTTON_X": "cycleMap",
            "BUTTON_Y": "toggleZoom",
            "LEFT_BUMPER": "prevThreat",
            "RIGHT_BUMPER": "nextThreat",
            "BACK": "resetView",
            "LEFT_STICK_BUTTON": "toggleTrackAll",
            "RIGHT_STICK_BUTTON": "resetNorth"
        }
    }
}

Global Settings

Field Type Default Description
enabled bool true Enable/disable controller input entirely
resourceId int -1 LDM resource ID of the controller gateway to accept. Set to -1 to accept input from any controller. When multiple controllers are connected, set this to the --id value of the specific gateway instance.
instanceId int -1 LDM instance ID to accept. Set to -1 to accept any instance. Use together with resourceId to match a specific (resourceId, instanceId) pair when multiple gateway instances share the same resource ID.
deadzone float 0.15 Trigger/stick dead zone at the HMI level (0.0–1.0). Values below this threshold are ignored.
triggerZoomSpeed float 1.5 Zoom factor per tick at full trigger deflection. Higher = faster zoom.
tickIntervalMs int 30 Continuous axis evaluation interval in milliseconds. Lower = smoother but more CPU.

Note

There are two deadzones in the pipeline. The gateway has its own (--deadzone, default 0.05) which suppresses jitter at the DDS level. The HMI's deadzone (default 0.15) sets the threshold for triggering an action — values between 0.05 and 0.15 are published but ignored by the HMI.

Axis Bindings

Axis bindings map analogue inputs (sticks, triggers) to continuous actions. The key is the role descriptor published by the gateway.

Role Descriptor Controller Input Available Actions
LEFT_STICK Left thumbstick (2D) pan
RIGHT_STICK Right thumbstick (2D) rotateTilt
DPAD D-Pad (2D, digital) pan
LEFT_TRIGGER Left trigger (1D, 0–1) zoomOut
RIGHT_TRIGGER Right trigger (1D, 0–1) zoomIn

2D axis options:

Option Type Default Description
action string Action to perform: pan or rotateTilt
invertY bool false Invert the Y axis (push-up = pan-down)

Button Bindings

Button bindings map momentary presses to discrete actions. Only the press event is used — release is ignored.

Role Descriptor Controller Input Default Action
BUTTON_A A (Xbox) / Cross (PS) select
BUTTON_B B (Xbox) / Circle (PS) back
BUTTON_X X (Xbox) / Square (PS) cycleMap
BUTTON_Y Y (Xbox) / Triangle (PS) toggleZoom
LEFT_BUMPER LB (Xbox) / L1 (PS) prevThreat
RIGHT_BUMPER RB (Xbox) / R1 (PS) nextThreat
BACK Back/View (Xbox) / Share (PS) resetView
START Start/Menu (Xbox) / Options (PS) (unbound)
GUIDE Xbox/PS button (unbound)
LEFT_STICK_BUTTON Left stick click toggleTrackAll
RIGHT_STICK_BUTTON Right stick click resetNorth

Available Actions

Map / Radar Actions (DEF Screen)

Action Description
zoomIn Zoom into the radar map (shrink displayed range). Proportional to trigger deflection — light press = slow zoom, full pull = fast zoom.
zoomOut Zoom out of the radar map (increase displayed range).
pan Pan the map in the direction of stick deflection.
nextThreat Select the next threat (further away by range) and centre the radar view on it.
prevThreat Select the previous threat (closer by range) and centre the radar view on it.
select Select/confirm the current threat or map feature.
back Deselect the current selection.
cycleMap Cycle through map background types (Off → Satellite → Topographic → Street → Off).
toggleZoom Toggle between minimum and maximum zoom range.
resetView Reset pan offset and centre the view on the vehicle position.
toggleTrackAll Toggle trail display for all tracked threats.
resetNorth Reset map rotation/tilt to north-up orientation.

Screen Navigation Actions

Action Description
rotateTilt Rotate and tilt the map (right stick X = rotation, Y = tilt). Only active on BMS.

DEF Screen Controls

On the DEF (Defence) screen, the controller provides hands-on interaction with the radar view:

Input Action
Right Trigger Zoom in (proportional — light press = slow, full pull = fast)
Left Trigger Zoom out (proportional)
RB Select next threat (further) and centre view
LB Select previous threat (closer) and centre view
X Cycle map type (Off → Satellite → Topo → Street)
Y Toggle min/max zoom
Back Reset view to vehicle position
Left Stick Click Toggle track-all trails

BMS Screen Controls

On the BMS screen, controller inputs are forwarded to the external BMS application (e.g. TALOS) via the Display Extension protocol:

Input Action
Right Trigger Zoom in (forwarded as wheel event)
Left Trigger Zoom out (forwarded as wheel event)
Left Stick Pan the map
Right Stick Rotate / tilt the map

DDS Topics

The joystick gateway publishes on three Displays_And_Controls_PSM topics, using the 2-arg constructor so QoS is resolved via PatternForTopic():

Topic Type Content
Displays_And_Controls__Input_Control_2D C_Input_Control_2D Stick X/Y in [-1, 1], D-Pad as {-1, 0, +1} per axis
Displays_And_Controls__Input_Control_1D C_Input_Control_1D Trigger value in [0, 1]
Displays_And_Controls__Button_Control C_Button_Control Press/release state per button

Each component is identified by a (resourceId, instanceId) pair, with a role descriptor string (e.g. "LEFT_TRIGGER", "RIGHT_BUMPER") published once at startup via C_Human_Interaction_Component_Specification.

Troubleshooting

Controller not detected

# Check if SDL2 sees the controller
gva-gateway-joystick --verbose
# If "no SDL_GameController-compatible device found":
#   - Check /dev/input/js* exists
#   - Ensure current user is in the 'input' group:
sudo usermod -aG input $USER
#   - Log out and back in for group change to take effect

Controller works in gateway but HMI doesn't respond

  1. Verify both processes are on the same DDS domain:

    gva-gateway-joystick --domain=0
    gva-hmi --domain=0
    

  2. Check DDS discovery:

    astutedds-discovery-dump -d 0
    
    Look for matched writer/reader pairs on the Displays_And_Controls__* topics.

  3. Verify the "controller" section in gva-hmi-config.json has "enabled": true.

  4. Check HMI logs for controller startup:

    QT_LOGGING_RULES="gva.hmi.hid_input.info=true" gva-hmi --domain=0
    
    You should see:
    gva.hmi.hid_input: created on domain 0 controller: enabled
    gva.hmi.hid_input: subscribed: Displays_And_Controls__Input_Control_1D
    

Trigger zoom is too fast or too slow

Adjust triggerZoomSpeed in the JSON config:

  • Slower: 0.51.0
  • Default: 1.5
  • Faster: 2.03.0

Stick/trigger jitter at rest

Increase the deadzone value in the JSON config (default 0.15). A value of 0.20.25 eliminates most controller jitter.

Multiple controllers

When multiple controllers are connected, each needs its own gva-gateway-joystick instance with a unique --id:

# Controller 1 — map control (joystick index 0)
gva-gateway-joystick --domain=0 --id=100 --joystick=0

# Controller 2 — weapon system (joystick index 1)
gva-gateway-joystick --domain=0 --id=101 --joystick=1

Then set resourceId (and optionally instanceId) in the JSON config to the --id / --instance of the controller you want the HMI to use for map control:

"controller": {
    "resourceId": 100,
    "instanceId": -1
}

With resourceId set to 100, the HMI ignores all controller input from resource 101. Set instanceId to further narrow the match when multiple gateways share the same resource ID. MFD bezel input (from gva-gateway-hid) is always accepted regardless of these filters.