SerialCP
Resumable, MD5-verified file transfer and a remote file explorer over a serial
console, inside VS Code.
Built for the case where rz/sz in a terminal emulator keeps stalling on
large files and an unstable line means a transfer that dies at 90% has to start
again from zero.

What you get
- Port and baud picker — choose the port, baud rate, parity and flow
control from the command palette; settings persist per workspace.
- Remote file tree — browse the far end like an SSH remote, with sizes,
timestamps and permissions.
- Drag and drop — drop files or whole folders from Explorer or the desktop
onto a remote directory to upload; drag within the tree to move.
- Resume (断点续传) — an interrupted transfer picks up from the last
verified block instead of restarting. Pull the cable and plug it back in; the
transfer carries on.
- MD5 end to end — every block is CRC32-checked on the wire and the whole
file is MD5-compared before the
.scpart is renamed into place. A file that
arrives is a file that matched.
- A terminal that keeps working — the remote shell runs over the same port
as the transfer, with keystrokes prioritised over bulk data.
- Edit remote files in place — remote paths mount as
serialcp:/…, so
small files open, edit and save in the editor.
- Status bar — live progress, rate and ETA, and a visible warning when the
link drops and a transfer is waiting to resume.
Requirements
- The far end runs Linux with
python3 on PATH. Nothing needs to be
installed there: SerialCP streams a ~37 KB agent over the console on first
connect and caches it under /tmp/.serialcp.
- You can reach a shell prompt on the console. If MobaXterm can drive it,
SerialCP can.
Install
npm install
npm run compile
Then either press F5 in VS Code to launch an Extension Development Host, or
package and install it:
npx @vscode/vsce package --allow-missing-repository
code --install-extension serialcp-1.0.0.vsix
Quick start
SerialCP: Connect… from the command palette, then pick the port and baud
rate. Close any other program holding the port first — a serial port has one
owner.
If the remote answers with a login prompt rather than a shell, run
SerialCP: Open Serial Console first. That pipes the port straight to a
terminal so you can log in, and the port stays open when you then connect —
which matters, because closing it drops DTR and a getty will log you
straight back out.
The SerialCP icon in the activity bar shows the remote filesystem.
Drag a file onto a folder in that tree to upload it. Right-click a remote
file and choose Download… to fetch it.
Open Serial Terminal gives you the remote shell, usable while a transfer
runs.
How it works
On connect, SerialCP drives the login shell in plain line mode just long enough
to stream a small Python agent across and start it. The agent puts the tty into
raw mode and from then on the line carries a framed binary protocol rather than
text.
'S' 'C' | type u8 | flags u8 | seq u16 | len u32 | hdr-crc8 | payload | crc32
Each frame carries a header CRC8 and a payload CRC32, and the decoder
resynchronises on the magic. Line noise, a burst of kernel log output on the
console, or a half-written frame costs one frame rather than the session.
The link is multiplexed: terminal traffic, filesystem requests and bulk file
data share it, and both ends send terminal bytes ahead of file data. Bulk
frames are written in 4 KiB pieces so a keystroke never waits behind a whole
block. On the board this tool was developed against, a keystroke still round
-tripped in 216 ms with an upload saturating the line.
Resume
An upload writes to name.scpart on the remote and is renamed only after the
whole-file MD5 matches. On a retry, SerialCP asks how much of the partial is
there and compares the MD5 of its own matching prefix. If they agree it resumes
at that offset; if they do not — a partial that was corrupted rather than
merely truncated — it pulls a per-block digest list and rewinds to the first
block that actually differs. Downloads work the same way in reverse.
Because the resume point is negotiated from scratch on every attempt, a
transfer survives the link dropping repeatedly. In testing, an upload
interrupted twice finished having resent 1.7% of the file.
Speed
Blocks are deflated before they go out and sent raw if they do not shrink, so
already-compressed data costs nothing extra. On a 921600 line the board saw
173 KB/s effective on mixed data — above the 92 KB/s the wire can carry — and
an identical file already on the far end is detected by MD5 and skipped
outright.
Settings worth knowing
| Setting |
Default |
Why you would change it |
serialcp.baudRate |
921600 |
Must match the remote console. |
serialcp.rtscts |
false |
Turn on above 921600 if your cable wires RTS/CTS; it stops the remote UART overrunning. |
serialcp.blockSize |
16384 |
Also the resume granularity. Smaller loses less on a bad line; larger is marginally faster on a good one. |
serialcp.window |
8 |
Blocks in flight. Higher hides latency, too high can overrun a remote with no flow control. |
serialcp.compression |
6 |
0 disables. Blocks that do not shrink are sent raw anyway. |
serialcp.agentDir |
/tmp/.serialcp |
Point at something persistent, e.g. /var/lib/serialcp, to survive a reboot without re-uploading the agent. |
serialcp.maxAttempts |
100 |
How many times a transfer may resume before giving up. |
serialcp.bootstrap.lineDelayMs |
8 |
Raise if the agent fails its md5 check while uploading on a lossy line. |
Troubleshooting
"Agent upload kept failing its md5 check" — the console is losing
characters during bootstrap. Lower serialcp.bootstrap.chunkSize to 128 and
raise serialcp.bootstrap.lineDelayMs to 20.
"No python3 on the remote" — set serialcp.pythonCommand to the full path
if the interpreter is somewhere unusual.
The port will not open — something else has it. A serial port has exactly
one owner; close the MobaXterm session first.
The console goes quiet and nothing responds — an agent may still be holding
the tty from a previous session. Connecting again clears it: SerialCP opens
with a handshake, adopts a live agent, and sends a BYE to free a wedged one.
If it is still silent, the login session on the far end has gone; replug the
adapter, and power-cycle the board if that does not bring it back.
The file tree goes blank and uploads stop responding — most often the line
is saturated by a program producing heavy terminal output, so listings queue
behind it. The tree now says so rather than showing nothing, and requests are
re-sent automatically. Terminal output is backpressured, so a chatty program
slows down instead of growing the agent without bound.
Transfers are slower than expected — check SerialCP: Show Link Statistics
for resyncs and bad CRCs. A nonzero count means the line is corrupting data and
blocks are being resent; try a lower baud rate or enable RTS/CTS.
Development
npm run compile # build, embedding the agent
npm run watch # rebuild on change
Three test suites, in increasing order of realism:
wsl python3 scripts/test_agent.py # the agent alone, over a real pty
node scripts/test-host.js # the whole host stack against bash in WSL
node scripts/test-reconnect.js # reconnect and resume, with a paced pipe
And against real hardware, which needs a board on the other end:
node scripts/probe-port.js COM5 921600 # is anything alive on the line
node scripts/probe-remote.js COM5 921600 # what tools does the remote have
node scripts/test-serial.js COM5 921600 # full end-to-end on the real port
node scripts/test-resilience.js COM5 921600
test-resilience.js deliberately closes the port mid-transfer. On a console
whose modem-control lines are wired through, that can drop DTR and take the
remote login session with it, which may need a replug or a power cycle to
recover. Run it knowingly.
The agent is a single dependency-free Python file, agent/serialcp_agent.py.
scripts/build-agent.js deflates it into src/core/agentSource.ts at build
time, so editing the agent and rebuilding is all it takes — the remote picks up
the new one automatically, since the cache is keyed by content hash.
Layout
agent/serialcp_agent.py the remote agent: framing, pty, fs ops, transfers
src/core/frame.ts the same codec on the host side
src/core/shell.ts drives the login shell until the agent is up
src/core/bootstrap.ts streams, verifies and caches the agent remotely
src/core/session.ts multiplexing, requests, keepalive, reconnection
src/core/transfer.ts resume negotiation, block windows, verification
src/ui/ tree, transfers view, terminal, status bar, fs provider
Licence
MIT.