While Elixir isn't the programming language most commonly associated with embedded software development, it offers features that could make a surprisingly good choice in this domain.

Table of contents

    The qualities for which we appreciate Elixir so much — concurrency and fault tolerance — also play a key role in more complex embedded systems, where multiple tasks must run in parallel and recover gracefully from failures.
    In this article, I want to show a rather simple case of using Elixir for embedded systems. I will use the Arduino Uno board to read a sensor and send data to the Elixir app via a serial connection. We must remember that in such a small-scale project, using Elixir would be an overkill, so it should be treated rather as an exemplary entry point than a real-case scenario. Yet, the techniques I'll use - handling serial communication, parsing data, maintaining sliding windows, and calculating statistics in real-time - surely could be building blocks for a more complex project.

    Connecting Arduino with Elixir: reasons and challenges

    Before we start playing with Arduino, let's look at the reasons and challenges which come with using it with Elixir.

    Possible reasons:

    • Integration with larger systems — for sure, one of the main reasons here would be using the Arduino data in projects that already use Elixir. It'd not only make the ecosystem simpler, but also let programmers already accustomed to Elixir write code faster and without additional learning.

    • Real-time monitoring and analysis — in Elixir, you can stream incoming serial data into processes that immediately transform, filter, or visualise it, e. g. with the use of the LiveView library.

    • Fault-tolerance pipelines — Elixir is known for handling crashing and restarts without taking down the whole system - it could be valuable in more complex embedded systems dealing with unreliable sensors or intermittent connections.

    Possible challenges:

    • Performance trade-offs — deciding what should be computed on the Arduino (low latency, less bandwidth) vs in Elixir (flexibility, more CPU power).

    • Resource mismatch — as Arduino has rather limited memory/CPU, and the Elixir side can be more powerful, the protocols should be designed considering this.

    • Tooling gap, shortage of resources — as using Elixir for embedded systems is quite niche, there will possibly still be cases when some knowledge of C/C++ will be needed. The code uploaded to Arduino Uno is one of the examples. Also, there aren't many resources and community support in this area.

    Equipment and setup

    In my case study, I will use the following tools:

    • Arduino Uno board (you can replace it with Arduino Nano, if you prefer).

    • LDR (Light Dependent Resistor) sensor.

    • Several wires + ~10 kΩ resistor (only for LDR).

    • Breadboard (not necessary, but helpful).

    • USB cable for connecting the Arduino board.

    • Arduino IDE installed on macOS.

    Data protocol: simple and robust

    The first thing we should do in case of Arduino-Elixir communication is to establish the protocol on which it'll be based. Arduino sends raw bytes, and Elixir is supposed to receive data, so we need to decide on a consistent format to keep everything clear.

    We have two main types of fitting protocols - CSV and NDJSON. CSV is the simplest one - we have here columns separated by commas and every frame is situated in a new line. The example representing frame, timestamp and value is:

    42,0.210,678

    The NDJSON is slightly more complex. Here, every line is a new JSON object. The above example in this protocol looks like that:

    "frame":42,"t":0.210,"value":678}

    In our case, for the sake of simplicity in creating it and parsing on the Elixir side, we'll stick to CSV.

    Arduino: data streaming at 200 Hz

    As I said earlier, we'll still need some amount of C++ to use Arduino. Yet, the code will be quite easy, using Arduino's native language (simplified C++). It's based on two main functions:

    void setup() { ... }
    void loop()  { ... }

    The first one is executed once, just after the device's start, while the second one defines the processes in an unfinished loop. So, considering our protocol, the code for Arduino should look like this:

    const unsigned long PERIOD_US = 5000;
    unsigned long next_ts = 0;
    unsigned long frame = 0;
    
    void setup() {
      Serial.begin(115200);
      analogReference(DEFAULT);
      next_ts = micros();
    }
    
    void loop() {
      unsigned long now = micros();
      if ((long)(now - next_ts) >=0 ) {
        int raw = analogRead(A0);
        float t = now / 1000000.0;
        Serial.print(frame);
        Serial.print(',');
        Serial.print(t, 6);
        Serial.print(',');
        Serial.println(raw);
        frame++;
        next_ts += PERIOD_US;
      }
    }

    Firstly, we define a constant PERIOD_US, which keeps the sampling period at 5000 microseconds (equivalent to 200 Hz)—it tells our program how often it should read data from our sensor. The other variables—next_ts and frame—hold the time for the next reading and the counter of already sent samples.

    Our setup() function initialises the serial communication at a 115200 baud rate and sets the analog reference to DEFAULT (5V on a typical Arduino board). We also use Arduino native micros() function, which returns the number of microseconds since the current program's start.

    At the start of each loop iteration, we check if it's time for the next sample. If so, we read the value from the A0 analog pin and send three comma-separated values over the serial port: the frame counter, the timestamp in seconds, and the raw sensor data.

    Elixir: Circuits.UART i GenServer for streaming

    In this section, we'll build the Elixir side of our app—a UART stream reader. We'll open the port, parse the CSV data, and calculate the median of our samples. We'll start by adding the Circuits.UART library to our dependencies (of course, we need to install Erlang/Elixir and set up our project first):

    defp deps do
      [
        {:circuits_uart, "~> 1.5"}
      ]
    end

    Then, we need to create a simple GenServer for reading our data and creating statistics:

    Real-time preview: Logger, ETS, and LiveView

    Medians and means — implementations and pitfalls

    Benchmark: Arduino vs Elixir

    Optimisations and fault tolerance

    Summary and repo with example

    FAQ

    1. What is the main purpose of this article?

    The article demonstrates how to use Elixir—via the Circuits.UART library—to read data from an Arduino Uno over a serial connection, enabling real‑time monitoring, processing, and even streaming to LiveView interfaces. It serves as a simple entry point for embedded systems integration with Elixir.

    2. Why use Elixir with Arduino, given that it seems like overkill?

    Elixir brings concurrency, fault-tolerance, and real-time data processing to embedded scenarios. While it's not necessary for small projects, Elixir’s robustness and transformation pipelines make it useful for building more complex, resilient systems.

    3. What Elixir library is used for serial communication with Arduino?

    The article uses the Circuits.UART library (previously known as Nerves.UART) to handle serial communication—discovering ports, opening connections, framing data, and reading input.

    4. How does Circuits.UART manage framing and data flow?

    Circuits.UART supports both active (message-driven) and passive (polling) modes. It offers flexible framing options like line-based framing via Circuits.UART.Framing.Line, handling message boundaries, separators, and timeouts efficiently.

    5. What are typical steps for connecting Arduino to an Elixir app?

    1. Add circuits_uart to your mix.exs dependencies.
    2. Use Circuits.UART.enumerate/0 to list serial ports.
    3. Start a UART GenServer with Circuits.UART.start_link/0.
    4. Open the desired port (e.g., "/dev/ttyACM0") via open/3, configure framing and set :active or :passive mode.
    5. Read incoming data using read/2 or via active messages.
    6. Process, transform, or visualize the data in real time (e.g., via LiveView).

    6. Can disconnects or reconnections cause issues?

    Yes—especially in active mode, disconnecting and reconnecting an Arduino may fail to send data correctly. Users on Elixir Forum note that handling reconnects robustly requires careful design in the GenServer, avoiding patterns like spawn inside handle_info, and instead using GenServer.cast for safer recovery.

    7. What additional hardware interfaces does Elixir support for embedded communication?

    Beyond UART, Elixir developers also use the Circuits family of libraries—like Circuits.GPIO, Circuits.I2C, and Circuits.SPI—to interact with buttons, sensors, displays, and more, often within Nerves or embedded Linux platforms.

    8. What are the benefits of using Elixir’s concurrency model here?

    Elixir's actor-like processes and fault-tolerant pipelines allow seamless chaining of data transformations—like parsing, filtering, analyzing, or visualizing data in real time—and can recover gracefully from hardware interruptions or sensor faults.

    9. Is this method limited to Linux environments?

    No. Circuits.UART supports multiple platforms—including macOS, Windows, Linux, and Nerves—making it versatile for both desktop and embedded use cases.

    10. How does this approach set the foundation for bigger embedded projects?

    Although the example is small-scale, the same architecture—real-time serial ingestion, concurrent processing, fault tolerance, and LiveView integration—can serve as a robust foundation for larger IoT, automation, or sensor-based systems using Elixir.

    Curiosum Elixir and React Developer Krzysztof
    Krzysztof Janiec Elixir & React Developer

    Read more
    on #curiosum blog