diff --git a/TIC.raw.sample b/TIC.raw.sample
new file mode 100644
index 0000000..45308c3
--- /dev/null
+++ b/TIC.raw.sample
@@ -0,0 +1,195 @@
+SE 000051519  
+
+PTEC TH.. $
+
+IINS
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 001 X
+
+IMAX 090 H
+
+PAPP 00250 (
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 001 X
+
+IMAX 090 H
+
+PAPP 00250 (
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 001 X
+
+IMAX 090 H
+
+PAPP 00250 (
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 005 \
+
+IMAX 090 H
+
+PAPP 01250 )
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 005 \
+
+IMAX 090 H
+
+PAPP 01160 )
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051521 Y
+
+PTEC TH.. $
+
+IINST 004 [
+
+IMAX 090 H
+
+PAPP 00980 2
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051522 Z
+
+PTEC TH.. $
+
+IINST 004 [
+
+IMAX 090 H
+
+PAPP 01020 $
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051522 Z
+
+PTEC TH.. $
+
+IINST 004 [
+
+IMAX 090 H
+
+PAPP 01020 $
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051522 Z
+
+PTEC TH.. $
+
+IINST 004 [
+
+IMAX 090 H
+
+PAPP 01010 #
+
+HHPHC A ,
+
+MOTDETAT 000000 B
+
+ADCO 042375023610 8
+
+OPTARIF BASE 0
+
+ISOUSC 30 9
+
+BASE 000051522 Z
+
+PTEC TH.. $
+
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index cf2c455..0498f3d 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -1,2 +1,2 @@
-idf_component_register(SRCS "main.c"
+idf_component_register(SRCS "tic.c" "uart.c" "main.c"
                     INCLUDE_DIRS ".")
diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild
new file mode 100644
index 0000000..fa9b66a
--- /dev/null
+++ b/main/Kconfig.projbuild
@@ -0,0 +1,44 @@
+menu "Enedis TIC configuration"
+
+    orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
+
+    config TIC_UART_PORT_NUM
+        int "UART port number"
+        range 0 2 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S3
+        default 1 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32S3
+        range 0 1
+        default 1
+        help
+            UART communication port number towards Enedis' TIC
+
+    config TIC_UART_BAUD_RATE
+        int "UART communication speed"
+        range 1200 115200
+        default 1200
+        help
+            UART communication speed for TIC. 1200 for historique, 9600 for
+            standard
+
+    config TIC_UART_RXD
+        int "UART RXD pin number"
+        range ENV_GPIO_RANGE_MIN ENV_GPIO_IN_RANGE_MAX
+        default 5
+        help
+            GPIO number for UART RX pin. See UART documentation for more information
+            about available pin numbers for UART.
+
+#   config TIC_UART_TXD
+#       int "UART TXD pin number"
+#       range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
+#       default 4
+#       help
+#           GPIO number for UART TX pin. See UART documentation for more information
+#           about available pin numbers for UART.
+
+    config TIC_UART_TASK_STACK_SIZE
+        int "UART task stack size"
+        range 1024 16384
+        default 2048
+        help
+            Defines stack size for UART echo example. Insufficient stack size can cause crash.
+endmenu
diff --git a/main/main.c b/main/main.c
index 7b66f33..8076a44 100644
--- a/main/main.c
+++ b/main/main.c
@@ -1,6 +1,14 @@
 #include <stdio.h>
+#include "driver/uart.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "uart.h"
+#include "esp_log.h"
+
+static const char *TAG = "MAIN";
 
 void app_main(void)
 {
-
+    ESP_LOGI(TAG, "Arriving in main\n");
+    xTaskCreate(uart_listen_task, "uart_listen_task", TIC_TASK_STACK_SIZE, NULL, 10, NULL);
 }
diff --git a/main/tic.c b/main/tic.c
new file mode 100644
index 0000000..5f963e2
--- /dev/null
+++ b/main/tic.c
@@ -0,0 +1,174 @@
+#include <string.h>
+#include <stdlib.h>
+#include "tic.h"
+
+static const char FRAME_START = 0x02;
+static const char GROUP_START = 0x0a;
+static const char FIELD_SEP = 0x20;
+
+DecodeState* create_decode_state(size_t queue_size) {
+    DecodeState* out = malloc(sizeof(DecodeState));
+    out->cur_frame = NULL;
+    out->frame_queue = xQueueCreate(queue_size, sizeof(InfoFrame*));
+    out->checksum = 0;
+    out->fill_step = GFS_NotStarted;
+    out->postpone_buffer[0] = '\0';
+    out->postpone_buffer_size = 0;
+    return out;
+}
+
+void free_decode_state(DecodeState* st) {
+    vQueueDelete(st->frame_queue);
+    if(st->cur_frame != NULL) {
+        tic_delete_frame(st->cur_frame);
+        free(st->cur_frame);
+    }
+    free(st);
+}
+
+void tic_delete_group(InfoGroup* group) {
+    free(group->time_str);
+    if(group->data_type == GROUP_DATA_STR)
+        free(group->data.str);
+}
+void tic_delete_frame(InfoFrame* frame) {
+    for(size_t g_id=0; g_id < frame->group_count; ++g_id)
+        tic_delete_group(frame->groups + g_id);
+}
+
+char* tic_discard_until_frame(char* buffer, size_t buf_size) {
+    size_t pos=0;
+    for(pos=0; pos < buf_size; ++pos) {
+        if(buffer[pos] == FRAME_START) {
+            break;
+        }
+    }
+    if(pos == buf_size)
+        return NULL;
+    return buffer + pos;
+}
+
+const uint32_t BAD_STR_TO_INT = ~0U;
+uint32_t str_to_int(char* buffer, const char* buffer_end) {
+    uint64_t res = 0;
+    for(char* chr=buffer; chr != buffer_end; ++chr) {
+        if('0' <= *chr && *chr <= '9') {
+            res *= 10;
+            res += (*chr - '0');
+        } else {
+            return BAD_STR_TO_INT;
+        }
+    }
+    if(res > BAD_STR_TO_INT)
+        return BAD_STR_TO_INT;
+    return res;
+}
+
+static void get_data_pointers(char** start, char** end, const char* buffer, char* pos, DecodeState* state) {
+    if(*start == NULL) { // Postponed data
+        *start = state->postpone_buffer;
+        size_t copy_size = pos - buffer;
+        if(copy_size + state->postpone_buffer_size > POSTPONE_BUFFER_MAXSIZE)
+            copy_size = POSTPONE_BUFFER_MAXSIZE - state->postpone_buffer_size;
+        char* postpone_end = state->postpone_buffer
+            + state->postpone_buffer_size;
+        memcpy(
+            postpone_end,
+            buffer,
+            copy_size);
+        *end = postpone_end + copy_size;
+    } else { // No postponed data
+        *end = pos;
+    }
+}
+
+int tic_decode(char* buffer, size_t buf_size, DecodeState* state) {
+    int frames_decoded = 0;
+    char* start = NULL;
+    char* data_end = NULL;
+
+    for(char* pos = buffer; pos != buffer + buf_size; ++pos) {
+        if(state->fill_step == GFS_Checksum) { // End of frame -- verify checksum
+            if(((state->checksum & 0x3F) + 0x20) != *pos) {
+                state->fill_step = GFS_Invalid;
+                continue;
+            }
+            state->fill_step = GFS_Done;
+            continue;
+        }
+        state->checksum += *pos; // Update checksum
+
+        if(*pos == FRAME_START) {
+            if(state->cur_frame != NULL) {
+                frames_decoded += 1;
+                if(uxQueueSpacesAvailable(state->frame_queue) == 0) {
+                    tic_delete_frame(state->cur_frame);
+                    free(state->cur_frame);
+                }
+                else
+                    xQueueSend(state->frame_queue, state->cur_frame, 0);
+            }
+            state->cur_frame = malloc(sizeof(InfoFrame));
+            memset(state->cur_frame, 0, sizeof(InfoFrame));
+            state->cur_frame->group_count = 0;
+        }
+        else if(*pos == GROUP_START) {
+            // Reset checksum
+            state->checksum = 0;
+
+            if(state->fill_step == GFS_Done) {
+                state->cur_frame->group_count++;
+                state->fill_step = GFS_NotStarted;
+            } else if(state->fill_step == GFS_Invalid) {
+                memset(state->cur_frame->groups + state->cur_frame->group_count,
+                        '0', sizeof(InfoGroup));
+                state->fill_step = GFS_NotStarted;
+            } else if(state->fill_step == GFS_NotStarted) {
+                state->fill_step = GFS_Tag;
+                start = pos + 1;
+            } else {
+                state->fill_step = GFS_Invalid;
+            }
+        }
+        else if(*pos == FIELD_SEP) {
+            InfoGroup* cur_group =
+                state->cur_frame->groups + state->cur_frame->group_count;
+            if(state->fill_step == GFS_Tag) {
+                get_data_pointers(&start, &data_end, buffer, pos, state);
+                size_t label_size = data_end - start + 1;
+                if(label_size > INFO_GROUP_LABEL_MAX_SIZE)
+                    label_size = INFO_GROUP_LABEL_MAX_SIZE;
+                memcpy(cur_group->label, start, label_size-1);
+                cur_group->label[label_size-1] = '\0';
+                state->fill_step = GFS_Data;
+                start = pos + 1;
+            }
+            else if(state->fill_step == GFS_Data) {
+                get_data_pointers(&start, &data_end, buffer, pos, state);
+                uint32_t int_val = str_to_int(start, data_end);
+                if(int_val == BAD_STR_TO_INT) {
+                    size_t len = data_end - start + 1;
+                    cur_group->data_type = GROUP_DATA_STR;
+                    cur_group->data.str = malloc(sizeof(char) * len);
+                    memcpy(cur_group->data.str, start, len-1);
+                    cur_group->data.str[len-1] = '\0';
+                } else {
+                    cur_group->data_type = GROUP_DATA_NUM;
+                    cur_group->data.num = int_val;
+                }
+                state->fill_step = GFS_Checksum;
+                state->checksum -= *pos;
+            }
+        }
+    }
+
+    if((state->fill_step == GFS_Tag || state->fill_step == GFS_Data) && start != NULL) {
+        size_t postpone_size = (buffer + buf_size) - start;
+        if(postpone_size > POSTPONE_BUFFER_MAXSIZE)
+            postpone_size = POSTPONE_BUFFER_MAXSIZE;
+        memcpy(state->postpone_buffer, start, postpone_size);
+        state->postpone_buffer_size = postpone_size;
+    }
+
+    return frames_decoded;
+}
diff --git a/main/tic.h b/main/tic.h
new file mode 100644
index 0000000..c964d5c
--- /dev/null
+++ b/main/tic.h
@@ -0,0 +1,75 @@
+#pragma once
+#include <stdint.h>
+
+#ifndef CPU_TEST
+#include "freertos/FreeRTOS.h"
+#include "freertos/queue.h"
+#else
+#include "cpu_test_harness.h"
+#endif
+
+#define INFO_GROUP_LABEL_MAX_SIZE 12lu
+typedef struct {
+    char label[INFO_GROUP_LABEL_MAX_SIZE];
+    char* time_str;
+    union {
+        uint32_t num;
+        char* str;
+    } data;
+    enum {
+        GROUP_DATA_NUM,
+        GROUP_DATA_STR
+    } data_type;
+} InfoGroup;
+
+#define INFO_FRAME_MAX_GROUPS 12lu
+typedef struct {
+    InfoGroup groups[INFO_FRAME_MAX_GROUPS];
+    uint8_t group_count;
+} InfoFrame;
+
+typedef QueueHandle_t InfoFrameQueue;
+
+typedef enum {
+    GFS_NotStarted,
+    GFS_Tag,
+    GFS_Data,
+    GFS_Checksum,
+    GFS_Done,
+    GFS_Invalid
+} GroupFillStep;
+
+#define POSTPONE_BUFFER_MAXSIZE 32lu
+typedef struct {
+    InfoFrameQueue frame_queue;
+    InfoFrame* cur_frame;
+    char checksum;
+    GroupFillStep fill_step;
+    char postpone_buffer[POSTPONE_BUFFER_MAXSIZE];
+    size_t postpone_buffer_size;
+} DecodeState;
+
+/// Initialize a decode state
+DecodeState* create_decode_state(size_t queue_size);
+/// Frees a DecodeState
+void free_decode_state(DecodeState*);
+
+/// Deletes the contents of an InfoGroup
+void tic_delete_group(InfoGroup* group);
+/** Deletes the contents of an InfoFrame. The call to free() itself must be
+ * done by the user */
+void tic_delete_frame(InfoFrame* frame);
+
+/**
+ * Discard characters until just before a frame is started, and return a
+ * pointer to the beginning of the next frame (including the start of frame
+ * marker), or NULL if no frame was started.
+ */
+char* tic_discard_until_frame(char* buffer, size_t buf_size);
+
+/**
+ * Decode the buffer, updating the decode state and emitting frames as they are
+ * finished.
+ * Returns how many groups were decoded.
+ */
+int tic_decode(char* buffer, size_t buf_size, DecodeState* state);
diff --git a/main/uart.c b/main/uart.c
new file mode 100644
index 0000000..c17f740
--- /dev/null
+++ b/main/uart.c
@@ -0,0 +1,102 @@
+/* UART reception file -- reads data from Linky's TIC */
+#include <stdio.h>
+#include "driver/uart.h"
+#include "driver/gpio.h"
+#include "esp_sleep.h"
+#include "esp_log.h"
+
+#include "tic.h"
+
+/**
+ * This is an example which echos any data it receives on configured UART back to the sender,
+ * with hardware flow control turned off. It does not use UART driver event queue.
+ *
+ * - Port: configured UART
+ * - Receive (Rx) buffer: on
+ * - Transmit (Tx) buffer: off
+ * - Flow control: off
+ * - Event queue: off
+ * - Pin assignment: see defines below (See Kconfig)
+ */
+
+#define TIC_RXD (CONFIG_TIC_UART_RXD)
+#define TIC_RTS (UART_PIN_NO_CHANGE)
+#define TIC_CTS (UART_PIN_NO_CHANGE)
+
+#define TIC_UART_PORT_NUM      (CONFIG_TIC_UART_PORT_NUM)
+#define TIC_UART_BAUD_RATE     (CONFIG_TIC_UART_BAUD_RATE)
+static const char *TAG = "TIC UART";
+
+#define BUF_SIZE (1024)
+#define TIC_QUEUE_SIZE 64
+
+static void uart_light_sleep() {
+    // TODO
+    /*
+    ESP_LOGI(TAG, "Entering sleep.\n");
+    vTaskDelay(10/portTICK_PERIOD_MS);
+    ESP_ERROR_CHECK(esp_light_sleep_start());
+    esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause();
+    ESP_LOGI(TAG, "Woken up: %d\n", wakeup_cause);
+    */
+}
+
+void uart_listen_task(void *arg)
+{
+    /* Configure parameters of an UART driver,
+     * communication pins and install the driver */
+    uart_config_t uart_config = {
+        .baud_rate = TIC_UART_BAUD_RATE,
+        .data_bits = UART_DATA_8_BITS,
+        .parity    = UART_PARITY_DISABLE,
+        .stop_bits = UART_STOP_BITS_1,
+        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+        .source_clk = UART_SCLK_DEFAULT,
+    };
+    int intr_alloc_flags = 0;
+
+#if CONFIG_UART_ISR_IN_IRAM
+    intr_alloc_flags = ESP_INTR_FLAG_IRAM;
+#endif
+
+    ESP_ERROR_CHECK(uart_param_config(TIC_UART_PORT_NUM, &uart_config));
+    ESP_ERROR_CHECK(uart_set_pin(TIC_UART_PORT_NUM, UART_PIN_NO_CHANGE, TIC_RXD, TIC_RTS, TIC_CTS));
+    ESP_ERROR_CHECK(uart_driver_install(TIC_UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags));
+    //ESP_ERROR_CHECK(uart_set_wakeup_threshold(TIC_UART_PORT_NUM, 3));
+    //ESP_ERROR_CHECK(esp_sleep_enable_uart_wakeup(TIC_UART_PORT_NUM));
+
+    // Configure a temporary buffer for the incoming data
+    char *data = (char *) malloc(BUF_SIZE);
+
+    // Setup the TIC decoding
+    DecodeState* tic_state = create_decode_state(TIC_QUEUE_SIZE);
+    int initialized = 0;
+
+    // Discard any data already buffered
+    ESP_ERROR_CHECK(uart_flush(TIC_UART_PORT_NUM));
+
+    ESP_LOGI(TAG, "Start receiving.\n");
+    while (1) {
+        int len = uart_read_bytes(TIC_UART_PORT_NUM, data, (BUF_SIZE - 1), 30 / portTICK_PERIOD_MS);
+        if (len) {
+            if(!initialized) {
+                char* init_begin = tic_discard_until_frame(data, len);
+                if(init_begin != NULL) {
+                    size_t discarded = init_begin - data;
+                    initialized = 1;
+                    tic_decode(init_begin, len - discarded, tic_state);
+                }
+            } else {
+                tic_decode(data, len, tic_state);
+            }
+
+            data[len] = '\0';
+            ESP_LOGI(TAG, "Recv str: %s", (char *) data);
+        }
+        else {
+            uart_light_sleep();
+        }
+    }
+
+    free_decode_state(tic_state);
+}
diff --git a/main/uart.h b/main/uart.h
new file mode 100644
index 0000000..889629b
--- /dev/null
+++ b/main/uart.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include "sdkconfig.h"
+#define TIC_TASK_STACK_SIZE    (CONFIG_TIC_UART_TASK_STACK_SIZE)
+
+void uart_listen_task(void* arg);
diff --git a/test_cpu/Makefile b/test_cpu/Makefile
new file mode 100644
index 0000000..adfa5a1
--- /dev/null
+++ b/test_cpu/Makefile
@@ -0,0 +1,18 @@
+ESP_DIR=../main/
+COMMON_OBJS=cpu_test_harness.o
+ESP_OBJS=tic.o
+CFLAGS=-Wextra -O2 -g -I$(ESP_DIR) -I. -DCPU_TEST
+CC=gcc
+
+TARGETS=test_tic.bin
+
+all: $(TARGETS)
+
+test_%.bin: test_%.o $(COMMON_OBJS) $(ESP_OBJS)
+	$(CC) $(CFLAGS) $^ -o $@
+
+%.o: %.c
+	$(CC) $(CFLAGS) -c $< -o $@
+
+%.o: $(ESP_DIR)/%.c
+	$(CC) $(CFLAGS) -c $< -o $@
diff --git a/test_cpu/README.md b/test_cpu/README.md
new file mode 100644
index 0000000..552e6a2
--- /dev/null
+++ b/test_cpu/README.md
@@ -0,0 +1,2 @@
+Harness to test relevant parts of the code on a standard CPU instead of the
+ESP32.
diff --git a/test_cpu/cpu_test_harness.c b/test_cpu/cpu_test_harness.c
new file mode 100644
index 0000000..f693071
--- /dev/null
+++ b/test_cpu/cpu_test_harness.c
@@ -0,0 +1,31 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include "cpu_test_harness.h"
+#include "tic.h"
+
+QueueHandle_t xQueueCreate(int a, int b) {
+    return NULL;
+}
+
+void vQueueDelete(QueueHandle_t _) {}
+
+int uxQueueSpacesAvailable(QueueHandle_t _) {
+    return 1;
+}
+
+void xQueueSend(QueueHandle_t _, void* elt, int _b) {
+    InfoFrame* frame = elt;
+    printf(">> SENT: Frame with %u groups.\n", frame->group_count);
+    for(size_t grp_id=0; grp_id < frame->group_count; ++grp_id) {
+        InfoGroup* grp = frame->groups + grp_id;
+        printf("Group %02lu: %s %s ", grp_id, grp->label,
+                (grp->data_type == GROUP_DATA_NUM) ? "num" : "str");
+        if(grp->data_type == GROUP_DATA_NUM)
+            printf("%u\n", grp->data.num);
+        else
+            printf("%s\n", grp->data.str);
+    }
+
+    tic_delete_frame(frame);
+    free(frame);
+}
diff --git a/test_cpu/cpu_test_harness.h b/test_cpu/cpu_test_harness.h
new file mode 100644
index 0000000..0280c43
--- /dev/null
+++ b/test_cpu/cpu_test_harness.h
@@ -0,0 +1,11 @@
+#pragma once
+#ifndef CPU_TEST
+#define CPU_TEST
+#endif
+
+// ========== FreeRtos queues ==========
+typedef void* QueueHandle_t;
+QueueHandle_t xQueueCreate(int, int);
+int uxQueueSpacesAvailable(QueueHandle_t);
+void xQueueSend(QueueHandle_t, void* elt, int);
+void vQueueDelete(QueueHandle_t);
diff --git a/test_cpu/cpu_test_harness.o b/test_cpu/cpu_test_harness.o
new file mode 100644
index 0000000..e8a034e
Binary files /dev/null and b/test_cpu/cpu_test_harness.o differ
diff --git a/test_cpu/test_tic.bin b/test_cpu/test_tic.bin
new file mode 100755
index 0000000..253a28e
Binary files /dev/null and b/test_cpu/test_tic.bin differ
diff --git a/test_cpu/test_tic.c b/test_cpu/test_tic.c
new file mode 100644
index 0000000..8d7da0e
--- /dev/null
+++ b/test_cpu/test_tic.c
@@ -0,0 +1,36 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "tic.h"
+
+#define BUF_SIZE 32
+
+int main(void) {
+    DecodeState* state = create_decode_state(42);
+    FILE* tic_handle = fopen("../TIC.raw.sample", "rb");
+
+    //int initialized = 0;
+    char buffer[BUF_SIZE];
+    int buf_size = 0;
+    while(1) {
+        buf_size = fread(buffer, sizeof(char), BUF_SIZE, tic_handle);
+        fprintf(stderr, "Read %d bytes\n", buf_size);
+        if(buf_size == 0)
+            break;
+        /*
+        if(!initialized) {
+            char* init_begin = tic_discard_until_frame(buffer, buf_size);
+            if(init_begin != NULL) {
+                size_t discarded = (init_begin - buffer);
+                initialized = 1;
+                tic_decode(init_begin, buf_size - discarded, state);
+            }
+        } else {
+        */
+        tic_decode(buffer, buf_size, state);
+        //}
+    }
+
+    free_decode_state(state);
+
+    return 0;
+}
diff --git a/test_cpu/tic.o b/test_cpu/tic.o
new file mode 100644
index 0000000..554bead
Binary files /dev/null and b/test_cpu/tic.o differ
diff --git a/tester/feed_tic.py b/tester/feed_tic.py
new file mode 100755
index 0000000..7501cc8
--- /dev/null
+++ b/tester/feed_tic.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+import argparse
+import typing as t
+import time
+from pathlib import Path
+
+import serial
+
+
+def iter_frames(raw_data: bytes) -> t.Iterator[bytes]:
+    FRAME_START = 0x02
+    next_frame = b""
+    # Discard first bytes
+    raw_data = raw_data[raw_data.find(FRAME_START) :]
+
+    for ch in raw_data:
+        # Discard first bytes
+        if ch == FRAME_START and next_frame:
+            yield next_frame
+            next_frame = b""
+        next_frame += bytes([ch])
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-p", "--port", default="/dev/ttyUSB1")
+    parser.add_argument("-b", "--baud", type=int, default=1200)
+    parser.add_argument(
+        "--frame-gap", type=int, default=1500, help="Time between two frames (in ms)"
+    )
+    parser.add_argument(
+        "recorded_data",
+        help="File containing real recorded data to replay",
+        type=Path,
+    )
+    args = parser.parse_args()
+
+    data: bytes = b""
+    with args.recorded_data.open("rb") as h:
+        data = h.read()
+
+    with serial.Serial(port=args.port, baudrate=args.baud) as ser:
+        for frame in iter_frames(data):
+            print(f"Feeding {len(frame)}b frame")
+            start_time = time.monotonic()
+            ser.write(frame)
+            elapsed_time = time.monotonic() - start_time
+            sleep_time = args.frame_gap / 1000 - elapsed_time
+            if sleep_time > 0:
+                time.sleep(sleep_time)
+
+
+if __name__ == "__main__":
+    main()
