HWC Key Mapping
Reactor's default configurations work across different panel types through HWC Key Mapping (HardWare Component Key Mapping) - a system that separates configuration logic from physical button assignments. For example: When you select a configuration from the homescreen dropdown, the system automatically maps this config to the correct panel ID.
HWC Key Mapping creates an indirection layer between behavior definitions and physical panel buttons. This enables configuration reusability across Reactor's ecosystem.
When you create a custom configuration through the UI, Reactor automatically generates a KeyMap with default entries. Understanding KeyMaps helps debugging configurations and build reusable configurations.
In this chapter, we'll examine how HWC Key Mapping works, why it's essential for configuration composition, and when you need to work with KeyMaps directly.
How Reactor Uses KeyMaps Automatically
Before we dive into the technical details, let's understand how you're already using KeyMaps without realizing it.
Automatic KeyMaps in Custom Configs
When you create a behavior in the Configurator by assigning it to a button, Reactor doesn't store a hardcoded physical reference like _p1.1. Instead, the system automatically:
- Picks an alias - a human-readable name for that button position
- Adds a KeyMap entry - mapping the physical button to the alias
- Defines the behavior - using the alias instead of the physical reference
Eg: You click on button 5 on panel 1, add a behavior, and behind the scenes Reactor creates the correct setup. If there is an existing valid mapping of your Component Reactor will use the existing one instead.
What you see in the UI:
- "Button 5 on Panel 1 controls Camera 1"
What Reactor stores:
{
"HWCKeyMap": {
"_p1.5": "CameraBtn"
},
"HWCBehaviors": {
"CameraBtn": {
"Name": "Camera 1",
"IOReference": {
"Raw": "DC:camera/Var:DeviceIndex/selectInput"
}
}
}
}
Automatic Panel Mapping on Config Import
On Reactor's homescreen, you can select configurations from dropdowns and apply them to specific panels. When you do this, Reactor automatically adds your config to ImportLayerFiles and sets up a KeyMap to map your panel ID to Nr 1 used in the config. On some special configs, like PTZPro+ Frameshot or the MegaPanel configs the subconfigs will allow for more than 1 panel to be mapped.
Mapping all Components of a panel can be done with a wildcard mapping. A wildcard KeyMap uses the * character to map entire ranges of buttons at once. For example, _p3.* → _p1.* means "any button on panel 3 should behave like the same-numbered button on panel 1." This single rule automatically maps button 1 to button 1, button 2 to button 2, and so on - without needing to list every button individually.
Example: You have a TCPRack (panel 3) and select "ATEM Switcher Control" from the dropdown.
What Reactor automatically adds:
{
"Name": "My Project",
"HWCKeyMap": {
"_p3.*": "_p1.*"
},
"ImportLayerFiles": [
"ATEM_Switcher_Control"
]
}
This single wildcard mapping (_p3.* → _p1.*) makes every behavior in the ATEM config work on panel 3, even though the original configuration uses panel 1 references.
RackFusion Live is a great example of configuration reuse - it combines the PTZ Fly and LiveFly configurations into a single product. Both sub-configurations use HWC aliases internally, and KeyMaps handle the mapping to the physical panels.
Without KeyMaps, the PTZ and Live control logic would need to be duplicated and maintained separately for each combined product.
When You Need to Understand KeyMaps
Most of the time, Reactor handles KeyMaps automatically without user intervention. Understanding KeyMaps becomes important when:
- Building reusable configurations - Creating configs meant to be shared or reused
- Composing complex multi-file configs - Including multiple configuration files that need remapping
- Customizing default configurations - Modifying SKAARHOJ defaults for specific use cases
- Debugging configuration issues - Understanding why a button isn't triggering the expected behavior
- Creating advanced multi-panel setups - Creating configurations that combine multiple panels in one might in some cases require understanding of KeyMaps
Physical References vs. Aliases
HWC Key Mapping creates a translation layer in your configuration:
graph LR
A["Physical Button<br/>_p1.1"] --> B["KeyMap Translation"] --> C["HWC Alias<br/>Cam1"] --> D["Behavior Definition<br/>Cam1"]
style A fill:#5B9BD5,stroke:#2E5C8A,color:#fff
style B fill:#66BB6A,stroke:#43A047,color:#fff
style C fill:#F39C12,stroke:#C87F0A,color:#fff
style D fill:#78909C,stroke:#546E7A,color:#fff
Physical References like _p1.1 represent actual hardware:
_p= Panel reference prefix- First number = Panel ID
- Second number = Hardware Component (HWC) ID on that panel
HWC Aliases like Cam1, Take, MenuNav are meaningful names you define:
- Human-readable
- Panel-agnostic
- Reusable across configurations
Mapping Syntax and Rules
The following sections explain KeyMap syntax in detail. This is helpful for understanding how configs work under the hood, debugging issues, or creating your own reusable configuration libraries.
HWCKeyMap supports several powerful mapping patterns. Let's explore each one.
Individual HWC Mapping
The most basic form maps specific panel buttons to aliases:
{
"HWCKeyMap": {
"_p1.1": "Cam1",
"_p1.2": "Cam2",
"_p1.48": "Take",
"_p1.72": "Menu"
}
}
- Key: Physical panel reference (
_p[PanelID].[HWCID]) - Value: HWC alias name
With this mapping:
- When a behavior is defined for
Cam1, it actually controls button_p1.1 - When you want to use the same config on panel 2, just change the KeyMap
- The behavior definitions remain unchanged
Full Panel Wildcard Mapping
Map all buttons from one panel to another using wildcards:
{
"HWCKeyMap": {
"_p7.*": "_p1.*"
}
}
This powerful pattern means:
- Any behavior defined for panel 1 (
_p1.X) will now work on panel 7 (_p7.X) - Button 1 on panel 1 maps to button 1 on panel 7
- Button 48 on panel 1 maps to button 48 on panel 7
- And so on...
This is essential for multi-panel setups where panels have identical layouts.
graph LR
A["_p7.1"] --> B["Wildcard Map<br/>_p7.* to _p1.*"] --> C["_p1.1"] --> D["Behavior"]
E["_p7.48"] --> B --> F["_p1.48"] --> G["Behavior"]
style A fill:#F39C12,stroke:#C87F0A,color:#fff
style E fill:#F39C12,stroke:#C87F0A,color:#fff
style B fill:#66BB6A,stroke:#43A047,color:#fff
style C fill:#5B9BD5,stroke:#2E5C8A,color:#fff
style F fill:#5B9BD5,stroke:#2E5C8A,color:#fff
style D fill:#78909C,stroke:#546E7A,color:#fff
style G fill:#78909C,stroke:#546E7A,color:#fff
Processing order matters: Full panel wildcards are processed first, then individual mappings.
The Blocking Rule
By default, any button references that don't match a KeyMap entry pass through unchanged. But you can change this with the blocking rule:
{
"HWCKeyMap": {
"_p1.1": "AllowedButton1",
"_p1.2": "AllowedButton2",
"_block": "*"
}
}
With "_block": "*":
- Only explicitly mapped buttons are accessible
- Any button not in the KeyMap is blocked from reaching behaviors
- This is useful for creating restricted sub-configurations
graph TD
A{Button reference} --> B{In KeyMap?}
B -->|Yes| C[Map to alias]
B -->|No| D{_block set?}
D -->|Yes| E[Block - no behavior]
D -->|No| F[Pass through unchanged]
style A fill:#78909C,stroke:#546E7A,color:#fff
style B fill:#78909C,stroke:#546E7A,color:#fff
style C fill:#66BB6A,stroke:#43A047,color:#fff
style D fill:#78909C,stroke:#546E7A,color:#fff
style E fill:#EF5350,stroke:#C62828,color:#fff
style F fill:#5B9BD5,stroke:#2E5C8A,color:#fff
Without _block: Unmapped references like _p1.50 would pass through and could match behaviors defined directly with _p1.50.
With _block: Only _p1.1 and _p1.2 work; everything else is blocked.
How KeyMaps Enable Configuration Composition
Let's say you have a sophisticated PTZ camera control configuration with presets, focus controls, and iris adjustments. You want to use this configuration across multiple projects with different panel types.
Step 1: Create a reusable configuration file (saved as PTZCameraControl.json):
{
"Name": "PTZ Camera Control",
"Variables": {
"DeviceID": {
"Name": "Camera Device ID",
"Default": ["1"]
}
},
"HWCBehaviors": {
"Preset1": {
"Name": "Recall Preset 1",
"IOReference": {
"Raw": "DC:ptz-camera/Var:DeviceID/recallPreset/Behavior:Const:PresetNum"
},
"Constants": {
"PresetNum": {
"Values": ["1"]
}
}
},
"Preset2": {
"Name": "Recall Preset 2",
"IOReference": {
"Raw": "DC:ptz-camera/Var:DeviceID/recallPreset/Behavior:Const:PresetNum"
},
"Constants": {
"PresetNum": {
"Values": ["2"]
}
}
},
"FocusNear": {
"Name": "Focus Near",
"IOReference": {
"Raw": "DC:ptz-camera/Var:DeviceID/focusNear"
}
},
"FocusFar": {
"Name": "Focus Far",
"IOReference": {
"Raw": "DC:ptz-camera/Var:DeviceID/focusFar"
}
}
}
}
Notice: No physical panel references! All behaviors use HWC aliases.
Step 2: Use it on a RackFlyTrio controller (buttons 1-4):
{
"Name": "RackFlyTrio Camera Control",
"HWCKeyMap": {
"_p1.1": "Preset1",
"_p1.2": "Preset2",
"_p1.3": "FocusNear",
"_p1.4": "FocusFar"
},
"ImportLayerFiles": [
"PTZCameraControl"
]
}
Step 3: Use the same config on a different controller with a different layout:
{
"Name": "StreamDeck Camera Control",
"HWCKeyMap": {
"_p5.10": "Preset1",
"_p5.11": "Preset2",
"_p5.20": "FocusNear",
"_p5.21": "FocusFar"
},
"ImportLayerFiles": [
"PTZCameraControl"
]
}
graph BT
A1["RackFlyTrio<br/>_p1.1-4 mapped to<br/>Preset1, Preset2, etc."] --> B["PTZ Camera Control<br/>Reusable Config<br/>Defines: Preset1, Preset2, FocusNear, FocusFar"]
A2["StreamDeck<br/>_p5.10-21 mapped to<br/>Preset1, Preset2, etc."] --> B
style B fill:#66BB6A,stroke:#43A047,color:#fff
style A1 fill:#F39C12,stroke:#C87F0A,color:#fff
style A2 fill:#F39C12,stroke:#C87F0A,color:#fff
Hierarchical Processing Through Layers
KeyMaps process hierarchically through the entire layer tree, enabling complex configuration composition.
How Resolution Works
When Reactor needs to find which behavior controls a physical button, it follows this process:
- Start at the leaf layer where the behavior is defined
- Apply that layer's KeyMap (if it has one)
- Move up to the parent layer
- Apply the parent's KeyMap (if it has one)
- Continue up the tree until reaching the root
- Result: Complete mapping path from HWC alias to physical button
graph BT
A["Root Layer<br/>HWCKeyMap: _p7.* to _p1.*<br/>Maps panel 7 to panel 1"] --> B["Controller Layer<br/>HWCKeyMap: _p1.1 to Cam1<br/>Maps button to alias"]
B --> C["Included Config Layer<br/>HWCBehaviors: Cam1<br/>Defines the behavior"]
D["Physical Button: _p7.1"] -.->|Root maps to| E["_p1.1"]
E -.->|Controller maps to| F["Cam1"]
F -.->|Behavior found| C
style A fill:#5B9BD5,stroke:#2E5C8A,color:#fff
style B fill:#F39C12,stroke:#C87F0A,color:#fff
style C fill:#66BB6A,stroke:#43A047,color:#fff
style D fill:#EF5350,stroke:#C62828,color:#fff
style E fill:#5B9BD5,stroke:#2E5C8A,color:#fff
style F fill:#F39C12,stroke:#C87F0A,color:#fff
Example: Multi-Layer Mapping
Let's see a practical example with three layers:
Root Layer (Main Project):
{
"Name": "Multi-Panel Project",
"HWCKeyMap": {
"_p2.*": "_p1.*"
},
"Layers": [
{ "Name": "CameraControl" }
]
}
Camera Control Layer (Child):
{
"Name": "Camera Control",
"HWCKeyMap": {
"_p1.1": "Cam1Select",
"_p1.2": "Cam2Select"
},
"ImportLayerFiles": [
"CameraLibrary"
]
}
Camera Library (Included Config):
{
"Name": "Camera Library",
"HWCBehaviors": {
"Cam1Select": {
"Name": "Select Camera 1",
"IOReference": {
"Raw": "Var:ActiveCamera/SetValue"
},
"Constants": {
"CameraIndex": {
"Values": ["1"]
}
}
},
"Cam2Select": {
"Name": "Select Camera 2",
"IOReference": {
"Raw": "Var:ActiveCamera/SetValue"
},
"Constants": {
"CameraIndex": {
"Values": ["2"]
}
}
}
}
}
Resolution path for a button press on _p2.1:
- Root layer:
_p2.1matches wildcard_p2.*→ maps to_p1.1 - Camera Control layer:
_p1.1matches KeyMap → maps toCam1Select - Camera Library: Behavior
Cam1Selectis found and executed
This three-layer mapping allows:
- The Camera Library to be completely panel-agnostic
- The Camera Control layer to define a logical button layout
- The Root layer to duplicate that layout to additional panels
Pass-Through Behavior
If a layer's KeyMap doesn't contain a matching entry, references pass through unchanged to the parent layer:
{
"Name": "Parent Layer",
"HWCKeyMap": {
"_p1.1": "ButtonA"
},
"Layers": [
{
"Name": "Child",
"HWCKeyMap": {
"_p1.2": "ButtonB"
},
"HWCBehaviors": {
"ButtonA": { "Name": "behavior A" },
"ButtonB": { "Name": "behavior B" }
}
}
]
}
For behavior ButtonA in the child layer:
- Child layer KeyMap doesn't have
ButtonA→ passes through - Parent layer KeyMap has
_p1.1 → ButtonA→ match found!
This allows child layers to define additional behaviors without requiring the parent to know about them.
Practical Examples from Real Configurations
Let's look at how KeyMaps are used in actual SKAARHOJ Default Configurations.
Example 1: RackFlyTrio Routing Control
This configuration creates a routing controller on a RackFlyTrio panel:
{
"Name": "Routing Control - Rack Fly Trio",
"HWCKeyMap": {
"_p1.1": "Select1",
"_p1.2": "Select2",
"_p1.3": "Select3",
"_p1.4": "Select4",
"_p1.5": "Select5",
"_p1.6": "Select6",
"_p1.24": "Shift",
"_p1.48": "Take",
"_p1.72": "Menu"
},
"HWCBehaviors": {
"Select1": {
"Name": "Input 1",
"IOReference": {
"Raw": "DC:router/Var:DeviceIndex/selectInput/Behavior:Const:InputNum"
},
"Constants": {
"InputNum": {
"Values": ["1"]
}
}
},
"Select2": {
"Name": "Input 2",
"IOReference": {
"Raw": "DC:router/Var:DeviceIndex/selectInput/Behavior:Const:InputNum"
},
"Constants": {
"InputNum": {
"Values": ["2"]
}
}
},
"Shift": {
"Name": "Shift Modifier",
"IOReference": {
"Raw": "Var:ShiftMode"
}
},
"Take": {
"Name": "Execute Route",
"IOReference": {
"Raw": "DC:router/Var:DeviceIndex/take"
}
},
"Menu": {
"Name": "Menu Button",
"IOReference": {
"Raw": "Var:MenuPage"
}
}
}
}
Benefits:
- Behavior names are semantic (
Take,Shift,Menu) instead of cryptic (_p1.48,_p1.24,_p1.72) - If SKAARHOJ releases a new panel, adapting this config requires only changing the KeyMap
- The routing logic can be tested and debugged independent of the physical panel
Example 2: Blocking Unwanted Buttons
The MKA4 QuickClass configuration uses blocking to limit which buttons are active:
{
"Name": "QuickClass Page 1 Controls",
"HWCKeyMap": {
"PageQ1": "PageQ1",
"_block": "*",
"_p1.23": "Q1",
"_p1.24": "Q2",
"_p1.25": "Q3",
"_p1.26": "Q4",
"_p1.27": "Q5",
"_p1.28": "Q6"
},
"HWCBehaviors": {
"Q1": { "Description": "quick button 1 logic" },
"Q2": { "Description": "quick button 2 logic" },
"Q3": { "Description": "quick button 3 logic" },
"Q4": { "Description": "quick button 4 logic" },
"Q5": { "Description": "quick button 5 logic" },
"Q6": { "Description": "quick button 6 logic" }
}
}
The _block rule ensures:
- Only buttons 23-28 on panel 1 are active
- The special
PageQ1variable passes through (used by parent layers) - All other buttons are blocked, preventing accidental behavior triggers
Edge Cases
Multiple Mappings to Same Destination
What happens if you map multiple physical buttons to the same alias?
{
"HWCKeyMap": {
"_p1.1": "TriggerButton",
"_p1.2": "TriggerButton",
"_p1.3": "TriggerButton"
},
"HWCBehaviors": {
"TriggerButton": {
"Name": "Any of three buttons",
"IOReference": {
"Raw": "Var:Trigger"
}
}
}
}
Result: All three physical buttons (_p1.1, _p1.2, _p1.3) will trigger the same behavior. This is perfectly valid and useful for creating "any of these buttons" scenarios.
Collision Warnings
If multiple behaviors in the same layer map to the same physical button, Reactor will log a warning:
Behavior [path] mapped to key [button] which was already used by another behavior
on this layer! This will create random selection of which behavior defines the HWC!
This happens when:
{
"HWCKeyMap": {
"_p1.1": "Behavior1",
"_p1.1": "Behavior2"
}
}
Wait, that's impossible in JSON (duplicate keys). But it can happen in more subtle ways:
{
"HWCBehaviors": {
"_p1.1": { "Description": "direct definition" },
"Alias1": { "Description": "aliased behavior" }
},
"HWCKeyMap": {
"_p1.1": "Alias1"
}
}
Both behaviors try to claim _p1.1! Reactor will pick one (often the last one processed), but the behavior is undefined. Avoid this pattern.
Order of Processing Matters
KeyMaps process in this order:
- Full panel wildcards first (e.g.,
_p2.*→_p1.*) - Individual mappings second (e.g.,
_p1.5→Button5)
This means:
{
"HWCKeyMap": {
"_p2.*": "_p1.*",
"_p2.5": "SpecialButton"
}
}
The individual mapping _p2.5 → SpecialButton overrides the wildcard for that specific button.