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.

Tip

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:

  1. Picks an alias - a human-readable name for that button position
  2. Adds a KeyMap entry - mapping the physical button to the alias
  3. 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.

What is a Wildcard KeyMap?

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.

Configuration Portability

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:

  1. Building reusable configurations - Creating configs meant to be shared or reused
  2. Composing complex multi-file configs - Including multiple configuration files that need remapping
  3. Customizing default configurations - Modifying SKAARHOJ defaults for specific use cases
  4. Debugging configuration issues - Understanding why a button isn't triggering the expected behavior
  5. 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:

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:

  1. Start at the leaf layer where the behavior is defined
  2. Apply that layer's KeyMap (if it has one)
  3. Move up to the parent layer
  4. Apply the parent's KeyMap (if it has one)
  5. Continue up the tree until reaching the root
  6. 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:

  1. Root layer: _p2.1 matches wildcard _p2.* → maps to _p1.1
  2. Camera Control layer: _p1.1 matches KeyMap → maps to Cam1Select
  3. Camera Library: Behavior Cam1Select is 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:

  1. Child layer KeyMap doesn't have ButtonA → passes through
  2. 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 PageQ1 variable 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:

  1. Full panel wildcards first (e.g., _p2.*_p1.*)
  2. Individual mappings second (e.g., _p1.5Button5)

This means:

{
  "HWCKeyMap": {
    "_p2.*": "_p1.*",
    "_p2.5": "SpecialButton"
  }
}

The individual mapping _p2.5 → SpecialButton overrides the wildcard for that specific button.