/*
 * CANopen NMT and Heartbeat producer object.
 *
 * @file        CO_NMT_Heartbeat.c
 * @ingroup     CO_NMT_Heartbeat
 * @author      Janez Paternoster
 * @copyright   2004 - 2020 Janez Paternoster
 *
 * This file is part of <https://github.com/CANopenNode/CANopenNode>, a CANopen Stack.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

#include "301/CO_NMT_Heartbeat.h"

/*
 * Read received message from CAN module.
 *
 * Function will be called (by CAN receive interrupt) every time, when CAN message with correct identifier
 * will be received. For more information and description of parameters see file CO_driver.h.
 */
static void
CO_NMT_receive(void *object, void *msg)
{
    uint8_t DLC = CO_CANrxMsg_readDLC(msg);
    const uint8_t *data = CO_CANrxMsg_readData(msg);
    CO_NMT_command_t command = (CO_NMT_command_t)data[0];
    uint8_t nodeId = data[1];

    CO_NMT_t *NMT = (CO_NMT_t *)object;

    if ((DLC == 2U) && ((nodeId == 0U) || (nodeId == NMT->nodeId)))
    {
        NMT->internalCommand = command;

#if ((CO_CONFIG_NMT) & CO_CONFIG_FLAG_CALLBACK_PRE) != 0
        /* Optional signal to RTOS, which can resume task, which handles NMT. */
        if (NMT->pFunctSignalPre != NULL)
        {
            NMT->pFunctSignalPre(NMT->functSignalObjectPre);
        }
#endif
    }
}

/*
 * Custom function for writing OD object "Producer heartbeat time"
 *
 * For more information see file CO_ODinterface.h, OD_IO_t.
 */
static ODR_t
OD_write_1017(OD_stream_t *stream, const void *buf, OD_size_t count, OD_size_t *countWritten)
{
    if ((stream == NULL) || (stream->subIndex != 0U) || (buf == NULL) || (count != sizeof(uint16_t)) || (countWritten == NULL))
    {
        return ODR_DEV_INCOMPAT;
    }

    CO_NMT_t *NMT = (CO_NMT_t *)stream->object;

    /* update object, send Heartbeat immediately */
    NMT->HBproducerTime_us = (uint32_t)CO_getUint16(buf) * 1000U;
    NMT->HBproducerTimer = 0;

    /* write value to the original location in the Object Dictionary */
    return OD_writeOriginal(stream, buf, count, countWritten);
}

CO_ReturnError_t
CO_NMT_init(CO_NMT_t *NMT, OD_entry_t *OD_1017_ProducerHbTime, CO_EM_t *em, uint8_t nodeId, uint16_t NMTcontrol,
            uint16_t firstHBTime_ms, CO_CANmodule_t *NMT_CANdevRx, uint16_t NMT_rxIdx, uint16_t CANidRxNMT,
#if (((CO_CONFIG_NMT) & CO_CONFIG_NMT_MASTER) != 0) || defined CO_DOXYGEN
            CO_CANmodule_t *NMT_CANdevTx, uint16_t NMT_txIdx, uint16_t CANidTxNMT,
#endif
            CO_CANmodule_t *HB_CANdevTx, uint16_t HB_txIdx, uint16_t CANidTxHB, uint32_t *errInfo)
{
    CO_ReturnError_t ret = CO_ERROR_NO;

    /* verify arguments */
    if ((NMT == NULL) || (OD_1017_ProducerHbTime == NULL) || (em == NULL) || (NMT_CANdevRx == NULL) || (HB_CANdevTx == NULL)
#if ((CO_CONFIG_NMT) & CO_CONFIG_NMT_MASTER) != 0
        || (NMT_CANdevTx == NULL)
#endif
    )
    {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    /* clear the object */
    (void)memset(NMT, 0, sizeof(CO_NMT_t));

    /* Configure object variables */
    NMT->operatingState = CO_NMT_INITIALIZING;
    NMT->operatingStatePrev = CO_NMT_INITIALIZING;
    NMT->nodeId = nodeId;
    NMT->NMTcontrol = NMTcontrol;
    NMT->em = em;
    NMT->HBproducerTimer = (uint32_t)firstHBTime_ms * 1000U;

    /* get and verify required "Producer heartbeat time" from Object Dict. */
    uint16_t HBprodTime_ms;
    ODR_t odRet = OD_get_u16(OD_1017_ProducerHbTime, 0, &HBprodTime_ms, true);
    if (odRet != ODR_OK)
    {
        if (errInfo != NULL)
        {
            *errInfo = OD_getIndex(OD_1017_ProducerHbTime);
        }
        return CO_ERROR_OD_PARAMETERS;
    }
    NMT->HBproducerTime_us = (uint32_t)HBprodTime_ms * 1000U;

    NMT->OD_1017_extension.object = NMT;
    NMT->OD_1017_extension.read = OD_readOriginal;
    NMT->OD_1017_extension.write = OD_write_1017;
    odRet = OD_extension_init(OD_1017_ProducerHbTime, &NMT->OD_1017_extension);
    if (odRet != ODR_OK)
    {
        if (errInfo != NULL)
        {
            *errInfo = OD_getIndex(OD_1017_ProducerHbTime);
        }
        return CO_ERROR_OD_PARAMETERS;
    }

    if (NMT->HBproducerTimer > NMT->HBproducerTime_us)
    {
        NMT->HBproducerTimer = NMT->HBproducerTime_us;
    }

    /* configure NMT CAN reception */
    ret = CO_CANrxBufferInit(NMT_CANdevRx, NMT_rxIdx, CANidRxNMT, 0x7FF, false, (void *)NMT, CO_NMT_receive);
    if (ret != CO_ERROR_NO)
    {
        return ret;
    }

#if ((CO_CONFIG_NMT) & CO_CONFIG_NMT_MASTER) != 0
    /* configure NMT CAN transmission */
    NMT->NMT_CANdevTx = NMT_CANdevTx;
    NMT->NMT_TXbuff = CO_CANtxBufferInit(NMT_CANdevTx, NMT_txIdx, CANidTxNMT, false, 2, false);
    if (NMT->NMT_TXbuff == NULL)
    {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }
#endif

    /* configure HB CAN transmission */
    NMT->HB_CANdevTx = HB_CANdevTx;
    NMT->HB_TXbuff = CO_CANtxBufferInit(HB_CANdevTx, HB_txIdx, CANidTxHB, false, 1, false);
    if (NMT->HB_TXbuff == NULL)
    {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    return ret;
}

#if ((CO_CONFIG_NMT) & CO_CONFIG_FLAG_CALLBACK_PRE) != 0
void CO_NMT_initCallbackPre(CO_NMT_t *NMT, void *object, void (*pFunctSignal)(void *object))
{
    if (NMT != NULL)
    {
        NMT->pFunctSignalPre = pFunctSignal;
        NMT->functSignalObjectPre = object;
    }
}
#endif

#if ((CO_CONFIG_NMT) & CO_CONFIG_NMT_CALLBACK_CHANGE) != 0
void CO_NMT_initCallbackChanged(CO_NMT_t *NMT, void (*pFunctNMT)(CO_NMT_internalState_t state))
{
    if (NMT != NULL)
    {
        NMT->pFunctNMT = pFunctNMT;
        if (NMT->pFunctNMT != NULL)
        {
            NMT->pFunctNMT(NMT->operatingState);
        }
    }
}
#endif

CO_NMT_reset_cmd_t
CO_NMT_process(CO_NMT_t *NMT, CO_NMT_internalState_t *NMTstate, uint32_t timeDifference_us, uint32_t *timerNext_us)
{
    (void)timerNext_us; /* may be unused */
    CO_NMT_internalState_t NMTstateCpy = NMT->operatingState;
    CO_NMT_reset_cmd_t resetCommand = CO_RESET_NOT;
    bool_t NNTinit = NMTstateCpy == CO_NMT_INITIALIZING;

    NMT->HBproducerTimer = (NMT->HBproducerTimer > timeDifference_us) ? (NMT->HBproducerTimer - timeDifference_us) : 0U;

    /* Send heartbeat producer message if:
     * - First start, send bootup message or
     * - HB producer enabled and: Timer expired or NMT->operatingState changed */
    if (NNTinit || ((NMT->HBproducerTime_us != 0U) && ((NMT->HBproducerTimer == 0U) || (NMTstateCpy != NMT->operatingStatePrev))))
    {
        NMT->HB_TXbuff->data[0] = (uint8_t)NMTstateCpy;
        (void)CO_CANsend(NMT->HB_CANdevTx, NMT->HB_TXbuff);

        if (NMTstateCpy == CO_NMT_INITIALIZING)
        {
            /* NMT slave self starting */
            NMTstateCpy = (((uint16_t)NMT->NMTcontrol & (uint16_t)CO_NMT_STARTUP_TO_OPERATIONAL) != 0U)
                              ? CO_NMT_OPERATIONAL
                              : CO_NMT_PRE_OPERATIONAL;
        }
        else
        {
            /* Start timer from the beginning. If OS is slow, time sliding may occur. However,
             * heartbeat is not for synchronization, it is for health report. In case of
             * initializing, timer is set in the CO_NMT_init() function with pre-defined value. */
            NMT->HBproducerTimer = NMT->HBproducerTime_us;
        }
    }
    NMT->operatingStatePrev = NMTstateCpy;

    /* process internal NMT commands, received from CO_NMT_receive() or CO_NMT_sendCommand() */
    if (NMT->internalCommand != CO_NMT_NO_COMMAND)
    {
        switch (NMT->internalCommand)
        {
        case CO_NMT_ENTER_OPERATIONAL:
            NMTstateCpy = CO_NMT_OPERATIONAL;
            break;
        case CO_NMT_ENTER_STOPPED:
            NMTstateCpy = CO_NMT_STOPPED;
            break;
        case CO_NMT_ENTER_PRE_OPERATIONAL:
            NMTstateCpy = CO_NMT_PRE_OPERATIONAL;
            break;
        case CO_NMT_RESET_NODE:
            resetCommand = CO_RESET_APP;
            break;
        case CO_NMT_RESET_COMMUNICATION:
            resetCommand = CO_RESET_COMM;
            break;
        case CO_NMT_NO_COMMAND:
        default:
            /* done */
            break;
        }
        NMT->internalCommand = CO_NMT_NO_COMMAND;
    }

    /* verify NMT transitions based on error register */
    bool_t ErrOnBusOffHB = (((uint16_t)NMT->NMTcontrol & (uint16_t)CO_NMT_ERR_ON_BUSOFF_HB) != 0U);
    bool_t ErrBusOff = CO_isError(NMT->em, CO_EM_CAN_TX_BUS_OFF);
    bool_t ErrHbCons = CO_isError(NMT->em, CO_EM_HEARTBEAT_CONSUMER);
    bool_t ErrHbConsRemote = CO_isError(NMT->em, CO_EM_HB_CONSUMER_REMOTE_RESET);
    bool_t busOff_HB = ErrOnBusOffHB && (ErrBusOff || ErrHbCons || ErrHbConsRemote);

    bool_t ErrNMTErrReg = (((uint16_t)NMT->NMTcontrol & (uint16_t)CO_NMT_ERR_ON_ERR_REG) != 0U);
    bool_t ErrNMTcontrol = ((CO_getErrorRegister(NMT->em) & (uint8_t)NMT->NMTcontrol) != 0U);
    bool_t errRegMasked = ErrNMTErrReg && ErrNMTcontrol;

    if ((NMTstateCpy == CO_NMT_OPERATIONAL) && (busOff_HB || errRegMasked))
    {
        NMTstateCpy = (((uint16_t)NMT->NMTcontrol & (uint16_t)CO_NMT_ERR_TO_STOPPED) != 0U) ? CO_NMT_STOPPED
                                                                                            : CO_NMT_PRE_OPERATIONAL;
    }
    else if ((((uint16_t)NMT->NMTcontrol & (uint16_t)CO_NMT_ERR_FREE_TO_OPERATIONAL) != 0U) && (NMTstateCpy == CO_NMT_PRE_OPERATIONAL) && (!busOff_HB && !errRegMasked))
    {
        NMTstateCpy = CO_NMT_OPERATIONAL;
    }
    else
    { /* MISRA C 2004 14.10 */
    }

#if ((CO_CONFIG_NMT) & CO_CONFIG_NMT_CALLBACK_CHANGE) != 0
    /* Notify operating state change */
    if ((NMT->operatingStatePrev != NMTstateCpy) || NNTinit)
    {
        if (NMT->pFunctNMT != NULL)
        {
            NMT->pFunctNMT(NMTstateCpy);
        }
    }
#endif

#if ((CO_CONFIG_NMT) & CO_CONFIG_FLAG_TIMERNEXT) != 0
    /* Calculate, when next Heartbeat needs to be send */
    if ((NMT->HBproducerTime_us != 0U) && (timerNext_us != NULL))
    {
        if (NMT->operatingStatePrev != NMTstateCpy)
        {
            *timerNext_us = 0;
        }
        else if (*timerNext_us > NMT->HBproducerTimer)
        {
            *timerNext_us = NMT->HBproducerTimer;
        }
        else
        { /* MISRA C 2004 14.10 */
        }
    }
#endif

    NMT->operatingState = NMTstateCpy;
    if (NMTstate != NULL)
    {
        *NMTstate = NMTstateCpy;
    }

    return resetCommand;
}

#if ((CO_CONFIG_NMT) & CO_CONFIG_NMT_MASTER) != 0
CO_ReturnError_t
CO_NMT_sendCommand(CO_NMT_t *NMT, CO_NMT_command_t command, uint8_t nodeID)
{
    /* verify arguments */
    if (NMT == NULL)
    {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    /* Apply NMT command also to this node, if set so. */
    if ((nodeID == 0U) || (nodeID == NMT->nodeId))
    {
        NMT->internalCommand = command;
    }

    /* Send NMT master message. */
    NMT->NMT_TXbuff->data[0] = (uint8_t)command;
    NMT->NMT_TXbuff->data[1] = nodeID;
    return CO_CANsend(NMT->NMT_CANdevTx, NMT->NMT_TXbuff);
}
#endif
