#
# 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");
    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);