Code Library
Machine DataPythonWindows · Python 3.8+ · FANUC Fwlib DLLsMIT license

FANUC FOCAS: log feedrate & spindle speed to CSV (Python)

Every FOCAS tutorial shows the three-line pattern; almost none ship the part that actually stops people — the ctypes struct definitions. This is a complete, runnable logger: it connects to the control over Ethernet, reads actual feedrate and spindle speed once a second with cnc_rdspeed, and appends them to a timestamped CSV you can chart in Excel. Structs included, error codes explained, handle freed on Ctrl+C like a good citizen.

Before you run it

  • FANUC's FOCAS library files (Fwlib32.dll and its companion DLLs) in the script's folder — they come from FANUC / your machine tool builder
  • Python whose bitness matches the DLL (32-bit Python for Fwlib32.dll, 64-bit for Fwlib64.dll)
  • FOCAS/Ethernet or Data Server option on the control, port 8193 reachable

The code

GitHub
"""Log feedrate + spindle speed from a FANUC control to CSV, once a second.

Usage:  python focas_logger.py 192.168.0.10

Requires Fwlib32.dll (+ companion DLLs) next to this script, and Python
whose bitness matches the DLL. The control needs the FOCAS/Ethernet
option; the standard port is 8193.
"""

import csv
import ctypes
import datetime as dt
import sys
import time

PORT = 8193
TIMEOUT_S = 10


class SpeedElm(ctypes.Structure):
    """One element of ODBSPEED (fwlib32.h: SPEEDELM)."""
    _fields_ = [
        ("data", ctypes.c_long),   # value as an integer
        ("dec", ctypes.c_short),   # decimal places in 'data'
        ("unit", ctypes.c_short),
        ("disp", ctypes.c_short),
        ("name", ctypes.c_char),
        ("suff", ctypes.c_char),
    ]


class OdbSpeed(ctypes.Structure):
    """fwlib32.h: ODBSPEED - actual feed (actf) + actual spindle (acts)."""
    _fields_ = [("actf", SpeedElm), ("acts", SpeedElm)]


def scaled(elm):
    """FOCAS returns integers plus a decimal-place count - recombine them."""
    return elm.data / (10 ** elm.dec)


def main():
    ip = sys.argv[1] if len(sys.argv) > 1 else "192.168.0.10"
    fwlib = ctypes.WinDLL("Fwlib32.dll")

    handle = ctypes.c_ushort()
    ret = fwlib.cnc_allclibhndl3(ip.encode(), PORT, TIMEOUT_S,
                                 ctypes.byref(handle))
    if ret != 0:
        sys.exit(
            f"Connect failed, FOCAS code {ret}. Common ones: "
            "-16 socket error (IP/port), -8 handle limit, "
            "7 EW_FUNC / option missing on the control."
        )

    out = f"machine_{ip.replace('.', '-')}.csv"
    print(f"Connected to {ip}. Logging to {out} - Ctrl+C to stop.")

    try:
        with open(out, "a", newline="") as fh:
            writer = csv.writer(fh)
            if fh.tell() == 0:
                writer.writerow(["timestamp", "feed", "spindle_rpm"])
            while True:
                speed = OdbSpeed()
                ret = fwlib.cnc_rdspeed(handle, -1, ctypes.byref(speed))
                if ret == 0:
                    writer.writerow([
                        dt.datetime.now().isoformat(timespec="seconds"),
                        scaled(speed.actf),
                        scaled(speed.acts),
                    ])
                    fh.flush()
                else:
                    print(f"read failed, FOCAS code {ret}")
                time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        fwlib.cnc_freelibhndl(handle)
        print("\nHandle freed. CSV is ready to chart.")


if __name__ == "__main__":
    main()

How it works

  • SpeedElm/OdbSpeed mirror SPEEDELM/ODBSPEED from fwlib32.h — this is the plumbing every tutorial hand-waves. ctypes.c_long is 32-bit on Windows, matching FOCAS's long.
  • FOCAS returns values as an integer plus a decimal-place count; scaled() recombines them (data=1234, dec=1 → 123.4). Skipping this is why people see feeds ten times too high.
  • cnc_allclibhndl3(ip, 8193, timeout, &handle) opens the Ethernet handle; every later call takes it, and cnc_freelibhndl in the finally block releases it even on Ctrl+C — leaked handles eventually exhaust the control's connection slots.
  • cnc_rdspeed(handle, -1, &speed) with -1 asks for both feed and spindle in one call.
  • The CSV opens in append mode and only writes the header on an empty file, so restarting the logger extends the same day's data.

Gotchas & honest limits

  • Bitness is the #1 failure: 64-bit Python cannot load Fwlib32.dll (WinError 193). Either install 32-bit Python or get Fwlib64.dll from FANUC.
  • Fwlib32.dll needs its companion DLLs (fwlibe1.dll etc.) in the same folder — copying just one file gives a load error that looks like a missing-DLL riddle.
  • A non-zero return is usually the machine talking, not a bug: no FOCAS option, wrong port, or a firewall. Code 7 (EW_FUNC) almost always means the option isn't enabled.
  • Feed units follow the control's display (mm/min vs inch); the CSV header says feed, not feed_mm_min, on purpose.
  • One-second polling is fine for dashboards and utilization; it is not a vibration or load-spike instrument.

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é