Howto:Implementing a simple GCA system

From FlightGear wiki
Revision as of 06:54, 20 August 2017 by Hooray (talk | contribs) (→‎Ideas)
Jump to navigation Jump to search
Canvas GUI dialog showing a simple UI for setting up a GCA approach/controller.

Objective

use Nasal to script AI/ATC interactions that pilots need to respond to properly. For example, another simple idea to get you started might be a Nasal script to implement a GCA ("ground controlled approach") mission: http://de.wikipedia.org/wiki/Ground_Controlled_Approach This would simulate an air traffic controller "talking you down", i.e. providing all the feedback an ILS would normally provide (vertical/horiontal deviation from the glide path). This could be very easily implemented in Nasal space. You would just need to configure the script (airport, runway, touchdown point, altitude, glide path) and the GCA script could vector you onto the right course and request you to change altitude as required.[1]


the helpers in geo.nas should be more than sufficient to help us come up with a GCA module and a simple "controller" class that controls an aircraft by monitoring its glide path/vector and issuing instructions accordingly. As a matter of fact, it would also not be that far-fetched to hook up the same control logic to a tanker.nas based AI-piloted aircraft[2]

Status

  • implemented simple helper class to regularly fetch the corresponding aircraft properties and compute the delta between required heading/altitude and actual one

Background

GCA is surprisingly accurate and easy to use, both as a pilot and a controller. In the 1970´s we tried it on the simulator that I ran at BAC Weybridge. My impression was that it was easier than watching ILS /GLS deviations, At decision height (200ft) when the runway light display projector was turned on, I as pilot) or the pilot that I was directing (as ATC operator) were always perfectly lined up. GCA, with the RAF vocabulary that we used, is very good for teaching students how to follow the glidepath.[3]


we may need to add some features, especially for piloting/flying, i.e. route manager/waypoint awareness, and ability to track course/bearing to certain positions (via geo.nas) - e.g. for navigating via VORs, NDBs or DMEs Currently, the tutorial system doesn't have any built-in support for "fly to the SFO VOR" - but once we have that, we could even support flying holding patterns ( "fly to sfo maintain 8000, hold left on the 180 radial"). Such things would be useful also for virtual flight instruction - but also for an ATC adventure, i.e. where a virtual ATC controller guides pilots down a GCA path using radar vectors and AGL altitudes[4]


All this stuff is already exposed to Nasal thanks to the work that Zakalawe & TheTom have done as part of "NasalPositioned" and those flightplan() APIs - so we really only need to expose a handful of building blocks so that people can create route/navaid-aware missions. This would be primarily useful for any "flight instructions"-based scenarios, i.e. perfectly in line with the original purpose of the "tutorials" system, while also allowing additional functionality to be developed on top, i.e. having scripted virtual pilots or ATC controller.[5]

Design

the logic used by glideslope.nas is definitely a good start - for starters, what is needed is a loop that is monitoring an aircraft, and then, it will be polling that aircraft's position and compute the vertical/horiontal offset to the destination airport/runway using a configurable glideslope (gradient, i.e. altitude change over ground per time interval) - once we have a vertical/horizontal "delta", we can convert those to instructions that are issued to the pilot, while monitoring the aircraft to see if the requested changes (in altitude/heading) are implemented or not, and adjust the gradient/vector accordingly.

[6]


We will be implementing a simple class that merely operates using 3 properties:

  • position/altitude-ft
  • position/latitude_deg
  • position/longitude_deg

This approach makes it possible to reuse the same class for both, the main aircraft, as well as arbitrary AI traffic - e.g. imagine controlling a tanker.nas based AI aircraft using this AI controller.

Good to know

Proof of concept

WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.
var GCAController = {
# constructor
new: func(timer_interval_secs) {
 var m = {parents:[GCAController] };
 m.aircraft_properties = {altitude_ft: "altitude-ft", latitude_deg: "latitude-deg", longitude_deg: "longitude-deg"};
 m.aircraft_state = {latitude_deg:0.00, longitude_deg:0.00, altitude_ft:0.00 }; 
 m.aircraft_object = geo.Coord.new();

 m.receivers = []; # callbacks to receive instructions from the GCA controller
 m.timer = maketimer(timer_interval_secs, func m.update() );
 
 return m;
}, # new()

# destructor
del: func() {
me.timer.stop();
}, # del()

#####
# this will be called by our timer
update: func() {
print("Updating GCA controller");

me.updatePosition();
me.computeRequiredAltitude();
me.computeRequiredHeading();
# compute offset/delta

var instruction = me.buildInstruction();
# now that we have an instruction, pass it to registered callbacks 
me.notifyReceivers(instruction);

}, # update()

##### our setup APIs: 

setAircraft: func(root) {
me.root = root;
# AI/MP models have their own callsign node:
var callsign_node = props.getNode(root).getNode("callsign");

# the main aircraft's callsign is stored under /sim/multiplay/callsign
if (callsign_node == nil) {
print("Using multiplayer callsign");
callsign_node = props.getNode("/sim/multiplay/callsign");
}
else {
print("Using AI/MP callsign");
}

var callsign = callsign_node.getValue();
me.callsign = callsign;
print("callsign: ", callsign);
}, # setAircraft()

setDestination: func(airport, runway, glidepath) {
 var apt = airportinfo(airport);
 if (apt == nil) die("Invalid ICAO for airport:"~ airport);

 var runways = apt.runways;
 # debug.dump(runways);
 if (typeof(runways)!="hash" or !size(keys(runways))) die ("runways invalid for "~airport);

 if (runways[runway] == nil) die("runway not found at airport:"~runway);
 
 print("Valid airport/runway combo found");
 me.destination = {airport:airport, runway:runway, glidepath:glidepath, runways:runways, rwy_object: runways[runway] };
}, # setDestination()

registerReceiver: func(receiver) {
append(me.receivers, receiver);
}, #registerReceiver()


notifyReceivers: func(instruction) {
foreach(var r; me.receivers) {
  r(instruction);
 }
},

# start the GCA
start: func() {
 me.timer.start();
}, # start()

# stop/interrupt the GCA
stop: func() {
 me.timer.stop();
}, # stop()

###
## Update helpers
###


# TODO: this can be easily optimized
updatePosition: func() {
foreach(var p; keys(me.aircraft_properties)) {
#print(me.aircraft_properties[p]);
me.aircraft_state[p] = getprop(me.root ~'/'~ me.aircraft_properties[p]);
}

#debug.dump( me.aircraft_state);

# FIXME: alt meters/feet conversion
me.aircraft_object.set_latlon(me.aircraft_state['latitude_deg'], me.aircraft_state['longitude_deg'], me.aircraft_state['altitude-ft']);

}, # updatePosition()

computeRequiredAltitude: func() {
}, # computeRequiredAltitude()

computeRequiredHeading: func() {
}, # computeRequiredHeading()

buildInstruction: func() {

return sprintf("%s Turn left",me.callsign);
}, # buildInstruction

}; # end of GCAController class

###
## now, let's test our new class: 
###

var demo = GCAController.new( timer_interval_secs: 0.5 );
demo.setAircraft( root: "/position", callsign:"foo");
demo.setDestination( airport: "KSFO", runway:  "28R", glidepath: 3.00 );

# this callback will be invoked by the GCA controller when it has a new instruction
# (could be just as well a tooltip or multiplayer chat message)
var receiver = func(instruction) {
print("GCA instruction is: ", instruction);
};


demo.registerReceiver( receiver );
demo.start();

GUI frontend

Extended GCA dialog showing additional configuration options

Once the underlying script is working well enough, we can look into providing a GUI on top of the script using the Canvas GUI library:

var (width,height) = (320,350);
var title = 'GCA Dialog ';

# create a new window, dimensions are WIDTH x HEIGHT, using the dialog decoration (i.e. titlebar)
var window = canvas.Window.new([width,height],"dialog").set('title',title);

##
# the del() function is the destructor of the Window
# which will be called upon termination (dialog closing)
# you can use this to do resource management (clean up timers, listeners or background threads)
window.del = func()
{
print("Cleaning up window:",title,"\n");
call(canvas.Window.del, [], me);
};

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color"));

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

# create a new layout
var myLayout = canvas.VBoxLayout.new();
# assign it to the Canvas
myCanvas.setLayout(myLayout);


var setupLabeledInput = func(root, layout, text, default_value, focus) {

var label = canvas.gui.widgets.Label.new(root, canvas.style, {wordWrap: 0}); 
label.setText(text);
layout.addItem(label);

var input = canvas.gui.widgets.LineEdit.new(root, canvas.style, {});
layout.addItem(input);
input.setText(default_value);

## TODO: add widget to hash

if (focus)
input.setFocus();
} # setupLabeledInput()

var validateAircraftRoot = func(input) {
var root = props.getNode(input);
if (root == nil) return 1; # error

var required_props = ['altitude-ft', 'longitude-deg', 'latitude-deg'];

foreach(var rp; required_props) {
 if (root.getNode(rp) == nil) return 1;
}

return 0; # valid root node
}

var validationHelpers = {

'AircraftRoot': func(input) {
return 0;
},

'Airport': func(input) {
return 0;
},

'Runway': func(input) {
return 0;
},

'FinalApproach': func(input) {
return 0;
},

'Glidepath': func(input) {
return 0;
},

'SafetySlope': func(input) {
return 0;
}, 

'DecisionHeight': func(input) {
return 0;
},

'TouchdownOffset': func(input) {
return 0;
}, 


}; # validationHelpers;


var inputs = [
{text: 'Aircraft root property:', default_value:'/position', focus:1, callback:nil, tooltip:nil, validate: 'AircraftRoot', convert:nil},
{text: 'Airport:', default_value:'KSFO', focus:0, callback:nil, tooltip:nil, validate: 'Airport', convert:nil},
{text: 'Runway:', default_value:'28R', focus:0, callback:nil, tooltip:nil, validate: 'Runway', convert:nil},
{text: 'Final Approach (nm):', default_value:'10.00', focus:0, callback:nil, tooltip:nil, validate: 'FinalApproach', convert:nil},

{text: 'Glidepath:', default_value:'3.00', focus:0, callback:nil, tooltip:nil, validate: 'Glidepath', convert:nil},
{text: 'Safety Slope:', default_value:'2.00', focus:0, callback:nil, tooltip:nil, validate: 'SafetySlope', convert:nil},

{text: 'Decision Height (ft):', default_value:'200.00', focus:0, callback:nil, tooltip:nil, validate: 'DecisionHeight', convert:nil},
{text: 'Touchdown Offset (m):', default_value:'0.00', focus:0, callback:nil, tooltip:nil, validate: 'TouchdownOffset', convert:nil},

]; # input fields

foreach(var input; inputs) {
 # TODO: pass input hash 
 setupLabeledInput(root, myLayout, input.text, input.default_value, input.focus);
}

var validateFields = func() {
foreach(var f; inputs) {
 
 var widget = nil; # TODO
 var result = validationHelpers[f.validate] ("/position");
 if (result == 1) {

canvas.MessageBox.critical(
  "Validation error",
  "Error validating "~f.text,
  cb = nil,
  buttons = canvas.MessageBox.Ok
); # MessageBox


 } # error handling
} # foreach
} # validateFields()

# click button
var button = canvas.gui.widgets.Button.new(root, canvas.style, {})
	.setText("Start/Stop")
	.setFixedSize(75, 25);

button.listen("clicked", func {
        # add code here to react on click on button.
        # TODO: toggle all fields read-only 
validateFields();
# print("Button clicked !");
});

myLayout.addItem(button);

Visualizing the Approach

Once we have a basic GCAController class working and a corresponding UI to set up the GCA, it seems to make sense to explore having a UI to monitor the ongoing approach by plotting a vertical and a horizontal view of the profile flown.

Basically, this is about plotting two diagrams and projecting the approach accordingly - we can do so by using the Canvas path drawing examples from the Canvas Snippets article and adapt those as needed.

<syntaxhighlight lang="nasal"> <syntaxhighlight>

Ideas

  • add features supported by real GCA/PAR/SAR software [1]
  • make the touchdown point configurable ?
  • Implement the whole thing as a GUI dialog for prototyping purposes (e.g. allow the aircraft/airport etc to be entered/changed easily) 60}% completed
  • support multiple instances ?
  • add terrain awareness support (querying the approach profile) ?
  • show a vertical/horizontal map visualizing the approach ?
  • use a subset of tanker.nas to add an ATC-enabled version that actually responds to GCA instructions ?
  • add a GCAManager class to register active GCAs, and implement mutual awareness ?

Roadmap

  • generalize the code to come up with a "TrackingController" for GCA, ASR/PAR respectively
  • add input validation to the GUI dialog
  • show tooltips
  • add a static label as a status bar to show internal state

References

References
  1. Hooray  (Dec 22nd, 2011).  Re: Achievements: New Motivator and Feature Introduction Met .
  2. Hooray  (Aug 14th, 2017).  Re: Spoken .
  3. Alant  (Aug 13th, 2017).  Re: Spoken .
  4. Hooray  (Apr 14th, 2014).  Re: Tutorials/Missions/Adventures: requests for features .
  5. Hooray  (Apr 14th, 2014).  Re: Tutorials/Missions/Adventures: requests for features .
  6. Hooray  (Aug 17th, 2017).  Re: Spoken ATC .
  7. Hooray  (Aug 18th, 2017).  Re: Spoken ATC .