Traffic Guide
MVP notice:
pkg/trafficreflects current knowledge of the SimConnect AI aircraft API. Ground routing (taxiway graph, pathfinding) is not yet implemented — callers supply explicit waypoint coordinates. The API surface will grow in later milestones as simulator behaviour is better understood.
The pkg/traffic package provides a typed, thread-safe abstraction over SimConnect’s AI
aircraft creation and management API. It handles the async create→acknowledge lifecycle
and groups active aircraft into a Fleet that can be queried and cleaned up at once.
Aircraft Kinds
SimConnect supports three creation modes, represented by traffic.AircraftKind:
| Kind | How created | Movement |
|---|---|---|
KindParked |
AICreateParkedATCAircraft[EX1] |
ATC-controlled, placed at a gate |
KindEnroute |
AICreateEnrouteATCAircraft[EX1] |
ATC-controlled, following a flight plan |
KindNonATC |
AICreateNonATCAircraftEX1 |
Waypoint chain — caller controls movement |
Fleet
Fleet is the central collection. Create one once and reuse it across
the lifetime of your application. The manager creates and owns one automatically —
access it via mgr.Fleet().
import "github.com/mrlm-net/simconnect/pkg/traffic"
// standalone (without manager)
fleet := traffic.NewFleet(engineClient)
// via manager — fleet is created and lifecycle-managed internally
fleet := mgr.Fleet()
Thread safety
All Fleet methods are safe to call from concurrent goroutines (message handler,
ticker, signal handler, etc.). Internally a sync.RWMutex guards the maps.
Lifecycle across reconnects
SimConnect ObjectIDs are invalidated whenever the connection drops. Fleet.SetClient
handles this: it replaces the internal client reference and discards all pending
and member state. The manager calls this automatically on every connect and disconnect.
If you create a Fleet outside the manager you must call it yourself:
// on connect
fleet.SetClient(newClient)
// on disconnect
fleet.SetClient(nil)
Creation Pattern (async)
Aircraft creation in SimConnect is asynchronous. You issue a create request with a
reqID of your choice, then receive SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID back
with the same reqID and the new ObjectID. Fleet models this as two steps:
Step 1 — Request
const reqSpawn uint32 = 5001
err := mgr.TrafficParked(traffic.ParkedOpts{
Model: "FSLTL A320 Air France SL",
Livery: "", // "" selects the model default
Tail: "AFR123",
Airport: "LFPG",
}, reqSpawn)
if err != nil {
log.Println("spawn failed:", err)
}
Step 2 — Acknowledge
In your OnMessage handler, watch for the assigned-object message and call
Fleet.Acknowledge (or the manager’s wrapper):
mgr.OnMessage(func(msg engine.Message) {
if msg.Err != nil { return }
switch types.SIMCONNECT_RECV_ID(msg.DwID) {
case types.SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID:
assigned := msg.AsAssignedObjectID()
aircraft, ok := mgr.Fleet().Acknowledge(assigned.DwRequestID, assigned.DwObjectID)
if ok {
log.Printf("spawned: %s (objectID=%d)", aircraft.Tail, aircraft.ObjectID)
}
}
})
Acknowledge returns (nil, false) when the reqID was not issued by this fleet,
so it is safe to call for every assigned-object message even if other subsystems also
create objects.
Parked Aircraft
Parked aircraft are placed at an airport gate and managed by the simulator’s ATC.
err := mgr.TrafficParked(traffic.ParkedOpts{
Model: "FSLTL B737 Ryanair SL",
Livery: "",
Tail: "EIN400",
Airport: "EIDW",
}, 5002)
To assign a flight plan after spawning:
err := mgr.TrafficSetFlightPlan(objectID, "C:/Plans/EIDW-EGLL.pln", 5003)
Enroute Aircraft
Enroute aircraft follow a .PLN flight plan file from a given phase offset.
err := mgr.TrafficEnroute(traffic.EnrouteOpts{
Model: "FSLTL A321 Iberia SL",
Livery: "",
Tail: "IBE001",
FlightNumber: 1,
FlightPlan: "C:/Plans/LEMD-LEBL.pln",
Phase: 0.0, // 0.0 = start, 1.0 = end
TouchAndGo: false,
}, 5010)
Non-ATC Aircraft with Waypoints
Non-ATC aircraft ignore ATC and follow an explicit waypoint chain. This is the only mode that supports ground movement sequences (pushback → taxi → takeoff).
Spawn at an explicit position
err := mgr.TrafficNonATC(traffic.NonATCOpts{
Model: "FSLTL A320 SAS SL",
Livery: "",
Tail: "SAS202",
Position: types.SIMCONNECT_DATA_INITPOSITION{
Latitude: 50.1008,
Longitude: 14.2600,
Altitude: 1247, // ft MSL (Václav Havel elevation)
Heading: 258,
OnGround: 1,
Airspeed: 0,
},
}, 5020)
Release control, then set waypoints
After Acknowledge, you must release simulator control before the aircraft will
follow your waypoints:
// release — objectID came from Acknowledge
if err := mgr.TrafficReleaseControl(objectID, 5021); err != nil {
log.Println("release failed:", err)
}
// build waypoint chain
wps := []types.SIMCONNECT_DATA_WAYPOINT{
traffic.PushbackWaypoint(50.1008, 14.2595, 1247, 3),
traffic.TaxiWaypoint(50.1000, 14.2580, 1247, 15),
traffic.LineupWaypoint(50.0982, 14.2560, 1247),
}
wps = append(wps, traffic.TakeoffClimb(50.0982, 14.2560, 258)...)
// defID must be registered for "AI Waypoint List" (see below)
if err := mgr.TrafficSetWaypoints(objectID, defWaypoints, wps); err != nil {
log.Println("waypoints failed:", err)
}
The defWaypoints define ID must be registered once at connect time:
mgr.OnConnectionStateChange(func(old, new manager.ConnectionState) {
if new == manager.StateConnected {
mgr.AddToDataDefinition(defWaypoints, "AI Waypoint List",
"", types.SIMCONNECT_DATATYPE_WAYPOINT, 0, defWaypoints)
}
})
Waypoint Helpers
All helpers are in pkg/traffic. Flags are set correctly for each manoeuvre type —
do not compose waypoints by hand unless you need something not covered here.
| Helper | Flags | Typical use |
|---|---|---|
PushbackWaypoint(lat, lon, alt, kts) |
ON_GROUND \| REVERSE \| SPEED_REQUESTED |
Reverse from gate |
TaxiWaypoint(lat, lon, alt, kts) |
ON_GROUND \| SPEED_REQUESTED |
Forward ground roll |
LineupWaypoint(lat, lon, alt) |
ON_GROUND \| SPEED_REQUESTED at 5 kts |
Final runway threshold node |
ClimbWaypoint(lat, lon, altAGL, kts, throttle%) |
SPEED_REQUESTED \| THROTTLE_REQUESTED \| COMPUTE_VERTICAL_SPEED \| ALTITUDE_IS_AGL |
Airborne climb point |
TakeoffClimb(rwyLat, rwyLon, hdgDeg) |
— | Returns 3 ClimbWaypoints at 1.5/5/12 nm |
The transition from the last ON_GROUND waypoint to the first airborne waypoint
triggers the simulator’s takeoff roll.
Fleet Management
// count active (acknowledged) aircraft
n := mgr.Fleet().Len()
// snapshot — safe to iterate outside the lock
for _, a := range mgr.Fleet().List() {
fmt.Printf("%s objectID=%d kind=%d\n", a.Tail, a.ObjectID, a.Kind)
}
// look up by object ID
if a, ok := mgr.Fleet().Get(objectID); ok {
fmt.Println(a.Tail)
}
// remove one
mgr.TrafficRemove(objectID, reqID)
// remove all — reqIDBase is incremented per aircraft
mgr.Fleet().RemoveAll(9000)
Manager Integration
All Fleet operations are available as thin wrappers on the manager so you never
need to import pkg/traffic directly in simple applications:
mgr.TrafficParked(opts, reqID)
mgr.TrafficEnroute(opts, reqID)
mgr.TrafficNonATC(opts, reqID)
mgr.TrafficRemove(objectID, reqID)
mgr.TrafficReleaseControl(objectID, reqID)
mgr.TrafficSetWaypoints(objectID, defID, waypoints)
mgr.TrafficSetFlightPlan(objectID, planPath, reqID)
mgr.Fleet() // direct fleet access for List/Get/Len/RemoveAll
The manager keeps the fleet’s client reference in sync with the connection state —
you do not need to call SetClient yourself.
Error Reference
| Error | Meaning |
|---|---|
traffic.ErrNotConnected |
No active engine client (not connected) |
traffic.ErrObjectNotFound |
ObjectID is not tracked in the fleet |
traffic.ErrCreationFailed |
SimConnect creation call returned an error |
traffic.ErrEmptyWaypoints |
SetWaypoints called with nil or zero-length slice |
Known Limitations
- No ground routing: Waypoint coordinates must be supplied by the caller. There is no taxiway graph or pathfinding — that is deferred to a later milestone once the full facility dataset for taxiways is understood.
- No arrival sequencing: Enroute aircraft land and park autonomously via ATC; custom arrival sequencing is not yet supported.
- ObjectIDs reset on reconnect: Any aircraft spawned before a disconnect are lost. Re-spawn after reconnect if persistence is required.