rfc2217

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package rfc2217 implements the telnet com-port-control option (RFC 2217), which carries baud rate, data framing, DTR/RTS, and break over TCP.

moat uses it because a pty cannot express control lines: TIOCMGET on a pty returns ENOTTY, so DTR and RTS — and therefore ESP32 auto-reset — have no pty representation at all. RFC2217 is the standard answer, and the one esptool and other pyserial-based tools already speak via rfc2217:// URLs. See docs/plans/2026-08-15-serial-devices-design.md.

Index

Constants

View Source
const (
	CmdSignature          byte = 0
	CmdSetBaudRate        byte = 1
	CmdSetDataSize        byte = 2
	CmdSetParity          byte = 3
	CmdSetStopSize        byte = 4
	CmdSetControl         byte = 5
	CmdNotifyLineState    byte = 6 // client asks the server to report line state
	CmdNotifyModemState   byte = 7 // client polls for the modem status byte
	CmdFlowControlSuspend byte = 8
	CmdFlowControlResume  byte = 9
	CmdSetLineStateMask   byte = 10
	CmdSetModemStateMask  byte = 11
	CmdPurgeData          byte = 12
)

Client-to-server com-port commands (RFC 2217 §3.3.1). Server-to-client replies use the same value plus 100 — the mapping the two blocks below keep apart. Mixing the namespaces once produced a notify sent as 207 on the wire.

View Source
const (
	SrvSetBaudRate        byte = 101
	SrvSetDataSize        byte = 102
	SrvSetParity          byte = 103
	SrvSetStopSize        byte = 104
	SrvSetControl         byte = 105
	SrvNotifyLineState    byte = 106
	SrvNotifyModemState   byte = 107
	SrvFlowControlSuspend byte = 108
	SrvFlowControlResume  byte = 109
	SrvSetLineStateMask   byte = 110
	SrvSetModemStateMask  byte = 111
	SrvPurgeData          byte = 112
)

Server-to-client com-port commands: the client-to-server value plus 100. SrvNotifyModemState doubles as the answer to a poll and as the unsolicited push on change: a client that has enabled polling (pyserial's `poll_modem` option) blocks reading .cts/.dsr/.ri/.cd until one arrives, raising "remote sends no NOTIFY_MODEMSTATE" otherwise.

View Source
const (
	// Query the current flow control (outbound/both).
	ControlQueryFlow   byte = 0
	ControlFlowNone    byte = 1
	ControlFlowXONXOFF byte = 2
	ControlFlowRTSCTS  byte = 3
	// Query the current break state.
	ControlQueryBreak byte = 4
	ControlBreakOn    byte = 5
	ControlBreakOff   byte = 6
	// Query the current DTR signal state.
	ControlQueryDTR byte = 7
	ControlDTROn    byte = 8
	ControlDTROff   byte = 9
	// Query the current RTS signal state.
	ControlQueryRTS byte = 10
	ControlRTSOn    byte = 11
	ControlRTSOff   byte = 12
	// Inbound flow control settings, and the remaining outbound variants.
	ControlQueryFlowIn   byte = 13
	ControlFlowNoneIn    byte = 14
	ControlFlowXONXOFFIn byte = 15
	ControlFlowRTSCTSIn  byte = 16
	ControlFlowDCD       byte = 17 // DCD flow control (outbound/both)
	ControlFlowDTRIn     byte = 18 // DTR flow control (inbound)
	ControlFlowDSR       byte = 19 // DSR flow control (outbound/both)
)

SET-CONTROL values (RFC 2217 §3.7). Each signal has both an assert and a deassert encoding: the ESP32 reset sequence is a series of transitions, so a deassert that is dropped or mistaken for an assert leaves the chip out of its bootloader.

The zero values are queries — the server answers with the current setting for that signal, the same byte the client would send to set it. pyserial's client never sends them today, but a client that does (any implementation following the RFC) expects an answer, not silence.

View Source
const (
	PurgeReceiveBuffer  byte = 1
	PurgeTransmitBuffer byte = 2
	PurgeBothBuffers    byte = 3
)

PURGE_DATA values. pyserial's client sends one of these from inside Serial.open() — reset_input_buffer()/reset_output_buffer() are part of its connect sequence — and blocks until the server replies, so a server without a PURGE case fails every pyserial connect with "timeout while waiting for option 'purge'".

View Source
const (
	ModemCTS byte = 1 << 4
	ModemDSR byte = 1 << 5
	ModemRI  byte = 1 << 6
	ModemCD  byte = 1 << 7
)

Modem state bits (RFC 2217 §3.3.2, NOTIFY-MODEMSTATE payload).

View Source
const (

	// Negotiation verbs, exported so peers can answer an offer.
	Will byte = 0xFB
	Wont byte = 0xFC
	Do   byte = 0xFD
	Dont byte = 0xFE
)

Telnet control bytes.

View Source
const (
	OptionBinary  byte = 0
	OptionEcho    byte = 1
	OptionSGA     byte = 3  // suppress go-ahead
	OptionComPort byte = 44 // com-port-control (RFC 2217)
)

Telnet options moat cares about. A serial stream must be in binary mode, or the peer is entitled to mangle high-bit bytes.

View Source
const MaxSubnegotiation = 4096

MaxSubnegotiation is the largest subnegotiation payload the reader accepts.

Real com-port subnegotiations carry at most a handful of bytes (baud is the largest at 4). A generous cap bounds the memory a hostile or broken peer can make the reader retain: the payload buffer grows with the stream until IAC SE arrives, and nothing else in the protocol stops it. A payload past the cap aborts the subnegotiation; the reader resynchronizes on the next frame.

Variables

This section is empty.

Functions

func ControlName

func ControlName(v byte) string

ControlName returns a printable label for a SET-CONTROL value, for logs and audit entries. Unknown values render as "unknown" rather than empty so a log line never silently loses its subject.

func DecodeBaud

func DecodeBaud(p []byte) uint32

DecodeBaud reads a 4-byte big-endian baud rate, returning 0 if the payload is malformed. Callers treat 0 as "leave the current rate alone".

func EncodeBaud

func EncodeBaud(baud uint32) []byte

EncodeBaud renders a baud rate as the 4-byte big-endian value RFC2217 uses.

func EscapeIAC

func EscapeIAC(dst, src []byte) []byte

EscapeIAC appends src to dst with every 0xFF doubled, as telnet requires.

This matters more than it looks: 0xFF is the dominant byte in erased flash, so an unescaped stream corrupts firmware uploads in a way that presents as failing hardware rather than a protocol bug.

func WriteCommand

func WriteCommand(w io.Writer, cmd byte, payload []byte) error

WriteCommand writes a com-port subnegotiation: IAC SB COM-PORT-OPTION cmd payload IAC SE, with the payload escaped.

func WriteNegotiate

func WriteNegotiate(w io.Writer, verb, option byte) error

WriteNegotiate writes a telnet negotiation command, e.g. IAC WILL BINARY.

Types

type Reader

type Reader struct {

	// OnCommand is called for each com-port subnegotiation, with the command
	// byte and its payload. The payload is only valid for the duration of the
	// call. Nil means commands are parsed and discarded.
	OnCommand func(cmd byte, payload []byte)

	// OnNegotiate is called for each IAC WILL/WONT/DO/DONT. A peer that offers
	// options and gets no answer will stall, so servers must respond to these.
	OnNegotiate func(verb, option byte)
	// contains filtered or unexported fields
}

Reader un-escapes a telnet stream, yielding only the data bytes from Read and dispatching com-port subnegotiations to OnCommand.

Commands for other telnet options are consumed and discarded so they never leak into the serial data stream.

func NewReader

func NewReader(src io.Reader) *Reader

NewReader wraps src.

func (*Reader) Read

func (r *Reader) Read(p []byte) (int, error)

Read implements io.Reader, returning decoded data bytes only.

func (*Reader) SetOnBadCommand

func (r *Reader) SetOnBadCommand(fn func()) *Reader

SetOnBadCommand registers the oversized-subnegotiation callback. It returns the Reader so it can be chained off NewReader.

Jump to

Keyboard shortcuts

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