This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pyads is a Python wrapper around Beckhoff's TwinCAT ADS protocol. It talks to the native ADS
library via ctypes: on Linux/macOS that's libAdsLib.so (compiled from the adslib git
submodule, https://github.com/Beckhoff/ADS), on Windows it's the vendor TcAdsDll.dll from a
TwinCAT installation. Python code lives in src/pyads/; tests live in tests/.
The adslib submodule must be checked out and compiled before the package can be imported —
import pyads will raise OSError: Failed to locate 'AdsLib.so' otherwise.
git submodule update --init --recursive # if not cloned with --recursive
pip install -e .[tests,dev] # triggers the native build, see belowpip install . (and editable installs) run a custom build_py step (see setup.py) that on
Unix does the equivalent of:
cd adslib && meson setup build && ninja -C build && cd .....then copies the resulting libAdsLib.so to src/AdsLib.so. If you rebuild the submodule
manually (e.g. cd adslib && make), re-run pip install -e . afterwards so the .so gets
copied/renamed into src/ where pyads_ex.py looks for it — a stray adslib.so or
libadslib.so in src/ without the exact name AdsLib.so will not be found.
On Windows, set TWINCAT3DIR if you need pyads to locate TcAdsDll.dll outside of PATH.
# Run the full test suite (uses the fake in-process ADS test server, no PLC needed)
pytest
# Run a single test file / class / test
pytest tests/test_ads.py
pytest tests/test_ads.py::AdsTest::test_AmsAddr
# Run across all supported Python versions via tox
tox
tox -e py312
# Lint (runs mypy, then flake8, then pydocstyle against the pyads package)
python lint.py
# Format
black src/pyads
# Build the docs (Sphinx)
cd doc && make html # output in doc/build/htmlNotes:
- Tests are plain
unittest.TestCaseclasses, run through pytest.tests/conftest.pyenablesfaulthandlerbecause bugs in the native library can otherwise cause silent segfaults. - On Windows, a running TwinCAT router can interfere with the test server on the ADS port; either
disable the TwinCAT runtime (
TcSwitchRuntime.exe) or run tests in Docker/WSL instead (seeCONTRIBUTING.mdandDockerfile). black,flake8, andmypyare configured to excludeadslib/and other build artifacts; flake8 config is in.flake8(not inpyproject.toml, since flake8 doesn't support that).- Docstrings follow Sphinx's info-field-lists style (
:param:,:type:,:return:,:rtype:) so API docs can be autogenerated — seeCONTRIBUTING.mdfor the exact format. - Add an entry under
## [Unreleased]inCHANGELOG.mdfor any user-facing change.
Layered from raw C bindings up to the public API:
pyads_ex.py— lowest layer. Loads the native ADS library viactypes(with platform-specific logic for Windows/Linux/macOS/FreeBSD DLL discovery) and wraps the raw C API functions (adsPortOpenEx,adsSyncReadReqEx2,adsSyncAddDeviceNotificationReqEx, ...) with Python-friendly signatures and error checking (raisesADSError).ads.py— the pythonic, function-based API built on top ofpyads_ex:open_port,close_port,add_route, plus structure (DUT) marshalling helperssize_of_structure,dict_from_bytes,bytes_from_dictthat convert between PLC struct byte layouts and Python dicts using aStructureDeftuple description.connection.py—Connection, the main high-level OOP entry point most users interact with. Wrapsads.pyfunctions, manages the AMS port/address lifecycle, and exposesread/write/read_by_name/write_by_name/notifications as methods with context-manager support (plc.open()/with plc:).symbol.py—AdsSymbol, representing a named PLC variable. Resolves and caches type info from the PLC symbol table for typed reads/writes and notification callbacks without needing to specify the PLC type manually each time.structs.py—ctypes.Structuredefinitions mirroring the ADS C structs (SAmsAddr,SAmsNetId,SAdsNotificationAttrib,SAdsSymbolEntry, ...) plus higher-level Python wrappers (AmsAddr,NotificationAttrib) used throughout the rest of the package.constants.py— ADS protocol constants (index groups/offsets,PORT_*,ADSSTATE_*,ADSTRANS_*) and thePLCTYPE_*ctypes type aliases plus theDATATYPE_MAP/ads_type_to_ctypemapping between PLC datatypes and Python/ctypes types.errorcodes.py/errorcodes_nc.py— ADS and NC error code → human-readable message tables consumed byADSError.filetimes.py— WindowsFILETIME⇄ Pythondatetimeconversions, used forDATE/DT/TODPLC types.utils.py— small platform-detection and string helpers shared across the modules above.testserver/— a pure-Python fake ADS TCP server (AdsTestServer) that speaks enough of the ADS wire protocol to exercisepyadswithout a real PLC/TwinCAT device; this is what the entire test suite runs against.handler.pydefines theAbstractHandlerprotocol and AMS packet dataclasses;basic_handler.pyandadvanced_handler.pyimplement two different response strategies;testserver.pyruns the threaded socket server itself. It is also shipped as a public feature (pyads.testserver) for users who want to test their own code against a fake PLC.
__init__.py defines the public API surface — re-exports from the modules above are curated
there rather than done with wildcard imports, so adding a new public symbol means adding an
explicit import in __init__.py.
- NEVER include "Co-Authored-By" or "Generated with Claude" in commit messages.
adslib/ is a separate C++ project (Beckhoff's open-source AdsLib) vendored as a git submodule
and built with meson/ninja (or make, per its own Makefile). pyads only depends on its compiled
shared library output; there's normally no need to edit code inside adslib/ unless fixing a bug
in the native layer itself.