f83caf7e45
In conjunction with some FlightGear changes, the delegate can now do all sequencing decisions, i.e the GPS C++ code does not make any policy decisions about sequencing. Update the default Nasal delegate to mimic the old behaviour, and set the config option, both to allow future developments and to demonstrate the new API.
283 lines
9.6 KiB
Text
283 lines
9.6 KiB
Text
# route_manager.nas - FlightPlan delegate(s) corresponding to the built-
|
|
# in route-manager dialog and GPS. Intended to provide a sensible default behaviour,
|
|
# but can be disabled by an aircraft-specific FMS / GPS system.
|
|
|
|
var RouteManagerDelegate = {
|
|
new: func(fp) {
|
|
# if this property is set, don't build a delegate at all
|
|
if (getprop('/autopilot/route-manager/disable-route-manager'))
|
|
return nil;
|
|
|
|
var m = { parents: [RouteManagerDelegate] };
|
|
m.flightplan = fp;
|
|
return m;
|
|
},
|
|
|
|
departureChanged: func
|
|
{
|
|
printlog('info', 'saw departure changed');
|
|
me.flightplan.clearWPType('sid');
|
|
if (me.flightplan.departure == nil)
|
|
return;
|
|
|
|
if (me.flightplan.departure_runway == nil) {
|
|
# no runway, only an airport, use that
|
|
var wp = createWPFrom(me.flightplan.departure);
|
|
wp.wp_role = 'sid';
|
|
me.flightplan.insertWP(wp, 0);
|
|
return;
|
|
}
|
|
# first, insert the runway itself
|
|
var wp = createWPFrom(me.flightplan.departure_runway);
|
|
wp.wp_role = 'sid';
|
|
me.flightplan.insertWP(wp, 0);
|
|
if (me.flightplan.sid == nil)
|
|
return;
|
|
|
|
# and we have a SID
|
|
var sid = me.flightplan.sid;
|
|
printlog('info', 'routing via SID ' ~ sid.id);
|
|
me.flightplan.insertWaypoints(sid.route(me.flightplan.departure_runway), 1);
|
|
},
|
|
|
|
arrivalChanged: func
|
|
{
|
|
me.flightplan.clearWPType('star');
|
|
me.flightplan.clearWPType('approach');
|
|
if (me.flightplan.destination == nil)
|
|
return;
|
|
|
|
if (me.flightplan.destination_runway == nil) {
|
|
# no runway, only an airport, use that
|
|
var wp = createWPFrom(me.flightplan.destination);
|
|
wp.wp_role = 'approach';
|
|
me.flightplan.appendWP(wp);
|
|
return;
|
|
}
|
|
|
|
var initialApproachFix = nil;
|
|
if (me.flightplan.star != nil) {
|
|
printlog('info', 'routing via STAR ' ~ me.flightplan.star.id);
|
|
var wps = me.flightplan.star.route(me.flightplan.destination_runway);
|
|
if (wps != nil) {
|
|
me.flightplan.insertWaypoints(wps, -1);
|
|
initialApproachFix = wps[-1]; # final waypoint of STAR
|
|
}
|
|
}
|
|
|
|
if (me.flightplan.approach != nil) {
|
|
var wps = me.flightplan.approach.route(initialApproachFix);
|
|
|
|
if ((initialApproachFix != nil) and (wps == nil)) {
|
|
# current GUI allows selected approach then STAR; but STAR
|
|
# might not be possible for the approach (no transition).
|
|
# since fixing the GUI flow is hard, let's route assuming no
|
|
# IAF. This will likely cause an ugly direct leg, but that's
|
|
# what the user asked for.
|
|
|
|
printlog('info', "couldn't route approach based on specified IAF "
|
|
~ initialApproachFix.wp_name);
|
|
wps = me.flightplan.approach.route(nil);
|
|
}
|
|
|
|
if (wps == nil) {
|
|
printlog('warn', 'routing via approach ' ~ me.flightplan.approach.id
|
|
~ ' failed entirely.');
|
|
} else {
|
|
printlog('info', 'routing via approach ' ~ me.flightplan.approach.id);
|
|
me.flightplan.insertWaypoints(wps, -1);
|
|
}
|
|
} else {
|
|
printlog('info', 'routing direct to runway ' ~ me.flightplan.destination_runway.id);
|
|
# no approach, just use the runway waypoint
|
|
var wp = createWPFrom(me.flightplan.destination_runway);
|
|
wp.wp_role = 'approach';
|
|
me.flightplan.appendWP(wp);
|
|
}
|
|
},
|
|
|
|
cleared: func
|
|
{
|
|
printlog('info', "saw active flightplan cleared, deactivating");
|
|
# see http://https://code.google.com/p/flightgear-bugs/issues/detail?id=885
|
|
fgcommand("activate-flightplan", props.Node.new({"activate": 0}));
|
|
},
|
|
|
|
endOfFlightPlan: func
|
|
{
|
|
printlog('info', "end of flight-plan, deactivating");
|
|
fgcommand("activate-flightplan", props.Node.new({"activate": 0}));
|
|
}
|
|
};
|
|
|
|
var GPSPath = "/instrumentation/gps";
|
|
|
|
# this delegate corresponds to the default behaviour of the built-in GPS.
|
|
# depending on the real GPS/FMS you are modelling, you probably need to
|
|
# replace this with your own.
|
|
#
|
|
# To do that, just set /autopilot/route-manager/disable-fms to true, which
|
|
# will block creation of this delegate.
|
|
#
|
|
# Of course you are then responsible for many basic FMS functions, such as
|
|
# route sequencing and activation
|
|
#
|
|
|
|
var DefaultGPSDeleagte = {
|
|
new: func(fp) {
|
|
# if this property is set, don't build a delegate at all
|
|
if (getprop('/autopilot/route-manager/disable-fms'))
|
|
return nil;
|
|
|
|
var m = { parents: [DefaultGPSDeleagte], flightplan:fp, landingCheck:nil };
|
|
|
|
printlog('info', 'creating default GPS FPDelegate');
|
|
|
|
# tell the GPS C++ code we will do sequencing ourselves, so it can disable
|
|
# its legacy logic for this
|
|
setprop(GPSPath ~ '/config/delegate-sequencing', 1);
|
|
|
|
# make FlightPlan behaviour match GPS config state
|
|
fp.followLegTrackToFix = getprop(GPSPath ~ '/config/follow-leg-track-to-fix') or 0;
|
|
|
|
# similarly, make FlightPlan follow the performance category settings
|
|
fp.aircraftCategory = getprop('/autopilot/settings/icao-aircraft-category') or 'A';
|
|
|
|
m._modeProp = props.globals.getNode(GPSPath ~ '/mode');
|
|
return m;
|
|
},
|
|
|
|
_landingCheckTimeout: func
|
|
{
|
|
var wow = getprop('gear/gear[0]/wow');
|
|
var gs = getprop('velocities/groundspeed-kt');
|
|
if (wow and (gs < 25)) {
|
|
printlog('info', 'GPS saw speed < 25kts on destination runway, end of route.');
|
|
me.landingCheck.stop();
|
|
# record touch-down time?
|
|
me.flightplan.finish();
|
|
}
|
|
},
|
|
|
|
_selectOBSMode: func
|
|
{
|
|
setprop(GPSPath ~ "/command", "obs");
|
|
},
|
|
|
|
waypointsChanged: func
|
|
{
|
|
},
|
|
|
|
activated: func
|
|
{
|
|
if (!me.flightplan.active)
|
|
return;
|
|
|
|
printlog('info', 'flightplan activated, default GPS to LEG mode');
|
|
setprop(GPSPath ~ "/command", "leg");
|
|
|
|
if (getprop(GPSPath ~ '/wp/wp[1]/from-flag')) {
|
|
printlog('info', 'at GPS activation, already passed active WP, sequencing');
|
|
me.sequence();
|
|
}
|
|
},
|
|
|
|
deactivated: func
|
|
{
|
|
if (me._modeProp.getValue() == 'leg') {
|
|
printlog('info', 'flightplan deactivated, default GPS to OBS mode');
|
|
me._selectOBSMode();
|
|
}
|
|
},
|
|
|
|
endOfFlightPlan: func
|
|
{
|
|
if (me._modeProp.getValue() == 'leg') {
|
|
printlog('info', 'end of flight-plan, switching GPS to OBS mode');
|
|
me._selectOBSMode();
|
|
}
|
|
},
|
|
|
|
cleared: func
|
|
{
|
|
if (!me.flightplan.active)
|
|
return;
|
|
|
|
if (me._modeProp.getValue() == 'leg') {
|
|
printlog('info', 'flight-plan cleared, switching GPS to OBS mode');
|
|
me._selectOBSMode();
|
|
}
|
|
},
|
|
|
|
sequence: func
|
|
{
|
|
if (!me.flightplan.active)
|
|
return;
|
|
|
|
var mode = me._modeProp.getValue();
|
|
if (mode == 'dto') {
|
|
# direct-to is done, check if we should resume the following leg
|
|
var index = me.flightplan.indexOfWP(getprop(GPSPath ~ '/wp/wp[1]/latitude-deg'),
|
|
getprop(GPSPath ~ '/wp/wp[1]/longitude-deg'));
|
|
if (index >= 0) {
|
|
printlog("info", "default GPS reached Direct-To, resuming FP leg at " ~ index);
|
|
me.flightplan.current = index + 1;
|
|
setprop(GPSPath ~ "/command", "leg");
|
|
} else {
|
|
# revert to OBS mode
|
|
printlog("info", "default GPS reached Direct-To, resuming to OSB");
|
|
me._selectOBSMode();
|
|
}
|
|
} else if (mode == 'leg') {
|
|
# standard leq sequencing
|
|
var nextIndex = me.flightplan.current + 1;
|
|
if (nextIndex >= me.flightplan.numWaypoints()) {
|
|
printlog("info", "default GPS sequencing, finishing flightplan");
|
|
me.flightplan.finish();
|
|
} else {
|
|
printlog("info", "default GPS sequencing to next WP");
|
|
me.flightplan.current = nextIndex;
|
|
}
|
|
} else {
|
|
# OBS, do nothing
|
|
}
|
|
},
|
|
|
|
currentWaypointChanged: func
|
|
{
|
|
if (!me.flightplan.active)
|
|
return;
|
|
|
|
if (me.landingCheck != nil) {
|
|
me.landingCheck.stop();
|
|
me.landingCheck = nil; # delete timer
|
|
}
|
|
|
|
#printlog('info', 'saw current WP changed, now ' ~ me.flightplan.current);
|
|
var active = me.flightplan.currentWP();
|
|
if (active == nil) return;
|
|
|
|
if (active.alt_cstr_type == "at") {
|
|
printlog('info', 'new WP has valid altitude restriction, setting on AP');
|
|
setprop('/autopilot/settings/target-altitude-ft', active.alt_cstr);
|
|
}
|
|
|
|
var activeRunway = active.runway();
|
|
# this check is needed to avoid problems with circular routes; when
|
|
# activating the FP we end up here, and without this check, immediately
|
|
# detect that we've 'landed' and finish the FP again.
|
|
var wow = getprop('gear/gear[0]/wow');
|
|
|
|
if (!wow and
|
|
(activeRunway != nil) and
|
|
(activeRunway.id == me.flightplan.destination_runway.id))
|
|
{
|
|
me.landingCheck = maketimer(2.0, me, DefaultGPSDeleagte._landingCheckTimeout);
|
|
me.landingCheck.start();
|
|
}
|
|
}
|
|
};
|
|
|
|
registerFlightPlanDelegate(DefaultGPSDeleagte.new);
|
|
registerFlightPlanDelegate(RouteManagerDelegate.new);
|
|
|