911 lines
29 KiB
Text
911 lines
29 KiB
Text
#
|
|
# Flight Control System for Helicopters by Tatsuhiro Nishioka
|
|
# $Id$
|
|
#
|
|
|
|
var enableDebug = func() {
|
|
setprop("/controls/flight/fcs/switches/debug", 1);
|
|
}
|
|
|
|
var debugEnabled = func() {
|
|
var debugStatus = getprop("/controls/flight/fcs/switches/debug");
|
|
if (debugStatus == 1) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
var dumpParameters = func() {
|
|
debug.dump(props.globals.getNode("/controls/flight/fcs/gains").getValues());
|
|
}
|
|
|
|
#
|
|
# FCSFilter - base class for FCS components like CAS and SAS
|
|
#
|
|
var FCSFilter = {
|
|
#
|
|
# new - constructor
|
|
# input_path: a property path for a filter input
|
|
# nil is equivalent to "/controls/flight/"
|
|
# output_path: a property path for a filter output
|
|
#
|
|
new : func(input_path, output_path) {
|
|
var obj = { parents : [FCSFilter],
|
|
input_path : input_path,
|
|
output_path : output_path };
|
|
obj.axis_conv = {'roll' : 'aileron', 'pitch' : 'elevator', 'yaw' : 'rudder' };
|
|
obj.body_conv = {'roll' : 'v', 'pitch' : 'u' };
|
|
obj.last_body_fps = {'roll' : 0.0, 'pitch' : 0.0 };
|
|
obj.last_pos = {'roll' : 0.0, 'pitch' : 0.0, 'yaw' : 0.0};
|
|
return obj;
|
|
},
|
|
|
|
#
|
|
# updateSensitivities: read sensitivitiy values for all axis from the property
|
|
#
|
|
updateSensitivities : func() {
|
|
me.sensitivities = props.globals.getNode("/controls/flight/fcs/gains/sensitivities").getValues();
|
|
},
|
|
|
|
#
|
|
# read - gets input command for a given axis from input_path
|
|
#
|
|
read : func(axis) {
|
|
if (me.input_path == nil or me.input_path == "") {
|
|
return getprop("/controls/flight/" ~ me.axis_conv[axis]);
|
|
} else {
|
|
var value = getprop(me.input_path ~ "/" ~ axis);
|
|
value = int(value * 1000) / 1000.0;
|
|
}
|
|
},
|
|
|
|
#
|
|
# write - outputs command for a given axis into output_path
|
|
# this will be the output of an next command filter (like SAS)
|
|
#
|
|
write : func(axis, value) {
|
|
if (me.output_path == nil or me.output_path == '') {
|
|
setprop("/controls/flight/fcs/" ~ axis, me.limit(value, 1.0));
|
|
} else {
|
|
setprop(me.output_path ~ "/" ~ axis, me.limit(value, 1.0));
|
|
}
|
|
},
|
|
|
|
#
|
|
# toggleFilterStatus - toggles engage/disengage FCS function
|
|
# name: FCS filter name; one of /controls/flight/fcs/switches/*
|
|
#
|
|
toggleFilterStatus : func(name) {
|
|
var messages = ["disengaged", "engaged"];
|
|
var path = "/controls/flight/fcs/switches/" ~ name;
|
|
var status = getprop(path);
|
|
setprop(path, 1 - status);
|
|
screen.log.write(name ~ " " ~ messages[1 - status]);
|
|
},
|
|
|
|
#
|
|
# getStatus - returns 1 if a given function is engaged
|
|
# name: FCS filter name; one of /controls/flight/fcs/switches/*
|
|
#
|
|
getStatus : func(name) {
|
|
var path = "/controls/flight/fcs/switches/" ~ name;
|
|
return getprop(path);
|
|
},
|
|
|
|
#
|
|
# limit - cut out a given value between +range to -range
|
|
# value: number to be adjusted
|
|
# range: absolute number for specifying the range
|
|
limit : func(value, range) {
|
|
if (value > range) {
|
|
return range;
|
|
} elsif (value < -range) {
|
|
return - range;
|
|
}
|
|
return value;
|
|
},
|
|
|
|
max : func(val1, val2) {
|
|
return (val1 > val2) ? val1 : val2;
|
|
},
|
|
|
|
min : func(val1, val2) {
|
|
return (val1 > val2) ? val2 : val1;
|
|
},
|
|
|
|
#
|
|
# calcCounterBodyFPS - calculates counter-force command to kill movement in each axis
|
|
# axis: one of 'roll', 'pitch', or 'yaw'
|
|
# input: input (0.0 - 1.0) for a given axis
|
|
# offset_deg:
|
|
#
|
|
calcCounterBodyFPS : func(axis, input, offset_deg) {
|
|
var position = getprop("/orientation/" ~ axis ~ "-deg");
|
|
var body_fps = 0;
|
|
var last_body_fps = me.last_body_fps[axis];
|
|
var reaction_gain = 0;
|
|
var heading = getprop("/orientation/heading-deg");
|
|
var wind_speed_fps = getprop("/environment/wind-speed-kt") * 1.6878099;
|
|
var wind_direction = getprop("/environment/wind-from-heading-deg");
|
|
var wind_direction -= heading;
|
|
var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
|
|
var gear_pos = getprop("/gear/gear[0]/compression-norm") + getprop("/gear/gear[1]/compression-norm");
|
|
var counter_fps = 0;
|
|
var fps_axis = me.body_conv[axis]; # convert from {roll, pitch} to {u, v}
|
|
var target_pos = offset_deg;
|
|
var brake_deg = 0;
|
|
|
|
body_fps = getprop("/velocities/" ~ fps_axis ~ "Body-fps");
|
|
if (axis == 'roll') {
|
|
var wind_fps = math.sin(wind_direction / 180 * math.pi) * wind_speed_fps;
|
|
} else {
|
|
var wind_fps = math.cos(wind_direction / 180 * math.pi) * wind_speed_fps;
|
|
}
|
|
var brake_freq = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-brake-freq");
|
|
var brake_gain = getprop("/controls/flight/fcs/gains/afcs/fps-brake-gain-" ~ axis);
|
|
body_fps -= wind_fps;
|
|
var dfps = body_fps - me.last_body_fps[axis];
|
|
var fps_coeff = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-coeff");
|
|
target_pos -= int(body_fps * 100) / 100 * fps_coeff;
|
|
if (axis == 'roll' and gear_pos > 0.0 and position > 0) {
|
|
target_pos -= position * gear_pos / 5;
|
|
}
|
|
reaction_gain = getprop("/controls/flight/fcs/gains/afcs/fps-reaction-gain-" ~ axis);
|
|
var brake_sensitivity = (axis == 'roll') ? 1 : 1;
|
|
if (math.abs(position + rate / brake_freq * brake_sensitivity) > math.abs(target_pos)) {
|
|
if (math.abs(dfps) > 1) {
|
|
dfps = 1;
|
|
}
|
|
var error_deg = target_pos - position;
|
|
brake_deg = (error_deg - rate / brake_freq) * math.abs(dfps * 10) * brake_gain;
|
|
if (target_pos > 0) {
|
|
brake_deg = me.min(brake_deg, 0);
|
|
} else {
|
|
brake_deg = me.max(brake_deg, 0);
|
|
}
|
|
}
|
|
counter_fps = me.limit((target_pos + brake_deg) * reaction_gain, 1.0);
|
|
if (debugEnabled() == 1) {
|
|
setprop("/controls/flight/fcs/afcs/status/ah-" ~ fps_axis ~ "body-fps", body_fps);
|
|
setprop("/controls/flight/fcs/afcs/status/ah-" ~ fps_axis ~ "body-wind-fps", wind_fps);
|
|
setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-target-deg", target_pos);
|
|
setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-rate", rate);
|
|
setprop("/controls/flight/fcs/afcs/status/ah-delta-" ~ fps_axis ~ "body-fps", dfps);
|
|
setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-brake-deg", brake_deg);
|
|
setprop("/controls/flight/fcs/afcs/status/counter-fps-" ~ axis, counter_fps);
|
|
}
|
|
|
|
me.last_pos[axis] = position;
|
|
me.last_body_fps[axis] = body_fps;
|
|
return me.limit(counter_fps + input * 0.2, 1.0);
|
|
},
|
|
|
|
};
|
|
|
|
#
|
|
# AFCS - Automatic Flight Control System
|
|
#
|
|
var AFCS = {
|
|
new : func(input_path, output_path) {
|
|
var obj = FCSFilter.new(input_path, output_path);
|
|
obj.parents = [FCSFilter, AFCS];
|
|
return obj;
|
|
},
|
|
|
|
#
|
|
# toggle* - I/F methods for Instruments
|
|
#
|
|
toggleAutoHover : func() {
|
|
me.toggleFilterStatus("auto-hover");
|
|
},
|
|
|
|
toggleAirSpeedLock : func() {
|
|
me.toggleFilterStatus("air-speed-lock");
|
|
},
|
|
|
|
toggleHeadingLock : func() {
|
|
me.toggleFilterStatus("heading-lock");
|
|
},
|
|
|
|
toggleAltitudeLock : func() {
|
|
me.toggleFilterStatus("altitude-lock");
|
|
},
|
|
|
|
#
|
|
# auto hover - locks vBody_fps and uBody_fps regardless of wind speed/direction
|
|
#
|
|
autoHover : func(axis, input) {
|
|
if (axis == 'yaw') {
|
|
return input;
|
|
} else {
|
|
var offset_deg = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-offset-deg");
|
|
return me.calcCounterBodyFPS(axis, input, offset_deg);
|
|
}
|
|
},
|
|
|
|
altitudeLock : func(axis, input) {
|
|
# not implemented yet
|
|
return input;
|
|
},
|
|
|
|
headingLock : func(axis, input) {
|
|
# not implementet yet
|
|
return input;
|
|
},
|
|
|
|
#
|
|
# applying all AFCS functions
|
|
# only auto hover is available at this moment
|
|
#
|
|
apply : func(axis) {
|
|
var input = me.read(axis);
|
|
var hover_status = me.getStatus("auto-hover");
|
|
if (hover_status == 0) {
|
|
me.write(axis, input);
|
|
return;
|
|
}
|
|
me.write(axis, me.autoHover(axis, input));
|
|
}
|
|
};
|
|
|
|
#
|
|
# SAS : Stability Augmentation System - a rate damper
|
|
#
|
|
var SAS = {
|
|
#
|
|
# new
|
|
# input_path: is a base path to input axis; nil for using raw input from KB/JS
|
|
# output_path: is a base path to output axis; nis for using /controls/flight/fcs
|
|
# with input_path / output_path, you can connect SAS, CAS, and more control filters
|
|
#
|
|
new : func(input_path, output_path) {
|
|
var obj = FCSFilter.new(input_path, output_path);
|
|
obj.parents = [FCSFilter, SAS];
|
|
return obj;
|
|
},
|
|
|
|
toggleEnable : func() {
|
|
me.toggleFilterStatus("sas");
|
|
},
|
|
|
|
#
|
|
# calcGain - get gain for each axis based on air speed and dynamic pressure
|
|
# axis: one of 'roll', 'pitch', or 'yaw'
|
|
#
|
|
calcGain : func(axis) {
|
|
var mach = getprop("/velocities/mach");
|
|
var initial_gain = getprop("/controls/flight/fcs/gains/sas/" ~ axis);
|
|
var gain = initial_gain - 0.1 * mach * mach;
|
|
if (math.abs(gain) < math.abs(initial_gain) * 0.01 or gain * initial_gain < 0) {
|
|
gain = initial_gain * 0.01;
|
|
}
|
|
return gain;
|
|
},
|
|
|
|
#
|
|
# calcAuthorityLimit - returns SAS authority limit using a given limit and mach number
|
|
#
|
|
calcAuthorityLimit : func() {
|
|
var mach = getprop("/velocities/mach");
|
|
var min_mach = 0.038;
|
|
me.authority_limit = getprop("/controls/flight/fcs/gains/sas/authority-limit");
|
|
var limit = me.authority_limit;
|
|
if (math.abs(mach < min_mach)) {
|
|
limit += (min_mach - math.abs(mach)) / min_mach * (1 - me.authority_limit) * 0.95;
|
|
}
|
|
if (debugEnabled() == 1) {
|
|
setprop("/controls/flight/fcs/sas/status/authority-limit", limit);
|
|
}
|
|
return limit;
|
|
},
|
|
|
|
#
|
|
# apply - apply SAS damper to a given input axis
|
|
# axis: one of 'roll', 'pitch', or 'yaw'
|
|
#
|
|
apply : func(axis) {
|
|
me.updateSensitivities();
|
|
var status = me.getStatus("sas");
|
|
var input = me.read(axis);
|
|
if (status == 0) {
|
|
me.write(axis, input);
|
|
return;
|
|
}
|
|
var mach = getprop("/velocities/mach");
|
|
var value = 0;
|
|
var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
|
|
var gain = me.calcGain(axis);
|
|
var limit = me.calcAuthorityLimit();
|
|
if (math.abs(rate) >= me.sensitivities[axis]) {
|
|
value = - gain * rate;
|
|
if (value > limit) {
|
|
value = limit;
|
|
} elsif (value < - limit) {
|
|
value = - limit;
|
|
}
|
|
}
|
|
me.write(axis, value + input);
|
|
}
|
|
};
|
|
|
|
#
|
|
# CAS : Control Augmentation System - makes your aircraft more meneuverable
|
|
#
|
|
var CAS = {
|
|
new : func(input_path, output_path) {
|
|
var obj = FCSFilter.new(input_path, output_path);
|
|
obj.parents = [FCSFilter, CAS];
|
|
setprop("/autopilot/locks/altitude", '');
|
|
setprop("/autopilot/locks/heading", '');
|
|
obj.setCASControlThresholds();
|
|
|
|
return obj;
|
|
},
|
|
|
|
calcRollRateAdjustment : func {
|
|
var position = getprop("/orientation/roll-deg");
|
|
return math.abs(math.sin(position / 180 * math.pi)) / 6;
|
|
},
|
|
|
|
#
|
|
# calcHeadingAdjustment - returns roll axis output for stabilizing heading
|
|
#
|
|
calcHeadingAdjustment : func {
|
|
if (getprop("/controls/flight/fcs/switches/heading-adjuster") == 1) {
|
|
var gain = getprop("/controls/flight/fcs/gains/cas/output/heading-adjuster-gain");
|
|
var yaw_rate = getprop("/orientation/yaw-rate-degps");
|
|
var limit = getprop("/controls/flight/fcs/gains/cas/output/heading-adjuster-limit");
|
|
var adjuster = yaw_rate * gain;
|
|
return me.limit(adjuster, limit);
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
#
|
|
# calcSideSlipAdjustment - returns yaw axis output for preventing side slip
|
|
#
|
|
calcSideSlipAdjustment : func {
|
|
if (getprop("/controls/flight/fcs/switches/sideslip-adjuster") == 0) {
|
|
return 0;
|
|
}
|
|
var mach = getprop("/velocities/mach");
|
|
var slip = -getprop("/orientation/side-slip-deg"); # inverted after a change in side-slip sign (bug #901)
|
|
var min_speed_threshold = getprop("/controls/flight/fcs/gains/cas/input/anti-side-slip-min-speed");
|
|
if (mach < min_speed_threshold) { # works only if air speed > min_speed_threshold
|
|
slip = 0;
|
|
}
|
|
var anti_slip_gain = getprop("/controls/flight/fcs/gains/cas/output/anti-side-slip-gain");
|
|
var roll_deg = getprop("/orientation/roll-deg");
|
|
var gain_adjuster = me.min(math.abs(mach) / 0.060, 1) * me.limit(0.2 + math.sqrt(math.abs(roll_deg)/10), 3);
|
|
anti_slip_gain *= gain_adjuster;
|
|
if (debugEnabled() == 1) {
|
|
setprop("/controls/flight/fcs/cas/status/anti-side-slip", slip * anti_slip_gain);
|
|
}
|
|
return slip * anti_slip_gain;
|
|
},
|
|
|
|
#
|
|
# isInverted - returns 1 if aircraft is inverted (roll > 90 or roll < -90)
|
|
#
|
|
isInverted : func() {
|
|
var roll_deg = getprop("/orientation/roll-deg");
|
|
if (roll_deg > 90 or roll_deg < -90)
|
|
return 1;
|
|
else
|
|
return 0;
|
|
},
|
|
|
|
# FIXME: command for CAS is just a temporal one
|
|
#
|
|
# calcCommand - returns CAS output for each axis
|
|
#
|
|
calcCommand: func (axis, input) {
|
|
var output = 0;
|
|
var mach = getprop("/velocities/mach");
|
|
var input_gain = me.calcGain(axis);
|
|
var output_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis);
|
|
var target_rate = input * input_gain;
|
|
var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
|
|
var drate = target_rate - rate;
|
|
if (axis == 'pitch' and me.isInverted() == 1) {
|
|
drate = - drate;
|
|
}
|
|
var attitudeControlThreshold = getprop("/controls/flight/fcs/gains/cas/input/attitude-control-threshold");
|
|
var rateControlThreshold = getprop("/controls/flight/fcs/gains/cas/input/rate-control-threshold");
|
|
var locks = {'pitch' : getprop("/autopilot/locks/altitude"),
|
|
'roll' : getprop("/autopilot/locks/heading")};
|
|
setprop("/controls/flight/fcs/cas/target_" ~ axis ~ "rate", target_rate);
|
|
setprop("/controls/flight/fcs/cas/delta_" ~ axis, drate);
|
|
|
|
if (axis == 'roll' or axis == 'pitch') {
|
|
if (math.abs(input) > rateControlThreshold) {
|
|
return input;
|
|
} elsif (math.abs(input) > attitudeControlThreshold or locks[axis] != '') {
|
|
output = drate * output_gain;
|
|
} else {
|
|
output = me.calcAttitudeCommand(axis);
|
|
}
|
|
if (axis == 'roll' and math.abs(mach) < 0.035) {
|
|
# FIXME: I don't know if OH-1 has this one
|
|
output += me.calcCounterBodyFPS(axis, input, -0.8);
|
|
}
|
|
} elsif (axis == 'yaw') {
|
|
if (getprop("/controls/flight/fcs/switches/tail-rotor-adjuster") == 0) {
|
|
output = input;
|
|
} else {
|
|
output = drate * output_gain + me.calcSideSlipAdjustment();
|
|
}
|
|
} else {
|
|
output = drate * output_gain;
|
|
}
|
|
return output;
|
|
},
|
|
|
|
toggleEnable : func() {
|
|
me.toggleFilterStatus("cas");
|
|
},
|
|
|
|
#
|
|
# toggle enable / disable attitude control
|
|
# you can make similar function that changes parameters
|
|
# in attitude-control-limit and rate-control-limit
|
|
# at controls/flight/fcs/gains/cas/input
|
|
# CAS changes its behavior when roll/pitch axis inputs reaches each limit.
|
|
# e.g. when attitude-control-limit is 0.7 and rate-control-limit is 0.9,
|
|
# giving 0.6 for roll holds bank angle, 0.8 keeps roll rate,
|
|
# and 1.0 makes roll at maximum roll rate.
|
|
# Sets of initial values for these limits are stored at
|
|
# controls/fcs/gains/cas/{attitude,rate}
|
|
#
|
|
toggleAttitudeControl : func() {
|
|
me.toggleFilterStatus("attitude-control");
|
|
me.setCASControlThresholds();
|
|
},
|
|
|
|
setCASControlThresholds : func()
|
|
{
|
|
if (me.getStatus("attitude-control") == 1) {
|
|
var params = props.globals.getNode("controls/flight/fcs/gains/cas/control/attitude").getValues();
|
|
props.globals.getNode("controls/flight/fcs/gains/cas/input").setValues(params);
|
|
} else {
|
|
var params = props.globals.getNode("controls/flight/fcs/gains/cas/control/rate").getValues();
|
|
props.globals.getNode("controls/flight/fcs/gains/cas/input").setValues(params);
|
|
}
|
|
},
|
|
|
|
#
|
|
# calcAttitudeCommand - Attitude base Augmentation output for roll and pitch axis
|
|
# axis: either 'roll' or 'pitch'
|
|
#
|
|
calcAttitudeCommand : func(axis) {
|
|
var input_gain = getprop("/controls/flight/fcs/gains/cas/input/attitude-" ~ axis);
|
|
var output_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis);
|
|
var brake_freq = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis ~ "-brake-freq");
|
|
var brake_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis ~ "-brake");
|
|
var trim = getprop("/controls/flight/" ~ me.axis_conv[axis] ~ "-trim");
|
|
|
|
var current_deg = getprop("/orientation/" ~ axis ~ "-deg");
|
|
var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
|
|
var target_deg = (me.read(axis) + trim) * input_gain;
|
|
if (axis == 'roll' and math.abs(target_deg) < 0.1) {
|
|
# rolls a bit to counteract the heading changes only if target roll rate = 0
|
|
target_deg += me.calcHeadingAdjustment();
|
|
}
|
|
var command_deg = 0;
|
|
if (target_deg != 0) {
|
|
command_deg = (0.094 * math.ln(math.abs(target_deg)) + 0.53) * target_deg;
|
|
}
|
|
|
|
var error_deg = command_deg - current_deg;
|
|
if (axis == 'pitch' and me.isInverted() == 1) {
|
|
error_deg = - error_deg;
|
|
}
|
|
var brake_deg = (error_deg - rate / brake_freq) * math.abs(error_deg) * brake_gain;
|
|
|
|
if (command_deg > 0) {
|
|
brake_deg = me.min(brake_deg, 0);
|
|
} else {
|
|
brake_deg = me.max(brake_deg, 0);
|
|
}
|
|
|
|
if (debugEnabled() == 1) {
|
|
var monitor_prefix = me.output_path ~ "/status/" ~ axis;
|
|
setprop(monitor_prefix ~ "-target_deg", target_deg);
|
|
setprop(monitor_prefix ~ "-error_deg", error_deg);
|
|
setprop(monitor_prefix ~ "-brake_deg", brake_deg);
|
|
setprop(monitor_prefix ~ "-deg", current_deg);
|
|
setprop(monitor_prefix ~ "-rate", -rate);
|
|
}
|
|
|
|
return (error_deg + brake_deg) * output_gain;
|
|
},
|
|
|
|
#
|
|
# calcGain - returns gain for a given axis using a given gain and speed
|
|
# FixMe: gain should be calculated using both speed and dynamic pressure
|
|
#
|
|
calcGain : func(axis) {
|
|
var mach = getprop("/velocities/mach");
|
|
var input_gain = getprop("/controls/flight/fcs/gains/cas/input/" ~ axis);
|
|
var gain = input_gain;
|
|
if (axis == 'pitch') {
|
|
gain += 0.1 * mach * mach;
|
|
} elsif (axis== 'yaw') {
|
|
gain *= ((1 - mach) * (1 - mach));
|
|
}
|
|
if (gain * input_gain < 0.0 ) {
|
|
gain = 0;
|
|
}
|
|
if (debugEnabled() == 1) {
|
|
setprop("/controls/flight/fcs/cas/gain-" ~ axis, gain);
|
|
}
|
|
return gain;
|
|
},
|
|
|
|
#
|
|
# apply - public method that outputs CAS command for a given axis to output_path
|
|
# input is read from input_path
|
|
# axis: one of 'roll', 'pitch', or 'yaw'
|
|
#
|
|
apply : func(axis) {
|
|
me.updateSensitivities();
|
|
var input = me.read(axis);
|
|
var status = me.getStatus("cas");
|
|
var cas_command = 0;
|
|
# FIXME : hmm, a bit nasty. CAS should be enabled even with auto-hover....
|
|
if (status == 0 or (me.getStatus("auto-hover") == 1 and axis != 'yaw')) {
|
|
me.write(axis, input);
|
|
return;
|
|
}
|
|
cas_command = me.calcCommand(axis, input);
|
|
me.write(axis, cas_command);
|
|
}
|
|
};
|
|
|
|
#
|
|
# Tail hstab, "stabilator," for stabilize the nose
|
|
#
|
|
var Stabilator = {
|
|
new : func() {
|
|
var obj = { parents : [Stabilator] };
|
|
me.gainTable = props.globals.getNode("/controls/flight/fcs/gains/stabilator").getChildren('gain-table');
|
|
return obj;
|
|
},
|
|
|
|
toggleEnable : func {
|
|
var status = getprop("/controls/flight/fcs/switches/auto-stabilator");
|
|
getprop("/controls/flight/fcs/switches/auto-stabilator", 1 - status);
|
|
},
|
|
|
|
#
|
|
# calcPosition - returns stabilator position (output) depending on
|
|
# predefined gain table and mach number
|
|
#
|
|
calcPosition : func() {
|
|
var speed = getprop("/velocities/mach") / 0.001497219; # in knot
|
|
var index = int(math.abs(speed) / 10);
|
|
if (index >= size(me.gainTable) - 1) {
|
|
index = size(me.gainTable) - 2;
|
|
}
|
|
var gain = me.gainTable[index].getValue();
|
|
var gainAmb = me.gainTable[index-1].getValue();
|
|
var mod = math.mod(int(math.abs(speed)), 10);
|
|
var position = gain * ((10 - mod) / 10) + gainAmb * mod / 10;
|
|
if (speed < -20) {
|
|
position = - position;
|
|
}
|
|
return position;
|
|
},
|
|
|
|
#
|
|
# apply - public method for Stabilator control
|
|
# no axis is required since it is only for hstab
|
|
#
|
|
apply : func() {
|
|
var status = getprop("/controls/flight/fcs/switches/auto-stabilator");
|
|
if (status == 0) {
|
|
return;
|
|
}
|
|
var gain = getprop("/controls/flight/fcs/gains/stabilator/stabilator-gain");
|
|
var mach = getprop("/velocities/mach");
|
|
var throttle = getprop("/controls/flight/throttle");
|
|
var stabilator_norm = 0;
|
|
|
|
stabilator_norm = me.calcPosition();
|
|
setprop("/controls/flight/fcs/stabilator", stabilator_norm);
|
|
}
|
|
};
|
|
|
|
#
|
|
# Automatic tail rotor adjuster depending on collective/throttle status
|
|
#
|
|
var TailRotorCollective = {
|
|
new : func() {
|
|
var obj = FCSFilter.new("/controls/engines/engine[1]", "/controls/flight/fcs/tail-rotor");
|
|
obj.parents = [FCSFilter, TailRotorCollective];
|
|
obj.adjuster = 0.0;
|
|
return obj;
|
|
},
|
|
|
|
#
|
|
# apply - public method for tail rotor adjuster
|
|
# no axis is required
|
|
#
|
|
apply : func() {
|
|
var throttle = me.read("throttle");
|
|
var pedal_pos_deg = getprop("/controls/flight/fcs/yaw");
|
|
var cas_input = cas.read('yaw');
|
|
var cas_input_gain = cas.calcGain('yaw');
|
|
var target_rate = cas_input * cas_input_gain;
|
|
var rate = getprop("/orientation/yaw-rate-degps");
|
|
var error_rate = getprop("/controls/flight/fcs/cas/delta_yaw");
|
|
var error_adjuster_gain = getprop("/controls/flight/fcs/gains/tail-rotor/error-adjuster-gain");
|
|
|
|
var minimum = getprop("/controls/flight/fcs/gains/tail-rotor/src-minimum");
|
|
var maximum = getprop("/controls/flight/fcs/gains/tail-rotor/src-maximum");
|
|
var low_limit = getprop("/controls/flight/fcs/gains/tail-rotor/low-limit");
|
|
var high_limit = getprop("/controls/flight/fcs/gains/tail-rotor/high-limit");
|
|
var authority_limit = getprop("/controls/flight/fcs/gains/tail-rotor/authority-limit");
|
|
var output = 0;
|
|
var range = maximum - minimum;
|
|
|
|
if (throttle < minimum) {
|
|
output = low_limit;
|
|
} elsif (throttle > maximum) {
|
|
output = high_limit;
|
|
} else {
|
|
output = low_limit + (throttle - minimum) / range * (high_limit - low_limit);
|
|
}
|
|
|
|
# CAS driven tail rotor thrust adjuster
|
|
me.adjuster = error_rate * error_adjuster_gain;
|
|
me.adjuster = me.limit(me.adjuster, authority_limit);
|
|
output += me.adjuster;
|
|
|
|
setprop("/controls/flight/fcs/tail-rotor/error-rate", error_rate);
|
|
setprop("/controls/flight/fcs/tail-rotor/adjuster", me.adjuster);
|
|
|
|
me.write("throttle", output);
|
|
}
|
|
};
|
|
|
|
# Back-up FCS
|
|
# It automatically disable CAS and shifts to
|
|
# the backup mode (e.g. SAS only or direct link mode)
|
|
#
|
|
var BackupFCS = {
|
|
new : func() {
|
|
var obj = { parents : [BackupFCS] };
|
|
obj.switches = {'cas' : 0, 'sas' : 1, 'attitude-control' : 0 }; # default backup switches
|
|
obj.normalSwitches = props.globals.getNode("/controls/flight/fcs/switches").getValues();
|
|
setprop("/controls/flight/fcs/failures/manual-backup-mode", 0);
|
|
setprop("/controls/flight/fcs/switches/backup-mode", 0);
|
|
return obj;
|
|
},
|
|
|
|
# checkFCSFailures - detects FCS failures
|
|
# returns 1 if failure (or manual backup mode) is detected, 0 otherwise
|
|
#
|
|
checkFCSFailures : func()
|
|
{
|
|
# not fully implemented yet
|
|
if (getprop("/controls/flight/fcs/failures/manual-backup-mode") == 1) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
#
|
|
# shiftToBackupMode - overwrites switches for force entering backup mode
|
|
#
|
|
shiftToBackupMode : func() {
|
|
if (me.switches != nil) {
|
|
var switchNode = props.globals.getNode("/controls/flight/fcs/switches");
|
|
switchNode.setValues(me.switches);
|
|
setprop("/controls/flight/fcs/switches/backup-mode", 1);
|
|
}
|
|
},
|
|
|
|
#
|
|
# shiftToNormalMode - bring switches back to normal mode
|
|
# switches for normalMode are captured at BackupFCS.new
|
|
shiftToNormalMode : func() {
|
|
if (me.normalSwitches != nil) {
|
|
props.globals.getNode("/controls/flight/fcs/switches").setValues(me.normalSwitches);
|
|
setprop("/controls/flight/fcs/switches/backup-mode", 0);
|
|
}
|
|
},
|
|
|
|
#
|
|
# setBackupMode - specifies set of values on FCS switches
|
|
# switches: hash of FCS switch values that will be set to
|
|
# controls/flight/fcs/switches on backup mode
|
|
# only values to be overwritten must be specified
|
|
# e.g. {'cas' : 0, 'sas' : 1}
|
|
#
|
|
setBackupMode : func(switches) {
|
|
me.switches = switches
|
|
},
|
|
|
|
#
|
|
# update - main I/F for BackupFCS
|
|
#
|
|
update : func() {
|
|
if (me.checkFCSFailures() == 1) {
|
|
me.shiftToBackupMode();
|
|
} elsif (getprop("/controls/flight/fcs/switches/backup-mode") == 1) {
|
|
me.shiftToNormalMode();
|
|
}
|
|
},
|
|
|
|
#
|
|
# toggleBackupMode - I/F for Cockpit Panel
|
|
#
|
|
toggleBackupMode : func() {
|
|
var mode = getprop("/controls/flight/fcs/failures/manual-backup-mode");
|
|
setprop("/controls/flight/fcs/failures/manual-backup-mode", 1 - mode);
|
|
}
|
|
};
|
|
|
|
var sas = nil;
|
|
var cas = nil;
|
|
var afcs = nil;
|
|
var stabilator = nil;
|
|
var tail = nil;
|
|
var backup = nil;
|
|
var count = 0;
|
|
|
|
#
|
|
# AFCS main loop
|
|
# This runs at 60Hz (on every other update of /rotors/main/cone-deg)
|
|
#
|
|
var update = func {
|
|
count += 1;
|
|
# AFCS, CAS, and SAS run at 60Hz
|
|
rpm = getprop("/rotors/main/rpm");
|
|
# AFCS, CAS, and SAS run at 60Hz only when engine rpm >= 10
|
|
# this rpm filter prevents CAS/SAS work when engine is not running,
|
|
# which may cause Nasal runtime error
|
|
if (math.mod(count, 2) == 0 or rpm < 10) {
|
|
return;
|
|
}
|
|
|
|
cas.apply('roll');
|
|
cas.apply('pitch');
|
|
cas.apply('yaw');
|
|
|
|
afcs.apply('roll');
|
|
afcs.apply('pitch');
|
|
afcs.apply('yaw');
|
|
|
|
sas.apply('roll');
|
|
sas.apply('pitch');
|
|
sas.apply('yaw');
|
|
stabilator.apply();
|
|
tail.apply();
|
|
backup.update();
|
|
}
|
|
|
|
# Factory default configuration values
|
|
# DO NOT CHANGE THESE VALUES.!
|
|
# You can change some of these values in per-aircraft nasal file.
|
|
# See Aircraft/OH-1/Nasal/OH1.nas for more detail
|
|
#
|
|
var default_fcs_params = {
|
|
'gains' : {
|
|
'afcs' : {
|
|
# Auto Hover parameters
|
|
'fps-brake-gain-pitch' : 1.8,
|
|
'fps-brake-gain-roll' : 0.8,
|
|
'fps-pitch-brake-freq' : 3,
|
|
'fps-pitch-coeff' : -0.95,
|
|
'fps-pitch-offset-deg' : 0.9,
|
|
'fps-reaction-gain-pitch' : -0.8,
|
|
'fps-reaction-gain-roll' : 0.3436,
|
|
'fps-roll-brake-freq' : 8,
|
|
'fps-roll-coeff' : 0.8,
|
|
'fps-roll-offset-deg' : -0.8
|
|
},
|
|
'cas' : {
|
|
'input' : { # Input gains for CAS
|
|
'roll' : 30,
|
|
'pitch' : -60,
|
|
'yaw' : 30,
|
|
'attitude-roll' : 80,
|
|
'attitude-pitch' : -80,
|
|
'attitude-control-threshold' : 0.0, # input threshold that CAS changes attitude-base control to rate-base control
|
|
'rate-control-threshold' : 0.95, # input threshold that CAS changes rate-base control to doing nothing
|
|
'anti-side-slip-min-speed' : 0.015
|
|
},
|
|
'output' : { # Output gains for CAS
|
|
'roll' : 0.06,
|
|
'pitch' : -0.1,
|
|
'yaw' : 0.5,
|
|
'roll-brake-freq' : 10,
|
|
'pitch-brake-freq' : 3,
|
|
'roll-brake' : 0.4,
|
|
'pitch-brake' : 6,
|
|
'anti-side-slip-gain' : -4.5,
|
|
'heading-adjuster-gain' : -5,
|
|
'heading-adjuster-limit' : 5,
|
|
},
|
|
'control' : { # configuration for CAS augumentation modes
|
|
'attitude' : { # Attitude control augmentation mode (e.g. ATTDAGMT button on OH-1)
|
|
# Note: attitude-control-threshold must be smaller than rate-control-threshold in any mode
|
|
'attitude-control-threshold' : 0.95, # Roll / Pitch attitude(angle) hold mode when 0 < input <= 0.95
|
|
'rate-control-threshold' : 1.0 # Rate hold mode when 0.95 < input <= 1.0
|
|
},
|
|
'rate' : { # Rate control augmentation mode
|
|
'attitude-control-threshold' : 0.0,
|
|
'rate-control-threshold' : 0.95
|
|
},
|
|
}
|
|
},
|
|
'sas' : { # gains for SAS
|
|
'roll' : 0.02,
|
|
'pitch' : -0.10,
|
|
'yaw' : 0.04,
|
|
'authority-limit' : 0.15 # How much SAS will take over pilot's control. 0.15 means 15%
|
|
},
|
|
'sensitivities' : {
|
|
'roll' : 1.0,
|
|
'pitch' : 1.0,
|
|
'yaw' : 3.0
|
|
},
|
|
'tail-rotor' : { # parameters for tail rotor control based on throttle / collective
|
|
'src-minimum' : 0.10, # throttle value that outputs low-limit
|
|
'src-maximum' : 1.00, # throttle value that outputs high-limit
|
|
'low-limit' : 0.00011,
|
|
'high-limit' : 0.0035,
|
|
'error-adjuster-gain' : -0.5, # gain that how much CAS adjust yaw rate
|
|
'authority-limit' : 0.3
|
|
},
|
|
'stabilator' : { # gain tables for adjusting either incidence or flap angle of hstab
|
|
# index is the speed (Kt) devided by 10
|
|
# 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160, 170, 180, .....
|
|
'gain-table' : [-0.9, -0.8, 0.1, -0.5, 0.0, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.8, 0.6, 0.4, 0.2, -1.0]
|
|
}
|
|
},
|
|
'switches' : { # master switches for AFCS, can be controlled by cockpit panel or keys
|
|
'auto-hover' : 0,
|
|
'cas' : 1,
|
|
'sas' : 1,
|
|
'attitude-control' : 0,
|
|
'auto-stabilator' : 1,
|
|
'sideslip-adjuster' : 1,
|
|
'tail-rotor-adjuster' : 1,
|
|
'heading-adjuster' : 0,
|
|
'air-speed-lock' : 0,
|
|
'heading-lock' : 0,
|
|
'altitude-lock' : 0,
|
|
}
|
|
};
|
|
|
|
#
|
|
# initialize - creates AFCS components and invokes AFCS main loop
|
|
#
|
|
var initialize = func {
|
|
cas = CAS.new(nil, "/controls/flight/fcs/cas");
|
|
afcs = AFCS.new("/controls/flight/fcs/cas", "/controls/flight/fcs/afcs");
|
|
sas = SAS.new("/controls/flight/fcs/afcs", "/controls/flight/fcs");
|
|
stabilator = Stabilator.new();
|
|
tail = TailRotorCollective.new();
|
|
backup = BackupFCS.new();
|
|
setlistener("/rotors/main/cone-deg", update);
|
|
}
|
|
|
|
#
|
|
# Stores default AFCS parameters at startup
|
|
#
|
|
var confNode = props.globals.getNode("/controls/flight/fcs", 1);
|
|
confNode.setValues(default_fcs_params);
|
|
|
|
#
|
|
# fcs-initialized signal must be set by per-aircraft nasal script
|
|
# to show that FCS configuration parameters are set
|
|
#
|
|
setlistener("/sim/signals/fcs-initialized", initialize);
|
|
|