/******************************************************************************
 *
 *  $Id$
 *
 *  Copyright (C) 2006-2008  Florian Pose, Ingenieurgemeinschaft IgH
 *
 *  This file is part of the IgH EtherCAT Master.
 *
 *  The IgH EtherCAT Master is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License version 2, as
 *  published by the Free Software Foundation.
 *
 *  The IgH EtherCAT Master 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 the IgH EtherCAT Master; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  ---
 *
 *  The license mentioned above concerns the source code only. Using the
 *  EtherCAT technology and brand is only permitted in compliance with the
 *  industrial property and similar rights of Beckhoff Automation GmbH.
 *
 *****************************************************************************/

/**
   \file
   EtherCAT state change FSM.
*/

/*****************************************************************************/

#include "globals.h"
#include "master.h"
#include "fsm_change.h"

/*****************************************************************************/

/** Timeout while waiting for AL state change [s].
 */
#define EC_AL_STATE_CHANGE_TIMEOUT 5

/*****************************************************************************/

void ec_fsm_change_state_start(ec_fsm_change_t *);
void ec_fsm_change_state_check(ec_fsm_change_t *);
void ec_fsm_change_state_status(ec_fsm_change_t *);
void ec_fsm_change_state_start_code(ec_fsm_change_t *);
void ec_fsm_change_state_code(ec_fsm_change_t *);
void ec_fsm_change_state_ack(ec_fsm_change_t *);
void ec_fsm_change_state_check_ack(ec_fsm_change_t *);
void ec_fsm_change_state_end(ec_fsm_change_t *);
void ec_fsm_change_state_error(ec_fsm_change_t *);

/*****************************************************************************/

/**
   Constructor.
*/

void ec_fsm_change_init(ec_fsm_change_t *fsm, /**< finite state machine */
                        ec_datagram_t *datagram /**< datagram */
                        )
{
    fsm->state = NULL;
    fsm->datagram = datagram;
    fsm->spontaneous_change = 0;
}

/*****************************************************************************/

/**
   Destructor.
*/

void ec_fsm_change_clear(ec_fsm_change_t *fsm /**< finite state machine */)
{
}

/*****************************************************************************/

/**
   Starts the change state machine.
*/

void ec_fsm_change_start(ec_fsm_change_t *fsm, /**< finite state machine */
                         ec_slave_t *slave, /**< EtherCAT slave */
                         ec_slave_state_t state /**< requested state */
                         )
{
    fsm->mode = EC_FSM_CHANGE_MODE_FULL;
    fsm->slave = slave;
    fsm->requested_state = state;
    fsm->state = ec_fsm_change_state_start;
}

/*****************************************************************************/

/**
   Starts the change state machine to only acknowlegde a slave's state.
*/

void ec_fsm_change_ack(ec_fsm_change_t *fsm, /**< finite state machine */
                       ec_slave_t *slave /**< EtherCAT slave */
                       )
{
    fsm->mode = EC_FSM_CHANGE_MODE_ACK_ONLY;
    fsm->slave = slave;
    fsm->requested_state = EC_SLAVE_STATE_UNKNOWN;
    fsm->state = ec_fsm_change_state_start_code;
}

/*****************************************************************************/

/**
   Executes the current state of the state machine.
   \return false, if the state machine has terminated
*/

int ec_fsm_change_exec(ec_fsm_change_t *fsm /**< finite state machine */)
{
    fsm->state(fsm);

    return fsm->state != ec_fsm_change_state_end
        && fsm->state != ec_fsm_change_state_error;
}

/*****************************************************************************/

/**
   Returns, if the state machine terminated with success.
   \return non-zero if successful.
*/

int ec_fsm_change_success(ec_fsm_change_t *fsm /**< Finite state machine */)
{
    return fsm->state == ec_fsm_change_state_end;
}

/******************************************************************************
 *  state change state machine
 *****************************************************************************/

/**
   Change state: START.
*/

void ec_fsm_change_state_start(ec_fsm_change_t *fsm
                               /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    ec_slave_t *slave = fsm->slave;

    fsm->take_time = 1;
    fsm->old_state = fsm->slave->current_state;

    // write new state to slave
    ec_datagram_fpwr(datagram, slave->station_address, 0x0120, 2);
    EC_WRITE_U16(datagram->data, fsm->requested_state);
    fsm->retries = EC_FSM_RETRIES;
    fsm->state = ec_fsm_change_state_check;
}

/*****************************************************************************/

/**
   Change state: CHECK.
*/

void ec_fsm_change_state_check(ec_fsm_change_t *fsm
                               /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    ec_slave_t *slave = fsm->slave;

    if (datagram->state == EC_DATAGRAM_TIMED_OUT && fsm->retries--)
        return;

    if (datagram->state != EC_DATAGRAM_RECEIVED) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to receive state datagram: ");
        ec_datagram_print_state(datagram);
        return;
    }

    if (fsm->take_time) {
        fsm->take_time = 0;
        fsm->jiffies_start = datagram->jiffies_sent;
    }

    if (datagram->working_counter == 0) {
        if (datagram->jiffies_received - fsm->jiffies_start >= 3 * HZ) {
            char state_str[EC_STATE_STRING_SIZE];
            ec_state_string(fsm->requested_state, state_str, 0);
            fsm->state = ec_fsm_change_state_error;
            EC_SLAVE_ERR(slave, "Failed to set state %s: ", state_str);
            ec_datagram_print_wc_error(datagram);
            return;
        }

        // repeat writing new state to slave
        ec_datagram_fpwr(datagram, slave->station_address, 0x0120, 2);
        EC_WRITE_U16(datagram->data, fsm->requested_state);
        fsm->retries = EC_FSM_RETRIES;
        return;
    }

    if (unlikely(datagram->working_counter > 1)) {
        char state_str[EC_STATE_STRING_SIZE];
        ec_state_string(fsm->requested_state, state_str, 0);
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to set state %s: ", state_str);
        ec_datagram_print_wc_error(datagram);
        return;
    }

    fsm->take_time = 1;

    // read AL status from slave
    ec_datagram_fprd(datagram, slave->station_address, 0x0130, 2);
    ec_datagram_zero(datagram);
    fsm->retries = EC_FSM_RETRIES;
    fsm->spontaneous_change = 0;
    fsm->state = ec_fsm_change_state_status;
}

/*****************************************************************************/

/**
   Change state: STATUS.
*/

void ec_fsm_change_state_status(ec_fsm_change_t *fsm
                                /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    ec_slave_t *slave = fsm->slave;

    if (datagram->state == EC_DATAGRAM_TIMED_OUT && fsm->retries--)
        return;

    if (datagram->state != EC_DATAGRAM_RECEIVED) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to receive state checking datagram: ");
        ec_datagram_print_state(datagram);
        return;
    }

    if (datagram->working_counter != 1) {
        char req_state[EC_STATE_STRING_SIZE];
        ec_state_string(fsm->requested_state, req_state, 0);
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to check state %s: ", req_state);
        ec_datagram_print_wc_error(datagram);
        return;
    }

    if (fsm->take_time) {
        fsm->take_time = 0;
        fsm->jiffies_start = datagram->jiffies_sent;
    }

    slave->current_state = EC_READ_U8(datagram->data);

    if (slave->current_state == fsm->requested_state) {
        // state has been set successfully
        fsm->state = ec_fsm_change_state_end;
        return;
    }

    if (slave->current_state != fsm->old_state) { // state changed
        char req_state[EC_STATE_STRING_SIZE], cur_state[EC_STATE_STRING_SIZE];

        ec_state_string(slave->current_state, cur_state, 0);

        if ((slave->current_state & 0x0F) != (fsm->old_state & 0x0F)) {
            // Slave spontaneously changed its state just before the new state
            // was written. Accept current state as old state and wait for
            // state change
            fsm->spontaneous_change = 1;
            fsm->old_state = slave->current_state;
            EC_SLAVE_WARN(slave, "Changed to %s in the meantime.\n",
                    cur_state);
            goto check_again;
        }

        // state change error

        slave->error_flag = 1;
        ec_state_string(fsm->requested_state, req_state, 0);

        EC_SLAVE_ERR(slave, "Failed to set %s state, slave refused state"
                " change (%s).\n", req_state, cur_state);

        ec_fsm_change_state_start_code(fsm);
        return;
    }

    // still old state

    if (datagram->jiffies_received - fsm->jiffies_start >=
            EC_AL_STATE_CHANGE_TIMEOUT * HZ) {
        // timeout while checking
        char state_str[EC_STATE_STRING_SIZE];
        ec_state_string(fsm->requested_state, state_str, 0);
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Timeout while setting state %s.\n", state_str);
        return;
    }

 check_again:
    // no timeout yet. check again
    ec_datagram_fprd(datagram, slave->station_address, 0x0130, 2);
    ec_datagram_zero(datagram);
    fsm->retries = EC_FSM_RETRIES;
}

/*****************************************************************************/

/** Enter reading AL status code.
 */
void ec_fsm_change_state_start_code(
        ec_fsm_change_t *fsm /**< finite state machine */
        )
{
    ec_slave_t *slave = fsm->slave;
    ec_datagram_t *datagram = fsm->datagram;

    // fetch AL status error code
    ec_datagram_fprd(datagram, slave->station_address, 0x0134, 2);
    ec_datagram_zero(datagram);
    fsm->retries = EC_FSM_RETRIES;
    fsm->state = ec_fsm_change_state_code;
}

/*****************************************************************************/

/**
   Application layer status messages.
*/

const ec_code_msg_t al_status_messages[] = {
    {0x0000, "No error"},
    {0x0001, "Unspecified error"},
    {0x0002, "No Memory"},
    {0x0011, "Invalid requested state change"},
    {0x0012, "Unknown requested state"},
    {0x0013, "Bootstrap not supported"},
    {0x0014, "No valid firmware"},
    {0x0015, "Invalid mailbox configuration"},
    {0x0016, "Invalid mailbox configuration"},
    {0x0017, "Invalid sync manager configuration"},
    {0x0018, "No valid inputs available"},
    {0x0019, "No valid outputs"},
    {0x001A, "Synchronization error"},
    {0x001B, "Sync manager watchdog"},
    {0x001C, "Invalid sync manager types"},
    {0x001D, "Invalid output configuration"},
    {0x001E, "Invalid input configuration"},
    {0x001F, "Invalid watchdog configuration"},
    {0x0020, "Slave needs cold start"},
    {0x0021, "Slave needs INIT"},
    {0x0022, "Slave needs PREOP"},
    {0x0023, "Slave needs SAFEOP"},
    {0x0024, "Invalid Input Mapping"},
    {0x0025, "Invalid Output Mapping"},
    {0x0026, "Inconsistent Settings"},
    {0x0027, "Freerun not supported"},
    {0x0028, "Synchronization not supported"},
    {0x0029, "Freerun needs 3 Buffer Mode"},
    {0x002A, "Background Watchdog"},
    {0x002B, "No Valid Inputs and Outputs"},
    {0x002C, "Fatal Sync Error"},
    {0x002D, "No Sync Error"},
    {0x0030, "Invalid DC SYNCH configuration"},
    {0x0031, "Invalid DC latch configuration"},
    {0x0032, "PLL error"},
    {0x0033, "DC Sync IO Error"},
    {0x0034, "DC Sync Timeout Error"},
    {0x0035, "DC Invalid Sync Cycle Time"},
    {0x0036, "DC Sync0 Cycle Time"},
    {0x0037, "DC Sync1 Cycle Time"},
    {0x0041, "MBX_AOE"},
    {0x0042, "MBX_EOE"},
    {0x0043, "MBX_COE"},
    {0x0044, "MBX_FOE"},
    {0x0045, "MBX_SOE"},
    {0x004F, "MBX_VOE"},
    {0x0050, "EEPROM No Access"},
    {0x0051, "EEPROM Error"},
    {0x0060, "Slave Restarted Locally"},
    {0xffff}
};


/*****************************************************************************/

/**
   Change state: CODE.
*/

void ec_fsm_change_state_code(ec_fsm_change_t *fsm
                              /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    uint32_t code;
    const ec_code_msg_t *al_msg;

    if (datagram->state == EC_DATAGRAM_TIMED_OUT && fsm->retries--)
        return;

    if (datagram->state != EC_DATAGRAM_RECEIVED) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(fsm->slave, "Failed to receive"
                " AL status code datagram: ");
        ec_datagram_print_state(datagram);
        return;
    }

    if (datagram->working_counter != 1) {
        EC_SLAVE_WARN(fsm->slave, "Reception of AL status code"
                " datagram failed: ");
        ec_datagram_print_wc_error(datagram);
    } else {
        code = EC_READ_U16(datagram->data);
        for (al_msg = al_status_messages; al_msg->code != 0xffff; al_msg++) {
            if (al_msg->code != code) {
                continue;
            }

            EC_SLAVE_ERR(fsm->slave, "AL status message 0x%04X: \"%s\".\n",
                    al_msg->code, al_msg->message);
            break;
        }
        if (al_msg->code == 0xffff) { /* not found in our list. */
            EC_SLAVE_ERR(fsm->slave, "Unknown AL status code 0x%04X.\n",
                    code);
        }
    }

    // acknowledge "old" slave state
    ec_datagram_fpwr(datagram, fsm->slave->station_address, 0x0120, 2);
    EC_WRITE_U16(datagram->data, fsm->slave->current_state);
    fsm->retries = EC_FSM_RETRIES;
    fsm->state = ec_fsm_change_state_ack;
}

/*****************************************************************************/

/**
   Change state: ACK.
*/

void ec_fsm_change_state_ack(ec_fsm_change_t *fsm /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    ec_slave_t *slave = fsm->slave;

    if (datagram->state == EC_DATAGRAM_TIMED_OUT && fsm->retries--)
        return;

    if (datagram->state != EC_DATAGRAM_RECEIVED) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to receive state ack datagram: ");
        ec_datagram_print_state(datagram);
        return;
    }

    if (datagram->working_counter != 1) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Reception of state ack datagram failed: ");
        ec_datagram_print_wc_error(datagram);
        return;
    }

    fsm->take_time = 1;

    // read new AL status
    ec_datagram_fprd(datagram, slave->station_address, 0x0130, 2);
    ec_datagram_zero(datagram);
    fsm->retries = EC_FSM_RETRIES;
    fsm->state = ec_fsm_change_state_check_ack;
}

/*****************************************************************************/

/**
   Change state: CHECK ACK.
*/

void ec_fsm_change_state_check_ack(ec_fsm_change_t *fsm
                                   /**< finite state machine */)
{
    ec_datagram_t *datagram = fsm->datagram;
    ec_slave_t *slave = fsm->slave;

    if (datagram->state == EC_DATAGRAM_TIMED_OUT && fsm->retries--)
        return;

    if (datagram->state != EC_DATAGRAM_RECEIVED) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Failed to receive state ack check datagram: ");
        ec_datagram_print_state(datagram);
        return;
    }

    if (datagram->working_counter != 1) {
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Reception of state ack check datagram failed: ");
        ec_datagram_print_wc_error(datagram);
        return;
    }

    if (fsm->take_time) {
        fsm->take_time = 0;
        fsm->jiffies_start = datagram->jiffies_sent;
    }

    slave->current_state = EC_READ_U8(datagram->data);

    if (!(slave->current_state & EC_SLAVE_STATE_ACK_ERR)) {
        char state_str[EC_STATE_STRING_SIZE];
        ec_state_string(slave->current_state, state_str, 0);
        if (fsm->mode == EC_FSM_CHANGE_MODE_FULL) {
            fsm->state = ec_fsm_change_state_error;
        }
        else { // EC_FSM_CHANGE_MODE_ACK_ONLY
            fsm->state = ec_fsm_change_state_end;
        }
        EC_SLAVE_INFO(slave, "Acknowledged state %s.\n", state_str);
        return;
    }

    if (datagram->jiffies_received - fsm->jiffies_start >=
            EC_AL_STATE_CHANGE_TIMEOUT * HZ) {
        // timeout while checking
        char state_str[EC_STATE_STRING_SIZE];
        ec_state_string(slave->current_state, state_str, 0);
        fsm->state = ec_fsm_change_state_error;
        EC_SLAVE_ERR(slave, "Timeout while acknowledging state %s.\n",
                state_str);
        return;
    }

    // reread new AL status
    ec_datagram_fprd(datagram, slave->station_address, 0x0130, 2);
    ec_datagram_zero(datagram);
    fsm->retries = EC_FSM_RETRIES;
}

/*****************************************************************************/

/**
   State: ERROR.
*/

void ec_fsm_change_state_error(ec_fsm_change_t *fsm
                               /**< finite state machine */)
{
}

/*****************************************************************************/

/**
   State: END.
*/

void ec_fsm_change_state_end(ec_fsm_change_t *fsm
                             /**< finite state machine */)
{
}

/*****************************************************************************/
