ESP32-based digital walkie-talkie (VoIP) for local Wi-Fi networks.
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
After covering the hardware in the last post, let's dive into what makes it tick: the software architecture.
When I refactored this project from its old Arduino-based monolith, my main goal was to create a clean, flexible, and scalable system. I built the entire architecture around FreeRTOS and a layered, event-driven model.
Here’s a breakdown of the key layers:
This is the lowest layer, separating the "application logic" from the physical hardware.
BSP (Board Support Package): This is the core of the hardware abstraction. It’s defined by a single struct (bsp_t) containing function pointers for all hardware operations. A global pointer (g_bsp) is used by all other modules to access hardware.
HAL (Hardware Abstraction Layer): These are the drivers using the BSP. They manage the specific peripherals (hal_display, hal_audio, hal_encoder).
This is the true center of the entire system. Instead of tasks calling each other directly, most modules send messages to a single, central queue: main_queue_event.
An app_event_t structure is as follows:
typedef struct {
app_event_source_t source;
uint8_t CmdCode;
app_event_payload_t payload;
} app_event_t;
can come from various sources:
typedef enum {
EVENT_SOURCE_INPUT,
EVENT_SOURCE_WIFI,
EVENT_SOURCE_CHANNEL,
EVENT_SOURCE_GUI,
EVENT_SOURCE_SNTP
// ... you can easily add any
} app_event_source_t;
as CmdCode could be used custom enums for specific source
typedef enum { INPUT_ENCODER_TURN, INPUT_KEY_PRESS } input_event_type_t;
typedef enum { WIFI_EVENT_CONNECTED, WIFI_EVENT_DISCONNECTED, WIFI_EVENT_STA_READY} wifi_event_type_t;
typedef enum { SNTP_EVENT_TIME_SYNCED } sntp_event_type_t;
typedef enum { CHANNEL_EVENT_PEER_SPEAKING, CHANNEL_EVENT_PEER_SPEAKING_END , CHANNEL_EVENT_PEER_LIST_CHANGED} channel_event_type_t;
typedef enum { GUI_EVENT_CONNECT_TO_SERVER, GUI_EVENT_PEER_SELECTED } gui_event_type_t;
payload also easily enchanced
typedef union {
struct {
uint8_t key_code;
int16_t press_type;
uint32_t timestamp_ms;
} input;
struct {
bool is_connected;
} wifi;
struct{
uint64_t peer_speaking;
}channel;
struct{
uint16_t selected_peer;
peer_info_t peer_info;
}gui;
} app_event_payload_t;
This means we have one primary handler in main() that receives all these events and decides what to do next.
If the main_queue is for sending messages, the system_event_group is for signaling states. This is crucial for synchronizing tasks. For example, the lan_task waits for the WIFI_CONNECTED_BIT (set by hal_net) before it starts running.
These are the other key modules that run as separate tasks and interact with the event system:
This is the most complex part of the project. It's a two-way process (Record/Transmit and Receive/Play) managed by FreeRTOS tasks, queues, and a shared memory pool.
typedef struct {
uint32_t magic_number; // "secret knock" (e.g. 0xDEADBEEF)
udp_packet_type_t type; // Packet type from enum
uint64_t sender_sn; // Unique sender serial number
char sender_name[...Hello everyone! Welcome to the first proper hardware log for the `stray` project.
My goal here is to walk you through the v1 prototype hardware: why we chose certain parts, how it's all connected, and (most importantly) all the "features" and quirks you'll find if you dig into the design files.
Design Philosophy
The main driver for this design was ergonomics. We wanted a device that felt comfortable to hold, with all the main functions "at your fingertips." This led to some... interesting design choices.
Core Components
Here’s a quick rundown of what's on the board:
---
Here's how it looks inside
The case is a 2-part custom assembly:
---
"Features, Not Bugs" (The Quirks)
This is the fun part. This board has character.
The foundations for this project were laid several years ago. It began as a remote Push-to-Talk (PTT) tangent controller for amateur radios. A colleague of mine designed a few hardware prototypes and handed them over to our team for firmware development.
The first implementation was built on Arduino. It quickly grew in complexity, becoming difficult to maintain and hitting several technical limitations. During this time, our focus also shifted—away from simple radio control and towards direct device-to-device communication over IP.
We managed to build an MVP that worked not only on a local network but also via a custom server on the global internet (WAN). We even had minimal transceiver control working. However, circumstances forced us to shelve the project for a while.
Even though it was on the shelf, we constantly returned to the project in our thoughts and conversations. So this year, I decided there was no better time than the present to resurrect it.
I started a complete refactoring of the entire codebase. I migrated everything to the ESP-IDF framework and designed a more flexible, modern architecture. My hope is that this new foundation will allow us (and the community) to easily and painlessly adapt the project to newer, simpler, or just different hardware in the future. Most importantly, I decided to take the project fully open-source.
As of today, these are the key, functional parts of the project:
components/hal_display/ui to modify it yourself.The stray device connects to your Wi-Fi network. It actively pings other units to maintain a live "presence" list of who is online. When you press the PTT button, it streams audio to the selected channel (either Broadcast to all or Unicast to one). When an incoming call is received, the UI automatically focuses on the speaker, making it easy to see who is talking and to reply.
So, keep in touch!
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates
Christoph Tack
stupid
Vedran
Andrew Benson