Talk:Spoken GCA

From FlightGear wiki
Jump to navigation Jump to search

General considerations

Concerning realism

In the real world, not all airports offer GCA/PAR service, even less for every runway.
That's true, but I do not know any aircraft ITRW that includes a "Pause" key.

So, it is not a sin to enable this service in the FGFS world. The point is how.
ITRW pilot must tune a specific freq to request GCA. It seems reasonable to enable the apt's App (or TWR) freq for this purpose.

UI dialog is a wonderful tool for editing GCA parameters. Especially thinking when called by other client (MP, Radar Controller, etc).
But each client has its own point of view. And from the pilot's point of view, to edit Airport or SafetlySlope fields has no sense. (since Airport was selected according Comm, and SafetlySlope depends on terrain).

So, my proposal is to allow each client to "mask" certain fields (making them as hidden or at least not-editable)

I love to take a look to PAR screen for debugging/testing purposes. But, once passed that stage, I fell is almost cheating to steer my aircraft with one eye on the radar screen.

Must PARScreen be shown by default to the pilot ? May be a "show-parscreen" boolean prop must be considered?

To be honest, I was primarily thinking about using this for testing/troubleshooting (debugging) the script. Equally, the PAR screen is a great aid in conjunction with being able to tune parameters (e.g. the heuristics). I was even considering adding a preview to show the planned route vs. the actually flown one, and how the script responds to the pilot missing the target altitude/course. Equally, the computed wind vector could be shown. But like you say, this is a matter of realism. But from a troubleshooting/development perspective, I find these tools pretty useful. What you suggest does sound sensible, we could accomplish that by allowing a profile or "mode" to be selected (pilot/controller or developer view). Equally, local/MP mode could be toggled. Again, I was really thinking in terms of coming up with a generic component to help refine the lower level gca classes.--Hooray (talk) 16:52, 3 October 2017 (EDT)
Okay, thinking about it, I would add flags to the inputs vector and each hash in it specifying the modes in which to show a certain field - e.g. PILOT|CONTROLLER|DEVELOPER - and then provide a button to toggle these modes (or just use a property. To make this work, we would only need to edit the foreach loop where each field is set up to add a check for input.mode == "PILOT" (or use bitwise operators), something along the lines of:
var current_mode = getprop(addon_root, "mode", "controller");
foreach(var input; inputs) {
 if (input.mode == current_mode) {
 input.widget = setupLabeledInput(root, myLayout, input);
 } 
}

The current mode could be also set via a getprop() call

Actually, we could also use a tabbed interface - with 3 tabs: PILOT, CONTROLLER and DEVELOPER

--Hooray (talk) 18:10, 3 October 2017 (EDT)

Concerning UI response

For now, configureGCA() responds calling gcaObject's setters. We implicitly assume that the client has all those setters to receive that response.

What if, instead of that, configureGCA() returns a hash with values (think what airportinfo() returns).
So that, the client can pick the ones he needs. Control() func (that is the pilot) do not needs to pick neither Airport nor PositionRoot (nor SafetlySlope nor VerticalGrid , etc).
Conversely, a PAR Controller does.

Pros and cons of both criteria? I have no position taken on this, they are only doubts.

That is one of the reasons why I suggested to simply pass a handle of the gcaObject to the PARScreen class, e.g. by doing something like setGCAObject( gca ); - otherwise, you are right, if we begin adding better separation to the code, there will be more and more implicit assumptions. I was trying to keep things simple suggesting the above, which is akin to using C++ friend classes - not elegant, but working. --Hooray (talk) 16:48, 3 October 2017 (EDT)

Concerning AI structure

For now, the main logic resides into the props tree. It's simple and elegant for the script.
The price we pay: a lot of setprop()s ! Because condition props can compare only 'prop vs. prop' and 'prop vs. value'. (different would be if 'myVar vs value' could be compared !)

What if we translate that logic to something like:

var conditions = [<cond1>,<cond2>,. . . ,<cond13>];
foreach(var cond; conditions) if(cond) break;

I think the runtime would be much shorter. Again, I have no position taken on that.

Rodolfo (talk) 15:58, 3 October 2017 (EDT)

Thinking about it, it would also be possible to use one of the existing scripted PID controllers to implement the controller that, i.e. by only being able to issue altitude/heading and speed changes every 5 seconds and watching the aircraft respond to those - at least, that would take winds into account automatically. I don't know if there is a good way to encode these heuristics - after all, it's a FSM (finite state machine). I don't know how well this stuff is supported by the built-in property/autopilot systems. And I usually don't use these for dynamic systems where the number of components/controllers is not known upfront. Otherwise, if you don't ever want the script to be able to deal with more than one aircraft, you could just as well implement the whole logic in XML space using property rules and state machines. I may have mentioned that already, the most applicable work here may be galvedro's failure manager which contains a Nasal space state machine implementation: A Failure Management Framework for FlightGear --Hooray (talk) 16:56, 3 October 2017 (EDT)

Emulating a tabbed dialog

I am modifying the configureGCA() func in order to emulate a "tabbed" dialog, and need help about managing layers/items. I'm making a mistake and I do not realize what it is. For further detalails, see my post You can paste the following code into the Nasal Console and execute it. (edit the first line to point your own path)

io.load_nasal("/home/rodolfo/.fgfs/Nasal/gca/gca_class.nas", "gca");
var STATUS = {SUCCESS:0 , FAILURE:1};
var UIwindow = nil;
var fieldsLayer = nil;
var mask = nil;
var gcaObject = gca.GCAController.new(  );

var configureGCA = func( gcaObject=nil, defValues=nil, defTab=0 ) {
var defaults = {icao: 'KSFO' , rwy:'28R', safety_slope:0.0, channel:'/sim/messages/approach', interval:5.00};
if(typeof(defValues) == 'hash') {
	foreach(var key; keys(defValues)) {
		defaults[key] = defValues[key];
		}
}

var masks = [ {"Safety Slope":'',"Decision Height":'',"Position root":'' ,"Terrain Resolution":'',"Transmission interval":'',"Transmission channel":'' },
			  {"Safety Slope":''},
			  {} ];

var inputs = [
{text: 'Position root', default_value:'/position', focus:0, callback:gcaObject.setPosition, tooltip:'property path to position node', validate: 'AircraftRoot', convert:nil, unit: 'property path'},
{text: 'Airport', default_value:defaults.icao, focus:0, callback:gcaObject.setAirport, tooltip:'ICAO ID, e.g. KSFO', validate: 'Airport', convert:nil, unit:'ICAO'},
{text: 'Runway', default_value:defaults.rwy, focus:0, callback:gcaObject.setRunway, tooltip:'runway identifier, e.g. 28L', validate: 'Runway', convert:nil, unit:'rwy'},
{text: 'Touchdown Offset', default_value:'0.00', focus:0, callback:gcaObject.setTouchdownOffset, tooltip:'touchdown offset', validate: 'TouchdownOffset', convert:num, unit:'m'},
{text: 'Final Approach', default_value:'10.00', focus:0, callback:gcaObject.setFinalApproach, tooltip:'length of final approach leg', validate: 'FinalApproach', convert:num, unit:'nm'},
{text: 'Glidepath', default_value:'3.00', focus:0, callback:gcaObject.setGlidepath, tooltip:'glidepath in degrees, e.g. 3', validate: 'Glidepath', convert:num, unit:'degrees'},

{text: 'Safety Slope', default_value:defaults.safety_slope, focus:0, callback:gcaObject.setSafetySlope, tooltip:'safety slope in degrees', validate: 'SafetySlope', convert:num, unit:'degrees'},
{text: 'Decision Height', default_value:'200.00', focus:0, callback:gcaObject.setDecisionHeight, tooltip:'decision height (vertical offset)', validate: 'DecisionHeight', convert:num, unit:'ft'},
{text: 'Terrain Resolution', default_value:'0.10', focus:0, callback:gcaObject.setTerrainResolution, tooltip:'granularity/resolution of the terrain sampling', validate: 'TerrainResolution', convert:num, unit:'nm'},
{text: 'Horizontal Grid', default_value:'1.00', focus:0, callback:gcaObject.setHzGrid, tooltip:'horizontal grid resolution in Radar screen', validate: 'HzGrid', convert:num, unit:'nm/div'},
{text: 'Vertical Grid', default_value:'1000', focus:0, callback:gcaObject.setVertGrid, tooltip:'vertical grid resolution in Radar screen', validate: 'VertGrid', convert:num, unit:'feet/div'},

{text: 'Transmission channel', default_value:defaults.channel, focus:0, callback:gcaObject.setTransmissionChannel, tooltip:'property to use for transmissions. For example: /sim/multiplay/chat or /sim/sound/voices/approach', validate: 'TransmissionProperty', convert:nil, unit:'property'},
{text: 'Transmission interval', default_value:defaults.interval, focus:0, callback:gcaObject.setTransmissionInterval, tooltip:'Controller/timer resolution', validate: 'TransmissionInterval', convert:num, unit:'secs'},
# Warning: 'Transmission interval' must be the last one, since it launches the timer !!
]; # input fields

var tab = 0;
var createButton = func(root, label, size, clickAction) {
	var button = canvas.gui.widgets.Button.new(root, canvas.style, {} )
        .setText(label)
		.setFixedSize(size[0], size[1]);
	button.listen("clicked", clickAction );
	return button;
} # createButton

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

var setLabeledInput = 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);

	var field = canvas.gui.widgets.LineEdit.new(root, canvas.style, {});
	field.setText(sprintf(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);
	
	});
    var m = {label:label, field:field, text:input.text};
	return m ; # return to caller
} # setLabeledInput()

var removeFields = func() {
	if(mask==nil) return;
	mask = masks[currentTab];
	foreach(hash;fields){
	  if(!contains(mask, hash.text)){
	    printf("removeItem(%s)",hash.text);
	 fieldsLayer.removeItem(hash.label);
	 fieldsLayer.removeItem(hash.field);
	 }
	}
} # removeFields

var placeFields = func(tab) {
	#~ fieldsLayer.clear(); # ??
	#~ fieldsLayer.removeAllItems(); # ??
	removeFields();

	mask = masks[tab];
printf("81) tab=%i, %i fields.",tab,size(fields)-size(mask));
debug.dump(mask);

	foreach(hash;fields) {
	  if(!contains(mask, hash.text)){
	    printf("addItem(%s)",hash.text);
		fieldsLayer.addItem(hash.label);
		fieldsLayer.addItem(hash.field);
		}
		}
	currentTab = tab;
} # placeFields

var validationHelpers = {
_internalState: {},

'AircraftRoot': func(input) {
var root = props.getNode(input);
if (root == nil) return STATUS.FAILURE; # error

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

foreach(var rp; required_props) {
 if (getprop(input ~"/" ~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;
validationHelpers._internalState.rwy = input; 

return STATUS.SUCCESS;
},

'FinalApproach': func(input) {
if (input <3) return STATUS.FAILURE;
validationHelpers._internalState.final = input; 
return STATUS.SUCCESS;
},

'Glidepath': func(input) {
if (input <1 or input>180) return STATUS.FAILURE;
return STATUS.SUCCESS;
},

'SafetySlope': func(input) {
if (input <0 or input>180) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

'TerrainResolution': func(input) {
if (input <0 or input>10) return STATUS.FAILURE;
return STATUS.SUCCESS;
},

'DecisionHeight': func(input) {
if (input <0) return STATUS.FAILURE;
return STATUS.SUCCESS;
},

'TouchdownOffset': func(input) {
if (input <0) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

'TransmissionInterval': func(input) {
if (input <0 ) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

'TransmissionProperty': func(input) {
if (getprop(input) == nil) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

'VertGrid': func(input) {
if (input <100 or input >4000) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

'HzGrid': func(input) {
if (input <0.1 or input >10) return STATUS.FAILURE;
return STATUS.SUCCESS;
}, 

}; # validationHelpers;


var (width,height) = (250,850);
var title = 'GCA Dialog ';
UIwindow = canvas.Window.new([width,height],"dialog").set('title',title); 

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = UIwindow.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();
myCanvas.setLayout(myLayout);

# create tabsLayer:
var tabsLayer = canvas.HBoxLayout.new();
myLayout.addItem(tabsLayer);
 
# create fieldsLayer:
var myFields = canvas.HBoxLayout.new();
myLayout.addItem(myFields);
fieldsLayer = canvas.VBoxLayout.new();
myFields.addItem(fieldsLayer);
 
# create buttonsLayer:
var buttonsLayer = canvas.HBoxLayout.new();
myLayout.addItem(buttonsLayer);
 
# create field widgets:
var fields = [];
foreach(input;inputs) {
	var hash = setLabeledInput(root, fieldsLayer, input);
	append(fields, hash);
	input.widget = hash.field;
}

# place buttons:
var pilot_btn = createButton(root,"Pilot",[75,25],func { placeFields(0);});
var controller_btn = createButton(root,"Controller",[75,25],func { placeFields(1);});
var debug_btn = createButton(root,"Debug",[75,25],func { placeFields(2);});

tabsLayer.addItem(pilot_btn);
tabsLayer.addItem(controller_btn);
tabsLayer.addItem(debug_btn);
	if(defTab==0) 	pilot_btn.setFocus();
	if(defTab==1) 	controller_btn.setFocus();
	if(defTab==2) 	debug_btn.setFocus();

var start_btn = createButton(root,"Start/Stop",[75,25],func {print("Start clicked:");});
var apply_btn = createButton(root,"Apply",[55,25],func {print("Apply clicked:");});
buttonsLayer.addItem(apply_btn);
buttonsLayer.addItem(start_btn);

var currentTab = defTab;
placeFields(currentTab);

} # configureGCA()
configureGCA(gcaObject, nil, defTab=2) ;