@@ -14,10 +14,15 @@ Distribution is currently available on all platforms with TCP/IP communication,
1414- ESP32
1515- RP2 (Pico)
1616
17- Two examples are provided:
17+ Distribution over serial (UART) is also available for point-to-point
18+ connections between any two nodes, including microcontrollers without
19+ networking (e.g. STM32). See [ Serial distribution] ( #serial-distribution ) .
20+
21+ Three examples are provided:
1822
1923- disterl in ` examples/erlang/disterl.erl ` : distribution on Unix systems
2024- epmd\_ disterl in ` examples/erlang/esp32/epmd_disterl.erl ` : distribution on ESP32 devices
25+ - serial\_ disterl in ` examples/erlang/serial_disterl.erl ` : distribution over serial (ESP32 and Unix)
2126
2227## Starting and stopping distribution
2328
@@ -94,6 +99,149 @@ fun (DistCtrlr, Length :: pos_integer(), Timeout :: timeout()) -> {ok, Packet} |
9499
95100AtomVM' s distribution is based on `socket_dist ` and `socket_dist_controller ` modules which can also be used with BEAM by definining `BEAM_INTERFACE ` to adjust for the difference .
96101
102+ ## Serial distribution
103+
104+ AtomVM supports distribution over serial (UART ) connections using the
105+ `serial_dist ` module . This is useful for microcontrollers that lack
106+ WiFi / TCP (e .g . STM32 ) but have UART , and for testing distribution
107+ locally using virtual serial ports .
108+
109+ ### Quick start
110+
111+ ```erlang
112+ {ok , _ } = net_kernel :start ('mynode@serial.local' , #{
113+ name_domain => longnames ,
114+ proto_dist => serial_dist ,
115+ avm_dist_opts => #{
116+ uart_opts => [{peripheral , " UART1" }, {speed , 115200 },
117+ {tx , 17 }, {rx , 16 }],
118+ uart_module => uart
119+ }
120+ }).
121+ ```
122+
123+ On Unix, the ` peripheral ` is a device path such as ` "/dev/ttyUSB0" ` and
124+ the ` uart_module ` is ` uart ` from the ` avm_unix ` library.
125+
126+ ### serial\_ dist options
127+
128+ - ` uart_opts ` — proplist passed to ` UartModule:open/1 ` (see ` uart_hal `
129+ for common parameters: ` peripheral ` , ` speed ` , ` data_bits ` , ` stop_bits ` ,
130+ ` parity ` , ` flow_control ` )
131+ - ` uart_module ` — module implementing the ` uart_hal ` behaviour. Defaults
132+ to ` uart ` .
133+
134+ ### Wire protocol
135+
136+ Serial distribution layers three protocols on the raw UART byte stream:
137+
138+ ** 1. Sync frames (link layer)**
139+
140+ Both sides periodically send 2-byte sync frames (` <<16#AA, 16#55>> ` ) on
141+ the UART. These serve two purposes:
142+
143+ - ** Liveness detection** : a node knows its peer is alive when it receives
144+ sync frames.
145+ - ** Garbage collection** : after a failed handshake attempt, stale bytes
146+ from the old handshake remain in the UART buffer. Sync frames are
147+ stripped from all received data, so stale data that happens to contain
148+ ` 16#AA 16#55 ` pairs is harmlessly consumed, and non-sync stale bytes
149+ eventually get discarded when the next handshake attempt interprets
150+ them as an invalid packet.
151+
152+ The sync magic (` 16#AA 16#55 ` ) is chosen so it cannot appear as the
153+ first two bytes of a valid length-prefixed handshake message (handshake
154+ messages have a 16-bit big-endian length prefix, and the longest
155+ handshake message is well under 100 bytes, so the first byte is always
156+ ` 0x00 ` ).
157+
158+ ** 2. Handshake packets (2-byte length prefix)**
159+
160+ During the Erlang distribution handshake, messages are framed as:
161+
162+ ```
163+ <<Length:16/big, Payload:Length/binary>>
164+ ```
165+
166+ This matches the TCP distribution handshake format. The handshake
167+ follows the standard Erlang distribution protocol (send\_ name,
168+ send\_ status, send\_ challenge, send\_ challenge\_ reply,
169+ send\_ challenge\_ ack).
170+
171+ ** 3. Data packets (4-byte length prefix)**
172+
173+ After the handshake completes, distribution data packets use a 4-byte
174+ length prefix:
175+
176+ ```
177+ <<Length:32/big, Payload:Length/binary>>
178+ ```
179+
180+ Tick (keepalive) messages are sent as ` <<0:32>> ` (4 zero bytes).
181+
182+ ### Peer-to-peer connection model
183+
184+ Unlike TCP distribution which uses a client/server model (one side
185+ listens, the other connects), serial is point-to-point: both nodes
186+ share a single UART link.
187+
188+ A ** link manager** process on each node is the sole owner of UART
189+ reads. On each iteration it:
190+
191+ 1 . Checks its mailbox for a ` setup ` request from ` net_kernel `
192+ (non-blocking).
193+ 2 . Sends a sync frame.
194+ 3 . Reads from the UART with a short timeout.
195+ 4 . Strips sync frames from received data.
196+ 5 . If handshake data remains, enters the ** accept** path (responder).
197+ 6 . If a ` setup ` request was pending, enters the ** setup** path
198+ (initiator).
199+ 7 . Otherwise, loops.
200+
201+ This design ensures only one process reads from the UART at any time,
202+ avoiding the race condition that would occur if separate accept and
203+ setup processes competed for the same byte stream.
204+
205+ If a handshake fails (the distribution controller process exits), the
206+ link manager flushes stale ` setup ` messages from its mailbox and
207+ restarts the loop, allowing retries.
208+
209+ ### Testing with socat
210+
211+ On Unix, ` socat ` can create virtual serial port pairs for testing:
212+
213+ ``` bash
214+ socat -d -d pty,raw,echo=0 pty,raw,echo=0
215+ ```
216+
217+ This creates two pseudo-terminal devices (e.g. ` /dev/ttys003 ` and
218+ ` /dev/ttys004 ` ) connected back-to-back. Each AtomVM node uses one side:
219+
220+ ``` erlang
221+ % % Node A
222+ {ok , _ } = net_kernel :start ('a@serial.local' , #{
223+ name_domain => longnames ,
224+ proto_dist => serial_dist ,
225+ avm_dist_opts => #{
226+ uart_opts => [{peripheral , " /dev/ttys003" }, {speed , 115200 }],
227+ uart_module => uart
228+ }
229+ }).
230+
231+ % % Node B (separate AtomVM process)
232+ {ok , _ } = net_kernel :start ('b@serial.local' , #{
233+ name_domain => longnames ,
234+ proto_dist => serial_dist ,
235+ avm_dist_opts => #{
236+ uart_opts => [{peripheral , " /dev/ttys004" }, {speed , 115200 }],
237+ uart_module => uart
238+ }
239+ }).
240+
241+ % % From Node B, trigger autoconnect:
242+ {some_registered_name , 'a@serial.local' } ! {self (), hello }.
243+ ```
244+
97245## Distribution features
98246
99247Distribution implementation is (very) partial. The most basic features are available:
0 commit comments