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/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())