Serial ports and the device capability

Open a port as a stream. The grant for that is device, which fs:write does not include.

std.serial opens a serial port and hands back a stream. The read and write verbs are the ones you already use on a file:

import std.io
import std.serial

p = serial.open("/dev/ttyUSB0", 115200)
io.timeout(p, 2000)

serial.flush(p)
io.write(p, "AT\r\n")
serial.drain(p)

print(io.read_line(p))
io.close(p)

flush drops whatever was already sitting in the buffer. drain waits until the write has actually left. dtr and rts drive the lines that reset a microcontroller board and switch an RS-485 transceiver between transmit and receive. std.io has no name for those four, which is why they live on serial. Everything else is io.read, io.read_exact, io.read_until, io.read_line, io.lines, io.write, io.timeout, io.close, or for chunk in p.

open also takes data_bits, stop_bits, parity, and flow. Baud rates are the standard set, and a rate the host rejects names the set it does accept. Linux and macOS disagree about that set, so the message is the list for the machine you are on.

A read that passes the deadline raises serial: timed out. The deadline is in milliseconds and has no ceiling. The termios field underneath saturates at 25.5 seconds, which is why this does not use it.

Opening the device file is the wrong tool

std.fs will open /dev/ttyUSB0. The port then keeps whatever baud, parity, and line discipline the last program left it in. Canonical mode buffers until a newline and rewrites CR and LF on the way through. There is no short read deadline, so a device that says nothing looks like a hang. serial.open sets the line up and puts it in raw mode, so the bytes on the wire are the bytes you read.

A sixth capability

Opening a real port needs device, scoped to a path the way fs is:

"dependencies": {
  "gps": { "path": "github.com/me/gps", "grant": ["device:/dev/ttyUSB0"] }
}

fs:write does not imply it. Granting a package permission to write files is ordinary. That grant must not also mean the package may drive whatever is plugged in. A manifest that never mentions device behaves as it did before this existed.

Nothing plugged in

p = serial.open("loop://", 115200)
io.write(p, "ping")

loop:// is an in-memory echo. It needs no grant, so a test and an example run with no hardware. It echoes. It will not show you a baud mismatch or a framing error.

A real port works on Linux and macOS. On Windows a real port reports that serial needs a unix terminal. loop:// works there too.

The module page is std.serial.