aboutsummaryrefslogtreecommitdiffstats
path: root/libs/isotp-c/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/isotp-c/src')
-rw-r--r--libs/isotp-c/src/isotp/isotp.c32
-rw-r--r--libs/isotp-c/src/isotp/isotp.h41
-rw-r--r--libs/isotp-c/src/isotp/isotp_types.h144
-rw-r--r--libs/isotp-c/src/isotp/receive.c166
-rw-r--r--libs/isotp-c/src/isotp/receive.h86
-rw-r--r--libs/isotp-c/src/isotp/send.c89
-rw-r--r--libs/isotp-c/src/isotp/send.h86
7 files changed, 644 insertions, 0 deletions
diff --git a/libs/isotp-c/src/isotp/isotp.c b/libs/isotp-c/src/isotp/isotp.c
new file mode 100644
index 00000000..ce87f1bd
--- /dev/null
+++ b/libs/isotp-c/src/isotp/isotp.c
@@ -0,0 +1,32 @@
+#include <isotp/isotp.h>
+#include <bitfield/bitfield.h>
+#include <inttypes.h>
+
+/* void isotp_set_timeout(IsoTpHandler* handler, uint16_t timeout_ms) { */
+ /* handler->timeout_ms = timeout_ms; */
+/* } */
+
+IsoTpShims isotp_init_shims(LogShim log, SendCanMessageShim send_can_message,
+ SetTimerShim set_timer) {
+ IsoTpShims shims = {
+ log: log,
+ send_can_message: send_can_message,
+ set_timer: set_timer,
+ frame_padding: ISO_TP_DEFAULT_FRAME_PADDING_STATUS
+ };
+ return shims;
+}
+
+void isotp_message_to_string(const IsoTpMessage* message, char* destination,
+ size_t destination_length) {
+ snprintf(destination, destination_length, "ID: 0x%" SCNd32 ", Payload: 0x%02x%02x%02x%02x%02x%02x%02x%02x",
+ message->arbitration_id,
+ message->payload[0],
+ message->payload[1],
+ message->payload[2],
+ message->payload[3],
+ message->payload[4],
+ message->payload[5],
+ message->payload[6],
+ message->payload[7]);
+}
diff --git a/libs/isotp-c/src/isotp/isotp.h b/libs/isotp-c/src/isotp/isotp.h
new file mode 100644
index 00000000..3a3658c7
--- /dev/null
+++ b/libs/isotp-c/src/isotp/isotp.h
@@ -0,0 +1,41 @@
+#ifndef __ISOTP_H__
+#define __ISOTP_H__
+
+#include <isotp/isotp_types.h>
+#include <isotp/send.h>
+#include <isotp/receive.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public: Initialize an IsoTpShims with the given callback functions.
+ *
+ * If any callbacks are not to be used, set them to NULL. For documentation of
+ * the function type signatures, see isotp_types.h. This struct is a handy
+ * encapsulation used to pass the shims around to the various isotp_* functions.
+ *
+ * Returns a struct with the fields initailized to the callbacks.
+ */
+IsoTpShims isotp_init_shims(LogShim log,
+ SendCanMessageShim send_can_message,
+ SetTimerShim set_timer);
+
+/* Public: Render an IsoTpMessage as a string into the given buffer.
+ *
+ * message - the message to convert to a string, for debug logging.
+ * destination - the target string buffer.
+ * destination_length - the size of the destination buffer, i.e. the max size
+ * for the rendered string.
+ */
+void isotp_message_to_string(const IsoTpMessage* message, char* destination,
+ size_t destination_length);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __ISOTP_H__
diff --git a/libs/isotp-c/src/isotp/isotp_types.h b/libs/isotp-c/src/isotp/isotp_types.h
new file mode 100644
index 00000000..3b7fd26d
--- /dev/null
+++ b/libs/isotp-c/src/isotp/isotp_types.h
@@ -0,0 +1,144 @@
+#ifndef __ISOTP_TYPES__
+#define __ISOTP_TYPES__
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#define CAN_MESSAGE_BYTE_SIZE 8
+#define MAX_ISO_TP_MESSAGE_SIZE 4096
+// TODO we want to avoid malloc, and we can't be allocated 4K on the stack for
+// each IsoTpMessage, so for now we're setting an artificial max message size
+// here - for most multi-frame use cases, 256 bytes is plenty.
+#define OUR_MAX_ISO_TP_MESSAGE_SIZE 127
+
+/* Private: IsoTp nibble specifics for PCI and Payload.
+ */
+#define PCI_NIBBLE_INDEX 0
+#define PAYLOAD_LENGTH_NIBBLE_INDEX 1
+#define PAYLOAD_BYTE_INDEX 1
+
+/* Private: The default timeout to use when waiting for a response during a
+ * multi-frame send or receive.
+ */
+#define ISO_TP_DEFAULT_RESPONSE_TIMEOUT 100
+
+/* Private: Determines if by default, padding is added to ISO-TP message frames.
+ */
+#define ISO_TP_DEFAULT_FRAME_PADDING_STATUS true
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public: A container for a sent or received ISO-TP message.
+ *
+ * completed - An IsoTpMessage is the return value from a few functions - this
+ * attribute will be true if the message is actually completely received.
+ * If the function returns but is only partially through receiving the
+ * message, this will be false, the multi_frame attribute will be true,
+ * and you should not consider the other data to be valid.
+ * multi_frame - Designates the message is being built with multi-frame.
+ * arbitration_id - The arbitration ID of the message.
+ * payload - The optional payload of the message - don't forget to check the
+ * size!
+ * size - The size of the payload. The size will be 0 if there is no payload.
+ */
+typedef struct {
+ const uint32_t arbitration_id;
+ uint8_t payload[OUR_MAX_ISO_TP_MESSAGE_SIZE];
+ uint16_t size;
+ bool completed;
+ bool multi_frame;
+} IsoTpMessage;
+
+/* Public: The type signature for an optional logging function, if the user
+ * wishes to provide one. It should print, store or otherwise display the
+ * message.
+ *
+ * message - A format string to log using the given parameters.
+ * ... (vargs) - the parameters for the format string.
+ */
+typedef void (*LogShim)(const char* message, ...);
+/* Public: The type signature for a function to send a single CAN message.
+ *
+ * arbitration_id - The arbitration ID of the message.
+ * data - The data payload for the message. NULL is valid if size is also 0.
+ * size - The size of the data payload, in bytes.
+ *
+ * Returns true if the CAN message was sent successfully.
+ */
+typedef bool (*SendCanMessageShim)(const uint32_t arbitration_id,
+ const uint8_t* data, const uint8_t size);
+
+/* Public: The type signature for a... TODO, not used yet.
+ */
+typedef bool (*SetTimerShim)(uint16_t time_ms, void (*callback));
+
+/* Public: The signature for a function to be called when an ISO-TP message has
+ * been completely received.
+ *
+ * message - The received message.
+ */
+typedef void (*IsoTpMessageReceivedHandler)(const IsoTpMessage* message);
+
+/* Public: the signature for a function to be called when an ISO-TP message has
+ * been completely sent, or had a fatal error during sending.
+ *
+ * message - The sent message.
+ * success - True if the message was sent successfully.
+ */
+typedef void (*IsoTpMessageSentHandler)(const IsoTpMessage* message,
+ const bool success);
+
+/* Public: The signature for a function to be called when a CAN frame has been
+ * sent as as part of sending or receive an ISO-TP message.
+ *
+ * This is really only useful for debugging the library itself.
+ *
+ * message - The ISO-TP message that generated this CAN frame.
+ */
+typedef void (*IsoTpCanFrameSentHandler)(const IsoTpMessage* message);
+
+/* Public: A container for the 3 shim functions used by the library to interact
+ * with the wider system.
+ *
+ * Use the isotp_init_shims(...) function to create an instance of this struct.
+ *
+ * By default, all CAN frames sent from this device in the process of an ISO-TP
+ * message are padded out to a complete 8 byte frame. This is often required by
+ * ECUs. To disable this feature, change the 'frame_padding' field to false on
+ * the IsoTpShims object returned from isotp_init_shims(...).
+ *
+ * frame_padding - true if outgoing CAN frames should be padded to a full 8
+ * bytes.
+ */
+typedef struct {
+ LogShim log;
+ SendCanMessageShim send_can_message;
+ SetTimerShim set_timer;
+ bool frame_padding;
+} IsoTpShims;
+
+/* Private: PCI types, for identifying each frame of an ISO-TP message.
+ */
+typedef enum {
+ PCI_SINGLE = 0x0,
+ PCI_FIRST_FRAME = 0x1,
+ PCI_CONSECUTIVE_FRAME = 0x2,
+ PCI_FLOW_CONTROL_FRAME = 0x3
+} IsoTpProtocolControlInformation;
+
+/* Private: PCI flow control identifiers.
+ */
+typedef enum {
+ PCI_FLOW_STATUS_CONTINUE = 0x0,
+ PCI_FLOW_STATUS_WAIT = 0x1,
+ PCI_FLOW_STATUS_OVERFLOW = 0x2
+} IsoTpFlowStatus;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __ISOTP_TYPES__
diff --git a/libs/isotp-c/src/isotp/receive.c b/libs/isotp-c/src/isotp/receive.c
new file mode 100644
index 00000000..35b7a2a6
--- /dev/null
+++ b/libs/isotp-c/src/isotp/receive.c
@@ -0,0 +1,166 @@
+#include <isotp/receive.h>
+#include <isotp/send.h>
+#include <bitfield/bitfield.h>
+#include <string.h>
+#include <stdlib.h>
+
+#define ARBITRATION_ID_OFFSET 0x8
+
+static void isotp_complete_receive(IsoTpReceiveHandle* handle, IsoTpMessage* message) {
+ if(handle->message_received_callback != NULL) {
+ handle->message_received_callback(message);
+ }
+}
+
+bool isotp_handle_single_frame(IsoTpReceiveHandle* handle, IsoTpMessage* message) {
+ isotp_complete_receive(handle, message);
+ return true;
+}
+
+bool isotp_handle_multi_frame(IsoTpReceiveHandle* handle, IsoTpMessage* message) {
+ // call this once all consecutive frames have been received
+ isotp_complete_receive(handle, message);
+ return true;
+}
+
+bool isotp_send_flow_control_frame(IsoTpShims* shims, IsoTpMessage* message) {
+ uint8_t can_data[CAN_MESSAGE_BYTE_SIZE] = {0};
+
+ if(!set_nibble(PCI_NIBBLE_INDEX, PCI_FLOW_CONTROL_FRAME, can_data, sizeof(can_data))) {
+ shims->log("Unable to set PCI in CAN data");
+ return false;
+ }
+
+ shims->send_can_message(message->arbitration_id - ARBITRATION_ID_OFFSET, can_data,
+ shims->frame_padding ? 8 : 1 + message->size);
+ return true;
+}
+
+
+IsoTpReceiveHandle isotp_receive(IsoTpShims* shims,
+ const uint32_t arbitration_id, IsoTpMessageReceivedHandler callback) {
+ IsoTpReceiveHandle handle = {
+ success: false,
+ completed: false,
+ arbitration_id: arbitration_id,
+ message_received_callback: callback
+ };
+
+ return handle;
+}
+
+IsoTpMessage isotp_continue_receive(IsoTpShims* shims,
+ IsoTpReceiveHandle* handle, const uint32_t arbitration_id,
+ const uint8_t data[], const uint8_t size) {
+ IsoTpMessage message = {
+ arbitration_id: arbitration_id,
+ completed: false,
+ multi_frame: false,
+ payload: {0},
+ size: 0
+ };
+
+ if(size < 1) {
+ return message;
+ }
+
+ if(handle->arbitration_id != arbitration_id) {
+ if(shims->log != NULL) {
+ // You may turn this on for debugging, but in normal operation it's
+ // very noisy if you are passing all received CAN messages to this
+ // handler.
+ /* shims->log("The arb ID 0x%x doesn't match the expected rx ID 0x%x", */
+ /* arbitration_id, handle->arbitration_id); */
+ }
+ return message;
+ }
+
+ IsoTpProtocolControlInformation pci = (IsoTpProtocolControlInformation)
+ get_nibble(data, size, 0);
+
+ // TODO this is set up to handle rx a response with a payload, but not to
+ // handle flow control responses for multi frame messages that we're in the
+ // process of sending
+
+ switch(pci) {
+ case PCI_SINGLE: {
+ uint8_t payload_length = get_nibble(data, size, 1);
+
+ if(payload_length > 0) {
+ memcpy(message.payload, &data[1], payload_length);
+ }
+
+ message.size = payload_length;
+ message.completed = true;
+ handle->success = true;
+ handle->completed = true;
+ isotp_handle_single_frame(handle, &message);
+ break;
+ }
+ //If multi-frame, then the payload length is contained in the 12
+ //bits following the first nibble of Byte 0.
+ case PCI_FIRST_FRAME: {
+ uint16_t payload_length = (get_nibble(data, size, 1) << 8) + get_byte(data, size, 1);
+
+ if(payload_length > OUR_MAX_ISO_TP_MESSAGE_SIZE) {
+ shims->log("Multi-frame response too large for receive buffer.");
+ break;
+ }
+
+ //Need to allocate memory for the combination of multi-frame
+ //messages. That way we don't have to allocate 4k of memory
+ //for each multi-frame response.
+ uint8_t* combined_payload = NULL;
+ combined_payload = (uint8_t*)malloc(sizeof(uint8_t)*payload_length);
+
+ if(combined_payload == NULL) {
+ shims->log("Unable to allocate memory for multi-frame response.");
+ break;
+ }
+
+ memcpy(combined_payload, &data[2], CAN_MESSAGE_BYTE_SIZE - 2);
+ handle->receive_buffer = combined_payload;
+ handle->received_buffer_size = CAN_MESSAGE_BYTE_SIZE - 2;
+ handle->incoming_message_size = payload_length;
+
+ message.multi_frame = true;
+ handle->success = false;
+ handle->completed = false;
+ isotp_send_flow_control_frame(shims, &message);
+ break;
+ }
+ case PCI_CONSECUTIVE_FRAME: {
+ uint8_t start_index = handle->received_buffer_size;
+ uint8_t remaining_bytes = handle->incoming_message_size - start_index;
+ message.multi_frame = true;
+
+ if(remaining_bytes > 7) {
+ memcpy(&handle->receive_buffer[start_index], &data[1], CAN_MESSAGE_BYTE_SIZE - 1);
+ handle->received_buffer_size = start_index + 7;
+ } else {
+ memcpy(&handle->receive_buffer[start_index], &data[1], remaining_bytes);
+ handle->received_buffer_size = start_index + remaining_bytes;
+
+ if(handle->received_buffer_size != handle->incoming_message_size){
+ free(handle->receive_buffer);
+ handle->success = false;
+ shims->log("Error capturing all bytes of multi-frame. Freeing memory.");
+ } else {
+ memcpy(message.payload,&handle->receive_buffer[0],handle->incoming_message_size);
+ free(handle->receive_buffer);
+ message.size = handle->incoming_message_size;
+ message.completed = true;
+ shims->log("Successfully captured all of multi-frame. Freeing memory.");
+
+ handle->success = true;
+ handle->completed = true;
+ isotp_handle_multi_frame(handle, &message);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return message;
+}
diff --git a/libs/isotp-c/src/isotp/receive.h b/libs/isotp-c/src/isotp/receive.h
new file mode 100644
index 00000000..6788914a
--- /dev/null
+++ b/libs/isotp-c/src/isotp/receive.h
@@ -0,0 +1,86 @@
+#ifndef __ISOTP_RECEIVE_H__
+#define __ISOTP_RECEIVE_H__
+
+#include <isotp/isotp.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public: A handle for beginning and continuing receiving a single ISO-TP
+ * message - both single and multi-frame.
+ *
+ * Since an ISO-TP message may contain multiple frames, we need to keep a handle
+ * around while waiting for subsequent CAN messages to complete the message.
+ * This struct encapsulates the local state required.
+ *
+ * completed - True if the received message request is completely finished.
+ * success - True if the message request was successful. The value if this field
+ * isn't valid if 'completed' isn't true.
+ */
+typedef struct {
+ bool completed;
+ bool success;
+
+ // Private
+ uint32_t arbitration_id;
+ IsoTpMessageReceivedHandler message_received_callback;
+ uint16_t timeout_ms;
+ // timeout_ms: ISO_TP_DEFAULT_RESPONSE_TIMEOUT,
+ uint8_t* receive_buffer;
+ uint16_t received_buffer_size;
+ uint16_t incoming_message_size;
+ // TODO timer callback for multi frame
+} IsoTpReceiveHandle;
+
+/* Public: Initiate receiving a single ISO-TP message on a particular
+ * arbitration ID.
+ *
+ * Note that no actual CAN data has been received at this point - this just sets
+ * up a handle to be used when new CAN messages to arrive, so they can be parsed
+ * as potential single or multi-frame ISO-TP messages.
+ *
+ * shims - Low-level shims required to send and receive CAN messages, etc.
+ * arbitration_id - The arbitration ID to receive the message on.
+ * callback - an optional function to be called when the message is completely
+ * received (use NULL if no callback required).
+ *
+ * Returns a handle to be used with isotp_continue_receive when a new CAN frame
+ * arrives. The 'completed' field in the returned IsoTpReceiveHandle will be true
+ * when the message is completely sent.
+ */
+IsoTpReceiveHandle isotp_receive(IsoTpShims* shims,
+ const uint32_t arbitration_id, IsoTpMessageReceivedHandler callback);
+
+/* Public: Continue to receive a an ISO-TP message, based on a freshly
+ * received CAN message.
+ *
+ * For a multi-frame ISO-TP message, this function must be called
+ * repeatedly whenever a new CAN message is received in order to complete
+ * receipt.
+ *
+ * TODO does this API work for if we wanted to receive an ISO-TP message and
+ * send our own flow control messages back?
+ *
+ * shims - Low-level shims required to send and receive CAN messages, etc.
+ * handle - An IsoTpReceiveHandle previously returned by isotp_receive(...).
+ * arbitration_id - The arbitration_id of the received CAN message.
+ * data - The data of the received CAN message.
+ * size - The size of the data in the received CAN message.
+ *
+ * Returns an IsoTpMessage with the 'completed' field set to true if a message
+ * was completely received. If 'completed' is false, more CAN frames are
+ * required to complete the messages, or the arbitration ID didn't match this
+ * handle. Keep passing the same handle to this function when CAN frames arrive.
+ */
+IsoTpMessage isotp_continue_receive(IsoTpShims* shims,
+ IsoTpReceiveHandle* handle, const uint32_t arbitration_id,
+ const uint8_t data[], const uint8_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __ISOTP_RECEIVE_H__
diff --git a/libs/isotp-c/src/isotp/send.c b/libs/isotp-c/src/isotp/send.c
new file mode 100644
index 00000000..e849bb2c
--- /dev/null
+++ b/libs/isotp-c/src/isotp/send.c
@@ -0,0 +1,89 @@
+#include <isotp/send.h>
+#include <bitfield/bitfield.h>
+#include <string.h>
+
+#define PCI_NIBBLE_INDEX 0
+#define PAYLOAD_LENGTH_NIBBLE_INDEX 1
+#define PAYLOAD_BYTE_INDEX 1
+
+void isotp_complete_send(IsoTpShims* shims, IsoTpMessage* message,
+ bool status, IsoTpMessageSentHandler callback) {
+ if(callback != NULL) {
+ callback(message, status);
+ }
+}
+
+IsoTpSendHandle isotp_send_single_frame(IsoTpShims* shims, IsoTpMessage* message,
+ IsoTpMessageSentHandler callback) {
+ IsoTpSendHandle handle = {
+ success: false,
+ completed: true
+ };
+
+ uint8_t can_data[CAN_MESSAGE_BYTE_SIZE] = {0};
+ if(!set_nibble(PCI_NIBBLE_INDEX, PCI_SINGLE, can_data, sizeof(can_data))) {
+ shims->log("Unable to set PCI in CAN data");
+ return handle;
+ }
+
+ if(!set_nibble(PAYLOAD_LENGTH_NIBBLE_INDEX, message->size, can_data,
+ sizeof(can_data))) {
+ shims->log("Unable to set payload length in CAN data");
+ return handle;
+ }
+
+ if(message->size > 0) {
+ memcpy(&can_data[1], message->payload, message->size);
+ }
+
+ shims->send_can_message(message->arbitration_id, can_data,
+ shims->frame_padding ? 8 : 1 + message->size);
+ handle.success = true;
+ isotp_complete_send(shims, message, true, callback);
+ return handle;
+}
+
+IsoTpSendHandle isotp_send_multi_frame(IsoTpShims* shims, IsoTpMessage* message,
+ IsoTpMessageSentHandler callback) {
+ // TODO make sure to copy message into a local buffer
+ shims->log("Only single frame messages are supported");
+ IsoTpSendHandle handle = {
+ success: false,
+ completed: true
+ };
+ // TODO need to set sending and receiving arbitration IDs separately if we
+ // can't always just add 0x8 (and I think we can't)
+ return handle;
+}
+
+IsoTpSendHandle isotp_send(IsoTpShims* shims, const uint16_t arbitration_id,
+ const uint8_t payload[], uint16_t size,
+ IsoTpMessageSentHandler callback) {
+ IsoTpMessage message = {
+ arbitration_id: arbitration_id,
+ size: size
+ };
+
+ memcpy(message.payload, payload, size);
+ if(size < 8) {
+ return isotp_send_single_frame(shims, &message, callback);
+ } else {
+ return isotp_send_multi_frame(shims, &message, callback);
+ }
+}
+
+bool isotp_continue_send(IsoTpShims* shims, IsoTpSendHandle* handle,
+ const uint16_t arbitration_id, const uint8_t data[],
+ const uint8_t size) {
+ // TODO this will need to be tested when we add multi-frame support,
+ // which is when it'll be necessary to pass in CAN messages to SENDING
+ // handles.
+ if(handle->receiving_arbitration_id != arbitration_id) {
+ if(shims->log != NULL) {
+ shims->log("The arb ID 0x%x doesn't match the expected tx continuation ID 0x%x",
+ arbitration_id, handle->receiving_arbitration_id);
+ }
+ return false;
+ }
+ return false;
+}
diff --git a/libs/isotp-c/src/isotp/send.h b/libs/isotp-c/src/isotp/send.h
new file mode 100644
index 00000000..1af32668
--- /dev/null
+++ b/libs/isotp-c/src/isotp/send.h
@@ -0,0 +1,86 @@
+#ifndef __ISOTP_SEND_H__
+#define __ISOTP_SEND_H__
+
+#include <isotp/isotp.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public: A handle for beginning and continuing sending a single ISO-TP
+ * message - both single and multi-frame.
+ *
+ * Since an ISO-TP message may contain multiple frames, we need to keep a handle
+ * around while waiting for flow control messages from the receiver.
+ * This struct encapsulates the local state required.
+ *
+ * completed - True if the message was completely sent, or the send was
+ * otherwise cancelled.
+ * success - True if the message send request was successful. The value if this
+ * field isn't valid if 'completed' isn't true.
+ */
+typedef struct {
+ bool completed;
+ bool success;
+
+ // Private
+ uint16_t sending_arbitration_id;
+ uint16_t receiving_arbitration_id;
+ IsoTpMessageSentHandler message_sent_callback;
+ IsoTpCanFrameSentHandler can_frame_sent_callback;
+ // TODO going to need some state here for multi frame messages
+} IsoTpSendHandle;
+
+/* Public: Initiate sending a single ISO-TP message.
+ *
+ * If the message fits in a single ISO-TP frame (i.e. the payload isn't more
+ * than 7 bytes) it will be sent immediately and the returned IsoTpSendHandle's
+ * 'completed' flag will be true.
+ *
+ * For multi-frame messages, see isotp_continue_send(...).
+ *
+ * shims - Low-level shims required to send CAN messages, etc.
+ * arbitration_id - The arbitration ID to send the message on.
+ * payload - The payload for the message. If no payload, NULL is valid is size
+ * is also 0.
+ * size - The size of the payload, or 0 if no payload.
+ * callback - an optional function to be called when the message is completely
+ * sent (use NULL if no callback required).
+ *
+ * Returns a handle to be used with isotp_continue_send to continue sending
+ * multi-frame messages. The 'completed' field in the returned IsoTpSendHandle
+ * will be true when the message is completely sent.
+ */
+IsoTpSendHandle isotp_send(IsoTpShims* shims, const uint16_t arbitration_id,
+ const uint8_t payload[], uint16_t size,
+ IsoTpMessageSentHandler callback);
+
+/* Public: Continue to send a multi-frame ISO-TP message, based on a freshly
+ * received CAN message (potentially from the receiver about flow control).
+ *
+ * For a multi-frame ISO-TP message, this function must be called
+ * repeatedly whenever a new CAN message is received in order to complete the
+ * send. The sender can't just blast everything onto the bus at once - it must
+ * wait for some response from the receiver to know how much to send at once.
+ *
+ * shims - Low-level shims required to send CAN messages, etc.
+ * handle - An IsoTpSendHandle previously returned by isotp_send(...).
+ * arbitration_id - The arbitration_id of the received CAN message.
+ * data - The data of the received CAN message.
+ * size - The size of the data in the received CAN message.
+ *
+ * Returns true if the message was completely sent, or the send was
+ * otherwise cancelled. Check the 'success' field of the handle to see if
+ * it was successful.
+ */
+bool isotp_continue_send(IsoTpShims* shims, IsoTpSendHandle* handle,
+ const uint16_t arbitration_id, const uint8_t data[],
+ const uint8_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __ISOTP_SEND_H__