Machine DataPythonPython 3.8+ · pyserial · USB-RS232 adapterMIT license
Send a CNC program over RS-232 with Python (DNC / drip-feed)
Thousands of perfectly good machines still take programs over a 9-pin plug, and shops still buy dedicated DNC boxes to feed them. This script is the minimum honest replacement: it opens the port with FANUC-style defaults (4800 baud, 7 data bits, even parity, 2 stop bits, XON/XOFF), streams the program line by line while the control's buffer paces it, and shows a byte counter so you know it's moving. Every parameter is a flag, because every old machine is its own museum.
Before you run it
pip install pyserial- A null-modem cable (or correctly wired adapter) to the control's serial port
- The control's I/O parameters (baud, bits, parity) — and the control put in READ mode first
The code
"""Send a CNC program over RS-232 (DNC / drip-feed style) with pyserial.
Defaults match a common FANUC setup: 4800 baud, 7 data bits, even parity,
2 stop bits, XON/XOFF flow control. Match YOUR control's I/O parameters,
and put the control in READ / tape mode BEFORE running this.
Usage: python send_program.py O1234.nc COM3
python send_program.py O1234.nc /dev/ttyUSB0 --baud 9600
"""
import argparse
import sys
import time
import serial # pip install pyserial
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("file", help="program file to send")
ap.add_argument("port", help="serial port, e.g. COM3 or /dev/ttyUSB0")
ap.add_argument("--baud", type=int, default=4800)
ap.add_argument("--settle", type=float, default=2.0,
help="seconds to wait after opening the port")
args = ap.parse_args()
try:
with open(args.file, "rb") as fh:
data = fh.read()
except OSError as e:
sys.exit(f"Cannot read {args.file}: {e}")
ser = serial.Serial(
port=args.port,
baudrate=args.baud,
bytesize=serial.SEVENBITS,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_TWO,
xonxoff=True, # the control pauses us with XOFF when its buffer fills
)
print(f"Port open ({args.port} @ {args.baud}). "
f"Waiting {args.settle}s - start READ on the control now.")
time.sleep(args.settle)
sent = 0
try:
for line in data.splitlines(keepends=True):
ser.write(line)
sent += len(line)
print(f"\r{sent}/{len(data)} bytes", end="", flush=True)
ser.flush()
print("\nDone. Let the control finish reading before you close anything.")
finally:
ser.close()
if __name__ == "__main__":
main()How it works
- The port settings (
SEVENBITS,PARITY_EVEN,STOPBITS_TWO) are the classic FANUC ISO-code trio — but they're defaults, and the flags exist because parameter 0101 on the actual control wins every argument. xonxoff=Truedelegates flow control to the OS driver: when the control's buffer fills, it sends XOFF and the write blocks; XON resumes it. That's the entire magic of a DNC box.- Reading the file in binary and sending
splitlines(keepends=True)preserves the file's own line endings — re-encoding line ends is a classic way to corrupt a transfer. - The byte counter over one line (
\r) is deliberately boring: you can watch a 200 kB mold program crawl and know instantly when flow control has stalled.
Gotchas & honest limits
- The cable is the project. Most transfers that "don't work" are straight-through cables where a null-modem (2-3 crossed) is needed, or missing handshake jumpers on hardware-flow controls.
- The control must already be in READ/tape mode when bytes arrive — hence the
--settlepause after opening the port. - FANUC ISO programs conventionally start and end with
%; most controls need them. This script sends the file verbatim — make sure the file is right. - For true drip-feed of programs bigger than control memory, the control's DNC mode (and often hardware flow control) is required — check your machine's I/O manual; this script covers the everyday load/unload case.
Goes deeper
Want this adapted to your shop — or built into a real tool?
Samples are the free 80%. The last 20% is the part I do for a living.