// -*- tab-width: 4; Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*-

/*
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
   The strategy for roll/pitch autotune is to give the user a AUTOTUNE
   flight mode which behaves just like FBWA, but does automatic
   tuning.

   While the user is flying in AUTOTUNE the gains are saved every 10
   seconds, but the saved gains are not the current gains, instead it
   saves the gains from 10s ago. When the user exits AUTOTUNE the
   gains are restored from 10s ago.

   This allows the user to fly as much as they want in AUTOTUNE mode,
   and if they are ever unhappy they just exit the mode. If they stay
   in AUTOTUNE for more than 10s then their gains will have changed.

   Using this approach users don't need any special switches, they
   just need to be able to enter and exit AUTOTUNE mode
*/

#include <AP_HAL.h>
#include <AP_Common.h>
#include <AP_Math.h>
#include "AP_AutoTune.h"

extern const AP_HAL::HAL& hal;

// time in milliseconds between autotune saves
#define AUTOTUNE_SAVE_PERIOD 10000U

// how much time we have to overshoot for to reduce gains
#define AUTOTUNE_OVERSHOOT_TIME 100

// how much time we have to undershoot for to increase gains
#define AUTOTUNE_UNDERSHOOT_TIME 200

// step size for increasing gains, percentage
#define AUTOTUNE_INCREASE_STEP 5

// step size for decreasing gains, percentage
#define AUTOTUNE_DECREASE_STEP 8

// min/max P gains
#define AUTOTUNE_MAX_P 5.0f
#define AUTOTUNE_MIN_P 0.3f

// tau ranges
#define AUTOTUNE_MAX_TAU 0.7f
#define AUTOTUNE_MIN_TAU 0.2f

#define AUTOTUNE_MIN_IMAX 2000
#define AUTOTUNE_MAX_IMAX 4000

// constructor
AP_AutoTune::AP_AutoTune(ATGains &_gains, ATType _type,
                         const AP_Vehicle::FixedWing &parms,
                         DataFlash_Class &_dataflash) :
    current(_gains),
    type(_type),
    aparm(parms),
    dataflash(_dataflash)
{}

#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
#include <stdio.h>
# define Debug(fmt, args ...)  do {::printf("%s:%d: " fmt "\n", __FUNCTION__, __LINE__, ## args); } while(0)
#else
 # define Debug(fmt, args ...)
#endif

/*
  auto-tuning table. This table gives the starting values for key
  tuning parameters based on a user chosen AUTOTUNE_LEVEL parameter
  from 1 to 10. Level 1 is a very soft tune. Level 10 is a very
  agressive tune.
 */
static const struct {
    float tau;
    float Dratio;
    float rmax;
} tuning_table[] PROGMEM = {
    { 0.70f, 0.050f,  20 },   // level 1
    { 0.65f, 0.055f,  30 },   // level 2
    { 0.60f, 0.060f,  40 },   // level 3
    { 0.55f, 0.065f,  50 },   // level 4
    { 0.50f, 0.070f,  60 },   // level 5
    { 0.45f, 0.075f,  75 },   // level 6
    { 0.40f, 0.080f,  90 },   // level 7
    { 0.30f, 0.085f, 120 },   // level 8
    { 0.20f, 0.090f, 160 },   // level 9
    { 0.10f, 0.095f, 210 },   // level 10
    { 0.05f, 0.100f, 300 },   // (yes, it goes to 11)
};

/*
  start an autotune session
*/
void AP_AutoTune::start(void)
{
    running = true;
    state = DEMAND_UNSATURATED;
    uint32_t now = hal.scheduler->millis();

    state_enter_ms = now;
    last_save_ms = now;

    last_save = current;
    restore = current;

    uint8_t level = aparm.autotune_level;
    if (level > sizeof(tuning_table)/sizeof(tuning_table[0])) {
        level = sizeof(tuning_table)/sizeof(tuning_table[0]);
    }
    if (level < 1) {
        level = 1;
    }

    current.rmax.set(pgm_read_float(&tuning_table[level-1].rmax));
    // D gain is scaled to a fixed ratio of P gain
    current.D.set(   pgm_read_float(&tuning_table[level-1].Dratio) * current.P);
    current.tau.set( pgm_read_float(&tuning_table[level-1].tau));

    current.imax = constrain_float(current.imax, AUTOTUNE_MIN_IMAX, AUTOTUNE_MAX_IMAX);

    // force a fixed ratio of I to D gain on the rate feedback path
    current.I = 0.5f * current.D / current.tau;

    next_save = current;

    Debug("START P -> %.3f\n", current.P.get());
}

/*
  called when we change state to see if we should change gains
 */
void AP_AutoTune::stop(void)
{
    running = false;
    save_gains(restore);
}


/*
  one update cycle of the autotuner
 */
void AP_AutoTune::update(float desired_rate, float achieved_rate, float servo_out)
{
    if (!running) {
        return;
    }
    check_save();

    // see what state we are in
    ATState new_state;
    float abs_desired_rate = fabsf(desired_rate);
    uint32_t now = hal.scheduler->millis();

    if (fabsf(servo_out) >= 45) {
        // we have saturated the servo demand (not including
        // integrator), we cannot get any information that would allow
        // us to increase the gain
        saturated_surfaces = true;
    }

    if (abs_desired_rate < 0.8f * current.rmax) {
        // we are not demanding max rate
        new_state = DEMAND_UNSATURATED;
    } else if (fabsf(achieved_rate) > abs_desired_rate) {
        new_state = desired_rate > 0 ? DEMAND_OVER_POS : DEMAND_OVER_NEG;
    } else {
        new_state = desired_rate > 0 ? DEMAND_UNDER_POS : DEMAND_UNDER_NEG;
    }
    if (new_state != state) {
        check_state_exit(now - state_enter_ms);
        state = new_state;
        state_enter_ms = now;
        saturated_surfaces = false;
    }
    if (state != DEMAND_UNSATURATED) {
        write_log(servo_out, desired_rate, achieved_rate);
    }
}

/*
  called when we change state to see if we should change gains
 */
void AP_AutoTune::check_state_exit(uint32_t state_time_ms)
{
    switch (state) {
    case DEMAND_UNSATURATED:
        break;
    case DEMAND_UNDER_POS:
    case DEMAND_UNDER_NEG:
        // we increase P if we have not saturated the surfaces during
        // this state, and we have
        if (state_time_ms >= AUTOTUNE_UNDERSHOOT_TIME && !saturated_surfaces) {
            current.P.set(current.P * (100+AUTOTUNE_INCREASE_STEP) * 0.01f);
            if (current.P > AUTOTUNE_MAX_P) {
                current.P = AUTOTUNE_MAX_P;
            }
            Debug("UNDER P -> %.3f\n", current.P.get());
        }
        current.D.set(   pgm_read_float(&tuning_table[aparm.autotune_level-1].Dratio) * current.P);
        break;
    case DEMAND_OVER_POS:
    case DEMAND_OVER_NEG:
        if (state_time_ms >= AUTOTUNE_OVERSHOOT_TIME) {
            current.P.set(current.P * (100-AUTOTUNE_DECREASE_STEP) * 0.01f);
            if (current.P < AUTOTUNE_MIN_P) {
                current.P = AUTOTUNE_MIN_P;
            }
            Debug("OVER P -> %.3f\n", current.P.get());
        }
        current.D.set(   pgm_read_float(&tuning_table[aparm.autotune_level-1].Dratio) * current.P);
        break;
    }
}

/*
  see if we should save new values
 */
void AP_AutoTune::check_save(void)
{
    if (hal.scheduler->millis() - last_save_ms < AUTOTUNE_SAVE_PERIOD) {
        return;
    }

    // save the next_save values, which are the autotune value from
    // the last save period. This means the pilot has
    // AUTOTUNE_SAVE_PERIOD milliseconds to decide they don't like the
    // gains and switch out of autotune
    ATGains tmp = current;

    save_gains(next_save);
    Debug("SAVE P -> %.3f\n", current.P.get());

    // restore our current gains
    current = tmp;

    // if the pilot exits autotune they get these saved values
    restore = next_save;

    // the next values to save will be the ones we are flying now
    next_save = current;
    last_save_ms = hal.scheduler->millis();
}

/*
  log a parameter change from autotune
 */
void AP_AutoTune::log_param_change(float v, const prog_char_t *suffix)
{
    if (!dataflash.logging_started()) {
        return;
    }
    char key[AP_MAX_NAME_SIZE+1];
    if (type == AUTOTUNE_ROLL) {
        strncpy_P(key, PSTR("RLL2SRV_"), 8);
        strncpy_P(&key[8], suffix, AP_MAX_NAME_SIZE-8);
    } else {
        strncpy_P(key, PSTR("PTCH2SRV_"), 9);
        strncpy_P(&key[9], suffix, AP_MAX_NAME_SIZE-9);
    }
    key[AP_MAX_NAME_SIZE] = 0;
    dataflash.Log_Write_Parameter(key, v);
}

/*
  set a float and save a float if it has changed by more than
  0.1%. This reduces the number of insignificant EEPROM writes
 */
void AP_AutoTune::save_float_if_changed(AP_Float &v, float value, const prog_char_t *suffix)
{
    float old_value = v.get();
    v.set(value);
    if (value <= 0 || fabsf((value-old_value)/value) > 0.001f) {
        v.save();
        log_param_change(v.get(), suffix);
    }
}

/*
  set a int16 and save if changed
 */
void AP_AutoTune::save_int16_if_changed(AP_Int16 &v, int16_t value, const prog_char_t *suffix)
{
    int16_t old_value = v.get();
    v.set(value);
    if (old_value != v.get()) {
        v.save();
        log_param_change(v.get(), suffix);
    }
}


/*
  save a set of gains
 */
void AP_AutoTune::save_gains(const ATGains &v)
{
    current = last_save;
    save_float_if_changed(current.tau, v.tau, PSTR("TCONST"));
    save_float_if_changed(current.P, v.P, PSTR("P"));
    save_float_if_changed(current.I, v.I, PSTR("I"));
    save_float_if_changed(current.D, v.D, PSTR("D"));
    save_int16_if_changed(current.rmax, v.rmax, PSTR("RMAX"));
    save_int16_if_changed(current.imax, v.imax, PSTR("IMAX"));
    last_save = current;
}

void AP_AutoTune::write_log(float servo, float demanded, float achieved)
{
    if (!dataflash.logging_started()) {
        return;
    }

    struct log_ATRP pkt = {
        LOG_PACKET_HEADER_INIT(LOG_ATRP_MSG),
        timestamp  : hal.scheduler->millis(),
        type       : type,
    	state      : (uint8_t)state,
        servo      : (int16_t)(servo*100),
        demanded   : demanded,
        achieved   : achieved,
        P          : current.P.get()
    };
    dataflash.WriteBlock(&pkt, sizeof(pkt));
}
