serial

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

Documentation

Overview

Package serial opens and configures a macOS serial port with no cgo.

It exists because a USB device's interesting protocol is not always on a HID interface or on endpoint 0. A composite device that publishes a CDC-ACM function gets a /dev/cu.* and /dev/tty.* pair from Apple's AppleUSBACMData driver, and that pair is a channel the sibling hid and usb packages in this module cannot reach: the kernel driver owns the bulk pipes, so the only way in is the tty.

cu versus tty, which is the whole trap

macOS publishes two nodes for one port.

  • /dev/tty.NAME is the dial-IN node. Opening it blocks until the device asserts carrier detect, and it raises DTR. A USB CDC device that never drives DCD will hang an open forever unless the caller passes O_NONBLOCK, which Open always does, and then sets CLOCAL.
  • /dev/cu.NAME is the call-OUT node. It opens immediately and does not wait for carrier -- and it does not raise DTR either.

A device whose firmware only starts talking once DTR is asserted is therefore mute on /dev/cu.* and alive on /dev/tty.*, with no error either way. Port.SetLines asserts the lines explicitly so the question can be asked of both nodes rather than guessed.

What an accepted write proves

Nothing, on its own. The lesson the sibling packages record applies here unchanged: write(2) to a tty returns the byte count as soon as the line discipline has queued the bytes, whether or not the device on the other end understood a single one of them. Only bytes coming back are evidence, which is why Loopback exists -- a probe that has not proved its reader on a known-good port has not proved that silence means anything.

Index

Constants

View Source
const (
	// CalloutPrefix names the dial-out node, which opens without waiting for
	// carrier and leaves DTR low.
	CalloutPrefix = "cu."
	// DialinPrefix names the dial-in node, which raises DTR and waits for
	// carrier.
	DialinPrefix = "tty."
)

Prefixes of the two node families macOS publishes for one port.

View Source
const (
	VMIN  = 16
	VTIME = 17
)

Indices into Termios.Cc.

View Source
const (
	IGNBRK = 0x00000001
	BRKINT = 0x00000002
	IGNPAR = 0x00000004
	PARMRK = 0x00000008
	INPCK  = 0x00000010
	ISTRIP = 0x00000020
	INLCR  = 0x00000040
	IGNCR  = 0x00000080
	ICRNL  = 0x00000100
	IXON   = 0x00000200
	IXOFF  = 0x00000400
	IXANY  = 0x00000800
)

Input flags, termios c_iflag.

View Source
const (
	OPOST = 0x00000001
	ONLCR = 0x00000002
)

Output flags, termios c_oflag.

View Source
const (
	CSIZE  = 0x00000300
	CS5    = 0x00000000
	CS6    = 0x00000100
	CS7    = 0x00000200
	CS8    = 0x00000300
	CSTOPB = 0x00000400
	CREAD  = 0x00000800
	PARENB = 0x00001000
	PARODD = 0x00002000
	HUPCL  = 0x00004000
	CLOCAL = 0x00008000
	// CRTSCTS is CCTS_OFLOW|CRTS_IFLOW: BSD splits hardware flow control into
	// the two directions and defines the joint name as their union.
	CCTSOFLOW = 0x00010000
	CRTSIFLOW = 0x00020000
	CRTSCTS   = CCTSOFLOW | CRTSIFLOW
)

Control flags, termios c_cflag.

View Source
const (
	ECHOE  = 0x00000002
	ECHOK  = 0x00000004
	ECHO   = 0x00000008
	ECHONL = 0x00000010
	ISIG   = 0x00000080
	ICANON = 0x00000100
	IEXTEN = 0x00000400
)

Local flags, termios c_lflag.

View Source
const DevDir = "/dev"

DevDir is where macOS publishes tty nodes.

View Source
const NCCS = 20

NCCS is the number of control characters in a BSD termios.

Variables

View Source
var ErrClosed = errors.New("serial: port is closed")

ErrClosed is returned by a method called after Port.Close.

View Source
var ErrUnsupported = errors.New("serial: unsupported on this platform")

ErrUnsupported is returned by every entry point on a platform that has no BSD termios: Linux and Windows builds compile, and fail cleanly.

Functions

func BaseName

func BaseName(path string) string

BaseName strips the directory and the cu./tty. prefix from a device path, returning the name the two nodes share. A path that is not a serial node comes back unchanged.

func Callout

func Callout(base string) string

Callout turns a port's base name into its /dev/cu.* path.

func Dialin

func Dialin(base string) string

Dialin turns a port's base name into its /dev/tty.* path.

func Drain

func Drain(r deadlineReader, buf []byte, until time.Time) (int, error)

Drain reads from r until the deadline passes or the buffer is full, returning everything that arrived. A timeout is not an error: it is the expected way for the call to end, and the byte count is the finding.

r must honour a read deadline, which is why it is asked for one: a reader that cannot time out would turn this into a hang.

func List

func List() ([]string, error)

List returns the base names of every serial port macOS currently publishes, sorted and deduplicated, so a port that has both nodes appears once. A platform with no /dev directory reports no ports rather than an error.

func Loopback

func Loopback() (*os.File, string, error)

Loopback returns a pseudo-terminal pair: an *os.File on the master side and the /dev/ttys* path of the slave, which Open accepts like any other tty.

It is the control instrument. Bytes written to the master come out of a Port opened on the slave path, so a probe can prove its reader works before concluding that a real device's silence means anything. The HID half of this module learned that lesson the expensive way.

Types

type Config

type Config struct {
	// Baud is the line rate. Any positive value is accepted; macOS takes
	// non-standard rates through IOSSIOSPEED after the termios call.
	Baud int
	// DataBits is 5, 6, 7 or 8. Zero means 8.
	DataBits int
	// StopBits is 1 or 2. Zero means 1.
	StopBits int
	// Parity defaults to none.
	Parity Parity
	// CLOCAL makes the driver ignore the carrier detect line. Without it a
	// read on a /dev/tty.* node blocks until DCD is asserted, which on a
	// USB CDC device that never raises DCD is forever.
	CLOCAL bool
	// HUPCL drops DTR on the last close.
	HUPCL bool
	// RTSCTS turns on hardware flow control. A device that does not drive CTS
	// will never be written to once this is set.
	RTSCTS bool
	// ReadMin is VMIN: the smallest number of bytes a blocking read returns.
	ReadMin uint8
	// ReadTimeoutDecis is VTIME, in tenths of a second.
	ReadTimeoutDecis uint8
}

Config is a line configuration. The zero value is not usable: Open rejects a zero baud rate rather than quietly picking one, because "the port was configured for 0 baud" is precisely the kind of silent misconfiguration that looks like a mute device.

func ConfigOf

func ConfigOf(t Termios) Config

ConfigOf reads a termios back as a Config. It is the inverse of Config.Termios for the fields this package sets, and it exists so a probe can print what the kernel actually accepted rather than what it was asked for.

func (Config) String

func (c Config) String() string

String renders a configuration the way a terminal program would: rate, then the classic 8N1 shorthand, then the flags that are on.

func (Config) Termios

func (c Config) Termios(cur Termios) Termios

Termios renders the configuration as a raw-mode termios: no canonical line editing, no echo, no signal generation, no input or output translation. What arrives is what the device sent.

It applies the configuration to a copy of cur rather than building from zero, so the bits macOS set at open that this package has no opinion about survive.

func (Config) Validate

func (c Config) Validate() error

Validate reports whether the configuration is one the kernel can honour.

type ErrBadConfig

type ErrBadConfig struct {
	Field  string
	Value  int
	Reason string
}

ErrBadConfig reports a configuration the kernel would reject or silently mangle.

func (*ErrBadConfig) Error

func (e *ErrBadConfig) Error() string

Error renders the offending field.

type Lines

type Lines uint32

Lines is a set of modem control line bits: LineDTR, LineDCD and friends.

const (
	LineDTR Lines = 0x0002
	LineRTS Lines = 0x0004
	LineCTS Lines = 0x0020
	LineDCD Lines = 0x0040 // TIOCM_CAR
	LineRI  Lines = 0x0080
	LineDSR Lines = 0x0100
)

Modem control line bits, as reported by TIOCMGET. They are typed Lines so that a mask built from them cannot be passed where a flag word is meant.

func (Lines) Has

func (l Lines) Has(mask Lines) bool

Has reports whether every bit in mask is set.

func (Lines) String

func (l Lines) String() string

String renders the asserted lines in a fixed order, so two readings can be compared by eye. An empty set prints "none".

type Parity

type Parity int

Parity selects the parity bit scheme.

const (
	ParityNone Parity = iota
	ParityOdd
	ParityEven
)

The parity schemes a CDC-ACM device can be asked for.

func (Parity) String

func (p Parity) String() string

String names the scheme.

type Port

type Port struct {
	// contains filtered or unexported fields
}

Port is an open serial port.

Reads and writes go through an *os.File on a non-blocking descriptor, so the Go runtime's poller services them and Port.SetReadDeadline works. That matters more than it sounds: a probe that cannot time out a read cannot tell "the device sent nothing" from "the program is wedged".

func Open

func Open(name string, cfg Config) (*Port, error)

Open opens the named device -- a full path such as /dev/cu.usbmodem14201 -- and applies cfg.

The descriptor is always opened O_NONBLOCK|O_NOCTTY, so opening a /dev/tty.* node does not hang waiting for carrier detect and the port does not become the process's controlling terminal. If cfg.CLOCAL is set the carrier requirement is then dropped for good; if it is not, a later blocking read on a dial-in node can still stall, which is the caller's choice to make.

func (*Port) Close

func (p *Port) Close() error

Close closes the port. Closing twice is not an error.

func (*Port) Config

func (p *Port) Config() Config

Config is the configuration last successfully applied.

func (*Port) Configure

func (p *Port) Configure(cfg Config) error

Configure applies cfg to an already open port.

The termios is read back after the write and compared, because tcsetattr is documented to succeed when it applied *some* of what it was asked for. A driver that silently refused the requested rate is the difference between a mute device and a misconfigured one, and this is the only place that distinction can be caught.

func (*Port) Flush

func (p *Port) Flush() error

Flush discards whatever the kernel has buffered in both directions, so a listen that follows starts from a known-empty queue.

func (*Port) Lines

func (p *Port) Lines() (Lines, error)

Lines reads the modem control lines.

func (*Port) Name

func (p *Port) Name() string

Name is the device path the port was opened with.

func (*Port) Read

func (p *Port) Read(b []byte) (int, error)

Read reads from the port. It honours the deadline set by Port.SetReadDeadline, returning an error for which os.IsTimeout is true.

func (*Port) SetLines

func (p *Port) SetLines(set, clear Lines) error

SetLines asserts the bits in set and clears the bits in clear. Passing LineDTR|LineRTS in set is the handshake a CDC device that waits for a host expects, and the one a /dev/cu.* node never performs on its own.

func (*Port) SetReadDeadline

func (p *Port) SetReadDeadline(t time.Time) error

SetReadDeadline bounds subsequent reads.

func (*Port) Termios

func (p *Port) Termios() (Termios, error)

Termios returns the port's current termios as the kernel holds it.

func (*Port) Write

func (p *Port) Write(b []byte) (int, error)

Write writes to the port. A successful return means the line discipline queued the bytes; it says nothing at all about the device.

type Termios

type Termios struct {
	Iflag  uint64
	Oflag  uint64
	Cflag  uint64
	Lflag  uint64
	Cc     [NCCS]uint8
	Ispeed uint64
	Ospeed uint64
}

Termios mirrors the macOS struct termios field for field, so a value can be copied into the x/sys/unix type without reinterpreting memory.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL