Howto talk:Implementing a simple GCA system

From FlightGear wiki
Jump to navigation Jump to search

GUI Integration

regarding the gca submodule: I've just been tinkering with it, the way its initialiation is currently set up using a gca.nas file in conjunction with a submodule is a bit problematic, because this basically means that the code will be loaded twice (unless I missed something of course).

In general, the gca.nas/loader module is not needed at all for submodules living in $FG_ROOT/Nasal[1]

The Gca folder extracts to $FG_ROOT/Nasal/Gca, but the submodule would load it into the "Gca" namespace, whereas you are explicitly loading into a "gca" namespace

looking at the code, I simply removed the gca.nas file and renamed the "Gca" folder to "gca", and everything seems to be working correctly.

To integrate the UI dialog with your existing code, I would simply open control.nas and navigate to the Control function there, and append a call to configureGUI after you create the gca class instance named "demo", commenting/removing the setPosition(), setGlidepath() and setFinalApproach() calls - because those can be handled by the UI now. The only thing missing to actually show the dialog is to add a new file to $FG_ROOT/Nasal/gca/gca_gui.nas with the code I added to the wiki a few weeks ago: Howto talk:Implementing a simple GCA system With your recent changes introducing separate setters for your variables , it should be straightforward then to hook up each UI field to the GCAController class.[2]

The only additional change I had to make was adding a "setTransmissionInterval" method to the GCAController class, because you didn't provide one.

Other than that, I changed the UI class so that it uses GCAController.callback_name instead of strings to look up the method handles, which also meant using the Nasal call() API. The next step would be hooking up the UI to the class and moving some of the heuristics in control.nas into the GUI file.[3]

From an integration standpoint, we are still a few steps away from hooking up everything correctly.

But I really only dropped the file into the submodule folder, and added a call to configureGUI() to control.nas, as well as extended GCAController to add the missing setTransmissionInterval() method.[4]

controls.nas is currently using heuristics for some of its default values, so we could optionally support funcs/callbacks for the default value, too.

# UI code starts here

var configureGCA = func( gcaObject ) {

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

var window =[width,height],"dialog").set('title',title);

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","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 =;
# assign it to the Canvas

var setupWidgetTooltip = func(widget, tooltip) {
 widget._view._root.addEventListener("mouseover", func gui.popupTip(tooltip) );
} # setupWidgetTooltip

var setupLabeledInput = func(root, layout, input) {

var label =,, {wordWrap: 0}); 
var unit_suffix = sprintf(" (%s):", input.unit);

var field =,, {});

if (input.focus)

setupWidgetTooltip(widget:field, tooltip: input.tooltip);

var el = field._view._root;
el.addEventListener("keypress", func (e) {

# colorize valid/invalid inputs
var color = (validationHelpers[input.validate]( field.text() ) == 0) ? [0,1,0] : [1,0,0];


return field; # return to caller
} # setupLabeledInput()

var validationHelpers = {

_internalState: {},

'AircraftRoot': 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 STATUS.FAILURE;
} # foreach

return STATUS.SUCCESS; # valid root node

'Airport': func(input) {
var match = airportinfo(input);
if (match == nil or typeof(match) != 'ghost') return STATUS.FAILURE; 
validationHelpers._internalState.airport = match; 


'Runway': func(input) {

var runways = validationHelpers._internalState.airport.runways;
if (typeof(runways)!="hash" or !size(keys(runways))) return STATUS.FAILURE;
if (runways[input] == nil) return STATUS.FAILURE;


'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;

'TransmissionInterval': func(input) {
return 0;

'TransmissionProperty': func(input) {
return 0;

}; # validationHelpers;

var inputs = [
{text: 'Position root', default_value:'/position', focus:1, callback:GCAController.setPosition, tooltip:'property path to position node', validate: 'AircraftRoot', convert:nil, unit: 'property path'},
{text: 'Airport', default_value:'KSFO', focus:0, callback:GCAController.setAirport, tooltip:'ICAO ID, e.g. KSFO', validate: 'Airport', convert:nil, unit:'ICAO'},
{text: 'Runway', default_value:'28R', focus:0, callback:GCAController.setRunway, tooltip:'runway identifier, e.g. 28L', validate: 'Runway', convert:nil, unit:'rwy'},
{text: 'Final Approach', default_value:'10.00', focus:0, callback:GCAController.setFinalApproach, tooltip:'length of final approach leg', validate: 'FinalApproach', convert:nil, unit:'nm'},

{text: 'Glidepath', default_value:'3.00', focus:0, callback:GCAController.setGlidepath, tooltip:'glidepath in degrees, e.g. 3', validate: 'Glidepath', convert:nil, unit:'degrees'},
{text: 'Safety Slope', default_value:'2.00', focus:0, callback:GCAController.setSafetySlope, tooltip:'safety slope in degrees', validate: 'SafetySlope', convert:nil, unit:'degrees'},

{text: 'Decision Height', default_value:'200.00', focus:0, callback:GCAController.setDecisionHeight, tooltip:'decision height (vertical offset)', validate: 'DecisionHeight', convert:nil, unit:'ft'},
{text: 'Touchdown Offset', default_value:'0.00', focus:0, callback:GCAController.setTouchdownOffset, tooltip:'touchdown offset', validate: 'TouchdownOffset', convert:nil, unit:'m'},
{text: 'Transmission interval', default_value:'5.00', focus:0, callback:GCAController.setTransmissionInterval, tooltip:'Controller/timer resolution', validate: 'TransmissionInterval', convert:nil, unit:'secs'},
{text: 'Transmission channel', default_value:'/sim/messages/approach', focus:0, callback:GCAController.setTransmissionChannel, tooltip:'property to use for transmissions. For example: /sim/multiplay/chat or /sim/sound/voices/approach', validate: 'TransmissionProperty', convert:nil, unit:'property'},
]; # input fields

foreach(var input; inputs) {
 # TODO: pass input hash 
 input.widget = setupLabeledInput(root, myLayout, input);

var validateFields = func() {
foreach(var f; inputs) {

 var result = validationHelpers[f.validate] ( f.widget.text() );
 if (result == STATUS.FAILURE) {

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

 } # error handling
} # foreach
return STATUS.SUCCESS; # all validations passed
} # validateFields()

# global stuff

var gcaRunning = 0;
# var myGCA = nil;

var toggleFields = func(enabled) {
foreach(var i; inputs) {

var buildCGA = func() {

# create new object
# myGCA =;

foreach(var field; inputs) {
var value = field.widget.text();
# var converted_value = typeof(field.convert != nil) ? field.convert(value) : value;
# call vector
call(GCAController[field.callback], [value], gcaObject );


# myGCA.start();


var toggleGCA = func() {

if (gcaRunning) {
gcaRunning = 0;
toggleFields(!gcaRunning); # set editable

if (!gcaRunning and validateFields()==0) {
gcaRunning = 1;
toggleFields(!gcaRunning); # set readonly

} # toggleGCA()

var button =,, {})
	.setFixedSize(75, 25)
	.listen("clicked", toggleGCA);

setupWidgetTooltip(widget:button, tooltip: "toggle GCA on/off");

}; # configureGCA();
  1. Hooray  (Sep 23rd, 2017).  Re: Spoken GCA .
  2. Hooray  (Sep 23rd, 2017).  Re: Spoken GCA .
  3. Hooray  (Sep 23rd, 2017).  Re: Spoken GCA .
  4. Hooray  (Sep 23rd, 2017).  Re: Spoken GCA .

PAR Screen Class

  • make the defaults for HGrid/VGrid configurable using the UI dialog (trivial to do by adding to the inputs vector in gca_gui.nas)
  • add timestamps to the PAR screen showing the issued instructions (should be useful for debugging/troubleshooting purposes) ?
  • consider adding a terrain/altitude profile ?
  • look at clm76's VSD code (based on omega95's 787 implementation
  • generalize the code to come up with helpers for plotting 2D diagrams (x/y graphs with two axes)