Howto talk:Implementing a simple GCA system: Difference between revisions
(Created page with "<syntaxhighlight lang="nasal"> io.include("gca.nas"); ### # # UI code starts here # ### var (width,height) = (320,450); var title = 'GCA Dialog '; var window = canvas.Wind...") |
|||
(8 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
== 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<ref>{{cite web | |||
|url = https://forum.flightgear.org/viewtopic.php?p=319255#p319255 | |||
|title = <nowiki> Re: Spoken GCA </nowiki> | |||
|author = <nowiki> Hooray </nowiki> | |||
|date = Sep 23rd, 2017 | |||
|added = Sep 23rd, 2017 | |||
|script_version = 0.40 | |||
}}</ref> | |||
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.<ref>{{cite web | |||
|url = https://forum.flightgear.org/viewtopic.php?p=319257#p319257 | |||
|title = <nowiki> Re: Spoken GCA </nowiki> | |||
|author = <nowiki> Hooray </nowiki> | |||
|date = Sep 23rd, 2017 | |||
|added = Sep 23rd, 2017 | |||
|script_version = 0.40 | |||
}}</ref> | |||
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.<ref>{{cite web | |||
|url = https://forum.flightgear.org/viewtopic.php?p=319259#p319259 | |||
|title = <nowiki> Re: Spoken GCA </nowiki> | |||
|author = <nowiki> Hooray </nowiki> | |||
|date = Sep 23rd, 2017 | |||
|added = Sep 23rd, 2017 | |||
|script_version = 0.40 | |||
}}</ref> | |||
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.<ref>{{cite web | |||
|url = https://forum.flightgear.org/viewtopic.php?p=319269#p319269 | |||
|title = <nowiki> Re: Spoken GCA </nowiki> | |||
|author = <nowiki> Hooray </nowiki> | |||
|date = Sep 23rd, 2017 | |||
|added = Sep 23rd, 2017 | |||
|script_version = 0.40 | |||
}}</ref> | |||
<syntaxhighlight lang="nasal"> | <syntaxhighlight lang="nasal"> | ||
### | ### | ||
Line 8: | Line 58: | ||
# | # | ||
### | ### | ||
var configureGCA = func( gcaObject ) { | |||
var (width,height) = (320,450); | var (width,height) = (320,450); | ||
Line 132: | Line 184: | ||
var inputs = [ | var inputs = [ | ||
{text: 'Position root', default_value:'/position', focus:1, callback: | {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: | {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: | {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: | {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: | {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: | {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: | {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: | {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: | {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: | {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 | ]; # input fields | ||
Line 176: | Line 228: | ||
var gcaRunning = 0; | var gcaRunning = 0; | ||
var myGCA = nil; | # var myGCA = nil; | ||
var toggleFields = func(enabled) { | var toggleFields = func(enabled) { | ||
Line 188: | Line 240: | ||
# create new object | # create new object | ||
myGCA = GCAController.new(); | # myGCA = GCAController.new(); | ||
foreach(var field; inputs) { | foreach(var field; inputs) { | ||
Line 195: | Line 247: | ||
# call vector | # call vector | ||
print(field.callback); | print(field.callback); | ||
call(GCAController[field.callback], [value], | # http://wiki.flightgear.org/Nasal_library#call.28.29 | ||
call(GCAController[field.callback], [value], gcaObject ); | |||
} | } | ||
myGCA.start(); | # myGCA.start(); | ||
gcaObject.start(); | |||
} | } | ||
Line 206: | Line 260: | ||
if (gcaRunning) { | if (gcaRunning) { | ||
gcaObject.stop(); | |||
gcaRunning = 0; | gcaRunning = 0; | ||
toggleFields(!gcaRunning); # set editable | toggleFields(!gcaRunning); # set editable | ||
Line 229: | Line 283: | ||
setupWidgetTooltip(widget:button, tooltip: "toggle GCA on/off"); | setupWidgetTooltip(widget:button, tooltip: "toggle GCA on/off"); | ||
myLayout.addItem(button); | myLayout.addItem(button); | ||
}; # configureGCA(); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
{{Appendix}} | |||
== PAR Screen Class == | |||
{{See also|Howto:Implement a Vertical Situation Display in Nasal}} | |||
{{See also|FGPlot}} | |||
* 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) |
Revision as of 19:18, 23 September 2017
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]
###
#
# UI code starts here
#
###
var configureGCA = func( gcaObject ) {
var (width,height) = (320,450);
var title = 'GCA Dialog ';
var window = canvas.Window.new([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", 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 setupWidgetTooltip = func(widget, tooltip) {
widget._view._root.addEventListener("mouseover", func gui.popupTip(tooltip) );
} # setupWidgetTooltip
var setupLabeledInput = func(root, layout, input) {
var label = canvas.gui.widgets.Label.new(root, canvas.style, {wordWrap: 0});
var unit_suffix = sprintf(" (%s):", input.unit);
label.setText(input.text~unit_suffix);
layout.addItem(label);
var field = canvas.gui.widgets.LineEdit.new(root, canvas.style, {});
layout.addItem(field);
field.setText(input.default_value);
if (input.focus)
field.setFocus();
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];
field._view._root.setColorFill(color);
});
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;
return STATUS.SUCCESS;
},
'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;
return STATUS.SUCCESS;
},
'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) {
canvas.MessageBox.critical(
"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) {
i.widget.setEnabled(enabled);
}
}
var buildCGA = func() {
# create new object
# myGCA = GCAController.new();
foreach(var field; inputs) {
var value = field.widget.text();
# var converted_value = typeof(field.convert != nil) ? field.convert(value) : value;
# call vector
print(field.callback);
# http://wiki.flightgear.org/Nasal_library#call.28.29
call(GCAController[field.callback], [value], gcaObject );
}
# myGCA.start();
gcaObject.start();
}
var toggleGCA = func() {
if (gcaRunning) {
gcaObject.stop();
gcaRunning = 0;
toggleFields(!gcaRunning); # set editable
return;
}
if (!gcaRunning and validateFields()==0) {
gcaRunning = 1;
toggleFields(!gcaRunning); # set readonly
buildCGA();
return;
}
} # toggleGCA()
var button = canvas.gui.widgets.Button.new(root, canvas.style, {})
.setText("Start/Stop")
.setFixedSize(75, 25)
.listen("clicked", toggleGCA);
setupWidgetTooltip(widget:button, tooltip: "toggle GCA on/off");
myLayout.addItem(button);
}; # configureGCA();
References
|
PAR Screen Class
- 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)