diff --git a/src/shared/radio/CC3135/CC3135.cpp b/src/shared/radio/CC3135/CC3135.cpp
index 9d8da293966a8e0bea83b7c8fd613aeeea3b7c71..a6f9985686208454e313ccd1e0627704f64b8816 100644
--- a/src/shared/radio/CC3135/CC3135.cpp
+++ b/src/shared/radio/CC3135/CC3135.cpp
@@ -1,4 +1,4 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
+/* Copyright (c) 2022 Skyward Experimental Rocketry
  * Author: Davide Mor
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,62 +22,226 @@
 
 #include "CC3135.h"
 
+#include <kernel/scheduler/scheduler.h>
+#include <utils/Debug.h>
+
+using namespace Boardcore::CC3135Defs;
+using namespace miosix;
+
 namespace Boardcore
 {
 
-using namespace CC3135Defs;
+CC3135::CC3135(std::unique_ptr<ICC3135Iface> &&iface) : iface(std::move(iface))
+{
+    // Start internal thread
+    this->start();
+    irq_wait_thread = thread;
+}
+
+CC3135Defs::DeviceVersion CC3135::getVersion()
+{
+    DeviceSetGet tx_command  = {};
+    tx_command.device_set_id = 1;   // Device get general
+    tx_command.option        = 12;  // Device get general version
+
+    DeviceSetGet rx_command  = {};
+    DeviceVersion rx_payload = {};
+
+    inoutPacketSync(OPCODE_DEVICE_DEVICEGET, Buffer::from(&tx_command),
+                    Buffer::null(), OPCODE_DEVICE_DEVICEGETRESPONSE,
+                    Buffer::from(&rx_command), Buffer::from(&rx_payload));
+
+    return rx_payload;
+}
+
+void CC3135::handleIrq()
+{
+    irq_count++;
+
+    if (irq_wait_thread)
+    {
+        irq_wait_thread->IRQwakeup();
+        if (irq_wait_thread->IRQgetPriority() >
+            Thread::IRQgetCurrentThread()->IRQgetPriority())
+        {
+            Scheduler::IRQfindNextThread();
+        }
+    }
+}
+
+void CC3135::waitForIrq()
+{
+    Thread *cur = Thread::getCurrentThread();
+
+    FastInterruptDisableLock dLock;
+    while (irq_wait_thread != cur || irq_count == 0)
+    {
+        irq_wait_thread->IRQwait();
+        {
+            FastInterruptEnableLock eLock(dLock);
+            Thread::yield();
+        }
+    }
+
+    irq_count--;
+}
+
+void CC3135::installAsServiceThread()
+{
+    FastInterruptDisableLock dLock;
+    irq_wait_thread = Thread::getCurrentThread();
+}
+
+void CC3135::restoreDefaultServiceThread()
+{
+    FastInterruptDisableLock dLock;
+    irq_wait_thread = thread;
+
+    // Wakeup just in case
+    irq_wait_thread->IRQwakeup();
+    if (irq_wait_thread->IRQgetPriority() >
+        Thread::IRQgetCurrentThread()->IRQgetPriority())
+    {
+        Scheduler::IRQfindNextThread();
+    }
+}
+
+void CC3135::defaultPacketHandler(CC3135Defs::ResponseHeader header)
+{
+    TRACE(
+        "[cc3135] Received packet:\n"
+        "- Opcode: %s (%4x)\n"
+        "- Status: %u\n"
+        "- Socket tx failures: %u\n"
+        "- Socket non blocking: %u\n",
+        opToStr(header.inner.opcode), header.inner.opcode, header.dev_status,
+        header.socket_tx_failure, header.socket_non_blocking);
+
+    // Dummy read rest of the data
+    // TODO: Add async commands
+    dummyRead(header.inner.len);
+}
+
+void CC3135::run()
+{
+    // TODO: Implement a way to stop this thread
+
+    while (true)
+    {
+        waitForIrq();
+
+        {
+            // Lock the device interface
+            Lock<FastMutex> lock(iface_mutex);
+
+            ResponseHeader header;
+            readHeader(&header);
+
+            defaultPacketHandler(header);
+        }
+    }
+}
 
-void CC3135Proto::handleIntr()
+void CC3135::inoutPacketSync(CC3135Defs::OpCode tx_opcode,
+                             CC3135::Buffer tx_command,
+                             CC3135::Buffer tx_payload,
+                             CC3135Defs::OpCode rx_opcode,
+                             CC3135::Buffer rx_command,
+                             CC3135::Buffer rx_payload)
 {
-    // TODO:
+    installAsServiceThread();
+
+    // Lock the device interface
+    Lock<FastMutex> lock(iface_mutex);
+    writePacket(tx_opcode, tx_command, tx_payload);
+
+    readPacket(rx_opcode, rx_command, rx_payload);
+
+    restoreDefaultServiceThread();
 }
 
-void CC3135::dummyRead()
+void CC3135::readPacketSync(CC3135Defs::OpCode opcode, CC3135::Buffer command,
+                            CC3135::Buffer payload)
 {
-    uint8_t buf[220];
-    proto.readPacket(buf);
+    installAsServiceThread();
 
-    ResponseHeader header;
-    memcpy(&header, &buf[0], sizeof(ResponseHeader));
+    // Lock the device interface
+    Lock<FastMutex> lock(iface_mutex);
+    readPacket(opcode, command, payload);
 
-    // TRACE("status: %d\n", header.dev_status);
-    // TRACE("opcode: %s\n", opToStr(header.gen_header.opcode));
-    // TRACE("len: %d\n", header.gen_header.len);
-    // TRACE("\n");
+    restoreDefaultServiceThread();
+}
 
-    // if(header.gen_header.opcode == OPCODE_DEVICE_DEVICEGETRESPONSE) {
-    //     for(int i = 0; i < header.gen_header.len + 4; i++) {
-    //         TRACE("%2x\n", buf[i]);
-    //     }
-    // }
+void CC3135::writePacketSync(CC3135Defs::OpCode opcode, CC3135::Buffer command,
+                             CC3135::Buffer payload)
+{
+    // Lock the device interface
+    Lock<FastMutex> lock(iface_mutex);
+    writePacket(opcode, command, payload);
 }
 
-DeviceVersion CC3135::getVersion()
+void CC3135::readPacket(OpCode opcode, CC3135::Buffer command,
+                        CC3135::Buffer payload)
 {
-    DeviceSetGet packet;
-    packet.gen_header.opcode = OPCODE_DEVICE_DEVICEGET;
-    packet.device_set_id     = 1;   // Device get general
-    packet.option            = 12;  // Device get version
+    while (true)
+    {
+        waitForIrq();
+
+        // Locking the interface is not needed
+
+        ResponseHeader header;
+        readHeader(&header);
 
-    proto.writePacket((uint8_t *)&packet, sizeof(packet));
+        if (header.inner.opcode != opcode)
+        {
+            defaultPacketHandler(header);
+        }
+        else
+        {
+            // Read the rest of the packet
+            size_t len = header.inner.len;
 
-    return {};
+            iface->read(command.ptr, std::min(len, command.len));
+            len -= std::min(len, command.len);
+
+            iface->read(payload.ptr, std::min(len, payload.len));
+            len -= std::min(len, payload.len);
+
+            // Read tail of remanining data
+            if (len > 0)
+                dummyRead(len);
+
+            break;
+        }
+    }
 }
 
-//! Get a generic header out of a buffer.
-GenericHeader *getHeader(uint8_t *buf) { return (GenericHeader *)(buf); }
+void CC3135::writePacket(OpCode opcode, CC3135::Buffer command,
+                         CC3135::Buffer payload)
+{
+    RequestHeader header{opcode,
+                         static_cast<uint16_t>(command.len + payload.len)};
+
+    writeHeader(&header);
 
-void CC3135Proto::readPacket(uint8_t *buf)
+    iface->write(command.ptr, command.len);
+    iface->write(payload.ptr, payload.len);
+}
+
+void CC3135::readHeader(ResponseHeader *header)
 {
     // 1. Write CNYS pattern (only if SPI)
     if (iface->is_spi())
     {
         SyncPattern sync = H2N_CNYS_PATTERN;
-        iface->write((uint8_t *)(&sync.short1), SYNC_PATTERN_LEN);
+        iface->write(reinterpret_cast<uint8_t *>(&sync.short1),
+                     SYNC_PATTERN_LEN);
     }
 
+    uint8_t buf[4];
+
     // 2. Read initial data from the device
-    iface->read(&buf[0], 8);
+    iface->read(&buf[0], 4);
 
     /*
     Here the TI driver does some weird stuff.
@@ -92,70 +256,52 @@ void CC3135Proto::readPacket(uint8_t *buf)
         memcpy(&sync, &buf[0], 4);
 
         if (n2hSyncPatternMatch(sync, tx_seq_num))
-        {
-            // Copy the bytes after the sync to the start
-            memcpy(&buf[0], &buf[4], 4);
             break;
-        }
 
         // TODO: The TI driver reads 4 bytes at a time, is this also good?
 
         // Shift everything
-        memmove(&buf[0], &buf[1], 7);
-        iface->read(&buf[7], 1);
-    }
-
-    // 4. Scan for double syncs
-    while (true)
-    {
-        uint32_t sync;
-        memcpy(&sync, &buf[0], 4);
-
-        if (!n2hSyncPatternMatch(sync, tx_seq_num))
-        {
-            break;
-        }
-
-        iface->read(&buf[0], 4);
+        memmove(&buf[0], &buf[1], 3);
+        iface->read(&buf[3], 1);
     }
 
     tx_seq_num++;
 
-    // 5. Parse generic header
-    GenericHeader *header = getHeader(&buf[0]);
+    // TODO: Is skipping double sync detection good?
 
-    // 6. Finalize and read rest of the data
-    if (header->len > 0)
-    {
-        // TODO: The TI driver reads a ResponseHeader, violating zero size
+    // 4. Read initial header
+    iface->read(reinterpret_cast<uint8_t *>(header), sizeof(ResponseHeader));
 
-        size_t aligned_len = alignSize(header->len);
-        iface->read(&buf[sizeof(GenericHeader)], aligned_len);
-    }
+    // 5. Adjust for bigger response header
+    header->inner.len -= sizeof(ResponseHeader) - sizeof(GenericHeader);
 }
 
-void CC3135Proto::writePacket(uint8_t *buf, size_t size)
+void CC3135::writeHeader(RequestHeader *header)
 {
     // 1. Write SYNC pattern
     if (iface->is_spi())
     {
         // Short pattern for SPI
         SyncPattern sync = H2N_SYNC_PATTERN;
-        iface->write((uint8_t *)&sync.short1, SYNC_PATTERN_LEN);
+        iface->write(reinterpret_cast<uint8_t *>(&sync.short1),
+                     SYNC_PATTERN_LEN);
     }
     else
     {
         // Long pattern for UART
         SyncPattern sync = H2N_SYNC_PATTERN;
-        iface->write((uint8_t *)&sync.long1, SYNC_PATTERN_LEN * 2);
+        iface->write(reinterpret_cast<uint8_t *>(&sync.long1),
+                     SYNC_PATTERN_LEN * 2);
     }
 
-    // 2. Setup header length
-    GenericHeader *header = getHeader(&buf[0]);
-    header->len           = size - sizeof(GenericHeader);
+    // 2. Write body
+    iface->write(reinterpret_cast<uint8_t *>(header), sizeof(RequestHeader));
+}
 
-    // 3. Write message
-    iface->write(buf, size);
+void CC3135::dummyRead(size_t n)
+{
+    uint8_t dummy[n];
+    iface->read(dummy, n);
 }
 
 }  // namespace Boardcore
diff --git a/src/shared/radio/CC3135/CC3135.h b/src/shared/radio/CC3135/CC3135.h
index 64629e3bba0313359f36e70a3d961fa35e618dd7..1925d1959733613d7c4c5b145e8e174c77e6df2d 100644
--- a/src/shared/radio/CC3135/CC3135.h
+++ b/src/shared/radio/CC3135/CC3135.h
@@ -1,4 +1,4 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
+/* Copyright (c) 2022 Skyward Experimental Rocketry
  * Author: Davide Mor
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,6 +22,10 @@
 
 #pragma once
 
+#include <ActiveObject.h>
+#include <miosix.h>
+
+#include <functional>
 #include <memory>
 
 #include "CC3135Defs.h"
@@ -29,50 +33,86 @@
 
 /*
 TODO(davide.mor): Write a small description of the CC3135
+
+TODO(davide.mor): Write about scatter/gather IO
 */
 
 namespace Boardcore
 {
 
-/**
- * @brief Abstraction over the comunication protocol.
- */
-class CC3135Proto
+class CC3135 : ActiveObject
 {
 public:
-    explicit CC3135Proto(std::unique_ptr<ICC3135Iface> &&iface)
-        : iface(std::move(iface)), tx_seq_num(0)
-    {
-    }
+    explicit CC3135(std::unique_ptr<ICC3135Iface> &&iface);
 
-    void handleIntr();
+    void handleIrq();
 
-    void readPacket(uint8_t *buf);
-    void writePacket(uint8_t *buf, size_t size);
+    CC3135Defs::DeviceVersion getVersion();
 
 private:
-    std::unique_ptr<ICC3135Iface> iface;
-    uint8_t tx_seq_num;
-};
-
-class CC3135
-{
-public:
-    explicit CC3135(std::unique_ptr<ICC3135Iface> &&iface)
-        : proto(std::move(iface))
+    //! Simple buffer for scatter/gather IO
+    struct Buffer
     {
-    }
+        uint8_t *ptr;
+        size_t len;
 
-    void handleIntr() { proto.handleIntr(); }
+        template <typename T>
+        static Buffer from(T *data)
+        {
+            return {reinterpret_cast<uint8_t *>(data), sizeof(T)};
+        }
 
-    //! Dummy packet read, used to test comunication.
-    void dummyRead();
+        static Buffer null() { return {nullptr, 0}; }
+    };
 
-    //! Retrieve information about the device
-    CC3135Defs::DeviceVersion getVersion();
+    //! Function for servicing async messages.
+    void run() override;
 
-private:
-    CC3135Proto proto;
+    void defaultPacketHandler(CC3135Defs::ResponseHeader header);
+
+    // Functions dedicated to interrupt servicing
+
+    //! Wait for an incoming interrupt (only callable in service thread).
+    void waitForIrq();
+    //! Install this thread as the service thread.
+    void installAsServiceThread();
+    //! Restore default service thread.
+    void restoreDefaultServiceThread();
+
+    // Functions for high level IO
+
+    //! Write a packet in output and wait for a packet in input
+    void inoutPacketSync(CC3135Defs::OpCode tx_opcode, Buffer tx_command,
+                         Buffer tx_payload, CC3135Defs::OpCode rx_opcode,
+                         Buffer rx_command, Buffer rx_payload);
+    //! Read packet in input, with proper synchronization.
+    void readPacketSync(CC3135Defs::OpCode opcode, Buffer command,
+                        Buffer payload);
+    //! Write a apcket in output, with proper synchronization.
+    void writePacketSync(CC3135Defs::OpCode opcode, Buffer command,
+                         Buffer payload);
+
+    // Functions for low level IO
+
+    //! Read a single packet.
+    void readPacket(CC3135Defs::OpCode opcode, Buffer command, Buffer payload);
+    //! Write a single packet.
+    void writePacket(CC3135Defs::OpCode opcode, Buffer command, Buffer payload);
+    //! Read a packet header.
+    void readHeader(CC3135Defs::ResponseHeader *header);
+    //! Write a packet header.
+    void writeHeader(CC3135Defs::RequestHeader *header);
+
+    //! Read dummy n bytes.
+    void dummyRead(size_t n);
+
+    miosix::Thread *irq_wait_thread = nullptr;  //< Thread waiting on IRQ
+    size_t irq_count                = 0;        //< Number of interrupts
+
+    miosix::FastMutex iface_mutex;
+    std::unique_ptr<ICC3135Iface> iface;
+
+    uint8_t tx_seq_num = 0;
 };
 
 }  // namespace Boardcore
diff --git a/src/shared/radio/CC3135/CC3135Defs.h b/src/shared/radio/CC3135/CC3135Defs.h
index 09031e0ea5795a11220a13083049a31ef20af827..affcb713ae3ebad92bdb2e4d2d3edbe9bdab0a5a 100644
--- a/src/shared/radio/CC3135/CC3135Defs.h
+++ b/src/shared/radio/CC3135/CC3135Defs.h
@@ -1,4 +1,4 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
+/* Copyright (c) 2022 Skyward Experimental Rocketry
  * Author: Davide Mor
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -53,6 +53,9 @@ enum OpCode : uint16_t
     OPCODE_DEVICE_DEVICESET                     = 0x84B7,
 };
 
+//! Is this message synchronous?
+inline bool isSync(OpCode op) { return op & OPCODE_SYNC; }
+
 struct SyncPattern
 {
     uint32_t long1;
@@ -79,7 +82,7 @@ struct GenericHeader
 
 struct ResponseHeader
 {
-    GenericHeader gen_header;
+    GenericHeader inner;
     uint8_t tx_pool_count;
     uint8_t dev_status;
     uint16_t min_max_payload;
@@ -87,9 +90,10 @@ struct ResponseHeader
     uint16_t socket_non_blocking;
 };
 
+typedef GenericHeader RequestHeader;
+
 struct DeviceSetGet
 {
-    GenericHeader gen_header;
     uint16_t status;
     uint16_t device_set_id;
     uint16_t option;
@@ -114,9 +118,6 @@ inline bool n2hSyncPatternMatch(uint32_t sync, uint8_t seq_num)
            (N2H_SYNC_PATTERN & N2H_SYNC_SPI_BUGS_MASK & N2H_SYNC_PATTERN_MASK);
 }
 
-//! Is this message synchronous?
-inline bool isSync(OpCode op) { return op & OPCODE_SYNC; }
-
 //! Align message size.
 inline size_t alignSize(size_t size) { return (size + 3) & (~3); }
 
diff --git a/src/shared/radio/CC3135/CC3135Iface.h b/src/shared/radio/CC3135/CC3135Iface.h
index ef468ebe3447f99da5e06c0dd87a072e2b6c1559..96c127f93c2f9dae56281e734d9e2db67a14e615 100644
--- a/src/shared/radio/CC3135/CC3135Iface.h
+++ b/src/shared/radio/CC3135/CC3135Iface.h
@@ -1,4 +1,4 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
+/* Copyright (c) 2022 Skyward Experimental Rocketry
  * Author: Davide Mor
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -37,6 +37,7 @@ public:
     virtual void read(uint8_t *buffer, size_t size)  = 0;
     virtual void write(uint8_t *buffer, size_t size) = 0;
     virtual bool is_spi()                            = 0;
+    virtual void reset()                             = 0;
 };
 
 /**
@@ -65,6 +66,8 @@ public:
 
     bool is_spi() override { return true; }
 
+    void reset() override {}
+
 private:
     SPISlave slave;
 };
@@ -92,6 +95,8 @@ public:
 
     bool is_spi() override { return false; }
 
+    void reset() override { usart.clearQueue(); }
+
 private:
     static constexpr USART::Baudrate DEFAULT_BAUD = USART::Baudrate::B115200;
 
diff --git a/src/tests/drivers/CC3135/test-cc3135.cpp b/src/tests/drivers/CC3135/test-cc3135.cpp
index 8d115fe3d2947f62c5d8e6fe3a7ce05c7acbd7a0..5f717f801138e9936ffc6b73116dbd2d860bf235 100644
--- a/src/tests/drivers/CC3135/test-cc3135.cpp
+++ b/src/tests/drivers/CC3135/test-cc3135.cpp
@@ -1,4 +1,4 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
+/* Copyright (c) 2022 Skyward Experimental Rocketry
  * Author: Davide Mor
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -23,6 +23,8 @@
 #include <drivers/interrupt/external_interrupts.h>
 #include <radio/CC3135/CC3135.h>
 
+#include <thread>
+
 using namespace Boardcore;
 using namespace miosix;
 
@@ -46,15 +48,17 @@ using hib = Gpio<GPIOA_BASE, 5>;
 
 CC3135 *cc3135 = nullptr;
 
+volatile size_t IRQ_COUNT = 0;
+
 void __attribute__((used)) EXTI4_IRQHandlerImpl()
 {
+    IRQ_COUNT += 1;
     if (cc3135)
-        cc3135->handleIntr();
+        cc3135->handleIrq();
 }
 
 void initBoard()
 {
-
 #ifdef CC3135_HIB
     {
         miosix::FastInterruptDisableLock dLock;
@@ -84,19 +88,60 @@ void initBoard()
 
 int main()
 {
+    // IRQ watcher thread
+    /*std::thread _watcher(
+        []()
+        {
+            size_t last = -1;
+            while (1)
+            {
+                if (last != IRQ_COUNT)
+                {
+                    printf("[cc3135] IRQ: %d\n", IRQ_COUNT);
+                    last = IRQ_COUNT;
+                }
+
+                // Sleep to avoid CPU hogging
+                Thread::sleep(10);
+            }
+        });*/
+
     initBoard();
 
+#ifdef CC3135_HIB
+    // Reset CC3135
+    hib::low();
+    Thread::sleep(10);
+    hib::high();
+
+    // Wait for the device to fully initialize.
+    // The device is very chatty at the beginning,
+    // but it's also in a weird state where the IRQ
+    // pin doesn't trigger properly. Just wait for it to calm down
+    Thread::sleep(2000);
+#endif
+
 #ifdef CC3135_UART
     std::unique_ptr<ICC3135Iface> iface(new CC3135Uart(CC3135_UART));
 #endif
 
+    printf("[cc3135] Initializing...\n");
     cc3135 = new CC3135(std::move(iface));
+    printf("[cc3135] Initialization complete!\n");
+
+    auto version = cc3135->getVersion();
+    printf(
+        "[cc3135] Chip Id: %lx\n"
+        "[cc3135] Fw version: %u.%u.%u.%u\n"
+        "[cc3135] Phy version: %u.%u.%u.%u\n"
+        "[cc3135] Nwp version: %u.%u.%u.%u\n"
+        "[cc3135] Rom version: %x\n",
+        version.chip_id, version.fw_version[0], version.fw_version[1],
+        version.fw_version[2], version.fw_version[3], version.phy_version[0],
+        version.phy_version[1], version.phy_version[2], version.phy_version[3],
+        version.nwp_version[0], version.nwp_version[1], version.nwp_version[2],
+        version.nwp_version[3], version.rom_version);
 
-    Thread::sleep(200);
-
-    cc3135->getVersion();
     while (true)
-    {
-        cc3135->dummyRead();
-    }
+        ;
 }