File isotp.c

File List > src > isotp.c

Go to the documentation of this file

/*******************************************************************************
 * ISO-TP-C: ISO 15765-2 Protocol Implementation
 *
 * Project:     ISO-TP-C - Embedded-Grade Refactoring & Optimization
 * Description: Core ISO-TP protocol implementation with multi-frame support
 *
 * Author:      Anton Vynohradov
 * Email:       avynohradov@systemfromscratch.com
 *
 * License:     MIT License
 *
 * Copyright (c) 2026 Anton Vynohradov
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 ******************************************************************************/

/* ==============================================================================
 * INCLUDES
 * =============================================================================*/

#include <assert.h>
#include <stdint.h>

#include "isotp.h"

/* ==============================================================================
 * DEFINES & MACROS
 * =============================================================================*/

/* ==============================================================================
 * PRIVATE TYPE DEFINITIONS
 * =============================================================================*/

/* ==============================================================================
 * PRIVATE VARIABLES (static)
 * =============================================================================*/

/* ==============================================================================
 * PRIVATE FUNCTION DECLARATIONS (static)
 * =============================================================================*/

static uint8_t isotp_us_to_st_min(uint32_t us);

static uint32_t isotp_st_min_to_us(uint8_t st_min);

static int isotp_send_flow_control(const IsoTpLink* link, uint8_t flow_status, uint8_t block_size,
                                   uint32_t st_min_us);

static int isotp_send_single_frame(const IsoTpLink* link, uint32_t id);

static int isotp_send_first_frame(IsoTpLink* link, uint32_t id);

static int isotp_send_consecutive_frame(IsoTpLink* link);

static int isotp_receive_single_frame(IsoTpLink* link, const IsoTpCanMessage* message, uint8_t len);

static int isotp_receive_first_frame(IsoTpLink* link, IsoTpCanMessage* message, uint8_t len);

static int isotp_receive_consecutive_frame(IsoTpLink* link, const IsoTpCanMessage* message,
                                           uint8_t len);

static int isotp_receive_flow_control_frame(IsoTpLink* link, IsoTpCanMessage* message, uint8_t len);

/* ==============================================================================
 * PRIVATE FUNCTION IMPLEMENTATIONS
 * =============================================================================*/

/* st_min to microsecond */
static uint8_t isotp_us_to_st_min(uint32_t us)
{
    // ISO 15765-2:2016 defines STmin encoding:
    // 0x00..0x7F: value in milliseconds (0..127 ms)
    // 0xF1..0xF9: value in 100 microsecond steps (100..900 us)
    const uint32_t STMIN_MS_MAX = 127000;  // 127 ms in us
    const uint32_t STMIN_US_MIN = 100;     // 100 us
    const uint32_t STMIN_US_MAX = 900;     // 900 us
    const uint8_t STMIN_US_BASE = 0xF0;    // base for 100us steps

    if (us <= STMIN_MS_MAX)
    {
        if (us >= STMIN_US_MIN && us <= STMIN_US_MAX)
        {
            return (uint8_t) (STMIN_US_BASE + (us / 100));
        }
        else
        {
            return (uint8_t) (us / 1000u);
        }
    }

    return 0;
}

/* st_min to usec  */
static uint32_t isotp_st_min_to_us(uint8_t st_min)
{
    // ISO 15765-2:2016 defines STmin encoding:
    // 0x00..0x7F: value in milliseconds (0..127 ms)
    // 0xF1..0xF9: value in 100 microsecond steps (100..900 us)
    const uint8_t STMIN_MS_MAX = 0x7F;       // 127 ms
    const uint8_t STMIN_US_MIN_CODE = 0xF1;  // 100 us
    const uint8_t STMIN_US_MAX_CODE = 0xF9;  // 900 us
    const uint8_t STMIN_US_BASE = 0xF0;      // base for 100us steps
    const uint32_t US_PER_MS = 1000;
    const uint32_t US_STEP = 100;

    if (st_min <= STMIN_MS_MAX)
    {
        return st_min * US_PER_MS;
    }
    else if (st_min >= STMIN_US_MIN_CODE && st_min <= STMIN_US_MAX_CODE)
    {
        return (st_min - STMIN_US_BASE) * US_STEP;
    }
    return 0;
}

static int isotp_send_flow_control(const IsoTpLink* link, uint8_t flow_status, uint8_t block_size,
                                   uint32_t st_min_us)
{
    IsoTpCanMessage message;
    (void) memset(&message, 0, sizeof(message));
    int ret;
    uint8_t size = 0;

    /* setup message  */
    message.as.flow_control.type = ISOTP_PCI_TYPE_FLOW_CONTROL_FRAME;
    message.as.flow_control.FS = flow_status;
    message.as.flow_control.BS = block_size;
    message.as.flow_control.STmin = isotp_us_to_st_min(st_min_us);

    /* send message */
#ifdef ISO_TP_FRAME_PADDING
    (void) memset(message.as.flow_control.reserve, ISO_TP_FRAME_PADDING_VALUE,
                  sizeof(message.as.flow_control.reserve));
    size = sizeof(message);
#else
    size = 3;
#endif

    ret = isotp_user_send_can(link->send_arbitration_id, message.as.data_array.ptr, size
#if defined(ISO_TP_USER_SEND_CAN_ARG)
                              ,
                              link->user_send_can_arg
#endif
    );

    return ret;
}

static int isotp_send_single_frame(const IsoTpLink* link, uint32_t id)
{
    (void) id;  // Prevent unused variable warning

    IsoTpCanMessage message;
    int ret;
    uint8_t size = 0;

    /* multi frame message length must greater than 7  */
    assert(link->send_size <= 7);

    /* setup message  */
    message.as.single_frame.type = ISOTP_PCI_TYPE_SINGLE;
    message.as.single_frame.SF_DL = (uint8_t) link->send_size;
    (void) memcpy(message.as.single_frame.data, link->send_buffer, link->send_size);

    /* send message */
#ifdef ISO_TP_FRAME_PADDING
    (void) memset(message.as.single_frame.data + link->send_size, ISO_TP_FRAME_PADDING_VALUE,
                  sizeof(message.as.single_frame.data) - link->send_size);
    size = sizeof(message);
#else
    size = link->send_size + (uint8_t) 1;
#endif

    ret = isotp_user_send_can(link->send_arbitration_id, message.as.data_array.ptr, size
#if defined(ISO_TP_USER_SEND_CAN_ARG)
                              ,
                              link->user_send_can_arg
#endif
    );

    return ret;
}

static int isotp_send_first_frame(IsoTpLink* link, uint32_t id)
{
    IsoTpCanMessage message = {0};
    int ret = 0;

    /* multi frame message length must greater than 7  */
    assert(link->send_size > 7);

    if (link->send_size <= 4095)
    {
        /* setup 'short' message */
        message.as.first_frame_short.type = ISOTP_PCI_TYPE_FIRST_FRAME;
        message.as.first_frame_short.FF_DL_low = (uint8_t) link->send_size;
        message.as.first_frame_short.FF_DL_high = (uint8_t) (0x0F & (link->send_size >> 8));
        (void) memcpy(message.as.first_frame_short.data, link->send_buffer,
                      sizeof(message.as.first_frame_short.data));

        /* send 'short' message */
        ret = isotp_user_send_can(id, message.as.data_array.ptr, sizeof(message)
#if defined(ISO_TP_USER_SEND_CAN_ARG)
                                                                     ,
                                  link->user_send_can_arg
#endif
        );

        if (ISOTP_RET_OK == ret)
        {
            link->send_offset += sizeof(message.as.first_frame_short.data);
        }
    }
    else
    {  // ISO15765-2:2016
        /* setup 'long' message */
        message.as.first_frame_long.set_to_zero_high = 0;
        message.as.first_frame_long.set_to_zero_low = 0;
        message.as.first_frame_long.type = ISOTP_PCI_TYPE_FIRST_FRAME;
        message.as.first_frame_long.FF_DL = LE32TOH(link->send_size);
        (void) memcpy(message.as.first_frame_long.data, link->send_buffer,
                      sizeof(message.as.first_frame_long.data));

        /* send 'long' message */
        ret = isotp_user_send_can(id, message.as.data_array.ptr, sizeof(message)
#if defined(ISO_TP_USER_SEND_CAN_ARG)
                                                                     ,
                                  link->user_send_can_arg
#endif
        );

        if (ISOTP_RET_OK == ret)
        {
            link->send_offset += sizeof(message.as.first_frame_long.data);
        }
    }

    link->send_sn = 1;

    return ret;
}

static int isotp_send_consecutive_frame(IsoTpLink* link)
{
    IsoTpCanMessage message;
    uint32_t data_length;
    int ret;
    uint8_t size = 0;

    /* multi frame message length must greater than 7  */
    assert(link->send_size > 7);

    /* setup message  */
    message.as.consecutive_frame.type = ISOTP_PCI_TYPE_CONSECUTIVE_FRAME;
    message.as.consecutive_frame.SN = link->send_sn;
    data_length = link->send_size - link->send_offset;
    if (data_length > sizeof(message.as.consecutive_frame.data))
    {
        data_length = sizeof(message.as.consecutive_frame.data);
    }
    (void) memcpy(message.as.consecutive_frame.data, link->send_buffer + link->send_offset,
                  data_length);

    /* send message */
#ifdef ISO_TP_FRAME_PADDING
    (void) memset(message.as.consecutive_frame.data + data_length, ISO_TP_FRAME_PADDING_VALUE,
                  sizeof(message.as.consecutive_frame.data) - data_length);
    size = sizeof(message);
#else
    size = data_length + 1;
#endif

    ret = isotp_user_send_can(link->send_arbitration_id, message.as.data_array.ptr, size
#if defined(ISO_TP_USER_SEND_CAN_ARG)
                              ,
                              link->user_send_can_arg
#endif
    );

    if (ISOTP_RET_OK == ret)
    {
        link->send_offset += data_length;
        if (++(link->send_sn) > 0x0F)
        {
            link->send_sn = 0;
        }
    }

    return ret;
}

static int isotp_receive_single_frame(IsoTpLink* link, const IsoTpCanMessage* message, uint8_t len)
{
    /* check data length */
    if ((0 == message->as.single_frame.SF_DL) || (message->as.single_frame.SF_DL > (len - 1)))
    {
        isotp_user_debug("Single-frame length too small.");
        return ISOTP_RET_LENGTH;
    }

    /* copying data */
    (void) memcpy(link->receive_buffer, message->as.single_frame.data,
                  message->as.single_frame.SF_DL);
    link->receive_size = message->as.single_frame.SF_DL;

    return ISOTP_RET_OK;
}

static int isotp_receive_first_frame(IsoTpLink* link, IsoTpCanMessage* message, uint8_t len)
{
    uint8_t is_long_packet = 0;
    uint32_t payload_length;

    if (8 != len)
    {
        isotp_user_debug("First frame should be 8 bytes in length.");
        return ISOTP_RET_LENGTH;
    }

    /* check data length */
    payload_length = message->as.first_frame_short.FF_DL_high;
    payload_length = (payload_length << 8) + message->as.first_frame_short.FF_DL_low;

    /* if length is ZERO we get a long message > 4095bytes of payload */
    if (payload_length == 0)
    {
        is_long_packet = 1;
        payload_length = LE32TOH(message->as.first_frame_long.FF_DL);
    }

    /* should not use multiple frame transmition */
    if (payload_length <= 7)
    {
        isotp_user_debug("Should not use multiple frame transmission.");
        return ISOTP_RET_LENGTH;
    }

    if (payload_length > link->receive_buf_size)
    {
        isotp_user_debug("Multi-frame response too large for receiving buffer.");
        return ISOTP_RET_OVERFLOW;
    }

    /* copying data */
    if (is_long_packet)
    {
        (void) memcpy(link->receive_buffer, message->as.first_frame_long.data,
                      sizeof(message->as.first_frame_long.data));
        link->receive_offset = sizeof(message->as.first_frame_long.data);
    }
    else
    {
        (void) memcpy(link->receive_buffer, message->as.first_frame_short.data,
                      sizeof(message->as.first_frame_short.data));
        link->receive_offset = sizeof(message->as.first_frame_short.data);
    }

    link->receive_size = payload_length;
    link->receive_sn = 1;

    return ISOTP_RET_OK;
}

static int isotp_receive_consecutive_frame(IsoTpLink* link, const IsoTpCanMessage* message,
                                           uint8_t len)
{
    uint32_t remaining_bytes;

    /* check sn */
    if (link->receive_sn != message->as.consecutive_frame.SN)
    {
        return ISOTP_RET_WRONG_SN;
    }

    /* check data length */
    remaining_bytes = link->receive_size - link->receive_offset;
    if (remaining_bytes > sizeof(message->as.consecutive_frame.data))
    {
        remaining_bytes = sizeof(message->as.consecutive_frame.data);
    }
    if (remaining_bytes > (uint32_t) (len - 1))
    {
        isotp_user_debug("Consecutive frame too short.");
        return ISOTP_RET_LENGTH;
    }

    /* copying data */
    (void) memcpy(link->receive_buffer + link->receive_offset, message->as.consecutive_frame.data,
                  remaining_bytes);

    link->receive_offset += remaining_bytes;
    if (++(link->receive_sn) > 0x0F)
    {
        link->receive_sn = 0;
    }

    return ISOTP_RET_OK;
}

static int isotp_receive_flow_control_frame(IsoTpLink* link, IsoTpCanMessage* message, uint8_t len)
{
    /* unused args */
    (void) link;
    (void) message;

    /* check message length */
    if (len < 3)
    {
        isotp_user_debug("Flow control frame too short.");
        return ISOTP_RET_LENGTH;
    }

    return ISOTP_RET_OK;
}

/* ==============================================================================
 * PUBLIC FUNCTION IMPLEMENTATIONS
 * =============================================================================*/

int isotp_send(IsoTpLink* link, const uint8_t payload[], uint32_t size)
{
    return isotp_send_with_id(link, link->send_arbitration_id, payload, size);
}

int isotp_send_with_id(IsoTpLink* link, uint32_t id, const uint8_t payload[], uint32_t size)
{
    int ret;

    if (link == 0x0)
    {
        isotp_user_debug("Link is null!");
        return ISOTP_RET_ERROR;
    }

    if (size > link->send_buf_size)
    {
        isotp_user_debug("Message size too large. Increase ISO_TP_MAX_MESSAGE_SIZE to set "
                         "a larger buffer\n");

        char message[ISOTP_MAX_ERROR_MSG_SIZE] = {0};
        int32_t writtenChars = snprintf(&message[0], ISOTP_MAX_ERROR_MSG_SIZE,
                                        "Attempted to send %u bytes; max size is %u!\n",
                                        (unsigned int) size, (unsigned int) link->send_buf_size);

        assert(writtenChars <= ISOTP_MAX_ERROR_MSG_SIZE);
        (void) writtenChars;

        isotp_user_debug(message);
        return ISOTP_RET_OVERFLOW;
    }

    if (ISOTP_SEND_STATUS_INPROGRESS == link->send_status)
    {
        isotp_user_debug("Abort previous message, transmission in progress.\n");
        return ISOTP_RET_INPROGRESS;
    }

    /* copy into local buffer */
    link->send_size = size;
    link->send_offset = 0;
    (void) memcpy(link->send_buffer, payload, size);

    if (link->send_size < 8)
    {
        /* send single frame */
        ret = isotp_send_single_frame(link, id);
#ifdef ISO_TP_TRANSMIT_COMPLETE_CALLBACK
        if (ret == ISOTP_RET_OK && link->tx_done_cb)
        {
            link->tx_done_cb(link, link->send_size, link->tx_done_cb_arg);
        }
#endif
    }
    else
    {
        /* send multi-frame */
        ret = isotp_send_first_frame(link, id);

        /* init multi-frame control flags */
        if (ISOTP_RET_OK == ret)
        {
            link->send_bs_remain = 0;
            link->send_st_min_us = 0;
            link->send_wtf_count = 0;
            link->send_timer_st = isotp_user_get_us();
            link->send_timer_bs = isotp_user_get_us() + link->param_n_bs_us;
            link->send_protocol_result = ISOTP_PROTOCOL_RESULT_OK;
            link->send_status = ISOTP_SEND_STATUS_INPROGRESS;
        }
    }

    return ret;
}

void isotp_on_can_message(IsoTpLink* link, const uint8_t* data, uint8_t len)
{
    IsoTpCanMessage message;
    int ret;

    if (len < 2 || len > 8)
    {
        return;
    }

    memcpy(message.as.data_array.ptr, data, len);
    memset(message.as.data_array.ptr + len, 0, sizeof(message.as.data_array.ptr) - len);

    switch (message.as.common.type)
    {
        case ISOTP_PCI_TYPE_SINGLE:
        {
            /* update protocol result */
            if (ISOTP_RECEIVE_STATUS_INPROGRESS == link->receive_status)
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_UNEXP_PDU;
            }
            else
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_OK;
            }

            /* handle message */
            ret = isotp_receive_single_frame(link, &message, len);

            if (ISOTP_RET_OK == ret)
            {
                /* change status */
                link->receive_status = ISOTP_RECEIVE_STATUS_FULL;
            }
            break;
        }
        case ISOTP_PCI_TYPE_FIRST_FRAME:
        {
            /* update protocol result */
            if (ISOTP_RECEIVE_STATUS_INPROGRESS == link->receive_status)
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_UNEXP_PDU;
            }
            else
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_OK;
            }

            /* handle message */
            ret = isotp_receive_first_frame(link, &message, len);

            /* if overflow happened */
            if (ISOTP_RET_OVERFLOW == ret)
            {
                /* update protocol result */
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_BUFFER_OVFLW;
                /* change status */
                link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;
                /* send error message */
                isotp_send_flow_control(link, PCI_FLOW_STATUS_OVERFLOW, 0, 0);
                break;
            }

            /* if receive successful */
            if (ISOTP_RET_OK == ret)
            {
                /* change status */
                link->receive_status = ISOTP_RECEIVE_STATUS_INPROGRESS;
                /* send fc frame */
                link->receive_bs_count = link->param_block_size;
                isotp_send_flow_control(link, PCI_FLOW_STATUS_CONTINUE, link->receive_bs_count,
                                        link->param_st_min_us);
                /* refresh timer cs */
                link->receive_timer_cr = isotp_user_get_us() + link->param_n_cr_us;
            }

            break;
        }
        case ISOTP_PCI_TYPE_CONSECUTIVE_FRAME:
        {
            /* check if in receiving status */
            if (ISOTP_RECEIVE_STATUS_INPROGRESS != link->receive_status)
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_UNEXP_PDU;
                break;
            }

            /* handle message */
            ret = isotp_receive_consecutive_frame(link, &message, len);

            /* if wrong sn */
            if (ISOTP_RET_WRONG_SN == ret)
            {
                link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_WRONG_SN;
                link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;
                break;
            }

            /* if success */
            if (ISOTP_RET_OK == ret)
            {
                /* refresh timer cs */
                link->receive_timer_cr = isotp_user_get_us() + link->param_n_cr_us;

                /* receive finished */
                if (link->receive_offset >= link->receive_size)
                {
                    link->receive_status = ISOTP_RECEIVE_STATUS_FULL;
                }
                else
                {
                    /* send fc when bs reaches limit */
                    if (link->receive_bs_count > 0 && 0 == --link->receive_bs_count)
                    {
                        link->receive_bs_count = link->param_block_size;
                        isotp_send_flow_control(link, PCI_FLOW_STATUS_CONTINUE,
                                                link->receive_bs_count, link->param_st_min_us);
                    }
                }
            }

            break;
        }
        case ISOTP_PCI_TYPE_FLOW_CONTROL_FRAME:
            /* handle fc frame only when sending in progress  */
            if (ISOTP_SEND_STATUS_INPROGRESS != link->send_status)
            {
                break;
            }

            /* handle message */
            ret = isotp_receive_flow_control_frame(link, &message, len);

            if (ISOTP_RET_OK == ret)
            {
                /* refresh bs timer */
                link->send_timer_bs = isotp_user_get_us() + link->param_n_bs_us;

                /* overflow */
                if (PCI_FLOW_STATUS_OVERFLOW == message.as.flow_control.FS)
                {
                    link->send_protocol_result = ISOTP_PROTOCOL_RESULT_BUFFER_OVFLW;
                    link->send_status = ISOTP_SEND_STATUS_ERROR;
                }

                /* wait */
                else if (PCI_FLOW_STATUS_WAIT == message.as.flow_control.FS)
                {
                    link->send_wtf_count += 1;
                    /* wait exceed allowed count */
                    if (link->send_wtf_count > ISO_TP_MAX_WFT_NUMBER)
                    {
                        link->send_protocol_result = ISOTP_PROTOCOL_RESULT_WFT_OVRN;
                        link->send_status = ISOTP_SEND_STATUS_ERROR;
                    }
                }

                /* permit send */
                else if (PCI_FLOW_STATUS_CONTINUE == message.as.flow_control.FS)
                {
                    if (0 == message.as.flow_control.BS)
                    {
                        link->send_bs_remain = ISOTP_INVALID_BS;
                    }
                    else
                    {
                        link->send_bs_remain = message.as.flow_control.BS;
                    }
                    uint32_t message_st_min_us = isotp_st_min_to_us(message.as.flow_control.STmin);
                    link->send_st_min_us =
                        message_st_min_us > ISO_TP_DEFAULT_ST_MIN_US ?
                            message_st_min_us :
                            ISO_TP_DEFAULT_ST_MIN_US;  // prefer as much st_min as possible
                                                       // for stability?
                    link->send_wtf_count = 0;
                }
            }
            break;
        default:
            break;
    };

#ifdef ISO_TP_RECEIVE_COMPLETE_CALLBACK
    /* Notify user via callback if registered */
    if (link->receive_status == ISOTP_RECEIVE_STATUS_FULL && link->rx_done_cb != NULL)
    {
        link->rx_done_cb(link, link->receive_buffer, link->receive_size, link->rx_done_cb_arg);
        link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;
    }
#endif
    return;
}

int isotp_receive(IsoTpLink* link, uint8_t* payload, const uint32_t payload_size,
                  uint32_t* out_size)
{
    uint32_t copylen;

#ifdef ISO_TP_RECEIVE_COMPLETE_CALLBACK
    /* If callback is registered, isotp_receive should not be used */
    if (link->rx_done_cb != NULL)
    {
        return ISOTP_RET_ERROR; /* Callback mode active, use callback instead */
    }
#endif

    if (ISOTP_RECEIVE_STATUS_FULL != link->receive_status)
    {
        return ISOTP_RET_NO_DATA;
    }

    copylen = link->receive_size;
    if (copylen > payload_size)
    {
        copylen = payload_size;
    }

    memcpy(payload, link->receive_buffer, copylen);
    *out_size = copylen;

    link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;

    return ISOTP_RET_OK;
}

void isotp_init_link(IsoTpLink* link, uint32_t sendid, uint8_t* sendbuf, uint32_t sendbufsize,
                     uint8_t* recvbuf, uint32_t recvbufsize)
{
    memset(link, 0, sizeof(*link));
    link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;
    link->send_status = ISOTP_SEND_STATUS_IDLE;
    link->send_arbitration_id = sendid;
    link->send_buffer = sendbuf;
    link->send_buf_size = sendbufsize;
    link->receive_buffer = recvbuf;
    link->receive_buf_size = recvbufsize;

    link->param_n_bs_us = ISO_TP_DEFAULT_RESPONSE_TIMEOUT_US;
    link->param_n_cr_us = ISO_TP_DEFAULT_RESPONSE_TIMEOUT_US;
    link->param_st_min_us = ISO_TP_DEFAULT_ST_MIN_US;
    link->param_block_size = ISO_TP_DEFAULT_BLOCK_SIZE;

#ifdef ISO_TP_TRANSMIT_COMPLETE_CALLBACK
    link->tx_done_cb = NULL;
    link->tx_done_cb_arg = NULL;
#endif

#ifdef ISO_TP_RECEIVE_COMPLETE_CALLBACK
    link->rx_done_cb = NULL;
    link->rx_done_cb_arg = NULL;
#endif

    return;
}

void isotp_destroy_link(IsoTpLink* link)
{
    if (link == NULL)
    {
        return;
    }

    // Clear callbacks
#ifdef ISO_TP_TRANSMIT_COMPLETE_CALLBACK
    link->tx_done_cb = NULL;
    link->tx_done_cb_arg = NULL;
#endif

#ifdef ISO_TP_RECEIVE_COMPLETE_CALLBACK
    link->rx_done_cb = NULL;
    link->rx_done_cb_arg = NULL;
#endif

    // Reset link state (optional, but good practice)
    memset(link, 0, sizeof(IsoTpLink));
}

void isotp_set_timeouts(IsoTpLink* link, uint32_t n_bs_us, uint32_t n_cr_us)
{
    if (link != NULL)
    {
        link->param_n_bs_us = n_bs_us;
        link->param_n_cr_us = n_cr_us;
    }
}

void isotp_set_fc_params(IsoTpLink* link, uint8_t block_size, uint32_t st_min_us)
{
    if (link != NULL)
    {
        link->param_block_size = block_size;
        link->param_st_min_us = st_min_us;
    }
}

void isotp_poll(IsoTpLink* link)
{
    int ret = 0;

    /* only polling when operation in progress */
    if (ISOTP_SEND_STATUS_INPROGRESS == link->send_status)
    {
        /* continue send data */
        if (/* send data if bs_remain is invalid or bs_remain large than zero */
            (ISOTP_INVALID_BS == link->send_bs_remain || link->send_bs_remain > 0) &&
            /* and if st_min is zero or go beyond interval time */
            (0 == link->send_st_min_us || IsoTpTimeAfter(isotp_user_get_us(), link->send_timer_st)))
        {
            ret = isotp_send_consecutive_frame(link);
            if (ISOTP_RET_OK == ret)
            {
                if (ISOTP_INVALID_BS != link->send_bs_remain)
                {
                    link->send_bs_remain -= 1;
                }
                link->send_timer_bs = isotp_user_get_us() + link->param_n_bs_us;
                link->send_timer_st = isotp_user_get_us() + link->send_st_min_us;

                /* check if send finish */
                if (link->send_offset >= link->send_size)
                {
                    link->send_status = ISOTP_SEND_STATUS_IDLE;
#ifdef ISO_TP_TRANSMIT_COMPLETE_CALLBACK
                    if (link->tx_done_cb != NULL)
                    {
                        link->tx_done_cb(link, link->send_size, link->tx_done_cb_arg);
                    }
#endif
                }
            }
            else if (ISOTP_RET_NOSPACE == ret)
            {
                /* shim reported that it isn't able to send a frame at present, retry on
                 * next call
                 */
            }
            else
            {
                link->send_status = ISOTP_SEND_STATUS_ERROR;
            }
        }

        /* check timeout */
        if (IsoTpTimeAfter(isotp_user_get_us(), link->send_timer_bs))
        {
            link->send_protocol_result = ISOTP_PROTOCOL_RESULT_TIMEOUT_BS;
            link->send_status = ISOTP_SEND_STATUS_ERROR;
        }
    }

    /* only polling when operation in progress */
    if (ISOTP_RECEIVE_STATUS_INPROGRESS == link->receive_status)
    {
        /* check timeout */
        if ((link->receive_timer_cr > 0)
            && IsoTpTimeAfter(isotp_user_get_us(), link->receive_timer_cr))
        {
            link->receive_protocol_result = ISOTP_PROTOCOL_RESULT_TIMEOUT_CR;
            link->receive_status = ISOTP_RECEIVE_STATUS_IDLE;
        }
    }

    return;
}

#ifdef ISO_TP_TRANSMIT_COMPLETE_CALLBACK
void isotp_set_tx_done_cb(IsoTpLink* link, isotp_tx_done_cb cb, void* arg)
{
    if (link != NULL)
    {
        link->tx_done_cb = cb;
        link->tx_done_cb_arg = arg;
    }
}
#endif

#ifdef ISO_TP_RECEIVE_COMPLETE_CALLBACK
void isotp_set_rx_done_cb(IsoTpLink* link, isotp_rx_done_cb cb, void* arg)
{
    if (link != NULL)
    {
        link->rx_done_cb = cb;
        link->rx_done_cb_arg = arg;
    }
}
#endif