# Hooking JSBSim and cFS Together Few important considerations about JSBSim. As we know by now, JSBSim is a Flight Dynamics Model (FDM), that is, it replicates the behavior of a flying vehicle utilizing elegantly designed software following an object-oriented design approach. But something must be clear: JSBSim strictly models the vehicle and to some extent certain onboard equipment, but it does not model the computing hardware that controls the vehicle, nor the flight software necessary for such control. If you refer to the part where we discussed JSBSim models and objects, you will see that most classes are modeling physical parts of the vehicle: engines, wings, ailerons, and landing gears, while some others are modeling equipment such as gyros, accelerometers, magnetometers. By combining JSBSim with cFS, we sort of complete that equation: cFS will execute the flight software as if it were running on an actual flight control computer onboard the vehicle which is physically modeled in JSBSim. How do we start? Let's start small. Let's imagine that we want to have an instance of cFS running, able to report on an operator sitting on the ground the content of the fuel tank of an aircraft. How would we do what? Ok, first of all, where in JSBSim do we find the content of the fuel tank? The answer to this question brings us back to Properties. JSBSim, in its Input model, includes a command interpreter using a plain telnet socket. Let's see how it works: We need to run JSBSim, and then we open a terminal, and we do: ```Console $ nc localhost 5137 ``` To what JSBSim will reply: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image149.png) Now, through this console, we gain access to all the properties that JSBSim has tied to relevant variables by using the familiar (and private) `bind()` method in each model. Let's now try to ask for a property value, for example, the content of the tank: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image150.png) Now we see the way to go. By means of this telnet access, we can read and write properties in the simulation environment, without tampering it too much. Quite a powerful way of not only knowing what's going on with the models being executed but also modifying their behavior as an external client. The fact that JSBSim has been designed as a simulation kernel is a great advantage: it was conceived to be integrated with other stuff, therefore integrating it with cFS should be reasonably simple. We might still slightly refactor the telnet server to make it easier for us to control it from our application in cFS. We will see how, worry not. ## Writing an application in cFS that interacts with models in JSBSim Now we need to imagine things a bit. Imagine that we have an aircraft that we want to control by means of an autopilot. To achieve such a goal, first and foremost, we will need to read values from the aircraft (position, attitude, velocity), execute a control logic (PID controller or similar), and then command actuators to modify the vehicle state as much as it might be off from a desired set point. Our flight computer will be of course on board the aircraft, and it will have access to the relevant variables utilizing acquisition hardware accompanied by complex harnessing. So, what we need is to write the routines to fetch the relevant values from the vehicle and either send them to the ground for the sake of awareness or use them for control computations. In any case, we will need to write in cFS a TCP/IP client to JSBSim. We will simply modify `SAMPLE_APP` for this goal. And we start right away by doing something like this: ![Graphical user interface, text, application, Teams Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image151.png) ## Querying JSBSim properties from cFS Then, we must write a function that will do the property request: ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image152.png) You can see there we already are trying to request the content of the fuel tank. But, before we can try this with JSBSim, we can test it with Netcat: ```Console $ nc -l 5137 ``` And we step over these two new functions. And... it does NOT work. Ok, what's going on here? ## Refactoring cFS to work with streaming sockets Upon a bit of inspection around the place, we find something interesting: `OS_SocketSendTo()` is returning an error, more specifically a cryptic -36. Diving a bit into the function, we can see this: ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image153.png) The function checks if the socket is of the type `OS_SocketType_DATAGRAM`, which is UDP. UDP is a simpler protocol that does not guarantee delivery, as opposed to TCP which does. It appears that the routine is restricted from working with TCP (which is the protocol JSBSim uses for its telnet). Why would cFS restrict sockets to UDP only? One plausible reason could be to avoid tasks getting locked in the TCP intricate state machine. After further inspection, one can see that the socket API in cFS is equipped with functions that work with streaming protocols like TCP, for example, `OS_SocketAccept()`. Therefore, we will take a bold decision now: we will modify `OS_SocketSendTo` to ignore this check in the if statement and still invoke `OS_SocketSendTo_Impl` (which eventually invokes `sendto()` under wraps. Let's give it a go. Again, we open netcat, we create a listening socket as if we were JSBSim and we run it. ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image154.png) And voilá, the function is indeed creating the data we wanted. But now we need to do the necessary work to catch what JSBSim would reply to this string. Time to add receiving capabilities to our function. We try something like this for now (a bit rudimentary, to be refined later): ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image155.png) And yes, we are not checking for errors, missing bytes, or anything for now, we will add all that later. But let's try it out: now after receiving the string from cFS, we will write something back in netcat and see if we catch it in cFS: ![Graphical user interface Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image156.png) We did receive the data we manually wrote in netcat. Why don't we try now with JSBSim? Now, there won't be any netcat, but the telnet server in JSBSim running and listening for incoming connections. You see both instances of gdb running together: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image157.png) And now we try to send our little, humble request for a property: ![Text Description automatically generated with low confidence](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image158.png) Almost a win! You see we got a "connected to JSBSim". This is a small but significant step in our rearchitecting adventure. For the first time, we can make our two programs talk to each other. But we also got an anodyne "Unknown property" as a reply from JSBSim. Why? Our bad: we had one extra slash in the property string. Instead of `/propulsion/tank/contents-lbs`, it should have been `propulsion/tank/contents-lbs`. (note the slash at the very beginning being removed). Let's fix it and try again. ![Graphical user interface Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image159.png) It works. We got the content of the aircraft tank fuel, which is 10000 pounds. This is a small but fundamental step in the code comprehension process. Think for a moment about all the steps that took us here, the little and the big things we had to change or to slightly modify. All that was contributing to understanding a code base that was a stranger to us not long ago. But, with great power comes also higher complexity. Now that both programs are related to each other, well, we have to have both of them open for debugging as we walk through our rearchitecting. Of course, we don't need to have both of them all the time in a debug session for our refactoring activity. We can selectively have one or another. Still, now the codebases of these two projects have been combined, functionally speaking, into something else. Our little Frankenstein-like creature is finally alive. With this very meaningful milestone achieved, our work ahead will be to refine the communication between cFS and JSBSim, and also add a few other things that will improve the overall rearchitecting process. ## Refactoring JSBSim's telnet server, slightly To make the conversation with JSBSim's telnet a tad simpler to parse and process, we will modify it to remove some unnecessary verbosity from it. The idea would be that JSBSim would just reply with the property value upon getting a "get" directive. So, we will tamper the `Receive()` method in the `FGfdmSocket` class, commenting out the line where the welcome message and the prompt are sent through the socket: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image160.png) And we also modify the output in the `Read()`, where the property is queried: ![Graphical user interface, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image161.png) And now JSBSim replies in a leaner way, with fewer characters to strip away: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image162.png) Now we refactor our main function in the sample app a bit more to make it a periodic task (with a 1Hz period), requesting fuel weight in the tank and one velocity. ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image163.png) Now it's maybe a good time to start thinking about how to make these variables reach the ground for an operator to observe them in a dashboard. We do this by modifying the housekeeping telemetry data structure of the sample application (`SAMPLE_APP_HkTlm_Payload`): ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image164.png) But let's now do things a bit more properly and also add the tank weight and velocity into the `SAMPLE_APP` global data structure (before we just created a few local variables in the task main): ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image165.png) Out run loop in the `SAMPLE_APP_Main` is looking more and more proper: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image166.png) And in the housekeeping data reporting function, we do something similar: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image167.png) And we run the thing again. Our netcat (combined with `hexdump`) shows something: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image168.png) Awesome, we see the bytes from the `HkTlm` structure sent to ground. How do we know? I bet you recognize the 0x0883 value from the `SAMPLE_APP_HK_TLM_MID`: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image169.png) ## Decoding the telemetry on the ground We managed to send telemetry from the aircraft to a ground operator through cFS. But how do we present this data in a more or less human-readable manner? Let's raise a bit, and only a bit, the UX (user experience) bar. To do so, we will use some Python and some (free) plotting tools. First, let's write a simple UDP server that will listen in the port that the `TO_LAB_APP` is sending frames to: ![Graphical user interface, text, application, email Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image170.png) But this will just get the whole frame, and we are interested in unpacking certain parts of it. Then, we can do some more work and start adding the logic to extract the relevant bytes form the incoming frame: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image171.png) Which prints: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image172.png) You can see the "Telemetry from aircraft" text showing the float values. ## Showing and plotting the telemetry We are indeed decoding the aircraft's telemetry on the ground. Nice. What now? Now it's maybe time to add a better interface than a terminal (although there is nothing as lean as a terminal). But for the sake of a better user experience, we will present the values coming from the vehicle on a website, in a way we can just look at them using a browser. So, how do we proceed? We will use Dash[^28]. Dash is a framework for rapidly building data apps in several languages, including Python, R, and others. Written on top of Plotly.js and React.js, Dash is easy for building and deploying data apps with customized user interfaces. It's particularly suited for anyone who works with data. Through a couple of simple patterns, Dash abstracts away most of the obscurities required to build a web app with interactive data visualization. More importantly, Dash apps are rendered in the web browser. Dash is an open-source library released under the MIT license. We will base ourselves on the example provided [here](https://dash.plotly.com/live-updates) but adapted to our own needs. Our script looks like this: ```Python import datetime import struct import dash from dash import dcc, html import plotly from dash.dependencies import Input, Output from pyorbital.orbital import Orbital import sys import socket localIP = "127.0.0.1" localPort = 1235 bufferSize = 1024 position_h_agl=0 tankWeight=0 data = { 'time': [], 'position_h_agl': [], 'tankWeight': [], } # Create a datagram socket UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) UDPServerSocket.setsockopt( socket.SOL_SOCKET,socket.SO_REUSEADDR, 1 ) UDPServerSocket.bind((localIP, localPort)) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div( html.Div([ html.H4('JSBSim/cFS: Aircraft Live Telemetry'), html.Div(id='live-update-text'), dcc.Graph(id='live-update-graph'), dcc.Interval( id='interval-component', interval=1*1000, # in milliseconds n_intervals=0 ) ]) ) @app.callback(Output('live-update-text', 'children'), Input('interval-component', 'n_intervals')) def update_metrics(n): global position_h_agl global tankWeight bytesAddressPair = UDPServerSocket.recvfrom(bufferSize) frame=bytesAddressPair[0] tankWeight = struct.unpack("d", frame[24:32])[0] velocity_eci_x=struct.unpack("d", frame[32:40])[0] velocity_eci_y=struct.unpack("d", frame[40:48])[0] velocity_eci_z=struct.unpack("d", frame[48:56])[0] position_eci_x=struct.unpack("d", frame[56:64])[0] position_eci_y=struct.unpack("d", frame[64:72])[0] position_eci_z=struct.unpack("d", frame[72:80])[0] position_h_agl=struct.unpack("d", frame[80:88])[0] style = {'padding': '5px', 'fontSize': '14px'} return [ html.Span('Tank Weight (lbs): {0:.2f}'.format(tankWeight), style=style), html.Span('Position (AGL)(ft): {0:.2f}'.format(position_h_agl), style=style), #html.Span('Altitude: {0:0.2f}'.format(alt), style=style) ] # Multiple components can update everytime interval gets fired. @app.callback(Output('live-update-graph', 'figure'), Input('interval-component', 'n_intervals')) def update_graph_live(n): global position_h_agl global tankWeight time = datetime.datetime.now() data['time'].append(time) data['position_h_agl'].append(position_h_agl) data['tankWeight'].append(tankWeight) # Create the graph with subplots fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.1) fig['layout']['margin'] = { 'l': 30, 'r': 10, 'b': 30, 't': 80 } #fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'} fig.append_trace({ 'x': data['time'], 'y': data['position_h_agl'], 'name': 'Altitude (AGL)', 'mode': 'lines+markers', 'type': 'scatter' }, 1, 1) fig.append_trace({ 'x': data['time'], 'y': data['tankWeight'], 'text': data['time'], 'name': 'Fuel weight (lbs)', 'mode': 'lines+markers', 'type': 'scatter' }, 2, 1) return fig if __name__ == '__main__': app.run_server(debug=True) ``` The result: ![Timeline Description automatically generated](image173.png) ## Setting Properties So far, we have been reading values from JSBSim. In a way, the FDM was read-only until this far. Now, it is time to be able to also write into JSBSim. The methodology will be the same: we will use JSBSim telnet client to send data into it and modify existing properties or create new ones in case some property we need does not exist. Let's try to modify the engine throttle in our simulated Boeing 737 and see in the telemetry the impact of such a change. ![Text Description automatically generated](image174.png) And we can see a change in the propulsion/engine properties from the JSBSim standard output: ![Text Description automatically generated](image175.png) Having reached to this point, we will leave this here now and move into a few other things before we can send a command to cFS for throttling the aircraft up or down. ## Reading from "real" on-board sensors Until here, we were reading properties that were more or less coming from "perfect" sources. This means, the value of the fuel weight we have read in the example above, meant 'magically' reading the physical weight out of the blue. Moreover, in aircraft, the fuel volume is measured via capacitance probes and then converted to mass by also measuring the fuel density with capacity density condensators ('cadensicons'). This means that reading a fuel mass/weight directly is clearly disconnected from reality. Sensors are never well-behaved devices, and they introduce their own artifacts in the readings. JSBSim takes this into account and models sensors using the FGSensor class. Using this class, we may read a property and add some "real-life" effects to it. How do we do that? `FGSensor` class can model imperfections and increase realism. The only required element in the sensor definition is the input element. For noise, if the type is PERCENT, then the value supplied is understood to be a percentage variance. That is, if the number given is 0.05, then the variance is understood to be +/-0.05 percent maximum variance. So, the actual value for the sensor will be *anywhere* from 0.95 to 1.05 of the actual "perfect" value at any time, even varying all the way from 0.95 to 1.05 in adjacent frames. The format of the sensor specification is: ```XML <sensor name=”name” > <input> property </input> <lag> number </lag> <noise variation={”PERCENT|ABSOLUTE”}> number </noise> <quantization name="name"> <bits> number </bits> <min> number </min> <max> number </max> </quantization> <drift_rate> number </drift_rate> <bias> number </bias> </sensor> ``` For example: ```XML <sensor name=”aero/sensor/qbar” > <input> aero/qbar </input> <lag> 0.5 </lag> <noise variation=”PERCENT”> 2 </noise> <quantization name="aero/sensor/quantized/qbar"> <bits> 12 </bits> <min> 0 </min> <max> 400 </max> </quantization> <bias> 0.5 </bias> </sensor> ``` ### Creating Our Own Sensor Let's use something more realistic now. We will create a sensor; a temperature to be more specific. So, first, we will need to read a "real" temperature, for example, the temperature given by the `FGAtmosphere` class, available in the property called `atmosphere/T-R`. The unit of this temperature is the somewhat eccentric Rankine unit. Rankine is commonly used in the aerospace industry in the United States. Rankine is to Fahrenheit what Kelvin is for Celsius. When people in the United States created programs implementing equations that needed an absolute temperature, they used Rankine before Celsius for their scientific calculations. The reason people still sometimes use it in the aerospace industry is that there are a lot of legacy programs that were developed using Rankine, so to be compatible with those old programs, it's often simpler to just use Rankine in the new programs too. Don't you love the pressure legacy code tends to exert on new code? Anyway, let's create now a sensor and modify our cFS code to read from it. First, we will create a tiny XML file (`SensorTemperature.xml`) with the definition of a "system" our Boeing 737 model will use. The system implements a function that points to the property of interest (remember, `atmosphere/T-R` from the `FGAtmosphere` object). Then, we create one channel that contains two sensors: one outputting the temperature in Rankine into a new property called `sensor/temp/temperatureOutputR` (with a bit of an injected bias compared to the real variable), and another sensor whose output is sensor/temp/temperatureOutputC which converts the physical value originally in Rankine to Celsius. What does this look like? Let's see: ```XML <?xml version="1.0" ?> <system name="Sensor - Temperature"> <function name="sensor/temp/temperatureReading"> <property>atmosphere/T-R</property> </function> <channel name="Temperature Models"> <sensor name="temp_R"> <input>sensor/temp/temperatureReading</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>1.0</gain> <bias>0.25</bias> <delay>0</delay> <output>sensor/temp/temperatureOutputR</output> </sensor> <sensor name="temp_C"> <input>sensor/temp/temperatureReading</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.55555</gain> <bias>-273.122685</bias> <delay>0</delay> <output>sensor/temp/temperatureOutputC</output> </sensor> </channel> </system> ``` Nice. Now, we will modify our cFS application to read this now more realistic values. But not before adding our system file in the aircraft definition file (`737.xml`): ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image176.png) So, remember, we need to do the following steps in cFS: - We need to add a new variable (temperatureC of type **double**) in `SAMPLE_APP_Data` structure: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image177.png) - We need to add a new variable in the payload part in the SAMPLE_APP housekeeping telemetry that we send to ground: ![Text Description automatically generated](image178.png - We need to connect all these together in the function which copies values from internal variables to the housekeeping structure: ![Graphical user interface, text, application Description automatically generated](image179.png) - We need of course to actually request the property in our task run loop: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image180.png) - We need to update our ground Python, more precisely our Dash callbacks, but before adding the new variable to the dictionary: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image181.png) Now we update the callback: ![](image182.png) - Also, we will modify the Dash plotting callback, making sure we are also adding a new trace to the plot: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image183.png) Quite some steps, and quite many things that can go wrong in the process. Let's give it a try: ![Application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image184.png) It works. The temperature, as read from the newly created sensor, appears on the telemetry webpage. ## Adding more variables and some 3D visualization in real time This work thus far has been a work of backends talking to backends. But what about some three-dimensional visualization? When it comes to developing or refactoring simulation backends, it is important to have a sound way of visualizing all vectors, rotations, and trigonometrical computations. What we will do is request from JSBSim the relevant variables for visualizing the vehicle's position, velocity, and orientation in relevant frames of reference, and send these values in real-time to a 3D visualization engine called STK (System Toolkit), made by Ansys. Please note that STK is a commercial tool, so this step is optional in case you have the tool and the right licenses. Alternatives to this is to use CesiumJS[^30], or Flightgear[^31]. Ok, what now? Now we need to let STK know information about our vehicle so it can draw it and refresh it. STK comes with a feature called Connect[^32] with which you can control the entirety of the software through a TCP socket. The Connect capability provides you with an easy way to connect with STK and work in a client-server environment. Connect communicates with STK and 3D Graphics so you can visualize events in real time. Using Connect commands, we will set up the visualization scenario, and we will define the position, velocity, and orientation of our vehicle using streaming the telemetry received from cFS. What commands will we use for that? For setting thigs up: ``` Realtime */Aircraft/737 SetEpoch * "[EPOCH_DATETIME]" Units_Set * Connect Distance Meter SetAnalysisTimePeriod * "[START_DATETIME] " "[END_DATETIME]" SetAttitude */Aircraft/737 ClearData ``` Ok but there is quite some work to do now. JSBSim is not currently reporting the attitude quaternion in ECI frame of reference in any property. Long story short, we need to create that property. But there's more than that, we will have to create the getter method for obtaining the quaternion elements, before having them accessible by a property "get" command. So, we need to fiddle a bit with `FGPropagate` model. Luckily, the `FGQuaternion` object exposes the element of the quaternions by means of overloading the parenthesis operator, so we do this: ![Graphical user interface, text, application, email Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image185.png) Ok, the getter is there. Now, we need to go to the known `bind()` method in FGPropagate (where all properties get attached to their variables), and do: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image186.png) Let's now compile everything and give it a try with netcat: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image187.png) Works like a charm. Now we need to do the homework to add this to the cFS housekeeping telemetry frame. Here's a little dilemma: do we send the "true" quaternion all the way (cheating), or do we create a sensor that would send a noisy quaternion? I think you got the point already from a previous section on how to create sensors, so for the sake of simplicity, we will send the real quaternion. But we will create a few sensors more, why? Because, if you remember the properties we used for position and velocity, are provided in feet and feet-per-second units, and we will work with meters in STK, so let's fix this in our tiny XML by means of creating sensors with the right outputs. Ok, let's get to work. The file looks like this: ```XML <?xml version="1.0" ?> <system name="SensorSuite"> <function name="sensor/temp/temperatureReading"> <property>atmosphere/T-R</property> </function> <function name="sensor/position/position_ECI_X"> <property>position/eci-x-ft</property> </function> <function name="sensor/position/position_ECI_Y"> <property>position/eci-y-ft</property> </function> <function name="sensor/position/position_ECI_Z"> <property>position/eci-z-ft</property> </function> <function name="sensor/velocity/velocity_ECI_X"> <property>velocities/eci-x-fps</property> </function> <function name="sensor/velocity/velocity_ECI_Y"> <property>velocities/eci-y-fps</property> </function> <function name="sensor/velocity/velocity_ECI_Z"> <property>velocities/eci-z-fps</property> </function> <channel name="Models"> <sensor name="temp_R"> <input>sensor/temp/temperatureReading</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>1.0</gain> <bias>0.0</bias> <delay>0</delay> <output>sensor/temp/temperatureOutputR</output> </sensor> <sensor name="temp_C"> <input>sensor/temp/temperatureReading</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.55555</gain> <bias>-273.122685</bias> <delay>0</delay> <output>sensor/temp/temperatureOutputC</output> </sensor> <sensor name="pos_ECI_X"> <input>sensor/position/position_ECI_X</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/position/position-eci-x-m</output> </sensor> <sensor name="pos_ECI_Y"> <input>sensor/position/position_ECI_Y</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/position/position-eci-y-m</output> </sensor> <sensor name="pos_ECI_Z"> <input>sensor/position/position_ECI_Z</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/position/position-eci-z-m</output> </sensor> <sensor name="vel_ECI_X"> <input>sensor/velocity/velocity_ECI_X</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/velocities/velocity-eci-x-ms</output> </sensor> <sensor name="vel_ECI_X"> <input>sensor/velocity/velocity_ECI_Y</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/velocities/velocity-eci-y-ms</output> </sensor> <sensor name="vel_ECI_Z"> <input>sensor/velocity/velocity_ECI_Z</input> <lag>0</lag> <noise distribution="GAUSSIAN" variation="ABSOLUTE">0.0</noise> <drift_rate>0</drift_rate> <gain>0.3048</gain> <bias>0</bias> <delay>0</delay> <output>sensor/velocities/velocity-eci-z-ms</output> </sensor> </channel> </system> ``` Now comes the part where we write a UDP client which will capture the position and attitude telemetry and send it to STK for visualization: ```Python # Rearchitecting Software: Source code comprehension and Refactoring applied to flight software and simulation # Author: Ignacio Chechile # 2023 import socket import struct import numpy as np import binascii import datetime import time localIP = "127.0.0.1" localPort = 1235 bufferSize = 1024 # Create a TCP/IP socket to connect and command STK stk_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) STK_CONNECT_SERVER_ADDR = ('192.168.89.129',5001) stk_sock.connect(STK_CONNECT_SERVER_ADDR) stk_sock.settimeout(1) VehicleName = "737" def sendSTKcommand(command): stk_sock.send(command.encode()) data="" end=False try: data = stk_sock.recv(16) #yes, this could be way better except socket.timeout: return 0 print ('sending: %s' % command) if(data == b'ACK'): print(data,"STK: Command acknowledged:",command) elif (data == b'NACK'): print(data,"STK: Command NOT acknowledged:",command) else: print("Some other response from STK:", data) return 0 # Create a datagram socket UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) # Bind to address and ip UDPServerSocket.bind((localIP, localPort)) print("UDP server up and listening") # Listen for incoming datagrams if __name__ == "__main__": command = 'Realtime */Aircraft/737 SetProp' sendSTKcommand(command) start_time = datetime.datetime.utcnow() print("now =", start_time) dt_string = start_time.strftime("%d %b %Y %H:%M:%S.%f")[:-3] end_time = start_time + datetime.timedelta(days=1) end_str = end_time.strftime("%d %b %Y %H:%M:%S.%f")[:-3] print("date and time =", dt_string) print("end date and time =",end_time) command = 'Units_Set * Connect Distance Meter\n' # Units_Set * Connect Date EpSec Distance Meter sendSTKcommand(command) command = 'SetEpoch * "' + dt_string +'"\n' sendSTKcommand(command) command = 'SetAnalysisTimePeriod * ' + '\"' + dt_string + '\"' + ' ' + '\"' + end_str + '\"' + '\n' sendSTKcommand(command) #command = 'Units_Get * Connect\n' #sendSTKcommand(command) command = 'SetAttitude */Aircraft/737 ClearData\n' sendSTKcommand(command) attitude_ECI=[None]*4 while(True): bytesAddressPair = UDPServerSocket.recvfrom(bufferSize) frame=bytesAddressPair[0] print(binascii.hexlify(frame), len(frame)) print(binascii.hexlify(frame[24:32]), len(frame)) velocity_eci_x=struct.unpack("d", frame[32:40])[0] velocity_eci_y=struct.unpack("d", frame[40:48])[0] velocity_eci_z=struct.unpack("d", frame[48:56])[0] position_eci_x=struct.unpack("d", frame[56:64])[0] position_eci_y=struct.unpack("d", frame[64:72])[0] position_eci_z=struct.unpack("d", frame[72:80])[0] position_h_agl=struct.unpack("d", frame[80:88])[0] temperatureC=struct.unpack("d", frame[88:96])[0] sim_time_sec=struct.unpack("d", frame[96:104])[0] attitude_ECI[0]=struct.unpack("d", frame[104:112])[0] attitude_ECI[1]=struct.unpack("d", frame[112:120])[0] attitude_ECI[2]=struct.unpack("d", frame[120:128])[0] attitude_ECI[3]=struct.unpack("d", frame[128:136])[0] SIM_TIME=start_time + datetime.timedelta(seconds=sim_time_sec) print(SIM_TIME) sim_time_str = SIM_TIME.strftime("%d %b %Y %H:%M:%S.%f")[:-3] print("Telemetry from aircraft:", sim_time_sec, temperatureC) command = 'SetPosition */Aircraft/737 ECI ' + '\"' + sim_time_str + '\" ' + str(position_eci_x) + " " + str(position_eci_y) + ' ' + str(position_eci_z) + " " + str(velocity_eci_x) + " " + " " + str(velocity_eci_y) + " " + str(velocity_eci_z) + '\n' print (command) sendSTKcommand(command) command = 'AddAttitude */Aircraft/737 Quat ' + '\"' + sim_time_str + '\" ' + str(attitude_ECI[1]) + " " + str(attitude_ECI[2]) + ' ' + str(attitude_ECI[3]) + " " + str(attitude_ECI[0]) + '\n' print (command) sendSTKcommand(command) command = 'SetAnimation * CurrentTime ' + '\"' + sim_time_str + '\"' + ' \n' print (command) sendSTKcommand(command) ``` Let's run it and see what happens. ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image188.png) We see what appears to be an aircraft in a leveled flight! The reason why the aircraft appears as flying leveled is that the script is commanding it for a trimmed flight, which we will eventually remove when we start playing with control surfaces. Without the trimming, the aircraft understandably plummets after a few hundred seconds of airtime. Let's add some axes for better visualization: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image189.png) And now we can stop for a while the work on visualization, and we will move into commanding. ## Sending Commands to CFS Until this point, we have been only reading housekeeping data from cFS; we haven't sent anything to it. We did some work before briefly analyzing the command ingestor application (remember `CI_LAB_APP` from several sections above?). It's about time we send some commands to it. To do so, we must first remember how it works. Besides initialization and some other minutiae, a relevant function in CI is `ReadUplink()`: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image190.png) This function is constantly polling for incoming bytes through the "uplink" (read, from ground to vehicle) UDP channel between cFS and ground. Let's see what this function does exactly: ![](image191.png) It basically allocates the message buffer (member of the `CI_LAB_Global` data structure), and then invokes OS_SocketRecvFrom which basically wraps, after a few onion layers, the familiar `recvfrom()`. After that, `CI_LAB_APP` invokes `CFE_SB_TransmitBuffer` which will post the incoming packets in the software bus. Everybody subscribed to the right message IDs will get notified in due time and will process the incoming commands accordingly. Let's send some bytes to it and see what happens. How do we send byes? First, we need to delve a bit into Space Packets and how they are structured. ### Space Packet Protocol The Space Packet Protocol[^33] (SPP) is designed as a self-delimited carrier of a data unit (i.e., a Space Packet) that contains an application ID (APID) used to identify the data contents, data source, and/or data user within a given enterprise. A typical use would be to carry data from a specific mission source to a mission user. Different data types often require additional information (such as time) to fully utilize the contained data, and those parameters and the format of the data contents must be identified, in the mission context, by using the APID. The SPP is designed to meet the requirements of missions to efficiently transfer application data of various types and characteristics between nodes, over one or more subnetworks, and possibly involving one or more ground-to-vehicle, vehicle-to-ground, vehicle-to-vehicle, or onboard communication links. The SPP can provide the functionality of an Application Layer protocol or a 'shim' protocol. At the Application Layer, the SPP defines the Space Packet, which can be used directly by the user to contain application data. The identification of the meaning of APID as to source or destination and the path that the SPP will traverse are entirely determined by the assignment of mission-specific meaning within the context of any given deployment. Most importantly, the SPP itself defines no path, network, or routing functionality and does not provide network services. Furthermore, SPP itself has no networking capabilities and fully relies on the services provided by the applicable subnetworks. The PDU of the SPP is the Space Packet, and the structure of this packet is shown in the next figure: ![Diagram, table Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image192.png) The header of the Space Packet is 6 bytes long, and its structure is as follows: ![Table Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image193.png) The fields are: - Packet Version Number: Bits 0--2 of the Packet Primary Header shall contain the (binary encoded) Packet Version Number (PVN). This 3-bit field shall identify the data unit as a Space Packet defined by the Recommended Standard; it shall be set to '000'. - Packet Identification Field: Bits 3--15 of the Packet Primary Header shall contain the Packet Identification Field. This 13-bit field shall be sub-divided into three sub-fields as follows: - Packet Type (1 bit, mandatory): Packet Type Bit 3 of the Packet Primary Header shall contain the Packet Type. The Packet Type shall be used to distinguish Packets used for telemetry (or reporting) from Packets used for telecommand (or requesting). For a telemetry (or reporting) Packet, this bit shall be set to '0'; for a telecommand (or requesting) Packet, this bit shall be set to '1'. NOTE -- The exact definition of 'telemetry Packets' and 'telecommand Packets' needs to be established by the project that uses this protocol. - Secondary Header Flag (1 bit, mandatory): Bit 4 of the Packet Primary Header shall contain the Secondary Header Flag. The Secondary Header Flag shall indicate the presence or absence of the Packet Secondary Header within this Space Packet. It shall be '1' if a Packet Secondary Header is present; it shall be '0' if a Packet Secondary Header is not present. The Secondary Header Flag shall be static with respect to the APID and managed data path throughout a Mission Phase. The Secondary Header Flag shall be set to '0' for Idle Packets. - APID (11 bits, mandatory): The APID is unique only within its naming domain. For the discussion of naming domains. The APID may uniquely identify the individual sending or receiving application process within a particular space vehicle. For Idle Packets the APID shall be '0b11111111111', that is, all ones. There are no restrictions on the selection of APIDs except for the APID for Idle Packets stated above. In particular, APIDs are not required to be numbered consecutively. Missions may use the optional Packet Secondary Header to create an extended naming domain, but such uses are not specifically defined in the Space Packet Protocol. - Packet Sequence Control: Bits 16--31 of the Packet Primary Header shall contain the Packet Sequence Control Field. This 16-bit field shall be sub-divided into two sub-fields as follows: - Sequence Flags (2 bits, mandatory): Bits 16--17 of the Packet Primary Header shall contain the Sequence Flags. The use of the Sequence Flags is not mandatory for the users of the SPP. However, the Sequence Flags may be used by the user of the Packet Service to indicate that the User Data contained within the Space Packet is a segment of a larger set of application data. The Sequence Flags shall be set as follows: - '00' if the Space Packet contains a continuation segment of User Data - '01' if the Space Packet contains the first segment of User Data. - '10' if the Space Packet contains the last segment of User Data. - '11' if the Space Packet contains unsegmented User Data. - Packet Sequence Count or Packet Name (14 bits, mandatory): Bits 18--31 of the Packet Primary Header shall contain the Packet Sequence Count or the Packet Name. For a Packet with the Packet Type set to '0' (i.e., a telemetry Packet), this field shall contain the Packet Sequence Count. For a Packet with the Packet Type set to '1' (i.e., a telecommand Packet), this field shall contain either the Packet Sequence Count or Packet Name. The Packet Sequence Count shall provide the sequential binary count of each Space Packet generated by the user application identified by the APID. Packet Sequence Counts are unique and independent per each user application as identified by the APID and are not shared across multiple APIDs. The Packet Sequence Count shall be continuous (modulo-16384). A re-setting of the Packet Sequence Count before reaching 16383 shall not take place unless it is unavoidable. The purpose of the Packet Sequence Count is to order the Packets generated by the same user application (identified by a single APID), even though their order may be disturbed during transport from the origin to the destination, as well as to support the detection of missing packets within each user application. If the Packet Sequence Count is reset because of an unavoidable reinitialization of a process, the completeness of a sequence of Packets cannot be determined. The Packet Name allows a particular Packet to be identified with respect to others occurring within the same communications session. There are no restrictions on binary encoding of the Packet Name. That is, the Packet Name can be any 14-bit binary pattern. - Packet Data Length: Bits 32--47 of the Packet Primary Header shall contain the Packet Data Length. This 16-bit field shall contain a length count C that equals one fewer than the length (in bytes) of the Packet Data Field. The length count C shall be expressed as: C = (Total Number of Bytes in the Packet Data Field) -- 1 - Packet Data: The Packet Data Field is mandatory and shall consist of at least one of the following two fields, positioned contiguously, in the following sequence: - Packet Secondary Header (variable length); - User Data Field (variable length). The Packet Data Field shall contain at least one octet. In cFS, the Space Packet Primary Header is defined in ccsds_hdr.h, and it looks like this: ![Text, letter Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image194.png) ### Sending Space Packets to cFS Ok, we need to craft our own Space Packets. How do we do this? cFS comes with a nice tool called cmdUtil which allows you to create handcrafted Space Packets using the command line. Not the most impressive UX tool ever, but it checks out. But before using it, some disambiguation. What on earth is the difference between cFS's message IDs and Space Packets AppIDs, and with StreamIDs and all that? It all boils down to how we assemble the Space Packet primary headers. The AppID is an 11-bit field embedded in the first 2 bytes of the header. Now, a message ID (as defined in cFS) refers to those two bytes. Then, when we define a message ID of, say, 0x1882 (like it is for our sample app), we specify the whole 2 first bytes of the CCSDS Primary Header. This took a bit longer than expected for me to realize. To add a bit to the confusion, the cmdUtil tool we will use refers to message IDs as `pktid`. See the description in the next figure: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image195.png) Let's try to send a Space Packet with this tool and try to hit our sample application command interpreter. To achieve so, we will pass these flags to the tool: ```Console ./cmdUtil --host=localhost --port=1234 --pktapid=0x1882 ``` ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image196.png) So, the command is hitting our sample app command interpreter. Let's see if it can unpack the message ID correctly: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image197.png) It does. But now, it continues and invokes a set of specific functions which depend on the "command code", which is defined in the command secondary header, along with a checksum: ![Text, letter Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image198.png) The switch case to discern the function or command code follows next: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image199.png) #### Function/command code What is this function or command code about? The command code is the field that indicates what it is that we want to do with our command in the application. The cmdUtil allows us to send user-defined command codes with the `pktfc` (packet function code) flag. For our sample app, the command code 1 is used to clear the command counter. Let's use it and see it working: ```Console ./cmdUtil --host=localhost --port=1234 --pktid=0x1882 --pktfc=1 ``` And it works as expected: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image200.png) With all this in place, it is about time we start creating commands to reach to our aircraft. This will take several steps. ### Creating our first command (Throttling the Engines) First, we need to create one more function code in sample_app_msg.h: ![Text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image201.png) Then, we need to create a new data structure for the command, including the relevant arguments for setting/modifying the aircraft's throttle (engine number and throttle command): ![Graphical user interface Description automatically generated with medium confidence](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image202.png) Now, it is time to add a new case in the switch statement which handles all command codes: ![Graphical user interface, text, application Description automatically generated](image203.png) We are not done yet. Now, we need to write the `SAMPLE_APP_SetThrottle`, which will take the input arguments and form the property string we will use to reach JSBSim: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image204.png) > [!Note] > When using commands that incorporate arguments, you need to mind the endianness in cmdUtil, otherwise, the arguments of the command will be interpreted the wrong way (lesson learned the hard way for me). ```Console ./cmdUtil --endian=LE --host=localhost --port=1234 --pktid=0x1882 --pktfc=3 -l 1 -f 0.2 ``` Remember, the parameters in this command are: `pktId`: it's the message ID (first two bytes of the Space Packet header) `pktfc`: function code (first byte of the secondary header of the command) -l is a flag to pass an argument of type int -f is a flag to pass an argument of type float Now, if everything goes as planned, we should be able to throttle up or down our aircraft's engines as we wish. First, let's inspect whether the command string is formed correctly: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image205.png) It appears like it. Let's now try to send it. ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image206.png) Voilá. We see the second engine with reduced thrust. We can command our aircraft from the ground using Space Packets. Let's put everything together now, and now you start to see how many things we have been putting together. We must: - Run JSBSim - Run cFS - Run the telemetry parsing and plotting utility - Run the real-time 3D visualization in STK (this also means running the virtual machine where STK executes) We start to see that some of these functionalities can be merged, for convenience and simplicity. For instance, the plotting and real-time 3D visualization forwarder can be combined into one single thing (considering they both feed from the telemetry coming through UDP that cFS sends); otherwise, they would collide with each other as they cannot bind to the same UDP port at the same time. The refactored Python code is as follows: ```Python import datetime import struct import dash from dash import dcc, html import plotly from dash.dependencies import Input, Output from pyorbital.orbital import Orbital import sys import socket localIP = "127.0.0.1" localPort = 1235 bufferSize = 1024 position_h_agl=0 tankWeight=0 # Create a TCP/IP socket to connect and command STK stk_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) STK_CONNECT_SERVER_ADDR = ('192.168.89.129',5001) stk_sock.connect(STK_CONNECT_SERVER_ADDR) stk_sock.settimeout(1) VehicleName = "737" def sendSTKcommand(command): stk_sock.send(command.encode()) data="" end=False try: data = stk_sock.recv(16) except socket.timeout: return 0 print ('sending: %s' % command) if(data == b'ACK'): print(data,"STK: Command acknowledged:",command) elif (data == b'NACK'): print(data,"STK: Command NOT acknowledged:",command) else: print("Some other response from STK:", data) return 0 data = { 'time': [], 'position_h_agl': [], 'tankWeight': [], 'temperatureC':[], 'sim_time_sec':[] } # Create a datagram socket UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) UDPServerSocket.setsockopt( socket.SOL_SOCKET,socket.SO_REUSEADDR, 1 ) UDPServerSocket.bind((localIP, localPort)) external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div( html.Div([ html.H4('JSBSim/cFS: Aircraft Live Telemetry'), html.Div(id='live-update-text'), dcc.Graph(id='live-update-graph'), dcc.Interval( id='interval-component', interval=1*1000, # in milliseconds n_intervals=0 ) ]) ) command = 'Realtime */Aircraft/737 SetProp' sendSTKcommand(command) start_time = datetime.datetime.utcnow() print("now =", start_time) dt_string = start_time.strftime("%d %b %Y %H:%M:%S.%f")[:-3] end_time = start_time + datetime.timedelta(days=1) end_str = end_time.strftime("%d %b %Y %H:%M:%S.%f")[:-3] print("date and time =", dt_string) print("end date and time =",end_time) command = 'Units_Set * Connect Distance Meter\n' # Units_Set * Connect Date EpSec Distance Meter sendSTKcommand(command) command = 'SetEpoch * "' + dt_string +'"\n' sendSTKcommand(command) command = 'SetAnalysisTimePeriod * ' + '\"' + dt_string + '\"' + ' ' + '\"' + end_str + '\"' + '\n' sendSTKcommand(command) #command = 'Units_Get * Connect\n' #sendSTKcommand(command) command = 'SetAttitude */Aircraft/737 ClearData\n' sendSTKcommand(command) attitude_ECI=[None]*4 def sendToSTK(start_time,position_eci_x,position_eci_y,position_eci_z,velocity_eci_x,velocity_eci_y,velocity_eci_z,attitudeECI): SIM_TIME=start_time + datetime.timedelta(seconds=sim_time_sec) print(SIM_TIME) sim_time_str = SIM_TIME.strftime("%d %b %Y %H:%M:%S.%f")[:-3] command = 'SetPosition */Aircraft/737 ECI ' + '\"' + sim_time_str + '\" ' + str(position_eci_x) + " " + str(position_eci_y) + ' ' + str(position_eci_z) + " " + str(velocity_eci_x) + " " + " " + str(velocity_eci_y) + " " + str(velocity_eci_z) + '\n' print (command) sendSTKcommand(command) command = 'AddAttitude */Aircraft/737 Quat ' + '\"' + sim_time_str + '\" ' + str(attitude_ECI[1]) + " " + str(attitude_ECI[2]) + ' ' + str(attitude_ECI[3]) + " " + str(attitude_ECI[0]) + '\n' print (command) sendSTKcommand(command) command = 'SetAnimation * CurrentTime ' + '\"' + sim_time_str + '\"' + ' \n' print (command) sendSTKcommand(command) @app.callback(Output('live-update-text', 'children'), Input('interval-component', 'n_intervals')) def update_metrics(n): global position_h_agl global tankWeight global temperatureC global sim_time_sec global attitudeECI bytesAddressPair = UDPServerSocket.recvfrom(bufferSize) frame=bytesAddressPair[0] tankWeight = struct.unpack("d", frame[24:32])[0] velocity_eci_x=struct.unpack("d", frame[32:40])[0] velocity_eci_y=struct.unpack("d", frame[40:48])[0] velocity_eci_z=struct.unpack("d", frame[48:56])[0] position_eci_x=struct.unpack("d", frame[56:64])[0] position_eci_y=struct.unpack("d", frame[64:72])[0] position_eci_z=struct.unpack("d", frame[72:80])[0] position_h_agl=struct.unpack("d", frame[80:88])[0] temperatureC=struct.unpack("d", frame[88:96])[0] sim_time_sec=struct.unpack("d", frame[96:104])[0] attitude_ECI[0]=struct.unpack("d", frame[104:112])[0] attitude_ECI[1]=struct.unpack("d", frame[112:120])[0] attitude_ECI[2]=struct.unpack("d", frame[120:128])[0] attitude_ECI[3]=struct.unpack("d", frame[128:136])[0] sendToSTK(start_time,position_eci_x,position_eci_y,position_eci_z,velocity_eci_x,velocity_eci_y,velocity_eci_z,attitude_ECI) style = {'padding': '5px', 'fontSize': '18px'} return [ html.Span('Tank Weight (lbs): {0:.2f}'.format(tankWeight), style=style), html.Span('Position (AGL)(ft): {0:.2f}'.format(position_h_agl), style=style), html.Span('Temperature: {0:0.2f}'.format(temperatureC), style=style) html.Span('attitude_ECI[0]: {0:0.2f}'.format(attitude_ECI[0]), style=style) html.Span('attitude_ECI[1]: {0:0.2f}'.format(attitude_ECI[1]), style=style) html.Span('attitude_ECI[2]: {0:0.2f}'.format(attitude_ECI[2]), style=style) html.Span('attitude_ECI[3]: {0:0.2f}'.format(attitude_ECI[3]), style=style) ] print("update_metrics") # Multiple components can update everytime interval gets fired. @app.callback(Output('live-update-graph', 'figure'), Input('interval-component', 'n_intervals')) def update_graph_live(n): global position_h_agl global tankWeight print("update_graph") time = datetime.datetime.now() data['time'].append(time) data['position_h_agl'].append(position_h_agl) data['tankWeight'].append(tankWeight) data['temperatureC'].append(temperatureC) # Create the graph with subplots fig = plotly.tools.make_subplots(rows=3, cols=1, vertical_spacing=0.1) fig['layout']['margin'] = { 'l': 30, 'r': 10, 'b': 30, 't': 80 } #fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'} fig.append_trace({ 'x': data['time'], 'y': data['position_h_agl'], 'name': 'Altitude (AGL)', 'mode': 'lines+markers', 'type': 'scatter' }, 1, 1) fig.append_trace({ 'x': data['time'], 'y': data['tankWeight'], 'text': data['time'], 'name': 'Fuel weight (lbs)', 'mode': 'lines+markers', 'type': 'scatter' }, 2, 1) fig.append_trace({ 'x': data['time'], 'y': data['temperatureC'], 'text': data['time'], 'name': 'Temperature (Celsius)', 'mode': 'lines+markers', 'type': 'scatter' }, 3, 1) return fig if __name__ == '__main__': app.run_server(debug=True) ``` Let's now try to throttle down one of the aircraft's engines with: ```Console ./cmdUtil --endian=LE --host=localhost --port=1234 --pktid=0x1882 --pktfc=3 -l 1 -f 0.0 ``` Before sending the command, the plane appears as flying leveled: ![A picture containing plane, outdoor, transport, glider Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image207.png) And telemetry shows the same: ![A picture containing timeline Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image208.png) So, let's throttle down one engine and see its impact: ![A picture containing calendar Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image209.png) We see the aircraft losing altitude, falling almost 18000 feet in a matter of minutes (telemetry glitches can be seen and need to be sorted out but let's add them to the infamous 'technical debt' for now). You also see the temperature increasing as the aircraft comes close to the ground. Fasten your seatbelts, I guess. Attitude-wise, it's no surprise that the aircraft has a big thrust imbalance between both engines therefore the attitude has also been affected. The STK visualization shows the aircraft with a steep roll angle to the right, consistent with the loss of thrust on that side (engine 1 in our command is the right one, remember the counting in JSBSim starts from 0). ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image210.png) Although the aircraft or the virtual passengers do not seem to be enjoying the nicest flight ever, we have proved we can command the aircraft to do different things from flight software, that we can read telemetry coming from it, and that we can visualize its orientation and position in three dimensions. Not bad. ## Commanding control surfaces Now, it is time to command flight control surfaces to modify the aircraft's lateral and longitudinal positions. For this, we will command ailerons, elevators, spoilers, rudders, flaps, and speed brakes. We will follow the same approach as we did for the throttle, and we will write a generic function that will handle the command of every control surface. Let's see how. First, we create a new command code: ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image211.png) Then, we create a new command: ![Graphical user interface, text, application Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image212.png) Next, we create the function to set the flight control surface properties (right after the one we wrote for the engine throttle): ![Graphical user interface, text Description automatically generated](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image213.png) And now we implement it (not before adding an enumeration for the control surface): ![](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image214.png) ![Text Description automatically generated with low confidence](NSD2/Resources/Simulation/Flight%20Simulation/media/media/image215.png) And that should do the trick and allow us to command the aircraft. >[!Note] >It is necessary to erase from the aircraft initialization file all the channels created for automatic flight, otherwise, those will override our commands from cFS. With all the flight control surfaces in our hands, it is up to us now to keep the aircraft flying in a controlled manner. It is by having this freedom to have access to entirely control the flight path and orientation of an aircraft in an "open loop" that we can only realize how tricky it is to keep a stable flight. ## Recapping A lot happened in this section as well. We: - Hooked together JSBSim and cFS by means of reading and writing properties as a way to access the FDM in a lean way. - We wrote our new properties - We created our own "real" sensors - We showed telemetry on the ground on a web page using some Python and Dash - We added real-time 3D visualization of the position, velocity, and attitude of our aircraft using STK. - We wrote commands to modify the dynamic status of the aircraft, namely throttle and flight control surfaces. [^1]: https://jsbsim.sourceforge.net/ [^2]: https://cfs.gsfc.nasa.gov/ [^3]: Mind that counting lines of code has never been an incredibly accurate method and it may only represent a rough approximation of code complexity. [^4]: UML stands for Unified Modeling Language, and it is a graphical way of describing software structure and behavior. [^5]: According to a 2011 report by Boeing, the 787 Dreamliner contains over 6.5 million lines of code. This code is spread across various systems, including the flight control system, avionics system, and passenger entertainment system. This number has likely increased. [^6]: Nedhal A. Al-Saiyd "Source Code Comprehension Analysis in Software Maintenance", Computer Science Department, Faculty of Information Technology Applied Science Private University, Amman-Jordan [^7]: M. A. Storey "Theories, Methods and Tools in Program Comprehension: Past, Present and Future", Software Quality Journal 14, pp. 187--208, DOI 10.1007/s11219-006-9216-4. [^8]: Adapted from "Peopleware, Third Edition" by Tom De Marco and Timothy Lister, Addison-Wesley [^9]: Summarized from https://www.gnu.org/software/automake/manual/html_node/GNU-Build-System.html [^10]: Adapted from: https://mesonbuild.com/Tutorial.html [^11]: Adapted from https://ninja-build.org/manual.html [^12]: This is just meant to be a brief introduction. For a deeper dive, see for example *Automotive Software Architectures*, by Miroslaw Staron (Springer, 2017) [^13]: https://en.wikipedia.org/wiki/Occam%27s_razor [^14]: https://en.wikipedia.org/wiki/Larry_Tesler [^15]: Saffer, D. (2009). Designing for interaction: Creating innovative applications and devices. New Riders. [^16]: JSBSim is licensed under the terms of the GNU Lesser GPL (LGPL) [^17]: https://en.wikipedia.org/wiki/RTFM [^18]: https://agilemanifesto.org/ [^19]: https://jsbsim.sourceforge.net/JSBSimReferenceManual.pdf [^20]: https://en.wikipedia.org/wiki/Euler_method [^21]: https://en.wikipedia.org/wiki/Trapezoidal_rule [^22]: https://en.wikipedia.org/wiki/Linear_multistep_method#Families_of_multistep_methods> [^23]: https://ntrs.nasa.gov/api/citations/19770009539/downloads/19770009539.pdf [^24]: https://ntrs.nasa.gov/api/citations/19980028448/downloads/19980028448.pdf [^25]: RTEMS is a Real-Time Operating System (RTOS) popular in mission-critical applications [^26]: https://public.ccsds.org/Pubs/133x0b2e1.pdf [^27]: https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/ [^28]: https://dash.plotly.com/ [^29]: Can you spot the error? Yes, there is a bug with the velocity vector being copied twice, this bug is fixed down below [^30]: https://cesium.com/ [^31]: https://www.flightgear.org/ [^32]: https://help.agi.com/stk/Subsystems/connect/connect.htm [^33]: https://public.ccsds.org/Pubs/133x0b2e1.pdf [^34]: Mind I use the JPL notation of quaternions (the scalar element at the end)