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!
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!!
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:8282opens a TCP port for incoming connectionsforkallows multiple sessions, so the process doesn’t die when a connection closesreuseaddrensures the port can be reused if the program restarts/dev/ttyACM0is your serial devicerawtells the Linux TTY driver not to process the dataecho=0disables local echob115200sets 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:
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!!
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!
All Posts in This Series
- Part 1: Getting Started with OpenOCD: A Beginner's Guide
- Part 2: Remote Debugging with Raspberry Pi and OpenOCD
- Part 3: Cross-Compiling OpenOCD: A Step-by-Step Walkthrough
- Part 4: Simplifying OpenOCD Deployment with a Debian Package
- Part 5: Automating OpenOCD Distribution with a Private Gitea Package Registry
- Part 6: Running OpenOCD in Docker: A "Tools as Service" Approach to Embedded DevOps
- Part 7: Containerizing Serial Communication: Extending the TaaS Model with UART (Current Post)
Including External Libraries in CMake Projects
Learn how to use CMake’s FetchContent module to automatically download and integrate libraries like CMSIS into your embedded projects, eliminating the hassle of manual copying and updates.
How to Find the Memory Address of a Symbol in an ELF File
A quick and useful tip for locating symbol memory addresses in ELF files using arm-none-eabi-nm combined with grep—perfect for embedded debugging scenarios like setting up SEGGER RTT or inspecting linker placements and runtime symbols.
Advanced RTT in Embedded Rust: A Guide to Multi-Channel Logging and Binary Streaming (Part 1)
Learn to set up multi-channel RTT on the Pi Pico: structured logging with defmt, debug prints, and host commands—all over a single SWD/JTAG probe.
Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.