A pure Python implementation of the DNP3 (Distributed Network Protocol 3) protocol for SCADA communications over TCP/IP. Install from PyPI as nfm-dnp3; import in Python as dnp3py.
This driver implements the DNP3 protocol stack to communicate with DNP3 outstations (slaves) as a master station. It supports reading data points, controlling outputs, and handling events.
- Full Protocol Stack: Implements Data Link, Transport, and Application layers
- TCP/IP Communication: Connect to DNP3 devices over IP networks
- Data Point Support:
- Binary Inputs (Group 1, 2)
- Binary Outputs (Group 10, 12)
- Analog Inputs (Group 30, 32)
- Analog Outputs (Group 40, 41)
- Counters (Group 20, 22)
- Control Operations:
- Direct Operate
- Select-Before-Operate (SBO)
- Pulse control
- Class-Based Polling: Support for Class 0, 1, 2, 3 data
- CRC-16 Error Detection: Compliant with DNP3 CRC polynomial
- Thread-Safe:
DNP3Mastersupports concurrent use (open/close/requests protected by a lock)
- Python 3.9+
# Clone the repository
git clone https://github.com/fxodell/dnp3py.git
cd dnp3py
# Install from PyPI
pip install nfm-dnp3
# Or install in development mode from source
pip install -e .After installation, use from dnp3py import ... from any directory.
from dnp3py import DNP3Master, DNP3Config
# Configure connection
config = DNP3Config(
host="192.168.1.100", # Outstation IP
port=20000, # DNP3 port (default: 20000)
master_address=1, # Master address
outstation_address=10, # Outstation address
)
# Create master instance
master = DNP3Master(config)
# Connect and read data
with master.connect():
# Integrity poll - read all data
result = master.integrity_poll()
if result.success:
print(f"Binary Inputs: {result.binary_inputs}")
print(f"Analog Inputs: {result.analog_inputs}")
print(f"Counters: {result.counters}")
# Read specific points
binary_inputs = master.read_binary_inputs(0, 9)
analog_inputs = master.read_analog_inputs(0, 4)
# Control a binary output
master.direct_operate_binary(0, value=True) # Turn ON
master.direct_operate_binary(0, value=False) # Turn OFF
# Control an analog output
master.direct_operate_analog(0, value=50.0)Run the interactive examples (requires nfm-dnp3 installed):
python examples/basic_usage.pydnp3py/
├── __init__.py # Package exports
├── setup.py # Package setup (pip install -e .)
├── test_connection.py # Quick connection test (edit host/port/address)
├── core/
│ ├── master.py # DNP3Master (main interface; thread-safe, connect() context manager)
│ ├── config.py # Configuration and protocol constants
│ └── exceptions.py # Custom exceptions
├── layers/
│ ├── datalink.py # Data Link Layer (frames, CRC)
│ ├── transport.py # Transport Layer (segmentation)
│ └── application.py # Application Layer (requests/responses)
├── objects/
│ ├── binary.py # Binary I/O objects
│ ├── analog.py # Analog I/O objects
│ ├── counter.py # Counter objects
│ └── groups.py # Object group definitions
├── utils/
│ ├── crc.py # CRC-16 calculation
│ └── logging.py # Logging utilities
├── examples/
│ ├── basic_usage.py # Basic usage examples
│ └── async_example.py # Async/threading example
├── docs/ # Documentation
└── tests/ # Unit tests
- FT3 frame format with 0x0564 start bytes
- Source and destination addressing (0-65519)
- CRC-16 error checking every 16 bytes
- Maximum frame size: 292 bytes (250 bytes user data)
- Message segmentation/reassembly
- 1-byte transport header with sequence numbering
- FIR (First) and FIN (Final) segment flags
- Maximum segment payload: 249 bytes
- Request/Response message formatting
- Function codes (READ, WRITE, SELECT, OPERATE, etc.)
- Object headers with qualifiers
- Internal Indications (IIN) handling
DNP3Config validates and normalizes values when you create a DNP3Master (e.g. host trimmed, port/addresses coerced to int). Invalid settings raise ValueError or TypeError.
config = DNP3Config(
# Network
host="192.168.1.100",
port=20000,
# Addressing
master_address=1,
outstation_address=10,
# Timeouts (seconds)
response_timeout=5.0,
connection_timeout=10.0,
select_timeout=10.0, # SBO: time between SELECT and OPERATE
# Retries
max_retries=3,
retry_delay=1.0,
# Data Link
confirm_required=True,
max_frame_size=250,
# Logging
log_level="INFO",
log_raw_frames=False,
)Immediately executes the control command:
# Turn output ON
master.direct_operate_binary(index=0, value=True)
# Turn output OFF
master.direct_operate_binary(index=0, value=False)
# Set analog setpoint
master.direct_operate_analog(index=0, value=50.0)Two-step control for safety-critical operations:
# SELECT then OPERATE
master.select_operate_binary(index=0, value=True)Generate timed pulses on outputs:
# Pulse ON for 500ms, 3 times
master.pulse_binary(
index=0,
on_time=500, # milliseconds
off_time=500, # milliseconds
count=3,
pulse_on=True,
)All DNP3 exceptions inherit from DNP3Error. Catch it for any driver error, or use specific types for context (e.g. host/port on DNP3CommunicationError, timeout_seconds on DNP3TimeoutError).
from dnp3py import (
DNP3Error,
DNP3CommunicationError,
DNP3TimeoutError,
DNP3ProtocolError,
DNP3CRCError,
)
try:
with master.connect():
result = master.integrity_poll()
except DNP3TimeoutError as e:
print(f"Timeout after {e.timeout_seconds}s")
except DNP3CommunicationError as e:
print(f"Connection failed: {e} (host={e.host}, port={e.port})")
except DNP3CRCError:
print("CRC validation failed")
except DNP3Error as e:
print(f"DNP3 error: {e}")For frame, object, or control-specific errors, use from dnp3py.core import DNP3FrameError, DNP3ObjectError, DNP3ControlError.
From the project root (with nfm-dnp3 or pip install -e .):
pytest tests/ -vTo quickly test a live outstation, edit test_connection.py with your host, port, and outstation address, then run:
python test_connection.pyLocal check (tests + lint + security, same as CI):
pytest tests/ -q && ruff check . && ruff format --check . && bandit -r . -c pyproject.toml -x tests,.venv,venv- IEEE Std 1815 - DNP3 Standard
- DNP Users Group
- DNP3 Protocol Overview
- Setup:
setup.pyusespackage_dir={"dnp3py": "."}(repo root is the package);find_packages()excludestestsso the test suite is not installed. Version is read from__init__.pyas the single source of truth. Long description is taken fromREADME.md. Install dev dependencies withpip install -e ".[dev]"for pytest, pytest-cov, bandit, ruff, and pyright. - Commands: Run tests:
pytest tests/ -v. Lint:ruff check .andruff format --check .. Security:bandit -r . -c pyproject.toml. Type check:pyright(optional; requiresreportMissingImports = falsein pyproject until run from an env wherednp3pyis installed). - Git:
.gitignoreexcludes bytecode (__pycache__/,*.pyc), build artifacts (build/,dist/,*.egg-info/), virtual envs (.venv/,venv/), IDE/editor dirs (.idea/,.vscode/), test/cache (.pytest_cache/,.coverage,htmlcov/), tool caches (.mypy_cache/,.ruff_cache/),*.log, and.claude/settings.local.json; OS cruft (.DS_Store) is ignored. Afterpip install -e ., thednp3py.egg-info/directory appears in the repo root; it is generated metadata and is correctly ignored—do not commit it. - Package layout: Install with
pip install -e .from the repo root;dnp3pyis the top-level package. The root__init__.pyexportsDNP3Master,DNP3Config, the exception classes (DNP3Error,DNP3CommunicationError,DNP3TimeoutError,DNP3ProtocolError,DNP3CRCError), and__version__via__all__. Subpackages use relative or absolute imports; each__init__.pyexposes a public API via__all__. - Core package:
core/__init__.pyre-exportsDNP3Master,DNP3Config,PollResult(return type ofintegrity_poll()andread_class()), and all eight DNP3 exception classes. The top-leveldnp3pypackage exports only the five most common exceptions; usefrom dnp3py.core import PollResult, DNP3FrameError, etc., when needed. - Master:
core/master.pyimplementsDNP3Master; it coordinates Data Link, Transport, and Application layers and is thread-safe (open/close and request/response use a single lock). Use theconnect()context manager oropen()/close()for connection life cycle.DNP3CommunicationErroris raised withhostandportset from config when send/receive or connection fails, for easier debugging. - Config:
core/config.pydefinesDNP3Configand protocol enums (LinkLayerFunction, AppLayerFunction, QualifierCode, ControlCode, ControlStatus, IINFlags).DNP3Config.validate()normalizes and validates all fields: host (non-empty string), port (1-65535), master/outstation addresses (0-65519), timeouts and retry_delay (coerced to float, positive or ≥0), max_frame_size (1-250), max_apdu_size (1-65536), poll intervals (≥0), and log_level (DEBUG/INFO/WARNING/ERROR/CRITICAL).IINFlags.from_bytes(iin1, iin2)validates that iin1/iin2 are coercible to int. Called automatically when creating aDNP3Master. - Objects: Binary, analog, and counter modules validate input length and value ranges in
from_bytes/to_bytesand inparse_*(e.g.count/start_index≥ 0); invalid data raises clearValueErrors orTypeError. Analog (objects/analog.py) additionally validatesdatatype (bytes/bytearray),index≥ 0, andvariationin valid range (1–6 for AnalogInput, 1–4 for AnalogOutput/AnalogOutputCommand) infrom_bytes/to_bytes/create()andparse_analog_inputs/parse_analog_outputs. - Objects package:
objects/__init__.pyre-exports data types (BinaryInput, AnalogInput, Counter, etc.),ObjectGroup,ObjectVariation,get_object_size, andget_group_name; parse functions (parse_binary_inputs,parse_analog_inputs, etc.) are in the binary, analog, and counter submodules. - Groups:
objects/groups.pydefinesObjectGroup,ObjectVariation,OBJECT_SIZES,get_object_size(), andget_group_name()for protocol and parsing use. - Layers:
layers/__init__.pyre-exportsDataLinkLayer,TransportLayer, andApplicationLayer; frame, segment, and request/response types live in the datalink, transport, and application submodules. Data Link (layers/datalink.py) validates addresses inbuild_frame,build_request_link_status, andbuild_reset_link;calculate_frame_sizevalidates the length byte; frame parsing checks CRCs and length. Transport (layers/transport.py) validates APDU length (≤ MAX_MESSAGE_SIZE) andmax_payload(1..MAX_SEGMENT_PAYLOAD) insegment();TransportSegment.from_bytesrejects oversized segments;parse_header()validates header byte 0-255; reassembly enforces sequence, size limit, and timeout. Application (layers/application.py) validatesObjectHeadergroup/variation/qualifier (0-255) and range/count per qualifier into_bytes();ObjectHeader.from_bytes()validates offset and range;ApplicationRequestvalidates sequence and function;build_confirm()andbuild_read_request()validate sequence (0-15) and group/variation/start/stop. - Utils:
utils/__init__.pyre-exportsCRC16DNP3,calculate_frame_crc,setup_logging,get_logger,log_frame, andlog_parsed_frame.utils/crc.pyprovides DNP3 CRC-16 (polynomial 0x3D65, reflected 0xA6BC, final XOR 0xFFFF);CRC16DNP3.calculate()andcalculate_frame_crc()validate bytes/bytearray;verify_bytes()validates 2-byte CRC.utils/logging.pyvalidates level (DEBUG/INFO/WARNING/ERROR/CRITICAL), uses UTF-8 for file output, setspropagate=False;log_frame()andlog_parsed_frame()validate frame/frame_info types. - Exceptions:
core/exceptions.pydefines the hierarchy:DNP3Error(base);DNP3CommunicationError(host, port),DNP3TimeoutError(timeout_seconds),DNP3ProtocolError(function_code, iin),DNP3CRCError(expected_crc, actual_crc),DNP3FrameError,DNP3ObjectError(group, variation),DNP3ControlError(status_code). The top-leveldnp3pypackage exports the first five;DNP3FrameError,DNP3ObjectError, andDNP3ControlErrorare available fromdnp3py.core. All useOptionalfor context attributes. - Publishing to PyPI: This project is published as nfm-dnp3 on PyPI (Trusted Publisher from GitHub Actions). (1) Bump version in
__init__.py, (2) tag and push (e.g.git tag v1.0.1 && git push origin v1.0.1); the publish workflow uploads to PyPI. For manual upload:pip install build twine,python -m build, thentwine upload dist/*with PyPI token.
- No unsafe patterns: The codebase does not use
eval,exec,__import__with untrusted input, orpickle.loadson network data. Protocol parsing is binary-only; no code execution from DNP3 payloads. - Checks: Run
bandit -r . -c pyproject.toml(excludestests/) to scan for common issues. CI runs bandit on every push/PR. - Reporting: If you find a security concern, please report it privately (e.g. via the repository's security policy or maintainer contact) rather than in a public issue.
This implementation is provided as-is for educational and development purposes.
This is a reference implementation. For production SCADA systems, consider evaluation of certified DNP3 stacks such as OpenDNP3.