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 ¶
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.
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.
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.
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'".
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).
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.
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.
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 ¶
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 ¶
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 ¶
EncodeBaud renders a baud rate as the 4-byte big-endian value RFC2217 uses.
func EscapeIAC ¶
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 ¶
WriteCommand writes a com-port subnegotiation: IAC SB COM-PORT-OPTION cmd payload IAC SE, with the payload escaped.
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 (*Reader) SetOnBadCommand ¶
SetOnBadCommand registers the oversized-subnegotiation callback. It returns the Reader so it can be chained off NewReader.