Code Library
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

GitHub
"""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=True delegates 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 --settle pause 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.

Get in touch
Home
Blog
Tools
Code
Email
LinkedIn
Résumé