diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt
index 7de732437632982afcced6cbb92a7e5fda66e284..9a24fbd838c1c6c9799241fe4ffaa3d23325f6de 100644
--- a/apps/CMakeLists.txt
+++ b/apps/CMakeLists.txt
@@ -1,3 +1,4 @@
 add_subdirectory(communicator)
 add_subdirectory(pinephone-communicator)
-add_subdirectory(usb-communicator)
\ No newline at end of file
+add_subdirectory(usb-communicator)
+add_subdirectory(gpio)
\ No newline at end of file
diff --git a/apps/communicator/include/PineDio/LoRa/Communicator.h b/apps/communicator/include/PineDio/LoRa/Communicator.h
index fc027053ed7d31face1fe3da246b9d677b7b116d..88bb46010de0cbae2ea7da09a522f0d19c091639 100644
--- a/apps/communicator/include/PineDio/LoRa/Communicator.h
+++ b/apps/communicator/include/PineDio/LoRa/Communicator.h
@@ -6,8 +6,9 @@ class PinedioLoraRadio;
 class Communicator {
 public:
   explicit Communicator(PineDio::LoRa::PinedioLoraRadio &radio);
-
+  ~Communicator();
   void Run();
+  void Stop();
 
 private:
   PineDio::LoRa::PinedioLoraRadio &radio;
diff --git a/apps/communicator/src/Communicator.cpp b/apps/communicator/src/Communicator.cpp
index 77dfc73198402bd69766cb55d2280adf3a5a8c45..f549f57a785dcfcfb8ec6a86e3806db2a27cd106 100644
--- a/apps/communicator/src/Communicator.cpp
+++ b/apps/communicator/src/Communicator.cpp
@@ -17,6 +17,12 @@ Communicator::Communicator(PineDio::LoRa::PinedioLoraRadio &radio) : radio{radio
   receiveTask.reset(new std::thread([this](){Receive();}));
 }
 
+Communicator::~Communicator() {
+  running = false;
+  receiveTask->join();
+}
+
+
 void Communicator::Run() {
   running = true;
 
@@ -39,8 +45,10 @@ void Communicator::Run() {
 
 }
 void Communicator::Receive() {
-  while(true){
-    auto data = radio.Receive();
+  while(running){
+    auto data = radio.Receive(std::chrono::milliseconds{100});
+    if(data.empty())
+      continue;
     std::cout << "Data received on LoRa radio : " << std::endl;
     std::cout << "\tHEX: ";
     for(auto d : data) {
@@ -55,3 +63,7 @@ void Communicator::Receive() {
   }
 }
 
+void Communicator::Stop() {
+  running = false;
+}
+
diff --git a/apps/gpio/CMakeLists.txt b/apps/gpio/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4d137640007b0595c3210f7571dc96f2f3a3c1e8
--- /dev/null
+++ b/apps/gpio/CMakeLists.txt
@@ -0,0 +1,5 @@
+project(gpio)
+cmake_minimum_required(VERSION 3.21)
+
+add_executable(gpio
+    main.cpp)
diff --git a/apps/gpio/main.cpp b/apps/gpio/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2051cd363b410e8125bee29d6b5bfbf3f81a2ba4
--- /dev/null
+++ b/apps/gpio/main.cpp
@@ -0,0 +1,99 @@
+#include <iostream>
+#include <dirent.h>
+#include <linux/gpio.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <vector>
+
+struct FileDescriptor {
+  int fd;
+};
+
+void PrintChipInfo(const std::string& chip);
+void PrintLineInfo(FileDescriptor chip, int line);
+std::string GetCh341ChipFilename();
+std::vector<std::string> GetChipChipNames();
+
+int main() {
+  auto chips = GetChipChipNames();
+  std::cout << "Number of GPIO chips :  " << chips.size() << std::endl;
+
+  for(const auto& chip : chips) {
+    PrintChipInfo(chip);
+  }
+
+  try {
+    auto ch341Index = GetCh341ChipFilename();
+    std::cout << std::endl << std::endl << " --> CH341 GPIO chip detected at index " << ch341Index << std::endl;
+  } catch(const std::runtime_error& error) {
+    std::cerr << "Error : " << error.what();
+  }
+
+  return 0;
+}
+
+std::vector<std::string> GetChipChipNames() {
+  auto gpioFilenameFilter = [](const struct dirent *entry) -> int {
+    std::string filename = entry->d_name;
+    return filename.rfind("gpiochip", 0) == 0;
+  };
+  static const char* devString = "/dev/";
+
+  std::vector<std::string> filenames;
+  struct dirent **entries;
+  auto nb = scandir(devString, &entries, gpioFilenameFilter, alphasort);
+  for(int i = 0; i < nb; i++) {
+    std::string name = std::string{devString} + std::string{entries[i]->d_name};
+    filenames.push_back(name);
+  }
+  return filenames;
+}
+
+void PrintChipInfo(const std::string& chip) {
+  struct gpiochip_info info;
+  auto fd = open(chip.c_str(), O_RDWR);
+
+  auto res = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
+  if(res == 0) {
+    std::cout << chip << " : " << info.name << " (" << info.label
+              << ") : " << info.lines << " lines" << std::endl;
+    for(int i = 0; i < info.lines; i++) {
+      PrintLineInfo({fd}, i);
+    }
+  } else {
+    std::cerr << "Error while getting chip info for " << chip << std::endl;
+  }
+}
+
+void PrintLineInfo(FileDescriptor fileDescriptor, int line) {
+  struct gpioline_info info;
+  info.line_offset = line;
+
+  auto res = ioctl(fileDescriptor.fd, GPIO_GET_LINEINFO_IOCTL, &info);
+  if(res == 0) {
+    std::cout << "\t[" << line << "] " << info.name << " - " << info.consumer << " - " << info.flags << std::endl;
+  } else {
+    std::cerr << "Error while getting line info for line " << line;
+  }
+}
+
+bool GetChipInfo(const std::string chip, struct gpiochip_info& info) {
+  auto fd = open(chip.c_str(), O_RDWR);
+  auto res = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
+  return res == 0;
+}
+
+std::string GetCh341ChipFilename() {
+  static const char* ch341ChipLabel = "ch341";
+  static const size_t ch341NbLines = 16;
+
+  auto chips = GetChipChipNames();
+  for(auto& chip : chips) {
+    struct gpiochip_info info;
+    if(GetChipInfo(chip, info)) {
+      if(std::string{info.label} == std::string{ch341ChipLabel} && info.lines == ch341NbLines)
+        return chip;
+    }
+  }
+  throw std::runtime_error("Cannot find a GPIO chip corresponding to the CH341");
+}
diff --git a/apps/usb-communicator/main.cpp b/apps/usb-communicator/main.cpp
index 7d27157aa43acee020c76507ab77211a211b04d2..0e8c9a4973c9db483e17dbea9dcd104cbbdd0b5c 100644
--- a/apps/usb-communicator/main.cpp
+++ b/apps/usb-communicator/main.cpp
@@ -2,12 +2,21 @@
 #include <iostream>
 #include "PineDio/LoRa/UsbAdapter.h"
 #include "PineDio/LoRa/Exceptions.h"
+#include <signal.h>
+
+PineDio::LoRa::UsbAdapter usbAdapter;
+PineDio::LoRa::PinedioLoraRadio radio(usbAdapter);
+PineDio::LoRa::Communicator communicator(radio);
+
+void sig_handler(int signum) {
+  std::cout << "Press ENTER to exit..." << std::endl;
+  communicator.Stop();
+}
 
 int main() {
+  signal(SIGINT, sig_handler);
+
   try {
-    PineDio::LoRa::UsbAdapter usbAdapter;
-    PineDio::LoRa::PinedioLoraRadio radio(usbAdapter);
-    PineDio::LoRa::Communicator communicator(radio);
     communicator.Run();
   } catch(const PineDio::LoRa::InitializationException& ex) {
     std::cerr << "Initialization error : " << ex.what() << std::endl;
diff --git a/include/PineDio/LoRa/PinedioLoraRadio.h b/include/PineDio/LoRa/PinedioLoraRadio.h
index 04f69046c2d095931324c3ecebba7f4770e0b16e..624e524d8456300368c05b284d6b020c6ec267f0 100644
--- a/include/PineDio/LoRa/PinedioLoraRadio.h
+++ b/include/PineDio/LoRa/PinedioLoraRadio.h
@@ -12,7 +12,7 @@ public:
 
   virtual void Initialize();
   virtual void Send(std::vector<uint8_t> data);
-  virtual std::vector<uint8_t> Receive();
+  virtual std::vector<uint8_t> Receive(std::chrono::milliseconds timeout);
 private:
   SX126x& radio;
   bool dataReceived {false};
diff --git a/include/PineDio/LoRa/UsbAdapter.h b/include/PineDio/LoRa/UsbAdapter.h
index 07b71f178adc8b898d0ca197e8937d6e14d3bd43..85925b33b5188237f0f67e465b6464fce7fa7306 100644
--- a/include/PineDio/LoRa/UsbAdapter.h
+++ b/include/PineDio/LoRa/UsbAdapter.h
@@ -1,19 +1,25 @@
 #pragma once
 #include <PineDio/LoRa/PinedioLoraRadio.h>
 #include <memory>
+#include <linux/gpio.h>
 
 namespace PineDio::LoRa {
 
 class UsbAdapter : public SX126x {
 public:
   UsbAdapter();
+  ~UsbAdapter() override;
 
 private:
   uint8_t HalGpioRead(GpioPinFunction_t func) override;
   void HalGpioWrite(GpioPinFunction_t func, uint8_t value) override;
   void HalSpiTransfer(uint8_t *buffer_in, const uint8_t *buffer_out, uint16_t size) override;
+  void OpenSpi();
+  void OpenGpio();
 
   int handle;
+  struct gpiohandle_request resetGpio;
+  struct gpiohandle_request busyGpio;
 
   void RxDone();
 };
diff --git a/src/PinedioLoraRadio.cpp b/src/PinedioLoraRadio.cpp
index 2303fdb6cd23c370d24fc6d98a9447d19d66a651..a71f9daae45f13ecdb8f571e419fbcd86bcf58f0 100644
--- a/src/PinedioLoraRadio.cpp
+++ b/src/PinedioLoraRadio.cpp
@@ -43,7 +43,7 @@ void PinedioLoraRadio::Initialize() {
 
 
 
-  static char* message = "Hello, I'm a Pinephone!";
+  static const char* message = "Hello, I'm a Pinephone!";
   auto s = strlen(message);
 
   SX126x::PacketParams_t packetParams;
@@ -63,9 +63,10 @@ void PinedioLoraRadio::Send(const std::vector<uint8_t> data) {
   transmitBuffer = data; //copy
 }
 
-std::vector<uint8_t> PinedioLoraRadio::Receive() {
-  std::cout << "[PinedioLoraRadio] Receive()" << std::endl;
+std::vector<uint8_t> PinedioLoraRadio::Receive(std::chrono::milliseconds timeout) {
   bool running = true;
+  auto startTime = std::chrono::steady_clock::now();
+  std::vector<uint8_t> buffer;
   while(running) {
     radio.ProcessIrqs();
 
@@ -87,13 +88,18 @@ std::vector<uint8_t> PinedioLoraRadio::Receive() {
     }
 
     if(dataReceived) {
+      buffer = receivedBuffer;
       break;
     }
+
+    if(std::chrono::steady_clock::now() - startTime > timeout)
+      break;
+
     std::this_thread::sleep_for(std::chrono::milliseconds(100));
   }
 
   dataReceived = false;
-  return receivedBuffer; //copy
+  return buffer; //copy
 }
 
 void PinedioLoraRadio::OnDataReceived() {
diff --git a/src/usb-adapter/UsbAdapter.cpp b/src/usb-adapter/UsbAdapter.cpp
index 636031045aff22a4fd0e18d89afd367275f9c0b8..64802b9af676402b1ced85e9269aaa3dbf2e94ef 100644
--- a/src/usb-adapter/UsbAdapter.cpp
+++ b/src/usb-adapter/UsbAdapter.cpp
@@ -3,97 +3,121 @@
 #include <unistd.h>
 
 #include <fcntl.h>
-#include <unistd.h>
 #include <sys/ioctl.h>
 #include <linux/types.h>
 #include <linux/spi/spidev.h>
+#include <dirent.h>
+#include <linux/gpio.h>
 
 using namespace PineDio::LoRa;
 
-UsbAdapter::UsbAdapter() {
-  std::cout << "[UsbAdapter] ctor()" << std::endl;
-
-  // TODO remove code duplication for exporting GPIO
-  if(access("/sys/class/gpio/ini", F_OK) != 0 ) {
-    int fd;
-    if ((fd = open("/sys/class/gpio/export", O_WRONLY)) == -1) {
-      perror("Error while opening GPIO export file");
-      // TODO ERROR
-    }
-    if (write(fd, "240", 3) == -1) { // TODO how to find the GPIO number programatically?
-      perror("Error while exporting GPIO \"ini\"");
-      // TODO ERROR
-    }
-    close(fd);
+namespace {
+const char *spiDriverOverrideFilePath ="/sys/class/spi_master/spi0/spi0.0/driver_override";
+const char *spidevOverride = "spidev";
+
+const char *spiBindFilePath = "/sys/bus/spi/drivers/spidev/bind";
+const char *spiUnbindFilePath = "/sys/bus/spi/drivers/spidev/unbind";
+const char *spiBind = "spi0.0";
+
+std::vector<std::string> GetChipChipNames() {
+  auto gpioFilenameFilter = [](const struct dirent *entry) -> int {
+    std::string filename = entry->d_name;
+    return filename.rfind("gpiochip", 0) == 0;
+  };
+  static const char *devString = "/dev/";
+
+  std::vector<std::string> filenames;
+  struct dirent **entries;
+  auto nb = scandir(devString, &entries, gpioFilenameFilter, alphasort);
+  for (int i = 0; i < nb; i++) {
+    std::string name = std::string{devString} + std::string{entries[i]->d_name};
+    filenames.push_back(name);
   }
+  return filenames;
+}
 
-  if(access("/sys/class/gpio/slct", F_OK) != 0 ) {
-    int fd;
-    if ((fd = open("/sys/class/gpio/export", O_WRONLY)) == -1) {
-      perror("Error while opening GPIO export file");
-      // TODO ERROR
-    }
-    if (write(fd, "244", 3) == -1) { // TODO how to find the GPIO number programatically?
-      perror("Error while exporting GPIO \"sclt\"");
-      // TODO ERROR
+bool GetChipInfo(const std::string chip, struct gpiochip_info &info) {
+  auto fd = open(chip.c_str(), O_RDWR);
+  auto res = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
+  return res == 0;
+}
+
+std::string GetCh341ChipFilename() {
+  static const char *ch341ChipLabel = "ch341";
+  static const size_t ch341NbLines = 16;
+
+  auto chips = GetChipChipNames();
+  for (auto &chip : chips) {
+    struct gpiochip_info info;
+    if (GetChipInfo(chip, info)) {
+      if (std::string{info.label} == std::string{ch341ChipLabel} &&
+          info.lines == ch341NbLines)
+        return chip;
     }
-    close(fd);
   }
+  throw std::runtime_error("Cannot find a GPIO chip corresponding to the CH341");
+}
 
-  handle = open("/dev/spidev0.0", O_RDWR);
-  if(handle == -1) {
-    // TODO error
-    printf("SPI IOCTL error %s\n", strerror(errno));
+int GetGpioLineIndex(const std::string &chip, const char *name) {
+  struct gpiochip_info info;
+  auto fd = open(chip.c_str(), O_RDWR);
+  auto res = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
+  if(res != 0)
+    throw std::runtime_error("IOCTL error (GPIO_GET_CHIPINFO_IOCTL failed)");
+
+  for (int i = 0; i < info.lines; i++) {
+    struct gpioline_info infoLine;
+    infoLine.line_offset = i;
+    res = ioctl(fd, GPIO_GET_LINEINFO_IOCTL, &infoLine);
+    if(res != 0)
+      throw std::runtime_error("IOCTL error (GPIO_GET_LINEINFO_IOCTL failed)");
+
+    if (strcmp(name, infoLine.name) == 0)
+      return i;
   }
-
-  uint8_t mmode = SPI_MODE_0;
-  uint8_t lsb = 0;
-  uint8_t bitsperword = 8;
-  ioctl(handle, SPI_IOC_RD_BITS_PER_WORD, &bitsperword);
-  ioctl(handle, SPI_IOC_WR_MODE, &mmode);
-  ioctl(handle, SPI_IOC_WR_LSB_FIRST, &lsb);
+  throw std::runtime_error("Cannot find GPIO line");
+}
 }
 
-uint8_t UsbAdapter::HalGpioRead(SX126x::GpioPinFunction_t func) {
-  if(func != GpioPinFunction_t::GPIO_PIN_BUSY) {
-    std::cout << "ERROR" << std::endl;
-    throw;
-  }
+UsbAdapter::UsbAdapter() {
+  std::cout << "[UsbAdapter] ctor()" << std::endl;
+  OpenGpio();
+  OpenSpi();
+}
 
-  int  fd;
-  if ((fd = open("/sys/class/gpio/slct/value", O_RDWR)) == -1)   {
-    perror("Error while opening GPIO \"busy\"");
-    return -1;
-  }
+UsbAdapter::~UsbAdapter() {
+  close(handle);
+  auto fd = open(spiUnbindFilePath, O_WRONLY);
+  write(fd, spiBind, strlen(spiBind));
 
-  char buf;
-  if (read(fd, &buf, 1) == -1) {
-    perror("Error while reading GPIO \"busy\"");
-    return -1;
-  }
   close(fd);
+}
+
+uint8_t UsbAdapter::HalGpioRead(SX126x::GpioPinFunction_t func) {
+  if(func != GpioPinFunction_t::GPIO_PIN_BUSY)
+    throw std::runtime_error("Invalid call to HalGpioRead()");
+
+  struct gpiohandle_data data;
+  memset(&data, 0, sizeof(data));
+  auto res = ioctl(busyGpio.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
+  if(res != 0)
+    throw std::runtime_error("IOCTL error (GPIOHANDLE_GET_LINE_VALUES_IOCTL failed)");
 
-  int value = (buf == '0') ? 0 : 1;
+  int value = data.values[0];
 
   std::this_thread::sleep_for(std::chrono::milliseconds{1}); // Why do I need this sleep()?
   return value;
 }
 
 void UsbAdapter::HalGpioWrite(SX126x::GpioPinFunction_t func, uint8_t value) {
-  if(func != GpioPinFunction_t::GPIO_PIN_RESET) {
-    std::cout << "ERROR" << std::endl;
-    throw;
-  }
-
-  int  fd;
-
-  if ((fd = open("/sys/class/gpio/ini/value", O_RDWR)) == -1)   {
-    perror("Error while opening GPIO \"reset\"");
-  }
-
-  if (write(fd, value ? "1" : "0", 1) == -1) {
-    perror ("Error while writing GPIO \"reset\"");
-  }
+  if(func != GpioPinFunction_t::GPIO_PIN_RESET)
+    throw std::runtime_error("Invalid call to HalGpioWrite()");
+
+  struct gpiohandle_data data;
+  data.values[0] = value;
+  auto res = ioctl(resetGpio.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
+  if(res != 0)
+    throw std::runtime_error("IOCTL error (GPIOHANDLE_SET_LINE_VALUES_IOCTL failed)");
 }
 
 void UsbAdapter::HalSpiTransfer(uint8_t *buffer_in, const uint8_t *buffer_out, uint16_t size) {
@@ -108,6 +132,68 @@ void UsbAdapter::HalSpiTransfer(uint8_t *buffer_in, const uint8_t *buffer_out, u
   spi_trans.cs_change = true;
   spi_trans.len = size;
 
-  int status = ioctl (handle, SPI_IOC_MESSAGE(1), &spi_trans);
+  int res = ioctl(handle, SPI_IOC_MESSAGE(1), &spi_trans);
+  if(res < 0)
+    throw std::runtime_error("IOCTL error (SPI_IOC_MESSAGE failed)");
+}
+
+void UsbAdapter::OpenSpi() {
+  auto fd = open(spiDriverOverrideFilePath, O_WRONLY);
+  if(fd == -1)
+    throw std::runtime_error("Cannot open SPI device (driver_override open error)");
+
+  auto res = write(fd, spidevOverride, strlen(spidevOverride));
+  if(res <= 0)
+    throw std::runtime_error("Cannot open SPI device (driver_override write error)");
+
+  close(fd);
+
+  fd = open(spiBindFilePath, O_WRONLY);
+  if(fd == -1)
+    throw std::runtime_error("Cannot open SPI device (bind open error)");
+
+  res = write(fd, spiBind, strlen(spiBind));
+  if(res <= 0)
+    throw std::runtime_error("Cannot open SPI device (bind write error)");
+
+  close(fd);
+
+  handle = open("/dev/spidev0.0", O_RDWR);
+  if(handle == -1)
+    throw std::runtime_error("Cannot open SPI device (spidev open error)");
+
+  uint8_t mmode = SPI_MODE_0;
+  uint8_t lsb = 0;
+  uint8_t bitsperword = 8;
+  ioctl(handle, SPI_IOC_RD_BITS_PER_WORD, &bitsperword);
+  ioctl(handle, SPI_IOC_WR_MODE, &mmode);
+  ioctl(handle, SPI_IOC_WR_LSB_FIRST, &lsb);
 }
 
+void UsbAdapter::OpenGpio() {
+  auto chipName = GetCh341ChipFilename();
+  std::cout << "CH341 detected at " << chipName << std::endl;
+
+  auto resetlineIndex = GetGpioLineIndex(chipName, "ini");
+  std::cout << "GPIO RESET detected at index " << resetlineIndex << std::endl;
+
+  struct gpiochip_info info;
+  auto fd = open(chipName.c_str(), O_RDWR);
+
+  resetGpio.flags = GPIOHANDLE_REQUEST_OUTPUT;
+  resetGpio.lines = 1;
+  resetGpio.lineoffsets[0] = resetlineIndex;
+  resetGpio.default_values[0] = 0;
+  strcpy(resetGpio.consumer_label, "LoRa SPI driver");
+  ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &resetGpio);
+
+  auto busylineIndex = GetGpioLineIndex(chipName, "slct");
+  std::cout << "GPIO BUSY detected at index " << busylineIndex << std::endl;
+
+  busyGpio.flags = GPIOHANDLE_REQUEST_INPUT;
+  busyGpio.lines = 1;
+  busyGpio.lineoffsets[0] = busylineIndex;
+  busyGpio.default_values[0] = 0;
+  strcpy(busyGpio.consumer_label, "LoRa SPI driver");
+  ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &busyGpio);
+}