Vectis is a serial console for fast debugging and device control during
development and integration of embedded systems with the OpenIPC UART
38x38 board. One small binary, vectis, plays three roles at once:
- console — bridges the keyboard to a serial port, either a local
device (
-u /dev/ttyUSB0) or one exposed by an RFC 2217 server (-h HOST -p PORT), with a reset pulse on RTS/DTR onCtrl+Pand a break onCtrl+B; - server —
-l PORTserves the local port over TCP to telnet, anothervectis, pyserial or nc, with Telnet/RFC 2217 negotiation, binary-safe data and out-of-band control of baud rate, line settings and RTS/DTR; - web —
-w PORTserves a power-control page (Enable / Disable / Reset on RTS+DTR) and a small JSON API for scripts.
It runs interactively in a terminal or as a daemon (systemd, no
terminal), reopens the device link on its own when an adapter is
unplugged, and needs nothing but libc.
vectis-bootrom.c, the original UART bridge this program grew out of, is
kept in the repository for its HiSilicon boot-ROM catching extension; it
is described at the end of this document and is not part of the default
build.
- Local serial devices (
/dev/ttyUSB*,/dev/ttyS*,/dev/ttyACM*) or RFC 2217 servers, configurable baud rate, data bits, stop bits, parity - RTS and DTR asserted on connect, deasserted on exit;
Ctrl+Preset pulse of configurable length on both lines Ctrl+Bserial break (stops U-Boot autoboot)- RFC 2217 client:
BINARY,SUPPRESS-GO-AHEAD,COM-PORT-OPTION,0xFFescaping, port parameters and DTR/RTS over the wire, automatic reconnect (-r) - RFC 2217 server (
-l): one client at a time, raw mode for nc/socat, Telnet mode for telnet/pyserial/vectis,SET-BAUDRATE,SET-DATASIZE,SET-PARITY,SET-STOPSIZE,SET-CONTROL,PURGE-DATA,SIGNATURE,BRK - Web power console (
-w) with a JSON API:/status,/power/enable,/power/disable,/power/reset - Daemon-friendly: no terminal required, listeners come up before the device link and stay up while it is away, one syslog line per outage
- Syslog logging of connections, power actions and errors
makeThis builds vectis (size-optimised and stripped). make DEBUG=1 builds
with debug symbols and no optimisation instead. make vectis-bootrom
builds the original bridge on demand; make all does not.
In RFC 2217 client mode vectis connects to a TCP server, puts the local terminal into raw mode,
and negotiates the following Telnet options with the server: BINARY, SGA,
and COM-PORT-OPTION (RFC 2217). After a short pause for the server's
negotiation replies it configures the remote serial port by sending SET_BAUDRATE, SET_DATASIZE,
SET_STOPSIZE, SET_PARITY, and SET_CONTROL sub-options (flow control
off, then DTR and RTS asserted). The server's replies are logged, so a
baud rate the server could not apply shows up as a different value in the
server confirmed baud line. The TCP socket uses a 10-second connect
timeout, TCP_NODELAY so keystrokes leave immediately, and keep-alive
probes that detect a silently dropped link within about 20 seconds.
The incoming byte stream is parsed as Telnet/RFC 2217: plain data bytes are
written to stdout, IAC sequences (0xFF) are intercepted and processed
separately, and escaped 0xFF 0xFF pairs are unescaped to a single 0xFF
before being forwarded to the terminal. Outgoing keyboard input is sent in
binary mode, with any 0xFF bytes escaped as IAC IAC per RFC 854.
Ctrl+P sends a reset pulse to the target device: it issues SET_CONTROL
commands to drop DTR and RTS for a configurable duration (default 200 ms), then
reasserts them. In direct serial mode the same pulse is applied to the local
port via ioctl(TIOCMBIC/TIOCMBIS). Ctrl+B sends a break: IAC BRK in
RFC 2217 mode, tcsendbreak() in direct serial mode.
In direct serial mode DTR and RTS are asserted when the device is opened and deasserted again on exit, and the original termios settings are restored.
./vectis -h <host> -p <port> [options]
./vectis -u <device> [options]Every option also has a long form. Note that -h is the host option;
help is --help or -?.
RFC 2217/Telnet mode:
-h HOST,--host HOST— RFC 2217 server address (name or IP, IPv4/IPv6)-p PORT,--port PORT— TCP port (1..65535)
Direct serial mode:
-u DEVICE,--device DEVICE— local tty device path, for example/dev/ttyUSB0
The two modes are mutually exclusive.
Port settings (default 115200 8N1):
-b BAUD,--baud BAUD— baud rate (default115200, maximum921600). Direct serial mode accepts the standard rates300,1200,2400,4800,9600,19200,38400,57600,115200,230400,460800and921600; RFC 2217 mode passes the value to the server as is.-d 5|6|7|8,--data-bits N— data bits (default 8)-s 1|2,--stop-bits N— stop bits (default 1)-y N|E|O,--parity X— parity None/Even/Odd (default N)
Connection options:
-r,--reconnect— reconnect automatically on disconnect, retrying every 5 seconds (RFC 2217 mode only; with-lor-wthe link is reopened automatically in both modes without this flag)-t MS,--reset-ms MS— reset pulse duration in milliseconds,1..60000(default 200)-n,--no-crlf— disable LF→CR+LF translation (for devices that already send\r\n, or for binary capture)-l [ADDR:]PORT,--listen [ADDR:]PORT— serve the local port (-u) to one TCP client at a time: telnet, anothervectis -h HOST -p PORT, pyserialrfc2217://, or raw nc/socat; see "Serving the port over TCP" below-w [ADDR:]PORT,--web [ADDR:]PORT— start the built-in web power console on that port (all interfaces unless an IPv4 address is given); see "Web power console" below-v,--version— print version and release date--help,-?— help
# 115200 8N1 — all parameters at their defaults
./vectis -h 192.168.1.10 -p 7000
# 9600 8E1
./vectis -h 192.168.1.10 -p 7000 -b 9600 -y E
# Auto-reconnect on disconnect
./vectis -h 192.168.1.10 -p 7000 -r
# Direct serial device at 115200
./vectis -u /dev/ttyUSB0
# Direct serial device at 460800 baud, 500 ms reset pulse
./vectis -u /dev/ttyUSB0 -b 460800 -t 500
# Capture device output to a file (non-interactive, no CRLF translation)
./vectis -h 192.168.1.10 -p 7000 -n > boot.log
# Serial console plus the web power console on http://<this host>:8080/
./vectis -u /dev/ttyUSB0 -w 8080
# One process for everything: local console, UART served on TCP 2021, web on 2081
./vectis -u /dev/ttyUSB0 -l 2021 -w 2081These key bindings are active only in interactive mode (when stdin is a terminal). In non-interactive mode (piped or redirected stdin), all bytes are forwarded to the device verbatim, so control characters in the data stream are never misinterpreted as commands.
| Key | Action |
|---|---|
Ctrl+P |
RTS+DTR pulse — reset the target device |
Ctrl+B |
Send break signal — stops U-Boot autoboot, triggers bootloader prompts |
Ctrl+] |
Exit |
Ctrl+C is passed to the device like any other byte; use Ctrl+] to
quit. Sending SIGINT or SIGTERM from elsewhere also ends the session
cleanly. If the controlling terminal disappears, vectis exits on its
own.
When stdin is not a terminal, vectis skips raw-terminal setup and
forwards all input directly to the device. Once stdin reaches EOF the
input side is finished, but the device's reply may still be on its way,
so vectis stops reading stdin and keeps forwarding device output to
stdout until the connection closes or it receives a signal. Bound a
scripted session with timeout:
# Send a U-Boot command and capture the response for 3 seconds
echo "printenv" | timeout 3 ./vectis -h 192.168.1.10 -p 7000 -n > env.txt
# Feed a sequence of commands from a file, collect output for 10 seconds
timeout 10 ./vectis -u /dev/ttyUSB0 < commands.txt > output.txtStatus messages go to stderr and to syslog, so stdout carries only the device's data.
-l PORT (or -l ADDR:PORT) turns a direct-serial session into a small
RFC 2217 server for the same port, so one vectis process can be the
local console, the network access point and, with -w, the web power
console at once:
./vectis -u /dev/ttyUSB0 -l 2021 -w 2081Clients that work against it:
vectis -h 192.168.1.20 -p 2021 # another vectis, with Ctrl+P / Ctrl+B
telnet 192.168.1.20 2021 # binary data path, "send brk" at the escape prompt
python3 -c "import serial; s = serial.serial_for_url('rfc2217://192.168.1.20:2021'); s.dtr = False"
socat -,raw,echo=0 TCP:192.168.1.20:2021 # raw mode, Ctrl+P resets the target
nc 192.168.1.20 2021 # raw mode, line by lineThe behaviour mirrors the original vectis-bootrom server:
- One client at a time; a new connection replaces the current one.
- A connection starts in legacy raw mode: bytes pass through,
LFandCR LFbecomeCRso Enter reaches U-Boot, andCtrl+P(0x10) fires the reset pulse. No echo is added. - The first Telnet
IACbyte from the client switches the connection to Telnet/RFC 2217 mode for good:BINARY,SUPPRESS-GO-AHEADandCOM-PORT-OPTIONare negotiated,0xFFis escaped in both directions, andCtrl+Pis no longer intercepted (the data path is binary safe). - In Telnet mode the client can change the local port:
SET-BAUDRATE(standard rates up to 921600),SET-DATASIZE(5..8),SET-PARITY(none/odd/even),SET-STOPSIZE(1 or 2),PURGE-DATA, and drive DTR and RTS withSET-CONTROL; each reply reports the value actually in effect.SIGNATUREanswersvectis <version>, and a TelnetBRKsends a serial break. The HiSiliconBOOTROM-CATCHextension ofvectis-bootromis not implemented here. - Everything the client does is logged with its address, and the web
page and
/statusshow the connected client. - EOF from the client ends the session, as on
vectis-bootrom. That is whatncdoes when its stdin runs out (echo cmd | nc host 2021), so for scripted one-shot commands prefervectis -h host -p 2021orsocat, which keep the connection open.
The local terminal, the web console and the TCP client all drive the same
port side by side: device output goes to all of them, input from any of
them goes to the device, and a power action from one is visible to the
others. If the device link drops, the listener stays up, clients can
connect and negotiate, their data is discarded until the link is back,
and the port is reopened with the command-line settings. Like -w, -l
makes the process reopen the link automatically; it requires -u.
-w PORT (or -w ADDR:PORT to listen on one IPv4 address only) starts a
built-in web server alongside the terminal session. It serves a single
page, styled as a brass-and-rivets control panel with the heading
Vectis Web Server, carrying three latching buttons:
| Button | Action | Indicator |
|---|---|---|
Enable |
assert RTS+DTR (device powered) | lit while power is on |
Disable |
deassert RTS+DTR (device unpowered) | lit while power is off |
Reset |
RTS+DTR pulse of -t milliseconds, exactly what Ctrl+P does |
lit, with a spinning gear, while the pulse runs |
A press animates the button, the lamps (link, RTS+DTR) and a gauge
needle follow the device state, and the page polls that state every
two seconds, so a Ctrl+P from the keyboard or a press in another
browser shows up too. The buttons use the very same transport as the
terminal session: ioctl on the local port in -u mode, SET-CONTROL
over RFC 2217 in -h/-p mode. The terminal session, hotkeys and all
other behaviour are unchanged and keep working side by side with the
page.
With -w the web server is listening before the device link is opened,
and the link is (re)established automatically whenever it fails or drops,
in both modes, so the page is reachable immediately and stays up while a
camera is unplugged or the server restarts. Meanwhile the page reports
Link offline and the buttons refuse to act. Only the first failure of
an outage is logged.
Everything the page does is available as plain URLs, so curl, a shell
script or a browser bookmark can drive the device the same way:
| URL | Method | Action | Reply |
|---|---|---|---|
/ |
GET | the page | HTML |
/status |
GET | current state | JSON |
/power/enable |
GET or POST | assert RTS+DTR | JSON, full state |
/power/disable |
GET or POST | deassert RTS+DTR | JSON, full state |
/power/reset |
GET or POST | RTS+DTR pulse; answers when it is over | JSON, full state |
curl -s http://192.168.1.20:8080/status
curl -s http://192.168.1.20:8080/power/reset{"link":"online","mode":"serial","target":"/dev/ttyUSB0","power":"on",
"rts":1,"dtr":1,"reset_ms":200,"tcp_client":"192.168.1.5",
"event":{"seq":7,"kind":"reset","source":"rfc2217","ago_ms":1830},
"version":"1.6.0"}link is online or offline, power is the state last commanded by
this process, rts/dtr are the real modem bits in -u mode (-1
when unavailable or in RFC 2217 mode), tcp_client is the address of
the -l client or null, and event is the last change of the power
lines from any side: kind is on, off or reset (an off followed by
an on within 5 seconds from the same source), source is web,
keyboard, tcp (raw client, Ctrl+P), rfc2217 (SET-CONTROL from a
Telnet client) or link (asserted when the port was opened), seq
grows with every event and ago_ms says how long ago it happened. The
page uses it to show a reset that was triggered elsewhere: a Ctrl+P
from the keyboard or from a vectis -h HOST -p PORT client makes the
Reset button light up and the needle swing, and the log line names the
source. A bare telnet sends Ctrl+P as data, so nothing happens on
the device or on the page in that case. The command URLs answer JSON to
every client, a browser included, so /power/reset opened from a
bookmark shows the resulting state as text. The page is served only for
a bare /; anything appended to it answers 404. While the link is
down the /power/ commands answer 503; an unknown command answers
404. Query strings on the command URLs are ignored.
There is no authentication. Bind the console to a trusted interface
(-w 127.0.0.1:8080) or keep the port inside the lab network, as with
the -l listener.
# Serial adapter, web console on every interface, port 8080
./vectis -u /dev/ttyUSB0 -w 8080
# Headless: no terminal, device output to a log, console on localhost only
./vectis -h 192.168.1.10 -p 35240 -w 127.0.0.1:8080 -n < /dev/null > uart.log 2>&1 &/dev/ttyUSB0, /dev/ttyUSB1, ... are numbered in the order the kernel
finds the adapters, which changes with the plugging order, with other USB
devices coming and going, and from one boot to the next. A daemon started
with -u /dev/ttyUSB0 can silently end up on a different board. Bind the
names to something that does not move instead.
Adapters with a factory serial number (FTDI, CP210x, CH343/CH9102) get a
stable name in /dev/serial/by-id/ automatically; use that path with
-u and nothing else is needed. CH340 adapters (1a86:7522, 1a86:7523
and clones) have no serial number, so several of them collapse into one
by-id entry. For those, bind the name to the physical USB socket
with a udev rule. Worked example with five CH340 adapters, one in a
socket of the PC and four on a USB hub:
-
Look at the topology udev already knows. The number after the last dot before
:1.0is the port on the last hub in the chain:ls -l /dev/serial/by-path/ # pci-0000:00:1d.0-usb-0:1.2:1.0-port0 -> ../../ttyUSB1 (port 2 of the built-in hub) # pci-0000:00:1d.0-usb-0:1.3.2:1.0-port0 -> ../../ttyUSB2 (port 2 of the external hub on port 3) # pci-0000:00:1d.0-usb-0:1.3.3:1.0-port0 -> ../../ttyUSB0 (port 3 of that hub)
(
usbv2-...entries are the same links under a newer naming scheme.) -
Get the kernel names of the same ports, the values for
KERNELS==:for d in /dev/ttyUSB*; do echo -n "$d "; udevadm info -q property -n $d | grep -oP 'DEVPATH=.*/\K[0-9.-]+(?=:1\.0/)'; done # /dev/ttyUSB0 1-1.3.3 # /dev/ttyUSB1 1-1.2 # /dev/ttyUSB2 1-1.3.2
To learn which socket is which, unplug one adapter, run the command again and see which line disappeared; then label the socket.
-
Write the rule, one line per socket,
/etc/udev/rules.d/99-vectis-boards.rules:SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", KERNELS=="1-1.2", SYMLINK+="vectis0", GROUP="dialout", MODE="0660", TAG+="systemd", ENV{SYSTEMD_WANTS}+="vectis@vectis0.service" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", KERNELS=="1-1.3.1", SYMLINK+="vectis1", GROUP="dialout", MODE="0660", TAG+="systemd", ENV{SYSTEMD_WANTS}+="vectis@vectis1.service" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", KERNELS=="1-1.3.2", SYMLINK+="vectis2", GROUP="dialout", MODE="0660", TAG+="systemd", ENV{SYSTEMD_WANTS}+="vectis@vectis2.service" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", KERNELS=="1-1.3.3", SYMLINK+="vectis3", GROUP="dialout", MODE="0660", TAG+="systemd", ENV{SYSTEMD_WANTS}+="vectis@vectis3.service" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", KERNELS=="1-1.3.4", SYMLINK+="vectis4", GROUP="dialout", MODE="0660", TAG+="systemd", ENV{SYSTEMD_WANTS}+="vectis@vectis4.service"Each line does two things: it names the port (
SYMLINK+=) and it tells systemd to start the matching service instance whenever the port appears (TAG+="systemd",SYSTEMD_WANTS).ATTRS{idVendor}andATTRS{idProduct}come fromlsusb; keep them so the rule never renames a keyboard that happens to sit in that socket. Sockets that are empty today (1-1.3.1,1-1.3.4above) may be listed in advance: nothing happens until an adapter is plugged in there. -
Apply and check:
udevadm control --reload && udevadm trigger --subsystem-match=tty --action=change && udevadm settle ls -l /dev/vectis* # /dev/vectis0 -> ttyUSB1 # /dev/vectis2 -> ttyUSB2 # /dev/vectis3 -> ttyUSB0
-
Use the names everywhere: interactively
vectis -u /dev/vectis3, and for the daemons the template service below, one instance per adapter, started and stopped by the device itself.
Notes:
- With hubs behind hubs, use the full path. A wildcard such as
KERNELS=="*.2"would match both1-1.2and1-1.3.2. - The path includes the socket the hub itself is plugged into: moving the
hub from
1-1.3to another socket changes1-1.3.xfor every adapter on it, and the rule must be edited. Adapters plugged straight into the PC depend only on their own socket. - Mixing adapter models is an alternative:
by-idtells a CH340, a CP2102 and an FTDI apart by vendor and product id even without serial numbers. Only identical serial-less adapters need the rule above.
With the rule above the daemons need no systemctl enable at all. A
template unit vectis@.service describes one instance; udev starts
vectis@vectisN when /dev/vectisN appears (at boot or on hot-plug)
and BindsTo= stops it when the adapter is pulled. As many adapters
are plugged into known sockets, as many instances run.
# /etc/systemd/system/vectis@.service — instance %i = vectis0, vectis1, ...
[Unit]
Description=Vectis console on /dev/%i
BindsTo=dev-%i.device
After=dev-%i.device network.target
[Service]
Type=simple
EnvironmentFile=/etc/vectis/%i.conf
ExecStart=/usr/local/bin/vectis -u /dev/%i -b ${BAUD} -t ${RESET_MS} -l ${TCP_PORT} -w ${WEB_PORT} -n
StandardInput=null
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5
User=vectis
SupplementaryGroups=dialout
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yesThere is deliberately no [Install] section: the device is the only
thing that starts an instance. Every adapter needs its own settings
file, because the TCP and web ports must differ between instances; an
instance whose file is missing fails to start and says so in the
journal.
# /etc/vectis/vectis3.conf — one file per adapter, name = the udev link
BAUD=115200
RESET_MS=300
TCP_PORT=2023
WEB_PORT=2083Put everything in place, then let udev re-evaluate the ports that are already plugged in:
sudo install -m 755 vectis /usr/local/bin/
sudo useradd --system --no-create-home --shell /usr/sbin/nologin vectis
sudo mkdir -p /etc/vectis # one vectisN.conf per adapter
sudo systemctl daemon-reload
sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=tty --action=change && udevadm settle
systemctl list-units 'vectis@*' # one active instance per plugged adapter
journalctl -u vectis@vectis3 -f
curl -s http://127.0.0.1:2083/statusFrom then on plugging an adapter into a known socket starts its instance, unplugging it stops the instance, and a reboot brings up exactly the adapters that are present.
vectis needs no terminal: with stdin closed it keeps forwarding the
device output (to the journal here), the listeners stay up, and the
device link is reopened automatically if the adapter goes away. For
several adapters use the device-started template from the previous
section. For a single adapter a plain unit is enough; this one serves
the adapter named /dev/vectis1 by the udev rule above on TCP port
2021, with the web console on port 2081 and a 300 ms reset pulse:
# /etc/systemd/system/vectis.service
[Unit]
Description=Vectis serial console: UART on TCP 2021, web power control on 2081
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/vectis -u /dev/vectis1 -b 115200 -t 300 -l 2021 -w 2081
# Device output goes to the journal; status messages are on stderr and in syslog
StandardInput=null
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5
# An unprivileged user in the group that owns the port (dialout, as set by the udev rule)
User=vectis
SupplementaryGroups=dialout
# Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.targetsudo install -m 755 vectis /usr/local/bin/
sudo useradd --system --no-create-home --shell /usr/sbin/nologin vectis
sudo systemctl daemon-reload
sudo systemctl enable --now vectis
systemctl status vectis
journalctl -u vectis -f # device output and status messages
curl -s http://127.0.0.1:2081/status
vectis -h 127.0.0.1 -p 2021 # interactive console through the servicesystemctl stop sends SIGTERM, which vectis handles by
deasserting RTS/DTR and restoring the port before exiting. Add -n to
ExecStart if the journal shows stray CR characters: the LF→CR+LF
translation exists for raw terminals, not for log files. The unit is
not bound to the device on purpose: the daemon starts at boot even when
the adapter is absent, the web page shows Link offline and the link is
opened as soon as the adapter appears. /dev/vectis1 comes from the
udev rule in the previous section; without such a rule use a
/dev/serial/by-id/... path, or /dev/ttyUSB0 for a single adapter. For the RFC 2217
client mode replace -u /dev/ttyUSB0 with -h HOST -p PORT, drop -l
and the .device lines.
vectis-bootrom.c is the program this repository started with: a UART
terminal and TCP bridge for the OpenIPC UART 38x38 board with the same
RTS/DTR reset pulse, later extended with RFC 2217 auto-detection on its
TCP listener, inetd/inittab integration, and a vendor extension,
BOOTROM-CATCH, that runs the HiSilicon boot-ROM 0x20-marker /
0xAA-ack handshake next to the UART so it works over links with high
round-trip time. Everything else it did is now covered by vectis
(-u for the console, -l for the TCP side, -w for power control),
so the file is kept for reference and for that boot-ROM extension. It is
not built by default:
make vectis-bootrom # or: cc -std=gnu99 -Os -o vectis-bootrom vectis-bootrom.cIn this section Vectis and vectis-bootrom mean this original binary.
./vectis-bootrom [options]-p, --port <port>— serial device path, default:/dev/ttyUSB0-b, --baud <rate>— baud rate, default:115200. Supported rates:9600,19200,38400,57600,115200,230400,460800,921600-t, --tcp-port [port]— enable TCP listener, default port:35240-s, --status— print RTS/DTR status-v, --version— print version and build date-h, --help— show help
Ctrl+P— generate an inverted reset pulse on RTS and DTRCtrl+C— exitCtrl+X— exit
- UART is configured for
8N1. - RTS and DTR are asserted on startup and deasserted on exit.
- RTS and DTR use normal asserted/deasserted signaling.
- The Ctrl+P pulse uses the opposite asserted/deasserted sequence.
- This tool is designed for rapid debugging and control, including power-related device handling, during integration work with OpenIPC UART 38x38.
- The TCP listener accepts one client at a time; a new connection replaces the current client.
- The TCP listener starts only when
-tis provided. When-tis given and stdin is not a terminal (for example a backgroundedvectis -t > log 2>&1 &), stdin is ignored and the console hotkeys are unavailable. - If the serial device disappears (a USB adapter is unplugged), Vectis
logs
UART closedand exits with status 1. Use theinittaborinetdrespawn setups below to bring it back automatically. - For interactive TCP use, run the local terminal side in raw mode so keys like
Ctrl+Pare sent immediately without waiting forEnter. - Example:
socat -,raw,echo=0 TCP:192.168.1.10:35240nc -C 192.168.1.10 35240The TCP listener auto-detects whether a connecting client speaks
[RFC 2217][rfc2217] (Telnet COM-Port Control Option). Detection is
purely passive: Vectis stays in legacy raw mode until the client
transmits a Telnet IAC byte (0xFF). Interactive humans never
type 0xFF, so existing socat/nc/cat workflows are unchanged.
The moment a programmatic client begins option negotiation Vectis
locks into Telnet mode for the rest of that connection and:
- stops intercepting
Ctrl+P(0x10) — the data path is binary-safe; - stops
\n→\rnormalisation and TCP-echo suppression; - escapes
0xFFin both directions as0xFF 0xFFper RFC 854; - accepts
SET-CONTROLto drive RTS/DTR independently (values8/9= DTR ON/OFF,11/12= RTS ON/OFF, as assigned by RFC 2217 §3.5;10reports the current RTS state and0the current DTR state; anything else, such as flow-control settings, is acknowledged as received so the client never hangs waiting); - accepts
SET-BAUDRATE(the same fixed list that-bsupports:9600,19200,38400,57600,115200,230400,460800,921600). A rate outside the list is answered with the rate still in effect, and0queries the current rate; - acknowledges
SET-DATASIZE,SET-PARITYandSET-STOPSIZEwith the values actually in effect. The UART is hard-wired to 8N1, so the reply is always8, none,1regardless of the request (pyserial waits synchronously for these acks when opening the port); - acknowledges
PURGE-DATA(1= RX,2= TX,3= both) and flushes the matching UART queue; - replies to
SIGNATUREwithVectis <version>; - accepts a vendor-extension sub-option
BOOTROM-CATCH(50) that pulses RTS/DTR and runs the HiSilicon bootrom0x20-marker /0xAA-ack handshake locally — see "BOOTROM-CATCH" below.
The HiSilicon boot ROM emits 0x20 markers and listens for 0xAA
in a ~100 ms window after reset. Over a network with one-way
latency above ~25 ms a remote client cannot complete a marker→ack
round trip in time, so the camera autoboots before the client sees
the markers. The BOOTROM-CATCH sub-option moves the catch loop
into Vectis itself, next to the UART, where the response time is
in microseconds — RTT and jitter no longer matter.
Wire format (client → server):
IAC SB COMPORT BOOTROM-CATCH
<pulse_ms : 4 bytes BE>
<max_wait_ms : 4 bytes BE>
[<mode : 1 byte>] # optional, default 0 = MARKER
[<min_markers : 1 byte>] # optional, MARKER threshold; 0 → default (5)
[<head_frame : ≤ 64 bytes>] # optional pipelined HEAD frame
IAC SE
-
pulse_ms(clamped to0..5000) — RTS+DTR low time before the catch loop starts. Set to0when external hardware (e.g. a PoE switch) handles the actual reset. -
max_wait_ms(clamped to100..30000) — overall catch deadline.5000is a comfortable default. -
mode(optional, default0):0 MARKER— wait formin_markersconsecutive0x20bytes from the chip, then send a final0xAA. Matches the documented hi3516 bootrom protocol; finishes early (typically ~500 ms) when the chip emits markers. ReturnsOKon confirmation,TIMEOUTif no qualifying run withinmax_wait_ms.1 BLIND— keep blasting0xAAfor the fullmax_wait_msand unconditionally returnOK. Use for chip variants that enter download mode silently without emitting markers we can observe. The client confirms by sending a HEAD frame and watching for an ACK.
-
min_markers(optional, MARKER mode only). How many consecutive0x20bytes the chip must emit before the catch is declared successful.0(and the no-byte default) → 5, the canonical HiSilicon protocol value. Some hi3516 variants emit shorter bursts and need4(or even3). Clamped server-side to1..16. An adaptive client can issue the catch with0, observe the reply'smax_marker_run, and retry withmin_markers = max_marker_runif the first attempt timed out — purely protocol-driven, no operator intervention. -
head_frame(optional, any bytes appended after byte 10). The first HEAD frame the client wants to send to the chip after the catch. When present, Vectis writes the final0xAAfollowed — with no network round trip in between — by the HEAD frame and captures the chip's reply byte locally. This is the bit that closes the RTT-window problem: on links where the chip's download-mode hold window is shorter than one client→server round trip, the chip otherwise moves on to SPL before any client-issued HEAD can arrive. Capped server-side to 64 bytes (a HiSilicon HEAD frame is 14, with margin for variants).
Older clients that send only the 8/9/10-byte payload default to MARKER mode, the canonical threshold, and no HEAD pipelining respectively — no behaviour change.
Wire format (server → client):
IAC SB COMPORT (BOOTROM-CATCH+100=150)
<status : 1 byte> 0=ok / 1=timeout / 2=io_err
<markers_seen : 4 bytes BE> total 0x20 bytes counted
<max_marker_run: 1 byte> longest consecutive 0x20 run (capped 255)
<bytes_rx : 4 bytes BE> total bytes received from UART
<bytes_tx : 4 bytes BE> total 0xAA bytes blasted
<elapsed_ms : 4 bytes BE> actual catch-loop runtime
<last_byte : 1 byte> last non-marker byte (0 if none)
<head_ack : 1 byte> chip's reply to the pipelined HEAD,
or 0 if no HEAD was provided
<head_ack_seen : 1 byte> 1 if any byte arrived during the
post-HEAD window (so a `head_ack=0`
can be told from "no reply at all")
IAC SE
The trailing counters are diagnostic: a high-RTT or noisy-link
client can see whether the catch failed because the chip really
emitted nothing (bytes_rx=0), because a different byte family
arrived (last_byte non-zero, markers_seen=0), or because some
markers arrived but never strung together (markers_seen high but
max_marker_run < 5). Older clients that read just the first
byte (status) still work — the extra counters are appended after
it.
While the catch loop runs, Vectis owns the local UART exclusively
(no bytes forwarded to the client). After it returns, normal
forwarding resumes — the client can immediately send a HEAD frame
and proceed with the firmware upload.
Example client (Python, with pyserial):
import serial, struct, socket
ser = serial.serial_for_url("rfc2217://vectis.lan:35240", baudrate=115200)
sock = ser._socket
# Ask Vectis to do a 200 ms RTS/DTR pulse, then catch the bootrom
# locally with a 5-second deadline. Mode 0 = MARKER (default), 1 = BLIND.
sock.sendall(
b"\xff\xfa\x2c\x32"
+ struct.pack(">IIB", 200, 5000, 0)
+ b"\xff\xf0",
)
# (Read the IAC SB 44 150 <status> IAC SE reply with your IAC parser.)
ser.write(head_frame) # bootrom is now in download mode
print(ser.read(1)) # → b"\xaa"RFC 2217 detection runs on both TCP entry points:
- the standalone
-t [port]listener, and - the inetd
nowaitsocket (where stdin/stdout is the TCP socket).
The legacy Ctrl+P hotkey on the local interactive console (no -t,
stdin is a tty) is unchanged — humans don't speak Telnet by hand, so
the detection is a no-op for them.
Vectis can also be launched from inetd:
# OpenIPC
#
35240 stream tcp nowait root /usr/local/sbin/vectis-bootrom vectis-bootrom -s -p /dev/ttyUSB0 -b 115200In nowait mode each TCP connection spawns a fresh Vectis with the
socket wired to its stdin/stdout. RFC 2217 detection works on this
path too: a programmatic client sending IAC flips the connection
into Telnet mode, sub-options like SET-CONTROL are answered over
the same socket, and 0xFF bytes are escaped per RFC 854.
On Debian/Ubuntu, install inetd with:
sudo apt update
sudo apt install inetutils-inetdVectis can also be started from inittab on systems with BusyBox init:
# OpenIPC
#
ttyS0::respawn:/usr/local/sbin/vectis-bootrom -s -p /dev/ttyUSB0 -b 115200