Using an External Flight Dynamics Model to Drive MSFS Visuals

Hey folks,

Sorry if this is a novice question, but I’ve been trying to do my research and couldn’t come to a clear conclusion.

I have an external simulation (doing all the flight dynamics modeling and state determination calculations), which sends a large UDP packet containing relevant state data to FlightGear. FlightGear is configured to have its own simulation off, and is just being used as a slave for visuals (receiving the UDP message from my simulation). This system works, but because FlightGear is only used for visuals, I’d like to upgrade to a simulator that has better graphics/visuals (hence, MSFS).

Is there anyway to interface an external simulation with MSFS such that MSFS is just acting as a slave to the state data and providing the visuals, while not doing any of the simulation calculations? Does anyone have any experience with this? From what I’ve read so far, it sounds like it’s possible but it requires some middleware to be created to convert the UDP state data (from my simulation) into something that’s digestible by SimConnect. Does anyone have more specifics on this? I’m not afraid to do this coding, I just am unsure of how to configure MSFS and interface using SimConnect to achieve this. I’m also not even sure if this is possible, so I’d like to know it’s possible before I begin trying to take a crack at it.

Thanks so much for your time!

All the best,

Canthev

2 Likes

You can communicate data from an external program to a WASM gauge in the aircraft using SimConnect’s client data areas. Then, what you would want to do is trigger SimConnect events to freeze the aircraft in all axes of motion so that you can manually control the position/rotation simvars of the aircraft as dictated by your external flight model:

Documentation: SimConnect API Reference

Let me know how this works out for you!

1 Like

Thanks so much for the insight. I’ll see how that works out, and I’ll let you know! Much appreciated :slight_smile:

@Iceman2152798 Thanks for the insight again. I’ve begun creating a C++ application to barter this communication between my UDP message (from my simulation) and feed the values into MSFS through the SimConnect API. I’ve come upon what feels like a roadblock based on my understanding of how the API works.

How can I send these events after connecting to the MSFS instance? I’m hoping to just trigger them at the start of the program to permanently freeze lat/long/alt/att through the above-mentioned events, but I can’t figure out how to send events. I’m currently able to listen to variables (by initializing a data definition using SimConnect_AddToDataDefinition, followed by requesting this initialized data definition using SimConnect_RequestDataOnSimObject). I’ve even tried setting variables using the same SimConnect_AddToDataDefinition followed by SimConnect_SetDataOnSimObject but setting variables seems to have no effect.

I’ve noticed that instead of sending the freezing position events, you can alternatively set the variables IS LATITUDE LONGITUDE FREEZE ON, IS ALTITUDE FREEZE ON, and IS ATTITUDE FREEZE ON to 1, and this will freeze the aircraft as well. I’ve checked this by launching the SimConnect example program called Simvar Watcher, however this program was written in C# and the API calls seem to behave differently.

Finally, I tried using the example code for SimConnect_SetDataOnSimObject, where it sets an initial position, and I’m executing this code right after connecting to the MSFS instance, but even this example code doesn’t seem to work. I’m not getting any build errors either.
For reference, here is the link for SimConnect_SetDataOnSimObject. I’ve noticed the example seems to actually be missing the ArrayCount argument from SimConnect_SetDataOnSimObject. I added an extra zero to my command, so mine looks like: SimConnect_SetDataOnSimObject(hSimConnect, DEFINITION_ID_INIT_POS, SIMCONNECT_OBJECT_ID_USER, 0, 0, sizeof(Init), &Init);. Is that an error in the documentation?

In summary: I’m trying to figure out 1) how to freeze the aircraft (either through sending the events you mentioned above, OR by setting the variables of the user to 1 for those freeze variables), and 2) I’ll need to figure out how to set variables anyway because after freezing the aircraft I’ll have to set the position and attitude variables based on what my simulation tells me.

Thanks for all the help thusfar!

Might be worth contacting the developers behind AirlandFS, which is the external flight dynamics application for many of the helicopters in MSFS.

1 Like

Thanks, I’ll send them a message!

Replying to my own post here.

The reason I wasn’t able to SimConnect_SetDataOnSimObject correctly was because I had incorrect units for my data definition. I used Feet instead of feet, so the function returned an S_OK but didn’t change anything because the units were wrong.

I’m still not sure how I can send the Freeze events though…

Further answering my own question.

In order to successfully trigger an “event”, I had to use SimConnect_MapClientEventToSimEvent to map a Client Event ID (CLIENT_EVENT_ID_FREEZE_LAT_LONG, CLIENT_EVENT_ID_FREEZE_ALT, and CLIENT_EVENT_ID_FREEZE_ATT) to Simulation events (FREEZE_LATITUDE_LONGITUDE_SET, FREEZE_ALTITUDE_SET, and FREEZE_ATTITUDE_SET respectively).

After mapping these Client events to Simulation events, I was able to trigger these Client events using SimConnect_TransmitClientEvent to transmit the CLIENT_EVENT_ID_FREEZE_LAT_LONG, CLIENT_EVENT_ID_FREEZE_ALT, and CLIENT_EVENT_ID_FREEZE_ATT events.

I was originally unsuccessful with this approach because I set the final input argument for TransmitClientEvent ( the flag argument) to NULL, however after setting the flag to SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY, this allowed me to successfully transmit the Client event.

Here’s a snippet of my code which allowed me to send Simulation events:

    hr = SimConnect_MapClientEventToSimEvent(hSimConnect, CLIENT_EVENT_ID_FREEZE_LAT_LONG, "FREEZE_LATITUDE_LONGITUDE_SET");
    hr = SimConnect_MapClientEventToSimEvent(hSimConnect, CLIENT_EVENT_ID_FREEZE_ALT, "FREEZE_ALTITUDE_SET");
    hr = SimConnect_MapClientEventToSimEvent(hSimConnect, CLIENT_EVENT_ID_FREEZE_ATT, "FREEZE_ATTITUDE_SET");
    hr = SimConnect_TransmitClientEvent(hSimConnect, aircraftID, CLIENT_EVENT_ID_FREEZE_LAT_LONG, 1, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY);
    hr = SimConnect_TransmitClientEvent(hSimConnect, aircraftID, CLIENT_EVENT_ID_FREEZE_ALT, 1, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY);
    hr = SimConnect_TransmitClientEvent(hSimConnect, aircraftID, CLIENT_EVENT_ID_FREEZE_ATT, 1, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY);

So now basically everything works! The only two issues I have right now are:

  1. I rely on the height of the ground in MSFS to feed into my simulation such that my simulation can correctly know when the aircraft is touching the ground, allowing me to stay “above ground” in the visuals. The issue I have is regarding initializing the simulation and MSFS such that the aircraft intializes to the correct altitude to always begin above ground. When initialized incorrectly, my aircraft shoots into the sky because I model the ground contact force as a spring, so a massive force can be generated if there’s a large discrepancy between the height of the ground and the altitude of the aircraft during initialization. I think this issue is out of the scope of this thread. I believe it has to do with how I initialize my variables in my simulation and in MSFS.
  2. I’d like to make a batch script or C++ file (possibly the same file that’s bartering this communication between my simulation and MSFS) to launch MSFS, directly into a free flight. Currently, to launch the visualization, I have to launch MSFS as if I’m going to play recreationally, then load my aircraft into the airport that I’d like to fly from, then once it’s all loaded up, I can start my C++ script to communicate with my simulation. Ideally, I’d like to launch one batch script or C++ file (from anywhere on my PC, just double click the file) that can automatically load up MSFS to the correct initial position, without having to click through the menu at all. Is this something that’s possible, or will I always have to use my mouse and keyboard to load up the flight I want before launching my C++ file?

Thanks for all the help thusfar everyone :slight_smile:

  1. You can query the altitude above terrain (or terrain altitude, can’t quite remember) for either the user aircraft (ID = 0) or any spawned “AI aircraft” (ID > 0). In order to get the terrain altitude you need to “move” your aircraft to the desired position first (latitude, longitude, some altitude of choice). There is a question in this forum asking exactly this, and this “spawn AI aircraft at desired location, query terrain altitude” technique seems to be applied since FS X days.

I haven’t tried it out myself just yet, and be aware that the SimConnect API is not only asynchronous, so if you create an AI aircraft you need to wait for its “object ID” first before being able to query “terrain altitude” for that AI aircraft. But I guess there might also be “streaming issues” involved (the terrain data first needs to be (fully) downloaded (?) - and what happens if you spawn your AI aircraft “too far away” from the user aircraft? Will the terrain data be downloaded at all? Etc. etc.

But I hope that gives you some ideas. Also note that SU10 is supposed to bring some additional “terrain API”, but not sure whether that will also be available via SimConnect (or only via the “internal API”, that is to the actual “aircraft / gauges API”).

  1. You can save and load a flight into / from a file, there are two SimConnect API calls for this. But MSFS needs to be already running. Plus I don’t know whether you can call the “LoadFlight” at any time, or whether an “active flight” must already be in progress.

You may also be interested in studying my C++ source code: my application records and replays various simulation variables (“flight recorder”), after having “frozen” the user aircraft:

The SimConnect specific code is all here, in the MSFS specific “plugin”:

Note that traditionally the SimConnect API supports both reading and writing “simulation variables” as well as interacting with MSFS via “events”. In my experience quite a lot of simulation variables / events functionality is overlapping, especially since more and more “simulation variables” have become “writeable”.

So for instance it is both possible to control various light states (taxi light, cabin light, landing lights, …) by setting the corresponding “simulation variable” (which in this case acts as a “bit field”, as documented in the API documentation) as well as via “events”.

In my application I tried to stick to “simulation variables” whenever possible (but especially the “freezing” of the user aircraft still only works with corresponding “events”).

1 Like

Thank you so much for the detailed response! I really appreciate the thoroughness and I’m definitely going to look at your source code. It’s really nice to be able to look at how others have implemented the SDK for their projects.

  1. I ended up solving this issue, the FDM was configured incorrectly for the MSFS altitude values. I was previously using FlightGear, which provided 40.3 feet as the height of the ground in feet from MSL. MSFS, for that same Lat/Long position provided 42.6 feet. After looking at the terrain elevation data, I think MSFS is far more accurate than FlightGear (at least how I have it configured). I think FlightGear has a much lower terrain elevation datapoint density, so it’s to be expected that their heights of ground don’t always line up perfeclty. I realized my FDM is hardcoded to assume the aircraft starts at a ground height of 40.3 feet, so when MSFS returned 42.6 feet, the aircraft was basically initializing underground. In the short-term I fixed this by ICing the aircraft to 42.6 feet, but in the long-term I’d like to have MSFS send an initial position message to my FDM to eliminate any hard-coding in the FDM and allow the aircraft to always initialize correctly. I don’t expect this to be too much of a challenge, especially considering SimConnect has an Initial Position datatype (SIMCONNECT_DATA_INITPOSITION). I’ll probably have one UDP message going from FDM to MSFS to set this IC state, then another message sending back from MSFS to FDM that confirms the initial position of the aircraft, with the height of the ground. Haven’t exactly ironed that part out, but hardcoding it to 42.6 feet works for now.

  2. Thanks for this information about loading a flight from a file. I’ll give this a whirl. Hopefully I can configure this cleanly such that other users can just run the C++ program and have this file in the same directory as the C++ program (or possibly compiled into the C++ program, but I believe MSFS would need access to the file, so that’s probably not possible). I’ll have to distribute this code at some point, and I’d like to make sure all necessary files are within the same folder (for version-control purposes, and to ensure smoother installation). I’ll do some testing about different conditions to load a flight from a file (during startup, in main menu screen, during active flight), and I’ll report back my findings. My guess is that you’ll only be able to load a flight from a file during an active flight, but hopefully that’s not the case :slight_smile: . FlightGear is architected in such a way that you can call fgfs from the command line with a plethora or input flags (like --disable-clouds or --aircraft=c172p for example). This means with one really simple bash script, you can launch up FlightGear with all the necessary flags to initialize your aircraft, position, and graphics configuration, all with one click. I’d ideally like something similar from MSFS, but I realize that might be wishful thinking. I’m hoping this loading of a flight from a file is one step closer to this. Thanks for pointing me in this direction.

Thanks so much for all your help! I’ll post again once I’ve made some more progress.

-Canthev

Yes, I was just about to mention this data record (structure), which does have an additional OnGround member: when this value is set to “true” (value 1) then MSFS will properly place the aircraft on ground (also such that its gear is properly aligned with the surface).

Note the remark given in

https://docs.flightsimulator.com/html/Programming_Tools/SimConnect/API_Reference/Structures_And_Enumerations/SIMCONNECT_DATA_INITPOSITION.htm

Quote:

The primary use of this structure is to initialize the positioning of the user aircraft, because it also optimizes some of the terrain systems and other Microsoft Flight Simulator systems. Simply setting parameters such as latitude, longitude and altitude does not perform this kind of optimization. This structure should not be used to incrementally move the user aircraft (as this will unnecessarily initiate the reloading of scenery)

In practice this means that MSFS will (re-)spawn other AI objects such as airport vehicles and other AI aircraft (which - unfortunately - might end up on the same parking spot as your “user aircraft” (= the aircraft normally controlled by the player, and being followed by the camera, having object ID = SIMCONNECT_OBJECT_ID_USER (= 0), in case the user aircraft also happens to be parked on a parking spot.

Also, as mentioned in the remark, only send this (special) data structure once, at the beginning of your “simulation” (in my case: at the beginning of the “replay”, or when completely “rewinding” to “timestamp 00:00:00”).

Have a look at my MSFSSimConnectPlugin#onInitialPositionSetup() method. Just mentally remove all the “C++ gibberish”, e.g. I am using “real C++ enumeration classes”, and the verbose “utility function” Enum::toUnderlyingType simply removes the underlying type, typically an int (integer). :slight_smile:

By the way, I am using an “event based recording/replay”, that is instead of an “infinite while loop with some sleep()” (which is the typical example given in various SDK example code) and polling for data (or sending data in a fixed interval) I am in fact “requesting data as it becomes available”, with a frequencey according to SIMCONNECT_PERIOD_SIM_FRAME (“simulated frame”). So during recording MSFS sends us data update for each “simulated frame” (which might be > the displayed FPS, but on average computer systems it is roughly equal to the displayed frames per second).

Likewise, each time we receive the “simulated frame” event we send data to MSFS. Like this we do not “oversaturate” the “SimConnect channel”, and send data whenever MSFS “is ready for it”.

(I also support a timer-based approach for recording, in order to reduce the sampled data).

But how then do we get “notified” about this “simulated frame event” (and any other available data), without “polling” for it ourselves (again, either in some “busy loop” (+ some sleep() in between, in order not to completely burn the CPU ;)), or timer-based)?

The trick is to use the underlying WIN32 event system.

The given SDK example uses WIN32 API and the example (IIRC) is already some kind of “UI class” which a) already has its own “window handle” (of type HWND) and b) (IIRC) already inherits some functionality to filter / receive the WIN32 events.

As my actual “connection plugin” is actually “without user interface” I am simply creating a “hidden QWidget” (yes, my app is a Qt toolkit application), have a look at the EventWidget class. This “hidden widget” re-implements the nativeEvent method which is called whenever a “native WIN32 event” has been rececived by the application (which in case of Qt applications “broadcasts” it to all its child widgets, such as my “hidden event widget” (“hidden” in the sense that this widget is simply never displayed anywhere).

The actual “logic” is very simple: we filter the SimConnnectUserMessage (which is defined to be 0x0402, but that is just some random “magic number” which happens to be the same as the given SDK example that I one found (somewhere ;)). I don’t think the actual value matters (but “client events” may have to be larger (and/or smaller) than some system / WIN32 API defined value, can’t remember - consult your WIN32 API docu ;)).

bool EventWidget::nativeEvent([[maybe_unused]] const QByteArray &eventType, void *message, [[maybe_unused]] long *result) noexcept
{
    bool handled;
    const MSG *msg = static_cast<MSG *>(message);
    switch(msg->message) {
    case SimConnnectUserMessage:
        emit simConnectEvent();
        handled = true;
        break;
    default:
        handled = false;
        break;
    }
    return handled;
}

Whenever we receive this “SimConnnectUserMessage” event we notify any “listener” (Qt uses “signals and slots” - think of it as some elegant “callback mechanism” (“observer pattern”), if you haven’t heard about Qt signals and slots).

So a final but important question remains: how does MSFS know that we are expecting this SimConnnectUserMessage user-defined WIN32 event (with its “magic ID” 0x0402)?

That’s because we request this when establishing the connection with MSFS, like so:

bool MSFSSimConnectPlugin::connectWithSim() noexcept
{
    HWND hWnd = reinterpret_cast<HWND>(d->eventWidget->winId());
    DWORD userEvent = EventWidget::SimConnnectUserMessage;
    HRESULT result = ::SimConnect_Open(&(d->simConnectHandle), ::ConnectionName, hWnd, userEvent, nullptr, ::SIMCONNECT_OPEN_CONFIGINDEX_LOCAL);
    if (result == S_OK) {
    ...
    }
}

Aha! We provide MSFS both our HWND (“window handle”), so MSFS knows who is expecting WIN32 events, and we are also telling which WIN32 event we’d like to receive whenever data is ready to be read (like the “simulated frame event” - and don’t confuse “native WIN32 events” with “SimConnect events”). That is, our defined userEvent. So you see, also MSFS “knows” about our “magic WIN32 event ID”.

So after the connection was successful MSFS knows that we (HWND) are expecting “native events”, and it also knows which event ID to send us whenever there is something to be read. So all that remains is to listen to those WIN32 events (-> EventWidget) and whenver we have received such a native event we can then call our “simconnect data dispatcher”, by calling SimConnect_CallDispatch() (which we would otherwise do in a “busy loop”, or timer-based).

Whether such a “native event” based processing or a simple (“fixed update rate”) “busy loop” is more appropriate depends on the frequency and the actual scenario. But I found that for my “replay (recording) scenario” this event-based approach suits me well, as I want to “send the data as often as possible - but not more often than necessary” (= don’t overwhelm MSFS with superfluous data requests it cannot handle anyway).

Again, to summarise:

  • Upon “connection” we provide the (optional) HWND “window handle” and our defined “native event ID”
  • We then receive and filter our “native event ID”
  • Whenever we have received this “native event ID” we simply call SimConnect_CallDispatch()
  • In case of replay we check whether we have received the “Simulated Frame Event” (= a “SimConnect event” to which we have subscribed); if we received this event, we send our request data (“update latitute, longitue, etc.”)
  • In case of “recording” we simply check the received data type and process / store it accordingly

This also gracefully handles situations where MSFS may have to lower its FPS due to increased scenery complexity (or “other hickups”), and hence we would also receive less “simulated frame events” and by imlication would send less data requests (during replay), until MSFS has “recovered” its performance (FPS).

And for the record, flights can be programmatically saved and loaded into a file (stored somewhere at a path of your choice, but MSFS needs to have access to this path) with:

https://docs.flightsimulator.com/html/Programming_Tools/SimConnect/API_Reference/Flights/SimConnect_FlightLoad.htm

But again, I haven’t tried this myself just yet.

1 Like

Awesome, thanks so much. I currently use a busy loop that calls SimConnect_CallDispatch() and has a Sleep(1) (1ms pause). Within the SimConnect_CallDispatch() function, when I receive a new Data Simobject (SIMCONNECT_RECV_ID_SIMOBJECT_DATA), I then update the data-structure that I send over UDP to my Flight Dynamics Model (FDM), and set the position/orientation of the aircraft in MSFS with SimConnect_SetDataOnSimObject. I know this probably isn’t the best approach, but I figured it would work because it ties the updating of the aircraft in MSFS with when MSFS sends out an updated data message (which should be every “sim frame” on MSFS).

I agree that having a “busy loop” and also updating the position whenever I receive a data frame is not the most elegant solution, and I’m definitely considering a more event-based approach, but I think that’s more of an optimization than an additional feature, and I’d like to finish implementing the features first, then worry about optimization.

I also handle the UDP communication in this “busy loop”, and I know that’s far from optimized. From what I’ve read, it looks like I should be multi-threading my application so one thread can handle receiving/sending UDP communications at a fixed rate, and the other thread can handle all the MSFS-specific tasks. I have no experience with multi-threading yet, so that’s another “optimization” that I’ll have to address once I complete all the features (or once the UDP messages no longer transmit at a reliable rate, whichever happens first lol).

I’ve also never had exposure to WIN32 events, so that’s a whole new learning opportunity for me as well. Thanks for your thorough explanation for your event-based approach. I definitely think that’s something I’d like to implement at some point in the future, and this explanation + code will be a great platform for me to accomplish this.

Regarding the INITPOSITION structure, I only send out this structure to MSFS on startup (or whenever it loses UDP connection with my Flight Dynamics Model indefinitely), and I rely on setting the correct variables for every update frame after initialization (PLANE LATITUDE, PLANE LONGITUDE, PLANE ALTITUDE, PLANE PITCH DEGREES, PLANE BANK DEGREES, PLANE HEADING DEGREES TRUE). That’s good to know about the differences between setting the INITPOSITION structure every frame vs. setting the above variables instead. I definitely was considering sending INITPOSITION every frame, and I’m glad that I didn’t, as I definitely would’ve ran into some of those issues that you described. I didn’t realize those differences existed, I just figured updating INITPOSITION every frame wasn’t really how the variables / SDK was designed. I’ll read up more about the unique characteristics about INITPOSITION.

I’m going to try and implement loading aircraft configurations / flights from a file now, I’ll report back with findings.

Thanks again! :slight_smile:

One other question: Do you know how to hide the cockpit visuals when using the cockpit view?

I’m hoping to use these visuals in an Ironbird setup, and want the camera to be locked relative to the airframe (like when in cockpit view), but I don’t need to see any of the cockpit (will have all physical controls + physical avionics to display necessary information).

The only solution for this that I’ve seen is to manually adjust the temporary camera files that are generated everytime a custom camera-view is saved. You manually adjust these temp files to move your perspective all the way in-front of the aircraft, essentially making the aircraft disappear (because the whole cockpit + aircraft is now behind your camera position), but as a result, the visuals / perspective is a little warped because your camera is no longer situated where you would expect (camera position is nowhere near where the pilot’s head would be). Here’s a link to what I’m describing about moving the camera’s perspective by manipulating the temporary camera files: Cockpit Camera Outside the Cockpit

Is there a more elegant solution for this, that could very simply disable the cockpit visuals for MSFS, allowing me to use the cockpit camera perspective without having to see the cockpit? I recognize this question is probably worthy of its own forum topic, but I figured I’d field the question while I’m here.

For smoothly animating aircraft in MSFS, you might find notes in this FSX thread on fsdeveloper (from me, 13 years younger) useful:

If of any interest the original code is now in a github repo: GitHub - ijl20/msfs_logger_replay: MSFS logger for IGC files that also can replay IGC log files in the sim

And the add-on docs moved to here: Sim_logger

2 Likes