Input Events
MSFS 2024 only. The Input Event API (
EnumerateInputEvents,GetInputEvent,SetInputEventDouble/SetInputEventString,SubscribeInputEvent,UnsubscribeInputEvent) is not present in the MSFS 2020 SimConnect SDK. Calling these methods with aSimConnect.dllthat lacks the export panics (the procedure is not found), so check the simulator version before calling them. TheAs*message helpers (AsEnumerateInputEvents(),AsGetInputEvent(),AsSubscribeInputEvent()) will always returnnilwhen connected to MSFS 2020 because the simulator never sends the correspondingDwIDvalues.
See also: Engine/Client Usage for the client API and the full list of message helpers.
What Are Input Events?
Input Events are hash-addressed simulator events that map to physical cockpit interactions. They represent things like button presses, switch states, and knob positions in aircraft panels. Unlike SimVars, which describe the state of the simulation world, Input Events are the raw input bindings — the same ones the simulator uses internally to trigger avionics logic.
You discover available events at runtime by enumerating them. Each event has a human-readable name, a 64-bit hash, and a value type (numeric or string). Once you have the name or hash you need, you can read the current value, write a new value, or subscribe to receive a notification every time the value changes.
Common use cases include:
- Building hardware panel integrations that reflect cockpit switch state
- Monitoring whether a specific button was pressed
- Driving cockpit state from an external source without going through SimVar writes
Enumerate Available Events
Call EnumerateInputEvents to request the full list of available input events. The simulator responds with one or more SIMCONNECT_RECV_ID_ENUMERATE_INPUT_EVENTS messages, each containing a batch of descriptors.
//go:build windows
package main
import (
"fmt"
"github.com/mrlm-net/simconnect/pkg/engine"
"github.com/mrlm-net/simconnect/pkg/types"
)
const EnumReqID uint32 = 1000
func main() {
client := engine.New("InputEventEnum")
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
if err := client.EnumerateInputEvents(EnumReqID); err != nil {
panic(err)
}
for msg := range client.Stream() {
if types.SIMCONNECT_RECV_ID(msg.DwID) != types.SIMCONNECT_RECV_ID_ENUMERATE_INPUT_EVENTS {
continue
}
recv := msg.AsEnumerateInputEvents()
if recv == nil {
continue
}
// Entries returns the recv.DwArraySize descriptors of this batch,
// read in place from the packed message (valid while msg is).
for _, desc := range recv.Entries() {
name := engine.BytesToString(desc.Name[:])
fmt.Printf("Event: %-64s hash=0x%016X type=%d\n", name, desc.Hash(), desc.Type)
}
// DwEntryNumber and DwOutOf let you track batched delivery.
if recv.DwEntryNumber+1 >= recv.DwOutOf {
fmt.Println("Enumeration complete.")
break
}
}
}
Signature: EnumerateInputEvents(requestID uint32) error
SIMCONNECT_INPUT_EVENT_DESCRIPTOR fields:
| Field | Type | Description |
|---|---|---|
Name |
[64]byte |
Human-readable event name, null-terminated. Use engine.BytesToString(desc.Name[:]) to convert. |
HashBytes |
[8]byte |
The event’s 64-bit hash, little-endian. Read it with the desc.Hash() method, which returns the uint64 that GetInputEvent, SetInputEvent*, SubscribeInputEvent and UnsubscribeInputEvent take. |
Type |
SIMCONNECT_INPUT_EVENT_TYPE |
Value type of the event (SIMCONNECT_INPUT_EVENT_TYPE_DOUBLE or SIMCONNECT_INPUT_EVENT_TYPE_STRING). |
Note: A descriptor is 76 bytes on the wire (
types.InputEventDescriptorSize). The hash is kept as[8]byteso the Go struct has no padding and matches that layout; auint64field would make it 80 bytes and every descriptor after the first would be read from the wrong place. Read the descriptors withrecv.Entries(), not by indexingRgData(a zero-length marker).
Get Event Value
To read the current value of a known event, call GetInputEvent with a request ID and the event’s 64-bit hash. The response arrives asynchronously as a SIMCONNECT_RECV_ID_GET_INPUT_EVENT message.
//go:build windows
package main
import (
"fmt"
"github.com/mrlm-net/simconnect/pkg/engine"
"github.com/mrlm-net/simconnect/pkg/types"
)
const GetReqID uint32 = 1001
func main() {
client := engine.New("InputEventGet")
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
// hash is a uint64 obtained from enumeration or a known constant.
var hash uint64 = 0x00000001
if err := client.GetInputEvent(GetReqID, hash); err != nil {
panic(err)
}
for msg := range client.Stream() {
if types.SIMCONNECT_RECV_ID(msg.DwID) != types.SIMCONNECT_RECV_ID_GET_INPUT_EVENT {
continue
}
recv := msg.AsGetInputEvent()
if recv == nil || uint32(recv.RequestID) != GetReqID {
continue
}
if f, ok := engine.InputEventValueAsFloat64(recv); ok {
fmt.Printf("Value (float64): %f\n", f)
}
if s, ok := engine.InputEventValueAsString(recv); ok {
fmt.Printf("Value (string): %s\n", s)
}
break
}
}
Signature: GetInputEvent(requestID uint32, hash uint64) error
Value extraction helpers for *types.SIMCONNECT_RECV_GET_INPUT_EVENT:
| Helper | Signature | Returns |
|---|---|---|
engine.InputEventValueAsFloat64 |
(recv *types.SIMCONNECT_RECV_GET_INPUT_EVENT) (float64, bool) |
Value and true when type is DOUBLE; (0, false) otherwise |
engine.InputEventValueAsString |
(recv *types.SIMCONNECT_RECV_GET_INPUT_EVENT) (string, bool) |
Value and true when type is STRING; ("", false) otherwise |
Set Event Value
To write a value to an input event, use SetInputEventDouble for numeric events or SetInputEventString for string-typed events. Both calls are fire-and-forget — there is no response message. Any HRESULT error is returned directly from the call.
//go:build windows
// Write a numeric value to a DOUBLE-typed event.
err := client.SetInputEventDouble(hash, 1.0)
if err != nil {
// HRESULT error returned directly — no response message.
panic(err)
}
// Write a string value to a STRING-typed event.
// Strings longer than 259 bytes are silently truncated to preserve the null terminator.
err = client.SetInputEventString(hash, "AUTOPILOT_ON")
if err != nil {
panic(err)
}
Signatures:
SetInputEventDouble(hash uint64, value float64) errorSetInputEventString(hash uint64, value string) error
Subscribe to Changes
Subscribing to an input event causes the simulator to push a notification message every time the event value changes. Use this instead of polling GetInputEvent in a loop.
//go:build windows
err := client.SubscribeInputEvent(hash)
Signature: SubscribeInputEvent(hash uint64) error
Change notifications arrive as SIMCONNECT_RECV_ID_SUBSCRIBE_INPUT_EVENT messages. Cast with msg.AsSubscribeInputEvent() and use the engine helpers to extract the hash and value:
//go:build windows
if recv := msg.AsSubscribeInputEvent(); recv != nil {
// Read the hash from the wire struct (stored as [8]byte due to alignment).
eventHash := engine.SubscribeInputEventHash(recv)
if f, ok := engine.SubscribeInputEventValueAsFloat64(recv); ok {
fmt.Printf("Event %d changed → %f\n", eventHash, f)
}
if s, ok := engine.SubscribeInputEventValueAsString(recv); ok {
fmt.Printf("Event %d changed → %s\n", eventHash, s)
}
}
Note:
SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT.HashBytesis stored as[8]byteat wire offset 12 rather thanuint64. This avoids the 4-byte alignment padding Go would otherwise insert, which would shift the field to the wrong offset. Always useengine.SubscribeInputEventHash(recv)rather than readingHashBytesdirectly withbinary.LittleEndian.Uint64.
Value extraction helpers for *types.SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT:
| Helper | Signature | Returns |
|---|---|---|
engine.SubscribeInputEventHash |
(recv *types.SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT) uint64 |
The 64-bit event hash |
engine.SubscribeInputEventValueAsFloat64 |
(recv *types.SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT) (float64, bool) |
Value and true when type is DOUBLE |
engine.SubscribeInputEventValueAsString |
(recv *types.SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT) (string, bool) |
Value and true when type is STRING |
Unsubscribe
Cancel a subscription by passing the same hash you used to subscribe:
//go:build windows
err := client.UnsubscribeInputEvent(hash)
Signature: UnsubscribeInputEvent(hash uint64) error
Call this before disconnecting if you want to be explicit, or when you no longer need change notifications for a particular event.
Hash Note
The hash is a 64-bit value in both places it appears. Both structs store it as raw bytes to match the packed wire layout, so read it with the helpers:
SIMCONNECT_INPUT_EVENT_DESCRIPTOR.HashBytes—[8]byteat offset 64 of each enumeration descriptor. Usedesc.Hash()to decode it.SIMCONNECT_RECV_SUBSCRIBE_INPUT_EVENT.HashBytes—[8]byteat wire offset 12 in the subscribe notification struct. Useengine.SubscribeInputEventHash(recv)to decode it.
Pass the decoded uint64 to the API calls, and use it to match notifications to your subscriptions.
Complete Example
This example enumerates all available input events, subscribes to the first one found, then prints every change notification until interrupted.
//go:build windows
package main
import (
"fmt"
"os"
"os/signal"
"github.com/mrlm-net/simconnect/pkg/engine"
"github.com/mrlm-net/simconnect/pkg/types"
)
const EnumReqID uint32 = 2000
func main() {
client := engine.New("InputEventDemo")
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
// Step 1: request the full list of available input events.
if err := client.EnumerateInputEvents(EnumReqID); err != nil {
panic(err)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
var subscribedHash uint64
for {
select {
case <-sigChan:
// Clean up subscription before exit.
if subscribedHash != 0 {
client.UnsubscribeInputEvent(subscribedHash)
}
fmt.Println("Shutting down.")
return
case msg, ok := <-client.Stream():
if !ok {
return
}
switch types.SIMCONNECT_RECV_ID(msg.DwID) {
case types.SIMCONNECT_RECV_ID_ENUMERATE_INPUT_EVENTS:
recv := msg.AsEnumerateInputEvents()
if recv == nil {
continue
}
descs := recv.Entries()
if len(descs) == 0 {
continue
}
// Print the first descriptor in this batch.
name := engine.BytesToString(descs[0].Name[:])
fmt.Printf("Found event: %s\n", name)
// Subscribe to the first event we see.
if subscribedHash == 0 {
subscribedHash = descs[0].Hash()
if err := client.SubscribeInputEvent(subscribedHash); err != nil {
fmt.Printf("SubscribeInputEvent failed: %v\n", err)
} else {
fmt.Printf("Subscribed to hash 0x%016X (%s)\n", subscribedHash, name)
}
}
case types.SIMCONNECT_RECV_ID_SUBSCRIBE_INPUT_EVENT:
recv := msg.AsSubscribeInputEvent()
if recv == nil {
continue
}
hash := engine.SubscribeInputEventHash(recv)
if f, ok := engine.SubscribeInputEventValueAsFloat64(recv); ok {
fmt.Printf("Event 0x%016X changed → %f\n", hash, f)
}
if s, ok := engine.SubscribeInputEventValueAsString(recv); ok {
fmt.Printf("Event 0x%016X changed → %s\n", hash, s)
}
case types.SIMCONNECT_RECV_ID_EXCEPTION:
recv := msg.AsException()
if recv != nil {
fmt.Printf("SimConnect exception: %d\n", recv.DwException)
}
}
}
}
}
See Also
- Engine/Client Usage — General dispatch loop patterns and message handling
- Input Events (Manager) — Using Input Events through the manager with lifecycle-managed reconnection and auto-reconnect considerations
- Client Configuration — Connection options