diff --git a/.gitignore b/.gitignore index 52ed8e0..6dc0790 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ cmake-build-debug/ tf.bin + +.idea/ + diff --git a/CMakeLists.txt b/CMakeLists.txt index 32c5953..9fbd81f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.7) project(tf) -set(CMAKE_CXX_STANDARD GNU99) +set(CMAKE_CXX_STANDARD GNU89) set(SOURCE_FILES test.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0124af5 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +build: tf.bin + +run: tf.bin + ./tf.bin + +debug: tf.bin + gdb -q -ex run ./tf.bin + +tf.bin: test.c TinyFrame.c TinyFrame.h + gcc -Os --std=gnu89 -Wall -Wno-main -Wno-unused -Wextra test.c TinyFrame.c -I. -o tf.bin diff --git a/README.md b/README.md index 8718fe5..99b054f 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,113 @@ # TinyFrame -TinyFrame is a simple library for building and parsing frames -(packets) to be sent over a serial interface (like UART). It's implemented -to be compatible with C89 and platform agnostic. +TinyFrame is a simple library for building and parsing frames to be sent +over a serial interface (e.g. UART, telnet etc.). The code is written +in `--std=gnu89`. -Frames are protected by a checksum and contain a "unique" ID, -which can be used for chaining messages. Each peer uses a different -value for the first bit of all IDs it generates (the "master flag" -or "peer_bit") to ensure there are no clashes. Typically the master -(PC, main microcontroller) will use "1" and the surrogate (WiFi module, -USB-serial connected gadget, display driver...) uses "0". +Frames are protected by a checksum (~XOR, CRC16 or CRC32) and contain +a unique ID field, which can be used for chaining messages. The highest value +of the ID is different for each peer (TF_MASTER or TF_SLAVE) to avoid collisions. -The library lets you bind listeners waiting for any frame, or a -particular ID. This allows for easy implementation of async communication. +All fields in the frame have configurable size (see the top of the header file). +By just changing a value, like `TF_LEN_BYTES`, the library seamlessly switches +from `uint8_t` to `uint16_t` or `uint32_t` to support longer payloads. -## Frame structure +The library lets you bind listeners waiting for any frame, a particular frame Type, +or a specific message ID. This lets you easily implement asynchronous +communication. -The frame makeup is inspired by that of SBMP (my other, more complicated -and advanced UART protocol library). +## Frame structure ``` - - -SOF ... start of frame, 0x01 -ID ... (master_flag | 7-bit counter) - the frame ID -NOB ... nr of payload bytes in the frame (1..256) -PAYLOAD ... NOB bytes of data, can contain any byte values 1..256 -CKSUM ... checksum, implemented as XOR of all preceding bytes in the message +,-----+----+-----+------+------------+- - - -+------------, +| SOF | ID | LEN | TYPE | HEAD_CKSUM | DATA | PLD_CKSUM | +| 1 | ? | ? | ? | ? | ... | ? | <- size (bytes) +'-----+----+-----+------+------------+- - - -+------------' + +SOF ......... start of frame, 0x01 +ID ......... the frame ID (MSb is the peer bit) +LEN ......... nr of data bytes in the frame +TYPE ........ message type (used to run Type Listeners, pick any values you like) +HEAD_CKSUM .. header checksum +DATA ........ LEN bytes of data +DATA_CKSUM .. checksum, implemented as XOR of all preceding bytes in the message ``` -The frame ID (in SBMP called "session ID") can be used to chain multiple related -messages and maintain the context this way. For example, a response may copy -the frame ID of the request frame, which then triggers a callback bound by the -requesting peer. Such behavior is application specific and is thus left to the -upper layers of the protocol. - - -## Usage hints - -- Both sides of the protocol (slave and master) should include the same TinyFrame -code. -- Master inits the lib with `TF_Init(1);`, while slave uses `TF_Init(0);`. This is to avoid a message ID conflict. -- Both sides can add Generic and Type listeners (callbacks) using `TF_AddGenericListener(func)` and `TF_AddTypeListener(type, func)`. The listener is a function as showin in the example file test.c or declared in TinyFrame.h - - `bool myListener(unsigned int frame_id, const unsigned char *buff, unsigned int len) { ... }` - - The listener returns `true` if the message was consumed. If it returns `false`, it can be handled by some other listener (possibly a Generic Listener, if you added one) -- A message is sent using `TF_Send()`, and to use it, the `TF_WriteImpl()` stub must be implemented in the application code. See `test.c` for an example. - - There are also helper functions `TF_Send1()` and `TF_Send2()` which send one or two bytes. -- To reply, use `TF_Respond()` with ID same as in the received message (the listener gets this as it's argument). A listener provided as the last parameter to `TF_Send()` will be called after receiving the response. - -- To remove a listener, use `TF_RemoveListener()`. *Always remove your ID listeners after handling the response!* There's a limit to the number of listeners. - -- The function `TF_Accept()` is used to handle received chars. Call this in your UART Rx interrupt handler or a similar place. +## Usage Hints + +- Both peers must include the library with the same parameters (config in the header file) +- Start by calling `TF_Init()` with MASTER or SLAVE as the argument +- Implement `TF_WriteImpl()` - declared at the bottom of the header file as `extern`. + This function is used by `TF_Send()` to write bytes to your UART (or other physical layer). + Presently, always a full frame is sent to this function. +- If you wish to use `TF_PARSER_TIMEOUT_TICKS`, periodically call `TF_Tick()`. The period + determines the length of 1 tick. This is used to time-out the parser in case it gets stuck + in a bad state (such as receiving a partial frame). +- Bind Type or Generic listeners using `TF_AddTypeListener()` or `TF_AddGenericListener()`. +- Send a message using `TF_Send()` or the other Send functions. + If you provide a listener callback (function pointer) to the function, + the listener will be added as an ID listener and wait for a response. +- To reply to a message (when your listener gets called), use `TF_Respond()` + with the same frame_id as in the received message. +- Remove the ID listener using `TF_RemoveIdListener()` when it's no longer + needed. (Same for other listener types.) The slot count is limited. +- If the listener function returns `false`, some other listener will get + a chance to handle it +- Manually reset the parser using `TF_ResetParser()` + +### The concept of listeners + +Listeners are callback functions that are called by TinyFrame when a message which +they can handle is received. + +There are 3 listener types: + +- ID listeners +- Type listeners +- Generic listeners + +They handle the message in this order, and if they decide not to handle it, they can return `false` +and let it be handled by some other listener, or discarded. + +### Implementing "synchronous query" + +Sometimes it's necessary to send a message and wait for a response to arrive. + +One (not too pretty) way to do this is using a global variable - pseudocode: + +```c +#define MSG_PING 42 +volatile bool onResponse_done = false; + +/** ID listener */ +bool onResponse(TF_ID frame_id, TF_TYPE type, const uint8_t *data, TF_LEN len) +{ + // ... Do something ... + // (eg. copy data to a global variable) + + onResponse_done = true; + return true; +} + +bool syncQuery(void) +{ + TF_ID id; + // Send our request + TF_Send0(MSG_PING, onResponse, &id); // Send0 sends zero bytes of data, just TYPE + + // Wait for the response + bool suc = true; + while (!onResponse_done) { + //delay + if (/*timeout*/) { + TF_RemoveIdListener(id); // free the listener slot + return false; + } + } + + // ... Do something with the received data? ... + // (can be passed from the listener using a global variable) + + return true; +} +``` diff --git a/TinyFrame.c b/TinyFrame.c index 5ac8a77..000346a 100644 --- a/TinyFrame.c +++ b/TinyFrame.c @@ -3,6 +3,13 @@ #include //--------------------------------------------------------------------------- +// Compatibility with ESP8266 SDK +#ifdef ICACHE_FLASH_ATTR + #define _TF_FN ICACHE_FLASH_ATTR +#else + #define _TF_FN +#endif + enum TFState { TFState_SOF = 0, //!< Wait for SOF TFState_LEN, //!< Wait for Number Of Bytes @@ -14,12 +21,12 @@ enum TFState { }; typedef struct _IdListener_struct { - unsigned int id; + TF_ID id; TF_LISTENER fn; } IdListener; typedef struct _TypeListener_struct_ { - unsigned char type; + TF_TYPE type; TF_LISTENER fn; } TypeListener; @@ -45,6 +52,7 @@ static struct TinyFrameStruct { TF_CKSUM cksum; //!< Checksum calculated of the data stream TF_CKSUM ref_cksum; //!< Reference checksum read from the message TF_TYPE type; //!< Collected message type number + bool discard_data; //!< Set if (len > TF_MAX_PAYLOAD) to read the frame, but ignore the data. /* --- Callbacks --- */ @@ -57,17 +65,30 @@ static struct TinyFrameStruct { size_t count_type_lst; size_t count_generic_lst; + // Buffer for building frames uint8_t sendbuf[TF_MAX_PAYLOAD + TF_OVERHEAD_BYTES]; } tf; +//region Checksums + +#if TF_CKSUM_TYPE == 0 + +// NONE +#define CKSUM_RESET(cksum) +#define CKSUM_ADD(cksum, byte) +#define CKSUM_FINALIZE(cksum) -//region Optional impls -#if TF_USE_CRC16 +#elif TF_CKSUM_TYPE == 8 -// ---- CRC16 checksum impl ---- +// ~XOR +#define CKSUM_RESET(cksum) do { cksum = 0; } while (0) +#define CKSUM_ADD(cksum, byte) do { cksum ^= byte; } while(0) +#define CKSUM_FINALIZE(cksum) do { cksum = ~cksum; } while(0) + +#elif TF_CKSUM_TYPE == 16 /** CRC table for the CRC-16. The poly is 0x8005 (x^16 + x^15 + x^2 + 1) */ -const uint16_t crc16_table[256] = { +static const uint16_t crc16_table[256] = { 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, @@ -102,28 +123,78 @@ const uint16_t crc16_table[256] = { 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 }; -static inline uint16_t crc16_byte(uint16_t crc, const uint8_t data) +static inline uint16_t crc16_byte(uint16_t cksum, const uint8_t byte) { - return (crc >> 8) ^ crc16_table[(crc ^ data) & 0xff]; + return (cksum >> 8) ^ crc16_table[(cksum ^ byte) & 0xff]; } -#endif -//endregion Optional impls +#define CKSUM_RESET(cksum) do { cksum = 0; } while (0) +#define CKSUM_ADD(cksum, byte) do { cksum = crc16_byte(cksum, byte); } while(0) +#define CKSUM_FINALIZE(cksum) + +#elif TF_CKSUM_TYPE == 32 + +static const uint32_t crc32_table[] = { /* CRC polynomial 0xedb88320 */ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, + 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, + 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, + 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, + 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, + 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, + 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, + 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, + 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, + 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; -// --- macros based on config --- +static inline uint32_t crc32_byte(uint32_t cksum, const uint8_t byte) +{ + return (crc32_table[((cksum) ^ ((uint8_t)byte)) & 0xff] ^ ((cksum) >> 8)); +} -#define CKSUM_RESET(cksum) do { cksum = 0; } while (0) +#define CKSUM_RESET(cksum) do { cksum = (TF_CKSUM)0xFFFFFFFF; } while (0) +#define CKSUM_ADD(cksum, byte) do { cksum = crc32_byte(cksum, byte); } while(0) +#define CKSUM_FINALIZE(cksum) do { cksum = (TF_CKSUM)~cksum; } while(0) -#if TF_USE_CRC16 - // CRC16 checksum - #define CKSUM_ADD(cksum, byte) do { cksum = crc16_byte(cksum, byte); } while(0) -#else - // XOR checksum - #define CKSUM_ADD(cksum, byte) do { cksum ^= byte; } while(0) #endif +//endregion -void TF_Init(TF_PEER peer_bit) + +void _TF_FN TF_Init(TF_PEER peer_bit) { // Zero it out memset(&tf, 0, sizeof(struct TinyFrameStruct)); @@ -133,7 +204,7 @@ void TF_Init(TF_PEER peer_bit) //region Listeners -bool TF_AddIdListener(TF_ID frame_id, TF_LISTENER cb) +bool _TF_FN TF_AddIdListener(TF_ID frame_id, TF_LISTENER cb) { size_t i; for (i = 0; i < TF_MAX_ID_LST; i++) { @@ -149,7 +220,7 @@ bool TF_AddIdListener(TF_ID frame_id, TF_LISTENER cb) return false; } -bool TF_AddTypeListener(unsigned char frame_type, TF_LISTENER cb) +bool _TF_FN TF_AddTypeListener(TF_TYPE frame_type, TF_LISTENER cb) { size_t i; for (i = 0; i < TF_MAX_TYPE_LST; i++) { @@ -165,7 +236,7 @@ bool TF_AddTypeListener(unsigned char frame_type, TF_LISTENER cb) return false; } -bool TF_AddGenericListener(TF_LISTENER cb) +bool _TF_FN TF_AddGenericListener(TF_LISTENER cb) { size_t i; for (i = 0; i < TF_MAX_GEN_LST; i++) { @@ -180,7 +251,7 @@ bool TF_AddGenericListener(TF_LISTENER cb) return false; } -bool TF_RemoveIdListener(TF_ID frame_id) +bool _TF_FN TF_RemoveIdListener(TF_ID frame_id) { size_t i; for (i = 0; i < tf.count_id_lst; i++) { @@ -196,7 +267,7 @@ bool TF_RemoveIdListener(TF_ID frame_id) return false; } -bool TF_RemoveTypeListener(unsigned char type) +bool _TF_FN TF_RemoveTypeListener(TF_TYPE type) { size_t i; for (i = 0; i < tf.count_type_lst; i++) { @@ -212,7 +283,7 @@ bool TF_RemoveTypeListener(unsigned char type) return false; } -bool TF_RemoveGenericListener(TF_LISTENER cb) +bool _TF_FN TF_RemoveGenericListener(TF_LISTENER cb) { size_t i; for (i = 0; i < tf.count_generic_lst; i++) { @@ -228,7 +299,7 @@ bool TF_RemoveGenericListener(TF_LISTENER cb) } /** Handle a message that was just collected & verified by the parser */ -static void TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data) +static void _TF_FN TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data, TF_LEN data_len) { size_t i; @@ -240,8 +311,8 @@ static void TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data // ID listeners first for (i = 0; i < tf.count_id_lst; i++) { - if (tf.id_listeners[i].fn && tf.id_listeners[i].id == tf.id) { - if (tf.id_listeners[i].fn(tf.id, tf.type, tf.data, tf.len)) { + if (tf.id_listeners[i].fn && (tf.id_listeners[i].id == frame_id)) { + if (tf.id_listeners[i].fn(frame_id, type, data, data_len)) { return; } } @@ -249,8 +320,8 @@ static void TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data // Type listeners for (i = 0; i < tf.count_type_lst; i++) { - if (tf.type_listeners[i].fn && tf.type_listeners[i].type == tf.type) { - if (tf.type_listeners[i].fn(tf.id, tf.type, tf.data, tf.len)) { + if (tf.type_listeners[i].fn && (tf.type_listeners[i].type == type)) { + if (tf.type_listeners[i].fn(frame_id, type, data, data_len)) { return; } } @@ -259,7 +330,7 @@ static void TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data // Generic listeners for (i = 0; i < tf.count_generic_lst; i++) { if (tf.generic_listeners[i].fn) { - if (tf.generic_listeners[i].fn(tf.id, tf.type, tf.data, tf.len)) { + if (tf.generic_listeners[i].fn(frame_id, type, data, data_len)) { return; } } @@ -268,7 +339,7 @@ static void TF_HandleReceivedMessage(TF_ID frame_id, TF_TYPE type, uint8_t *data //endregion Listeners -void TF_Accept(const uint8_t *buffer, size_t count) +void _TF_FN TF_Accept(const uint8_t *buffer, size_t count) { size_t i; for (i = 0; i < count; i++) { @@ -276,16 +347,28 @@ void TF_Accept(const uint8_t *buffer, size_t count) } } -void TF_ResetParser(void) +void _TF_FN TF_ResetParser(void) { tf.state = TFState_SOF; } -void TF_AcceptChar(unsigned char c) -{ - // QUEUE IF PARSER LOCKED - // FIRST PROCESS ALL QUEUED +/** SOF was received */ +static void _TF_FN TF_ParsBeginFrame(void) { + // Reset state vars + CKSUM_RESET(tf.cksum); + #if TF_USE_SOF_BYTE + CKSUM_ADD(tf.cksum, TF_SOF_BYTE); + #endif + + tf.discard_data = false; + // Enter ID state + tf.state = TFState_ID; + tf.rxi = 0; +} + +void _TF_FN TF_AcceptChar(unsigned char c) +{ // Timeout - clear if (tf.parser_timeout_ticks >= TF_PARSER_TIMEOUT_TICKS) { TF_ResetParser(); @@ -296,81 +379,109 @@ void TF_AcceptChar(unsigned char c) #define COLLECT_NUMBER(dest, type) dest = ((dest) << 8) | c; \ if (++tf.rxi == sizeof(type)) + #if !TF_USE_SOF_BYTE + if (tf.state == TFState_SOF) { + TF_ParsBeginFrame(); + } + #endif + switch (tf.state) { case TFState_SOF: if (c == TF_SOF_BYTE) { + TF_ParsBeginFrame(); + } + break; + + case TFState_ID: + CKSUM_ADD(tf.cksum, c); + COLLECT_NUMBER(tf.id, TF_ID) { // Enter LEN state tf.state = TFState_LEN; - tf.len = 0; tf.rxi = 0; - - // Reset state vars - CKSUM_RESET(tf.cksum); } break; case TFState_LEN: CKSUM_ADD(tf.cksum, c); COLLECT_NUMBER(tf.len, TF_LEN) { - // enter HEAD_CKSUM state - tf.state = TFState_HEAD_CKSUM; - tf.ref_cksum = 0; + // Enter TYPE state + tf.state = TFState_TYPE; tf.rxi = 0; } break; + case TFState_TYPE: + CKSUM_ADD(tf.cksum, c); + COLLECT_NUMBER(tf.type, TF_TYPE) { + #if TF_CKSUM_TYPE == 0 + tf.state = TFState_DATA; + tf.rxi = 0; + #else + // enter HEAD_CKSUM state + tf.state = TFState_HEAD_CKSUM; + tf.rxi = 0; + tf.ref_cksum = 0; + #endif + } + break; + case TFState_HEAD_CKSUM: - COLLECT_NUMBER(tf.ref_cksum, TF_CKSUM){ + COLLECT_NUMBER(tf.ref_cksum, TF_CKSUM) { // Check the header checksum against the computed value + CKSUM_FINALIZE(tf.cksum); + if (tf.cksum != tf.ref_cksum) { TF_ResetParser(); break; } - // Enter ID state - tf.state = TFState_ID; - tf.rxi = 0; - - tf.cksum = 0; // Start collecting the payload - } - break; - - case TFState_ID: - CKSUM_ADD(tf.cksum, c); - COLLECT_NUMBER(tf.id, TF_ID) { - // Enter TYPE state - tf.state = TFState_TYPE; - tf.rxi = 0; - } - break; + if (tf.len == 0) { + TF_HandleReceivedMessage(tf.id, tf.type, NULL, 0); + TF_ResetParser(); + break; + } - case TFState_TYPE: - CKSUM_ADD(tf.cksum, c); - COLLECT_NUMBER(tf.type, TF_TYPE) { // Enter DATA state tf.state = TFState_DATA; tf.rxi = 0; + + CKSUM_RESET(tf.cksum); // Start collecting the payload + + if (tf.len >= TF_MAX_PAYLOAD) { + // ERROR - frame too long. Consume, but do not store. + tf.discard_data = true; + } } break; case TFState_DATA: - CKSUM_ADD(tf.cksum, c); - tf.data[tf.rxi++] = c; + if (tf.discard_data) { + tf.rxi++; + } else { + CKSUM_ADD(tf.cksum, c); + tf.data[tf.rxi++] = c; + } + if (tf.rxi == tf.len) { - // Enter DATA_CKSUM state - tf.state = TFState_DATA_CKSUM; - tf.rxi = 0; - tf.ref_cksum = 0; + #if TF_CKSUM_TYPE == 0 + // All done + TF_HandleReceivedMessage(tf.id, tf.type, tf.data, tf.len); + TF_ResetParser(); + #else + // Enter DATA_CKSUM state + tf.state = TFState_DATA_CKSUM; + tf.rxi = 0; + tf.ref_cksum = 0; + #endif } break; case TFState_DATA_CKSUM: COLLECT_NUMBER(tf.ref_cksum, TF_CKSUM) { // Check the header checksum against the computed value - if (tf.cksum == tf.ref_cksum) { - // LOCK PARSER - TF_HandleReceivedMessage(tf.id, tf.type, tf.data); - // UNLOCK PARSER + CKSUM_FINALIZE(tf.cksum); + if (!tf.discard_data && tf.cksum == tf.ref_cksum) { + TF_HandleReceivedMessage(tf.id, tf.type, tf.data, tf.len); } TF_ResetParser(); @@ -379,7 +490,20 @@ void TF_AcceptChar(unsigned char c) } } -int TF_Compose(uint8_t *outbuff, TF_ID *id_ptr, +/** + * Compose a frame (used internally by TF_Send and TF_Respond). + * The frame can be sent using TF_WriteImpl(), or received by TF_Accept() + * + * @param outbuff - buffer to store the result in + * @param msgid - message ID is stored here, if not NULL + * @param type - message type + * @param data - data buffer + * @param len - payload size in bytes + * @param explicit_id - ID to use in the frame (8-bit) + * @param use_expl_id - whether to use the previous param + * @return nr of bytes in outbuff used by the frame, TF_ERROR (-1) on failure + */ +static int _TF_FN TF_Compose(uint8_t *outbuff, TF_ID *id_ptr, TF_TYPE type, const uint8_t *data, TF_LEN data_len, TF_ID explicit_id, bool use_expl_id) @@ -402,12 +526,15 @@ int TF_Compose(uint8_t *outbuff, TF_ID *id_ptr, id = explicit_id; } else { - id = (TF_ID) ((tf.next_id++) & TF_ID_MASK); + id = (TF_ID) (tf.next_id++ & TF_ID_MASK); if (tf.peer_bit) { id |= TF_ID_PEERBIT; } } + if (id_ptr != NULL) + *id_ptr = id; + // DRY helper for writing a multi-byte variable to the buffer #define WRITENUM_BASE(type, num, xtra) \ for (i = sizeof(type)-1; i>=0; i--) { \ @@ -420,36 +547,48 @@ int TF_Compose(uint8_t *outbuff, TF_ID *id_ptr, #define WRITENUM_CKSUM(type, num) WRITENUM_BASE(type, num, CKSUM_ADD(cksum, b)) // --- Start --- + CKSUM_RESET(cksum); - outbuff[pos++] = TF_SOF_BYTE; - cksum = 0; - WRITENUM_CKSUM(TF_LEN, data_len); - WRITENUM(TF_CKSUM, cksum); + #if TF_USE_SOF_BYTE + outbuff[pos++] = TF_SOF_BYTE; + CKSUM_ADD(cksum, TF_SOF_BYTE); + #endif - // --- payload begin --- - cksum = 0; WRITENUM_CKSUM(TF_ID, id); + WRITENUM_CKSUM(TF_LEN, data_len); WRITENUM_CKSUM(TF_TYPE, type); - // DATA - for (i = 0; i < data_len; i++) { - b = data[i]; - outbuff[pos++] = b; - CKSUM_ADD(cksum, b); - } + #if TF_CKSUM_TYPE != 0 + CKSUM_FINALIZE(cksum); + WRITENUM(TF_CKSUM, cksum); + #endif - WRITENUM(TF_CKSUM, cksum); + // --- payload begin --- + if (data_len > 0) { + CKSUM_RESET(cksum); + + // DATA + for (i = 0; i < data_len; i++) { + b = data[i]; + outbuff[pos++] = b; + CKSUM_ADD(cksum, b); + } - if (id_ptr != NULL) - *id_ptr = id; + #if TF_CKSUM_TYPE != 0 + CKSUM_FINALIZE(cksum); + WRITENUM(TF_CKSUM, cksum); + #endif + } return pos; } -bool TF_Send(TF_TYPE type, const uint8_t *payload, TF_LEN payload_len, - TF_LISTENER listener, TF_ID *id_ptr) +bool _TF_FN TF_Send(TF_TYPE type, + const uint8_t *payload, TF_LEN payload_len, + TF_LISTENER listener, + TF_ID *id_ptr) { - TF_ID msgid; + TF_ID msgid = 0; int len; len = TF_Compose(tf.sendbuf, &msgid, type, payload, payload_len, 0, false); if (len == TF_ERROR) return false; @@ -461,7 +600,8 @@ bool TF_Send(TF_TYPE type, const uint8_t *payload, TF_LEN payload_len, return true; } -bool TF_Respond(TF_TYPE type, +// Like TF_Send, but with explicit frame ID +bool _TF_FN TF_Respond(TF_TYPE type, const uint8_t *data, TF_LEN data_len, TF_ID frame_id) { @@ -473,10 +613,20 @@ bool TF_Respond(TF_TYPE type, return true; } +/** + * Like TF_Send(), but with no data + */ +bool _TF_FN TF_Send0(TF_TYPE type, + TF_LISTENER listener, + TF_ID *id_ptr) +{ + return TF_Send(type, NULL, 0, listener, id_ptr); +} + /** * Like TF_Send(), but with just 1 data byte */ -bool TF_Send1(TF_TYPE type, uint8_t b1, +bool _TF_FN TF_Send1(TF_TYPE type, uint8_t b1, TF_LISTENER listener, TF_ID *id_ptr) { @@ -487,7 +637,7 @@ bool TF_Send1(TF_TYPE type, uint8_t b1, /** * Like TF_Send(), but with just 2 data bytes */ -bool TF_Send2(TF_TYPE type, uint8_t b1, uint8_t b2, +bool _TF_FN TF_Send2(TF_TYPE type, uint8_t b1, uint8_t b2, TF_LISTENER listener, TF_ID *id_ptr) { @@ -495,7 +645,8 @@ bool TF_Send2(TF_TYPE type, uint8_t b1, uint8_t b2, return TF_Send(type, b, 2, listener, id_ptr); } -void TF_Tick(void) +/** Timebase hook - for timeouts */ +void _TF_FN TF_Tick(void) { if (tf.parser_timeout_ticks < TF_PARSER_TIMEOUT_TICKS) { tf.parser_timeout_ticks++; diff --git a/TinyFrame.h b/TinyFrame.h index a28cfe7..b6c0b2a 100644 --- a/TinyFrame.h +++ b/TinyFrame.h @@ -1,13 +1,22 @@ #ifndef TinyFrameH #define TinyFrameH + //--------------------------------------------------------------------------- #include // for uint8_t etc #include // for bool #include // for NULL +//#include "messages.h" // for your message IDs (enum or defines) +//#include // when using with esphttpd //--------------------------------------------------------------------------- -// A static buffer of this size will be allocated -#define TF_MAX_PAYLOAD 2048 + +//----------------------------- PARAMETERS ---------------------------------- + +// Maximum send / receive payload size (static buffers size) +// Larger payloads will be rejected. +#define TF_MAX_PAYLOAD 1024 + +// --- Listener counts - determine sizes of the static slot tables --- // Frame ID listeners (wait for response / multi-part message) #define TF_MAX_ID_LST 20 @@ -20,55 +29,109 @@ // ticks = number of calls to TF_Tick() #define TF_PARSER_TIMEOUT_TICKS 10 -#define TF_USE_CRC16 1 +//----------------------------- FRAME FORMAT --------------------------------- +// The format can be adjusted to fit your particular application needs + +// If the connection is reliable, you can disable the SOF byte and checksums. +// That can save up to 9 bytes of overhead. + +// ,-----+----+-----+------+------------+- - - -+------------, +// | SOF | ID | LEN | TYPE | HEAD_CKSUM | DATA | PLD_CKSUM | +// | 1 | ? | ? | ? | ? | ... | ? | <- size (bytes) +// '-----+----+-----+------+------------+- - - -+------------' + +// !!! BOTH SIDES MUST USE THE SAME SETTINGS !!! + +// Adjust sizes as desired (1,2,4) +#define TF_ID_BYTES 1 +#define TF_LEN_BYTES 2 +#define TF_TYPE_BYTES 1 + +// Select checksum type (0 = none, 8 = ~XOR, 16 = CRC16 0x8005, 32 = CRC32) +#define TF_CKSUM_TYPE 16 + +// Use a SOF byte to mark the start of a frame +#define TF_USE_SOF_BYTE 1 +// Value of the SOF byte (if TF_USE_SOF_BYTE == 1) +#define TF_SOF_BYTE 0x01 + +//------------------------- End of user config ------------------------------ + + +//----------------------------- USAGE HINTS --------------------------------- //--------------------------------------------------------------------------- -// ,-----+-----+-----------+----+------+- - - -+------------, -// | SOF | LEN | LEN_CKSUM | ID | TYPE | DATA | PLD_CKSUM | -// | 1 | 2? | 1? | 1? | 1? | ... | 1? | -// '-----+-----+-----------+----+------+- - - -+------------' -// '----- payload -----' +//region Resolve data types + +#if TF_LEN_BYTES == 1 + typedef uint8_t TF_LEN; +#elif TF_LEN_BYTES == 2 + typedef uint16_t TF_LEN; +#elif TF_LEN_BYTES == 4 + typedef uint32_t TF_LEN; +#else + #error Bad value of TF_LEN_BYTES, must be 1, 2 or 4 +#endif -// The data section is designed thus so the higher levels of TinyFrame -// (message chaining, listeners etc) could be re-used for a different -// framing layer (e.g. sent in UDP packets) -// Fields marked '?' can have their size adjusted by changing the -// typedefs below +#if TF_TYPE_BYTES == 1 + typedef uint8_t TF_TYPE; +#elif TF_TYPE_BYTES == 2 + typedef uint16_t TF_TYPE; +#elif TF_TYPE_BYTES == 4 + typedef uint32_t TF_TYPE; +#else + #error Bad value of TF_TYPE_BYTES, must be 1, 2 or 4 +#endif -// Change those typedefs to adjust the field sizes. BOTH PEERS MUST HAVE THE SAME SIZES! -typedef uint8_t TF_ID; // Message ID. Effectively limits the nr of concurrent request-response sessions. -typedef uint8_t TF_TYPE; // Message type (sent together with the data payload, which can be empty) -typedef uint16_t TF_LEN; // Length of the data payload. -// TF_OVERHEAD_BYTES - added to TF_MAX_PAYLOAD for the send buffer size. -// (TODO - buffer-less sending) -#if TF_USE_CRC16 - typedef uint16_t TF_CKSUM; - #define TF_OVERHEAD_BYTES 9 +#if TF_ID_BYTES == 1 + typedef uint8_t TF_ID; +#elif TF_ID_BYTES == 2 + typedef uint16_t TF_ID; +#elif TF_ID_BYTES == 4 + typedef uint32_t TF_ID; #else + #error Bad value of TF_ID_BYTES, must be 1, 2 or 4 +#endif + + +#if TF_CKSUM_TYPE == 8 || TF_CKSUM_TYPE == 0 + // ~XOR (if 0, still use 1 byte - it won't be used) typedef uint8_t TF_CKSUM; - #define TF_OVERHEAD_BYTES 7 +#elif TF_CKSUM_TYPE == 16 + // CRC16 + typedef uint16_t TF_CKSUM; +#elif TF_CKSUM_TYPE == 32 + // CRC32 + typedef uint32_t TF_CKSUM; +#else + #error Bad value for TF_CKSUM_TYPE, must be 8, 16 or 32 #endif -//--------------------------------------------------------------------------- -#define TF_SOF_BYTE 0x01 +// Bytes added to TF_MAX_PAYLOAD for the send buffer size. +#define TF_OVERHEAD_BYTES (1+sizeof(TF_ID)+sizeof(TF_LEN)+sizeof(TF_CKSUM)+sizeof(TF_TYPE)+sizeof(TF_CKSUM)) + +//endregion + +//--------------------------------------------------------------------------- #define TF_ERROR -1 // Type-dependent masks for bit manipulation in the ID field -#define TF_ID_MASK ((1 << (sizeof(TF_ID)*8 - 1)) - 1) -#define TF_ID_PEERBIT (1 << (sizeof(TF_ID)*8) - 1) +#define TF_ID_MASK (TF_ID)(((TF_ID)1 << (sizeof(TF_ID)*8 - 1)) - 1) +#define TF_ID_PEERBIT (TF_ID)((TF_ID)1 << ((sizeof(TF_ID)*8) - 1)) + +//--------------------------------------------------------------------------- +/** Peer bit enum (used for init) */ typedef enum { TF_SLAVE = 0, TF_MASTER = 1, } TF_PEER; -//--------------------------------------------------------------------------- - /** * TinyFrame Type Listener callback * @param frame_id - ID of the received frame @@ -146,25 +209,6 @@ bool TF_AddGenericListener(TF_LISTENER cb); */ bool TF_RemoveGenericListener(TF_LISTENER cb); -/** - * Compose a frame (used internally by TF_Send and TF_Respond). - * The frame can be sent using TF_WriteImpl(), or received by TF_Accept() - * - * @param outbuff - buffer to store the result in - * @param msgid - message ID is stored here, if not NULL - * @param type - message type - * @param data - data buffer - * @param len - payload size in bytes - * @param explicit_id - ID to use in the frame (8-bit) - * @param use_expl_id - whether to use the previous param - * @return nr of bytes in outbuff used by the frame, TF_ERROR (-1) on failure - */ -int TF_Compose(uint8_t *outbuff, - TF_ID *id_ptr, - TF_TYPE type, - const uint8_t *data, TF_LEN data_len, - TF_ID explicit_id, bool use_expl_id); - /** * Send a frame, and optionally attach an ID listener. * @@ -180,6 +224,11 @@ bool TF_Send(TF_TYPE type, const uint8_t *data, TF_LEN data_len, TF_LISTENER listener, TF_ID *id_ptr); +/** + * Like TF_Send(), but no data, just the type + */ +bool TF_Send0(TF_TYPE type, TF_LISTENER listener, TF_ID *id_ptr); + /** * Like TF_Send(), but with just 1 data byte */ diff --git a/test.c b/test.c index 01a5e93..e41dc4f 100644 --- a/test.c +++ b/test.c @@ -1,22 +1,9 @@ #include #include +#include #include "TinyFrame.h" -// helper func for testing -static void dumpFrame(const uint8_t *buff, TF_LEN len) -{ - int i; - for(i = 0; i < len; i++) { - printf("%3u \033[34m%02X\033[0m", buff[i], buff[i]); - if (buff[i] >= 0x20 && buff[i] < 127) { - printf(" %c", buff[i]); - } else { - printf(" \033[31m.\033[0m", buff[i]); - } - printf("\n"); - } - printf("--- end of frame ---\n"); -} +static void dumpFrame(const uint8_t *buff, TF_LEN len); /** * This function should be defined in the application code. @@ -24,65 +11,67 @@ static void dumpFrame(const uint8_t *buff, TF_LEN len) */ void TF_WriteImpl(const uint8_t *buff, TF_LEN len) { - printf("\033[32;1mTF_WriteImpl - sending frame:\033[0m\n"); + printf("--------------------\n"); + printf("\033[32mTF_WriteImpl - sending frame:\033[0m\n"); dumpFrame(buff, len); + + // Send it back as if we received it + TF_Accept(buff, len); } /** An example listener function */ bool myListener(TF_ID frame_id, TF_TYPE type, const uint8_t *buff, TF_LEN len) { - printf("\033[33mrx frame %s, len %d, id %d\033[0m\n", buff, len, frame_id); + printf("\033[33mRX frame\n" + " type: %02Xh\n" + " data: \"%.*s\"\n" + " len: %u\n" + " id: %Xh\033[0m\n", type, len, buff, len, frame_id); return true; } -void main() +bool testIdListener(TF_ID frame_id, TF_TYPE type, const uint8_t *buff, TF_LEN len) { - int i; - TF_ID msgid; - int len; - char buff[100]; + printf("OK - ID Listener triggered for msg (type %02X, id %Xh)!", type, frame_id); + return true; +} +void main(void) +{ // Set up the TinyFrame library TF_Init(TF_MASTER); // 1 = master, 0 = slave + TF_AddGenericListener(myListener); printf("------ Simulate sending a message --------\n"); - // Send a message - // args - payload, length (0 = strlen), listener, id_ptr (for storing the frame ID) - // (see the .h file for details) - TF_Respond(0xA5, (unsigned char*)"Hello TinyFrame", 16, 0x15); - // This builds the frame in an internal buffer and sends it to - // TF_WriteImpl() + TF_Send(0x22, (unsigned char*)"Hello TinyFrame", 16, NULL, NULL); + const char *longstr = "Lorem ipsum dolor sit amet."; + TF_Send(0x33, (unsigned char*)longstr, (TF_LEN)(strlen(longstr)+1), NULL, NULL); - printf("------ Simulate receiving a message --------\n"); + TF_Send(0x44, (unsigned char*)"Hello2", 7, NULL, NULL); - // Adding global listeners - // Listeners can listen to any frame (fallback listeners), - // or to a specific Frame Type (AddTypeListener). There are - // also ID listeners that can be bound automatically in TF_Send(). - // - // Type listeners are matched by the first character of the payload, - // ID listeners by the message ID (which is the same in the response as in the request) - TF_AddGenericListener(myListener); - //TF_AddTypeListener(0xF1, myTypeListener); - //TF_AddIdListener(msgID, myIdListener); + TF_Send0(0xF0, NULL, NULL); - // This lets us compose a frame (it's also used internally by TF_Send and TF_Respond) - len = TF_Compose((unsigned char*)buff, // Buffer to write the frame to - &msgid, // Int to store the message ID in - 0xA5, - (unsigned char*)"Hello TinyFrame", // Payload bytes - 16, // Length - this will cut it at "ALPHA" (showing that it works) - // For string, we can use "0" to use strlen() internally - 0x15, true); // Message ID - we could specify a particular ID if we were - // trying to build a response frame, which has the same ID - // as the request it responds to. + TF_Send1(0xF1, 'Q', NULL, NULL); - printf("The frame we'll receive is:\n"); - dumpFrame((unsigned char*)buff, (TF_LEN)len); + TF_Send2(0xF2, 'A', 'Z', NULL, NULL); + + TF_Send0(0x77, testIdListener, NULL); +} - // Accept the frame - // You will normally call this method in the UART IRQ handler etc - TF_Accept((unsigned char*)buff, (TF_LEN)len); +// helper func for testing +static void dumpFrame(const uint8_t *buff, TF_LEN len) +{ + int i; + for(i = 0; i < len; i++) { + printf("%3u \033[34m%02X\033[0m", buff[i], buff[i]); + if (buff[i] >= 0x20 && buff[i] < 127) { + printf(" %c", buff[i]); + } else { + printf(" \033[31m.\033[0m"); + } + printf("\n"); + } + printf("--- end of frame ---\n"); }