Containerizing Serial Communication: Extending the TaaS Model with UART

Learn how to set up remote serial communication for embedded development using Socat, Docker, and Raspberry Pi. Streamline UART access and debug from anywhere!

Andre Leppik

This post is part of the Tools as a Service Series. Full architecture overview here.

The Need for Remote Serial Communication

In the previous post, we explored the Tool as a Service (TaaS) approach for remote embedded development, focusing on debugging with OpenOCD. While debugging is crucial, embedded development often requires more, for example interfacing with UART peripherals.

Today, we'll extend our setup to include remote serial communication, allowing us to monitor and interact with our device’s serial output from anywhere.

Setting Up Serial Output on the Pico

Before diving into remote access, let's first start with a simple C example and view the output on the host. The following example program prints a message to the serial console every two seconds.

#include <stdio.h>
#include "pico/stdlib.h"

int main() {
    stdio_init_all();

    while (true) {
        sleep_ms(2000);
        printf("make progress!!\n");
    }
}

Flash this program to your Pico. To view the output, we need to first SSH into our Raspberry Pi and locate the serial device (probably something like ttyACM0). The simplest method is to find the device by id using ls:

ls -l /dev/serial/by-id/

You should see something like this:

lrwxrwxrwx 1 root root 13 Feb 12 09:58 usb-Raspberry_Pi_Debug_Probe__CMSIS-DAP__E6633861A3725538-if01 -> ../../ttyACM0

Now to read the serial output we can simply use cat:

$ cat /dev/ttyACM0
make progress!!
make progress!!
Pro Tip

If the output looks gibberish, reset the serial port settings as it might be a problem with the previous configuration - ask me how I know (╥‸╥)

sudo stty -F /dev/ttyACM0 115200 raw -echo -echoe -echok -echoctl -echoke

Making Serial Communication Remote

To access the serial output remotely, we need a way to expose the serial port over the network. We will be using socat, a versatile tool that establishes bidirectional byte streams between two points. Just what we needed!

If you do not have socat installed you can grab it with apt :

sudo apt install socat

Run it (if port 8282 is in use on your system pick another port):

socat tcp-listen:8282,fork,reuseaddr /dev/ttyACM0,raw,echo=0,b115200

Lets break it down:

  • tcp-listen:8282 opens a TCP port for incoming connections
  • fork allows multiple sessions, so the process doesn’t die when a connection closes
  • reuseaddr ensures the port can be reused if the program restarts
  • /dev/ttyACM0 is your serial device
  • raw tells the Linux TTY driver not to process the data
  • echo=0 disables local echo
  • b115200 sets the baud rate to 115200

Now, from your development PC on the same network, connect using nc (netcat) or a serial monitor in VS Code:

Serial output from Pico monitored from VS Code over the network

Voilà! You’re now remotely monitoring your Pico’s serial output.

Containerizing the Serial Bridge

Continuing the the TaaS mindset we'll be containerizing socat and create a reusable, isolated environment that simplifies deployment and ensures consistency.

Dockerfile:

FROM alpine:latest

RUN apk add --no-cache socat

EXPOSE 8282

ENTRYPOINT ["sh", "-c", "socat tcp-listen:${LISTEN_PORT},fork,reuseaddr ${SERIAL_DEV},raw,echo=0,b${BAUD}"]

To make device access seamless, create a udev rule to map the Pi Probe’s serial device to a consistent symlink.

Edit /etc/udev/rules.d/99-usb-debugger.rules and add:

KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000c", SYMLINK+="debug/pi-probe-serial", MODE="0666", GROUP="plugdev"

For the new rules to take effect, reload them with:

sudo udevadm control --reload-rules
sudo udevadm trigger

Finally we can run the container:

docker run -d --name pico_bridge                            \
    --device=$(readlink -f /dev/debug/pi-probe-serial)      \
    -p 8282:8282                                            \
    -e LISTEN_PORT=8282                                     \
    -e SERIAL_DEV=$(readlink -f /dev/debug/pi-probe-serial) \
    -e BAUD=115200                                          \
    --restart unless-stopped                                \
    pi-serial-bridge

Finally we can get back to our development pc and try connecting to the remote serial output again:

$ nc 192.168.20.200 8282
make progress!!
make progress!!
Dr.House: Smells like Victory!

What’s Next?

In the next post, we’ll explore adding even more tools to our TaaS toolkit. Stay tuned for more ways to streamline your embedded development workflow!

Need help with embedded systems development?

Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.