How virtual serial ports work
PDComm virtual serial creates ports that the operating system and your applications treat as real serial ports. A software engine plays the part of the cables and buses between them. This page explains the concepts you need before you test with virtual ports.
Labs and nodes
Section titled “Labs and nodes”The unit you run is a lab, defined in one JSON file:
- A node is one named port. Each node has its own UART character format, flow control, receive FIFO, and fault settings.
- A medium is the cable or bus that joins nodes: a null-modem cable, a loopback plug, or an RS-422/RS-485 bus.
- A port binding connects a node to the endpoint that an application opens: a pseudo terminal, a port of the installed driver, or a TCP server.
/tmp/pdcomm-plc ◀──▶ node plc ═══ null_modem ═══ node hmi ◀──▶ /tmp/pdcomm-hmi 19200 8E1 19200 8E1Here application A opens /tmp/pdcomm-plc, application B opens /tmp/pdcomm-hmi, and the lab plays the null-modem cable between them.
Character format and character time
Section titled “Character format and character time”A serial port sends bytes as characters. Each character has one start bit, 5 to 8 data bits, an optional parity bit, and 1, 1.5, or 2 stop bits. 8E1 (8 data bits, even parity, 1 stop bit), for example, takes 11 bits per character, so one character time at 19200 bit/s is about 0.57 ms.
The engine uses these settings for each node:
| Setting | Allowed values | Default |
|---|---|---|
baud |
1 to 16000000 | 9600 |
data_bits |
5 to 8 | 8 |
parity |
none, odd, even, mark, space |
none |
stop_bits |
"1", "1.5", "2" (strings) |
"1" |
The lab-wide pacing decides how fast characters travel:
realisticspends one character time per character, like a real line. Use it for protocols where the gaps between characters matter, such as Modbus RTU.instantkeeps the order but delivers at once. Use it to test logic quickly.
The receiving UART samples the bits with its own settings. If the two sides disagree on speed, data bits, or parity, the engine delivers the same garbled bytes, framing errors, and parity errors that a real receiver reports, so you can reproduce a mismatch exactly.
Receive FIFO
Section titled “Receive FIFO”A 16550-class UART collects received characters in a FIFO and hands them over in groups. The engine does the same. By default it delivers when 14 characters are waiting or when the line has been idle for 4 character times ("rx_fifo": {"trigger": 14, "idle_chars": 4}). Set "rx_fifo": null to deliver every character on its own. That is finer than any real UART or USB adapter, which makes it a stress test for receive code.
Modem signals and flow control
Section titled “Modem signals and flow control”A port drives RTS and DTR and reads CTS, DSR, DCD, and RI. By default a null-modem cable connects each side’s RTS to the other side’s CTS, and DTR to DSR and DCD. Use the medium’s pins field for other wiring.
Flow control is none, rts_cts (hardware), or xon_xoff (software). When an application changes the settings of its port, the lab follows them. Break conditions travel across the line too.
Wiring
Section titled “Wiring”kind |
Physical equivalent | Fields |
|---|---|---|
null_modem |
RS-232 crossover cable, or point-to-point RS-422 | a, b, optional pins |
loopback |
A loopback plug on one port | node, optional pins |
four_wire |
RS-422 or four-wire RS-485: the master drives every slave, and the slaves share the return pair | master, slaves |
two_wire |
Two-wire RS-485: every port shares one half-duplex pair | nodes |
Each node can be wired to only one medium. A two-wire bus needs at least two nodes, and a four-wire bus needs at least one slave.
Two-wire RS-485 bus (two_wire)
master drive1 drive2 │ │ │════╪═════════════╪═════════════╪════ one A/B pair (half duplex)RS-485 driver control
Section titled “RS-485 driver control”On a shared bus only one port may transmit at a time. A node’s attach setting decides when its line driver is on:
"driver_enable": "always"always drives the line. This is the default for null-modem cables and loopback plugs."driver_enable": "auto"models a transceiver with automatic direction control. This is the default on buses."driver_enable": {"rts": {"asserted": true}}drives the line only while RTS is at the given level, for applications that switch direction with RTS.
"echo": true keeps the receiver on while the port transmits, like a transceiver that returns the port’s own characters. When two ports transmit at once, the engine records a collision.
A port’s rts_toggle setting raises and lowers RTS around every transmission. This is how the Linux RS-485 mode (TIOCSRS485) and the Windows RTS_CONTROL_TOGGLE setting behave.
Fault injection
Section titled “Fault injection”You can inject faults in two ways:
- Random faults go in a node’s
faultssetting, in parts per million of received characters:drop_ppm,flip_ppm,parity_ppm,framing_ppm,overrun_ppm, andnoise_ppm, plus delivery delayslatency_nsandjitter_ns. The sameseedalways reproduces the same run. - One-shot faults are commands you send to a running lab. Each applies to the next character the node receives:
"drop",{"xor": n}to flip bits, or{"error": "parity"}withparity,framing,overrun, orbreak.{"noise": {"byte": n, "error": ...}}delivers one noise character immediately.
You can also unplug a node’s cable with a command. An unplugged node neither sends to nor receives from its medium.
Port backends
Section titled “Port backends”type |
What the application opens | Operating systems |
|---|---|---|
pty |
A pseudo terminal, such as /dev/ttys… or /dev/pts/…. link adds a symbolic link at a fixed path. |
macOS, Linux |
driver |
Port index of the installed port driver |
Windows (COMn), Linux (/dev/ttyPDVn) |
tcp_server |
A raw TCP socket at listen. Only bytes pass; the line settings come from the lab file. One client at a time. |
All |
rfc2217_server |
An RFC 2217 server at listen. The client controls speed, format, signals, and break remotely. |
All |
RFC 2217 is the IETF standard for controlling a remote serial port through the Telnet COM-PORT-OPTION. It has no authentication, so the lab opens TCP servers on loopback addresses (127.0.0.1) only. To listen on another address, set "allow_remote": true in the lab, and do so only on a network you trust.
The bridges setting works the other way round: it connects a local node to the real port of a remote RFC 2217 device server. Speed, format, signals, and break that your application sets pass through to the remote port, and the remote UART paces the line.
How pseudo terminals behave
Section titled “How pseudo terminals behave”A pseudo terminal is a terminal device that the operating system provides without a UART. It behaves as follows on each system:
- Linux pseudo terminals ignore the data bits and parity that an application requests. The lab uses the data bits and parity of the node and follows the application only for speed and stop bits. macOS pseudo terminals keep the full format, so every value follows the application.
- On Linux, glibc’s
tcsetattr()returnsEINVAL(Invalid argument) when a pseudo terminal drops the parity or data bits you asked for. A request that only changes parity, such as 8N1 to 8E1, therefore fails, and programs that configure the port withtcsetattr(), such as pyserial, can fail to open it. Open such programs with 8N1 on Linux and set the real format in the node’sline. Programs that use the kernel’sTCSETS2ioctl, such as the PDComm Modbus SDK, don’t get this error. - BSD pseudo terminals can’t request mark or space parity; the node setting stays in effect.
- The lab rereads the line settings of every pseudo terminal right before it handles data. A change your application makes applies to all data sent after it, from either end, so you can send right after you open and configure the port. A change with no data after it appears as a
portevent in the trace within about 20 ms.
Traces
Section titled “Traces”Set trace in the lab to a file path to record every line event as JSON Lines, one event per line. t_ns is nanoseconds since the lab started, node is the node name, and event is the event type.
{"t_ns":443484625,"node":"master","event":"tx","byte":1,"on_wire":true}{"t_ns":444057542,"node":"drive1","event":"rx","byte":1,"error":null}{"t_ns":842903959,"node":"master","event":"rx","byte":1,"error":"parity"}{"t_ns":1851553667,"node":"drive2","event":"attached","attached":false}event |
Meaning |
|---|---|
tx |
The node sent a character. on_wire: false means no driver was enabled or the cable was unplugged. |
rx |
The node received a character. error is null, break, framing, parity, or overrun. |
collision, dropped |
A bus collision or a dropped character. |
signals, inputs, break |
Changes of the modem outputs (RTS, DTR), inputs (CTS, DSR, DCD, RI), or break. |
port |
The application changed its line settings. |
attached |
The cable was plugged in or unplugged. |
tx_empty |
The transmit buffer drained. |
What to test with virtual ports and with hardware
Section titled “What to test with virtual ports and with hardware”Virtual ports test the software layers on top of the UART. Test the electrical layer with real hardware.
| Test with virtual ports | Test with real hardware |
|---|---|
| Speed, data bits, parity, stop bits, and mismatches between them | Differential signalling, termination, biasing, grounding, cable length, and noise immunity |
| Character-time timing, frame gaps, and the 16550 receive FIFO | Direction-switch delays of real transceivers and latency of USB serial chips |
| RTS/CTS, DTR/DSR/DCD, XON/XOFF, and break | Acceptance tests with the real equipment |
| RS-485 multidrop addressing, collisions, and RTS direction control | |
| Reproducible fault injection from a seed |
Next steps
Section titled “Next steps”- Install virtual serial ports
- Create your first virtual pair in the quickstart.
- Test Modbus RTU on a virtual RS-485 bus