Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/API/discovery.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Discovery
=========

.. automodule:: snap7.discovery
:members:
:undoc-members:
:show-inheritance:
178 changes: 178 additions & 0 deletions doc/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
Command-Line Interface
======================

python-snap7 includes a CLI tool called ``s7`` for interacting with Siemens S7 PLCs
from the terminal. Install the CLI dependencies with::

pip install python-snap7[cli]

All subcommands are available via ``s7 <subcommand>``. Use ``s7 --help`` to see
available commands, or ``s7 <subcommand> --help`` for detailed usage.

Common Options
--------------

.. option:: -v, --verbose

Enable debug logging output.

.. option:: --version

Show the python-snap7 version and exit.

server
------

Start an emulated S7 PLC server with default values::

s7 server
s7 server --port 1102

.. option:: -p, --port PORT

Port the server will listen on (default: 1102).

read
----

Read data from a PLC data block::

# Read 16 raw bytes from DB1 at offset 0
s7 read 192.168.1.10 --db 1 --offset 0 --size 16

# Read a typed value
s7 read 192.168.1.10 --db 1 --offset 0 --type int
s7 read 192.168.1.10 --db 1 --offset 4 --type real

# Read a boolean (bit 3 of byte at offset 0)
s7 read 192.168.1.10 --db 1 --offset 0 --type bool --bit 3

.. option:: --db DB

DB number to read from (required).

.. option:: --offset OFFSET

Byte offset to start reading (required).

.. option:: --size SIZE

Number of bytes to read (required for ``--type bytes``).

.. option:: --type TYPE

Data type to read. Choices: ``bool``, ``byte``, ``int``, ``uint``, ``word``,
``dint``, ``udint``, ``dword``, ``real``, ``lreal``, ``string``, ``bytes``
(default: ``bytes``).

.. option:: --bit BIT

Bit offset within the byte (only for ``bool`` type, default: 0).

.. option:: --rack RACK

PLC rack number (default: 0).

.. option:: --slot SLOT

PLC slot number (default: 1).

.. option:: --port PORT

PLC TCP port (default: 102).

write
-----

Write data to a PLC data block::

# Write raw bytes (hex)
s7 write 192.168.1.10 --db 1 --offset 0 --type bytes --value "01 02 03 04"

# Write a typed value
s7 write 192.168.1.10 --db 1 --offset 0 --type int --value 42
s7 write 192.168.1.10 --db 1 --offset 4 --type real --value 3.14

# Write a boolean
s7 write 192.168.1.10 --db 1 --offset 0 --type bool --bit 3 --value true

.. option:: --db DB

DB number to write to (required).

.. option:: --offset OFFSET

Byte offset to start writing (required).

.. option:: --type TYPE

Data type to write (required). Same choices as ``read``.

.. option:: --value VALUE

Value to write (required). For ``bytes`` type, provide hex (e.g. ``"01 02 FF"``).
For ``bool``, use ``true``/``false``/``1``/``0``.

.. option:: --bit, --rack, --slot, --port

Same as ``read``.

dump
----

Dump the contents of a data block as a hex dump::

s7 dump 192.168.1.10 --db 1
s7 dump 192.168.1.10 --db 1 --size 512 --format hex

.. option:: --db DB

DB number to dump (required).

.. option:: --size SIZE

Number of bytes to dump (default: 256).

.. option:: --format FORMAT

Output format: ``hex`` (default) or ``bytes`` (raw hex string).

.. option:: --rack, --slot, --port

Same as ``read``.

info
----

Get PLC information including CPU info, state, order code, protection level,
and block counts::

s7 info 192.168.1.10
s7 info 192.168.1.10 --rack 0 --slot 2

.. option:: --rack, --slot, --port

Same as ``read``.

discover
--------

Discover PROFINET devices on the local network using DCP (Discovery and basic
Configuration Protocol). Requires the ``discovery`` extra::

pip install python-snap7[discovery]

Usage::

# Discover all devices (IP is the local network interface to use)
s7 discover 192.168.1.1
s7 discover 192.168.1.1 --timeout 10

.. option:: --timeout SECONDS

How long to listen for responses (default: 5.0).

.. note::

Network discovery uses raw sockets and may require elevated privileges
(root/administrator) depending on your platform.
2 changes: 2 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Welcome to python-snap7's documentation!
reading-writing
multi-variable
server
cli
tia-portal-config

.. toctree::
Expand Down Expand Up @@ -49,6 +50,7 @@ Welcome to python-snap7's documentation!
API/connection
API/s7protocol
API/datatypes
API/discovery


Indices and tables
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-html", "hypothesis", "
s7commplus = ["cryptography"]
cli = ["rich", "click" ]
doc = ["sphinx", "sphinx_rtd_theme"]
discovery = ["pnio-dcp"]

[tool.setuptools.package-data]
snap7 = ["py.typed"]
Expand Down
137 changes: 137 additions & 0 deletions snap7/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
PROFINET DCP network discovery for finding Siemens PLCs.

Uses the pnio-dcp library for the underlying DCP protocol.
Install with: pip install python-snap7[discovery]
"""

from __future__ import annotations

import dataclasses
import logging

logger = logging.getLogger(__name__)


@dataclasses.dataclass(frozen=True)
class Device:
"""A discovered PROFINET device on the network."""

name: str
ip: str
mac: str
netmask: str = ""
gateway: str = ""
family: str = ""

def __str__(self) -> str:
return f"{self.name} ({self.ip}) [{self.mac}]"


def discover(ip: str, timeout: float = 5.0) -> list[Device]:
"""Discover PROFINET devices on the network using DCP Identify All.

Args:
ip: IP address of the local network interface to use for discovery.
timeout: How long to listen for responses in seconds (default 5.0).

Returns:
List of discovered devices.

Raises:
ImportError: If pnio-dcp is not installed.
NotImplementedError: If the current platform is not supported by pnio-dcp.
"""
try:
from pnio_dcp import DCP
except ImportError:
raise ImportError("pnio-dcp is required for network discovery. Install it with: pip install python-snap7[discovery]")

dcp = DCP(ip)
dcp.identify_all_timeout = int(timeout) if timeout >= 1 else 1

raw_devices = dcp.identify_all(timeout=int(timeout) if timeout >= 1 else 1)

devices = []
for raw in raw_devices:
device = Device(
name=raw.name_of_station,
ip=raw.IP,
mac=raw.MAC,
netmask=getattr(raw, "netmask", ""),
gateway=getattr(raw, "gateway", ""),
family=getattr(raw, "family", ""),
)
devices.append(device)
logger.debug(f"Discovered: {device}")

logger.info(f"Discovery complete: found {len(devices)} device(s)")
return devices


def identify(ip: str, mac: str) -> Device:
"""Identify a specific device by MAC address.

Args:
ip: IP address of the local network interface to use.
mac: MAC address of the target device (colon-separated, e.g. "00:1b:1b:12:34:56").

Returns:
The identified device.

Raises:
ImportError: If pnio-dcp is not installed.
TimeoutError: If the device does not respond.
"""
try:
from pnio_dcp import DCP, DcpTimeoutError
except ImportError:
raise ImportError("pnio-dcp is required for network discovery. Install it with: pip install python-snap7[discovery]")

dcp = DCP(ip)
try:
raw = dcp.identify(mac)
except DcpTimeoutError:
raise TimeoutError(f"No response from device {mac}")

return Device(
name=raw.name_of_station,
ip=raw.IP,
mac=raw.MAC,
netmask=getattr(raw, "netmask", ""),
gateway=getattr(raw, "gateway", ""),
family=getattr(raw, "family", ""),
)


try:
import click

@click.command()
@click.argument("ip")
@click.option("--timeout", type=float, default=5.0, help="Discovery timeout in seconds.")
def discover_command(ip: str, timeout: float) -> None:
"""Discover PROFINET devices on the network.

IP is the address of the local network interface to use for discovery.
Requires pnio-dcp: pip install python-snap7[discovery]
"""
try:
devices = discover(ip, timeout)
except ImportError as e:
click.echo(str(e), err=True)
raise SystemExit(1)
except NotImplementedError as e:
click.echo(f"Platform not supported: {e}", err=True)
raise SystemExit(1)

if not devices:
click.echo("No devices found.")
return

click.echo(f"Found {len(devices)} device(s):\n")
for device in devices:
click.echo(f" {device.name:<30s} {device.ip:<16s} {device.mac}")

except ImportError:
pass
Loading
Loading