Skip to content
Start free evaluation

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.

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 8E1

Here application A opens /tmp/pdcomm-plc, application B opens /tmp/pdcomm-hmi, and the lab plays the null-modem cable between them.

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:

  • realistic spends one character time per character, like a real line. Use it for protocols where the gaps between characters matter, such as Modbus RTU.
  • instant keeps 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.

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.

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.

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)

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.

You can inject faults in two ways:

  • Random faults go in a node’s faults setting, in parts per million of received characters: drop_ppm, flip_ppm, parity_ppm, framing_ppm, overrun_ppm, and noise_ppm, plus delivery delays latency_ns and jitter_ns. The same seed always 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"} with parity, framing, overrun, or break. {"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.

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.

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() returns EINVAL (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 with tcsetattr(), such as pyserial, can fail to open it. Open such programs with 8N1 on Linux and set the real format in the node’s line. Programs that use the kernel’s TCSETS2 ioctl, 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 port event in the trace within about 20 ms.

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