profinet-py 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- profinet/__init__.py +489 -0
- profinet/alarm_listener.py +455 -0
- profinet/alarms.py +522 -0
- profinet/blocks.py +1111 -0
- profinet/cli.py +328 -0
- profinet/cyclic.py +535 -0
- profinet/dcp.py +1510 -0
- profinet/device.py +1044 -0
- profinet/diagnosis.py +635 -0
- profinet/exceptions.py +505 -0
- profinet/indices.py +778 -0
- profinet/protocol.py +665 -0
- profinet/py.typed +2 -0
- profinet/rpc.py +2633 -0
- profinet/rt.py +446 -0
- profinet/util.py +1333 -0
- profinet/vendors.py +2220 -0
- profinet_py-0.4.0.dist-info/METADATA +131 -0
- profinet_py-0.4.0.dist-info/RECORD +23 -0
- profinet_py-0.4.0.dist-info/WHEEL +5 -0
- profinet_py-0.4.0.dist-info/entry_points.txt +2 -0
- profinet_py-0.4.0.dist-info/licenses/LICENSE +674 -0
- profinet_py-0.4.0.dist-info/top_level.txt +1 -0
profinet/cli.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROFINET command-line interface.
|
|
3
|
+
|
|
4
|
+
Provides CLI commands for device discovery and interaction.
|
|
5
|
+
|
|
6
|
+
Credits:
|
|
7
|
+
Original implementation by Alfred Krohmer (2015)
|
|
8
|
+
https://github.com/alfredkrohmer/profinet
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from . import dcp, rpc
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
DCPDeviceNotFoundError,
|
|
22
|
+
PermissionDeniedError,
|
|
23
|
+
ProfinetError,
|
|
24
|
+
RPCError,
|
|
25
|
+
)
|
|
26
|
+
from .protocol import PNInM0, PNInM1
|
|
27
|
+
from .util import ethernet_socket, get_mac
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def setup_logging(verbose: bool = False, debug: bool = False) -> None:
|
|
33
|
+
"""Configure logging based on verbosity flags."""
|
|
34
|
+
if debug:
|
|
35
|
+
level = logging.DEBUG
|
|
36
|
+
elif verbose:
|
|
37
|
+
level = logging.INFO
|
|
38
|
+
else:
|
|
39
|
+
level = logging.WARNING
|
|
40
|
+
|
|
41
|
+
logging.basicConfig(
|
|
42
|
+
level=level,
|
|
43
|
+
format="%(levelname)s: %(message)s",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_discover(args: argparse.Namespace) -> int:
|
|
48
|
+
"""Execute discover command."""
|
|
49
|
+
sock = ethernet_socket(args.interface, 3)
|
|
50
|
+
try:
|
|
51
|
+
src = get_mac(args.interface)
|
|
52
|
+
|
|
53
|
+
print(f"Discovering PROFINET devices on {args.interface}...")
|
|
54
|
+
dcp.send_discover(sock, src)
|
|
55
|
+
responses = dcp.read_response(sock, src, timeout_sec=args.timeout, debug=args.verbose)
|
|
56
|
+
|
|
57
|
+
if not responses:
|
|
58
|
+
print("No devices found")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
print(f"\nFound {len(responses)} device(s):\n")
|
|
62
|
+
for mac, blocks in responses.items():
|
|
63
|
+
desc = dcp.DCPDeviceDescription(mac, blocks)
|
|
64
|
+
print(f" {desc.name}")
|
|
65
|
+
print(f" MAC: {desc.mac}")
|
|
66
|
+
print(f" IP: {desc.ip}")
|
|
67
|
+
print(f" Vendor: {desc.vendor_name} (0x{desc.vendor_id:04X})")
|
|
68
|
+
print(f" Device: 0x{desc.device_id:04X}")
|
|
69
|
+
print()
|
|
70
|
+
|
|
71
|
+
return 0
|
|
72
|
+
finally:
|
|
73
|
+
sock.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_get_param(args: argparse.Namespace) -> int:
|
|
77
|
+
"""Execute get-param command."""
|
|
78
|
+
sock = ethernet_socket(args.interface, 3)
|
|
79
|
+
try:
|
|
80
|
+
src = get_mac(args.interface)
|
|
81
|
+
|
|
82
|
+
result = dcp.get_param(sock, src, args.target, args.param)
|
|
83
|
+
if result:
|
|
84
|
+
if args.param == "name":
|
|
85
|
+
print(result.decode("utf-8", errors="replace"))
|
|
86
|
+
elif args.param == "ip":
|
|
87
|
+
from .util import s2ip
|
|
88
|
+
print(s2ip(result[:4]))
|
|
89
|
+
else:
|
|
90
|
+
print(result.hex())
|
|
91
|
+
else:
|
|
92
|
+
print(f"Could not read parameter '{args.param}'")
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
finally:
|
|
97
|
+
sock.close()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cmd_set_param(args: argparse.Namespace) -> int:
|
|
101
|
+
"""Execute set-param command."""
|
|
102
|
+
sock = ethernet_socket(args.interface, 3)
|
|
103
|
+
try:
|
|
104
|
+
src = get_mac(args.interface)
|
|
105
|
+
|
|
106
|
+
success = dcp.set_param(sock, src, args.target, args.param, args.value)
|
|
107
|
+
if success:
|
|
108
|
+
print(f"Set {args.param} = {args.value}")
|
|
109
|
+
return 0
|
|
110
|
+
else:
|
|
111
|
+
print(f"Failed to set {args.param}")
|
|
112
|
+
return 1
|
|
113
|
+
finally:
|
|
114
|
+
sock.close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_read(args: argparse.Namespace) -> int:
|
|
118
|
+
"""Execute read command."""
|
|
119
|
+
sock = ethernet_socket(args.interface, 3)
|
|
120
|
+
try:
|
|
121
|
+
src = get_mac(args.interface)
|
|
122
|
+
|
|
123
|
+
print(f"Connecting to {args.target}...")
|
|
124
|
+
info = rpc.get_station_info(sock, src, args.target)
|
|
125
|
+
|
|
126
|
+
with rpc.RPCCon(info) as conn:
|
|
127
|
+
conn.connect(src)
|
|
128
|
+
|
|
129
|
+
idx = int(args.index, 16) if args.index.startswith("0x") else int(args.index)
|
|
130
|
+
iod = conn.read(api=args.api, slot=args.slot, subslot=args.subslot, idx=idx)
|
|
131
|
+
|
|
132
|
+
print(f"Read {len(iod.payload)} bytes:")
|
|
133
|
+
print(iod.payload.hex())
|
|
134
|
+
|
|
135
|
+
return 0
|
|
136
|
+
finally:
|
|
137
|
+
sock.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_read_inm0_filter(args: argparse.Namespace) -> int:
|
|
141
|
+
"""Execute read-inm0-filter command."""
|
|
142
|
+
sock = ethernet_socket(args.interface, 3)
|
|
143
|
+
try:
|
|
144
|
+
src = get_mac(args.interface)
|
|
145
|
+
|
|
146
|
+
print(f"Connecting to {args.target}...")
|
|
147
|
+
info = rpc.get_station_info(sock, src, args.target)
|
|
148
|
+
|
|
149
|
+
with rpc.RPCCon(info) as conn:
|
|
150
|
+
conn.connect(src)
|
|
151
|
+
data = conn.read_inm0filter()
|
|
152
|
+
|
|
153
|
+
print("\nDevice Topology:")
|
|
154
|
+
for api in data.keys():
|
|
155
|
+
print(f"\nAPI {api}:")
|
|
156
|
+
for slot_number, (module_id, subslots) in data[api].items():
|
|
157
|
+
print(f" Slot {slot_number}: Module 0x{module_id:04X}")
|
|
158
|
+
for subslot_number, submodule_id in subslots.items():
|
|
159
|
+
print(f" Subslot {subslot_number}: Submodule 0x{submodule_id:04X}")
|
|
160
|
+
|
|
161
|
+
return 0
|
|
162
|
+
finally:
|
|
163
|
+
sock.close()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_read_inm0(args: argparse.Namespace) -> int:
|
|
167
|
+
"""Execute read-inm0 command."""
|
|
168
|
+
sock = ethernet_socket(args.interface, 3)
|
|
169
|
+
try:
|
|
170
|
+
src = get_mac(args.interface)
|
|
171
|
+
|
|
172
|
+
print(f"Connecting to {args.target}...")
|
|
173
|
+
info = rpc.get_station_info(sock, src, args.target)
|
|
174
|
+
|
|
175
|
+
with rpc.RPCCon(info) as conn:
|
|
176
|
+
conn.connect(src)
|
|
177
|
+
iod = conn.read(api=args.api, slot=args.slot, subslot=args.subslot, idx=PNInM0.IDX)
|
|
178
|
+
|
|
179
|
+
if iod.payload:
|
|
180
|
+
im0 = PNInM0(iod.payload)
|
|
181
|
+
print(im0)
|
|
182
|
+
else:
|
|
183
|
+
print("No IM0 data available")
|
|
184
|
+
|
|
185
|
+
return 0
|
|
186
|
+
finally:
|
|
187
|
+
sock.close()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_read_inm1(args: argparse.Namespace) -> int:
|
|
191
|
+
"""Execute read-inm1 command."""
|
|
192
|
+
sock = ethernet_socket(args.interface, 3)
|
|
193
|
+
try:
|
|
194
|
+
src = get_mac(args.interface)
|
|
195
|
+
|
|
196
|
+
print(f"Connecting to {args.target}...")
|
|
197
|
+
info = rpc.get_station_info(sock, src, args.target)
|
|
198
|
+
|
|
199
|
+
with rpc.RPCCon(info) as conn:
|
|
200
|
+
conn.connect(src)
|
|
201
|
+
iod = conn.read(api=args.api, slot=args.slot, subslot=args.subslot, idx=PNInM1.IDX)
|
|
202
|
+
|
|
203
|
+
if iod.payload:
|
|
204
|
+
im1 = PNInM1(iod.payload)
|
|
205
|
+
print(im1)
|
|
206
|
+
else:
|
|
207
|
+
print("No IM1 data available")
|
|
208
|
+
|
|
209
|
+
return 0
|
|
210
|
+
finally:
|
|
211
|
+
sock.close()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
215
|
+
"""Create argument parser."""
|
|
216
|
+
parser = argparse.ArgumentParser(
|
|
217
|
+
prog="profinet",
|
|
218
|
+
description="PROFINET IO-Controller CLI",
|
|
219
|
+
epilog="Credits: Original implementation by Alfred Krohmer (2015)",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"-i", "--interface",
|
|
224
|
+
required=True,
|
|
225
|
+
metavar="IFACE",
|
|
226
|
+
help="Network interface to use",
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"-v", "--verbose",
|
|
230
|
+
action="store_true",
|
|
231
|
+
help="Enable verbose output",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--debug",
|
|
235
|
+
action="store_true",
|
|
236
|
+
help="Enable debug output",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"-t", "--timeout",
|
|
240
|
+
type=int,
|
|
241
|
+
default=10,
|
|
242
|
+
help="Discovery timeout in seconds (default: 10)",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
246
|
+
|
|
247
|
+
# discover
|
|
248
|
+
sub = subparsers.add_parser("discover", help="Discover PROFINET devices")
|
|
249
|
+
sub.set_defaults(func=cmd_discover)
|
|
250
|
+
|
|
251
|
+
# get-param
|
|
252
|
+
sub = subparsers.add_parser("get-param", help="Read device parameter")
|
|
253
|
+
sub.add_argument("target", help="Target MAC address")
|
|
254
|
+
sub.add_argument("param", choices=["name", "ip"], help="Parameter to read")
|
|
255
|
+
sub.set_defaults(func=cmd_get_param)
|
|
256
|
+
|
|
257
|
+
# set-param
|
|
258
|
+
sub = subparsers.add_parser("set-param", help="Write device parameter")
|
|
259
|
+
sub.add_argument("target", help="Target MAC address")
|
|
260
|
+
sub.add_argument("param", choices=["name", "ip"], help="Parameter to write")
|
|
261
|
+
sub.add_argument("value", help="New value")
|
|
262
|
+
sub.set_defaults(func=cmd_set_param)
|
|
263
|
+
|
|
264
|
+
# read
|
|
265
|
+
sub = subparsers.add_parser("read", help="Read data record")
|
|
266
|
+
sub.add_argument("target", help="Target station name")
|
|
267
|
+
sub.add_argument("--api", type=int, default=0, help="API (default: 0)")
|
|
268
|
+
sub.add_argument("--slot", type=int, required=True, help="Slot number")
|
|
269
|
+
sub.add_argument("--subslot", type=int, required=True, help="Subslot number")
|
|
270
|
+
sub.add_argument("--index", required=True, help="Record index (hex with 0x prefix)")
|
|
271
|
+
sub.set_defaults(func=cmd_read)
|
|
272
|
+
|
|
273
|
+
# read-inm0-filter
|
|
274
|
+
sub = subparsers.add_parser("read-inm0-filter", help="Read device topology")
|
|
275
|
+
sub.add_argument("target", help="Target station name")
|
|
276
|
+
sub.set_defaults(func=cmd_read_inm0_filter)
|
|
277
|
+
|
|
278
|
+
# read-inm0
|
|
279
|
+
sub = subparsers.add_parser("read-inm0", help="Read IM0 identification data")
|
|
280
|
+
sub.add_argument("target", help="Target station name")
|
|
281
|
+
sub.add_argument("--api", type=int, default=0, help="API (default: 0)")
|
|
282
|
+
sub.add_argument("--slot", type=int, default=0, help="Slot number (default: 0)")
|
|
283
|
+
sub.add_argument("--subslot", type=int, default=1, help="Subslot number (default: 1)")
|
|
284
|
+
sub.set_defaults(func=cmd_read_inm0)
|
|
285
|
+
|
|
286
|
+
# read-inm1
|
|
287
|
+
sub = subparsers.add_parser("read-inm1", help="Read IM1 tag data")
|
|
288
|
+
sub.add_argument("target", help="Target station name")
|
|
289
|
+
sub.add_argument("--api", type=int, default=0, help="API (default: 0)")
|
|
290
|
+
sub.add_argument("--slot", type=int, default=0, help="Slot number (default: 0)")
|
|
291
|
+
sub.add_argument("--subslot", type=int, default=1, help="Subslot number (default: 1)")
|
|
292
|
+
sub.set_defaults(func=cmd_read_inm1)
|
|
293
|
+
|
|
294
|
+
return parser
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
298
|
+
"""CLI entry point."""
|
|
299
|
+
parser = create_parser()
|
|
300
|
+
args = parser.parse_args(argv)
|
|
301
|
+
|
|
302
|
+
setup_logging(args.verbose, args.debug)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
return args.func(args)
|
|
306
|
+
except PermissionDeniedError:
|
|
307
|
+
print("Error: Root privileges required for raw socket access", file=sys.stderr)
|
|
308
|
+
return 1
|
|
309
|
+
except DCPDeviceNotFoundError as e:
|
|
310
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
311
|
+
return 1
|
|
312
|
+
except RPCError as e:
|
|
313
|
+
print(f"RPC Error: {e}", file=sys.stderr)
|
|
314
|
+
return 1
|
|
315
|
+
except ProfinetError as e:
|
|
316
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
317
|
+
return 1
|
|
318
|
+
except KeyboardInterrupt:
|
|
319
|
+
print("\nInterrupted")
|
|
320
|
+
return 130
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.exception("Unexpected error")
|
|
323
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
sys.exit(main())
|