aioads 0.1.0.dev4__tar.gz → 0.1.0.dev6__tar.gz
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.
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/.gitignore +3 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/.vscode/settings.json +10 -1
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/PKG-INFO +10 -12
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/README.md +9 -11
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ads_client.py +37 -52
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ams_address.py +1 -1
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ams_header.py +1 -1
- aioads-0.1.0.dev6/aioads/ams_service_port.py +83 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ams_tcp_header.py +1 -1
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_enable_route.py +17 -12
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_sum_read.py +21 -16
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_sum_read_write.py +17 -15
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_datatype_by_name.py +38 -25
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_datatype_upload.py +4 -1
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_info_by_name_ex.py +60 -34
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_table_version.py +6 -5
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_upload.py +10 -4
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_symbol_upload_info.py +10 -8
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/stream.py +19 -11
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_cmd_reuse_mqtt.py +6 -9
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_cycles.py +3 -4
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_cycles_mqtt.py +5 -6
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_multiple.py +9 -7
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_multiple_mqtt.py +11 -8
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/examples/read_single.py +2 -3
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/pyproject.toml +1 -1
- aioads-0.1.0.dev6/tests/integration/README.md +36 -0
- aioads-0.1.0.dev6/tests/integration/base.py +27 -0
- aioads-0.1.0.dev6/tests/integration/config.example.toml +37 -0
- aioads-0.1.0.dev6/tests/integration/config.py +58 -0
- aioads-0.1.0.dev6/tests/integration/test_connection.py +20 -0
- aioads-0.1.0.dev6/tests/integration/test_performance.py +49 -0
- aioads-0.1.0.dev6/tests/integration/test_read_symbols.py +32 -0
- aioads-0.1.0.dev6/tests/unit/functions/__init__.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_enable_route.py +12 -3
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_stream.py +34 -0
- aioads-0.1.0.dev6/tests/unit/utils/__init__.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/.github/dependabot.yml +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/.github/workflows/ci.yml +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/.github/workflows/publish.yml +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/LICENSE +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ads_error_codes.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ads_notifications.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ads_symbol_cache.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/ads_symbol_parser.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_add_notification.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_command.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_delete_notification.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_read.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_read_device_info.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_read_state.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_read_write.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_write.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/ads_write_state.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/commands/errors.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/errors.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/functions/ads_function.py +0 -0
- /aioads-0.1.0.dev4/tests/__init__.py → /aioads-0.1.0.dev6/aioads/py.typed +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/transport.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/aioads/utils/local_ip.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/docs/transmission_mode.md +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/docs/unittest_style_guide.html +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/pdm.lock +0 -0
- {aioads-0.1.0.dev4/tests/unit → aioads-0.1.0.dev6/tests}/__init__.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/builders.py +0 -0
- {aioads-0.1.0.dev4/tests/unit/commands → aioads-0.1.0.dev6/tests/integration}/__init__.py +0 -0
- {aioads-0.1.0.dev4/tests/unit/functions → aioads-0.1.0.dev6/tests/unit}/__init__.py +0 -0
- {aioads-0.1.0.dev4/tests/unit/utils → aioads-0.1.0.dev6/tests/unit/commands}/__init__.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_add_notification.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_command.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_delete_notification.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_read.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_read_device_info.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_read_state.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_read_write.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_write.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_ads_write_state.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/commands/test_errors.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_function.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_sum_read.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_sum_read_write.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_datatype_by_name.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_datatype_upload.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_info_by_name_ex.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_table_version.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_upload.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/functions/test_ads_symbol_upload_info.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ads_client.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ads_error_codes.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ads_notifications.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ads_symbol_cache.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ads_symbol_parser.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ams_address.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ams_header.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_ams_tcp_header.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_errors.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/test_transport.py +0 -0
- {aioads-0.1.0.dev4 → aioads-0.1.0.dev6}/tests/unit/utils/test_local_ip.py +0 -0
|
@@ -160,3 +160,6 @@ cython_debug/
|
|
|
160
160
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
161
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
162
|
#.idea/
|
|
163
|
+
|
|
164
|
+
# Local integration test endpoint configuration
|
|
165
|
+
tests/integration/config.toml
|
|
@@ -10,5 +10,14 @@
|
|
|
10
10
|
},
|
|
11
11
|
"terminal.integrated.env.windows": {
|
|
12
12
|
"PYTHONPATH": "${workspaceFolder}"
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
"python.testing.unittestArgs": [
|
|
15
|
+
"-v",
|
|
16
|
+
"-s",
|
|
17
|
+
"./tests",
|
|
18
|
+
"-p",
|
|
19
|
+
"test*.py"
|
|
20
|
+
],
|
|
21
|
+
"python.testing.pytestEnabled": false,
|
|
22
|
+
"python.testing.unittestEnabled": true
|
|
14
23
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aioads
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev6
|
|
4
4
|
Summary: An asynchronous Python library for communicating with Beckhoff TwinCAT PLCs
|
|
5
5
|
Author-email: MkKiefer <102972583+MkKiefer@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -49,33 +49,34 @@ pip install aioads[aiomqtt]
|
|
|
49
49
|
```python
|
|
50
50
|
import asyncio
|
|
51
51
|
from aioads import AdsClient, AmsAddress
|
|
52
|
+
from aioads.ams_service_port import AmsServicePort
|
|
52
53
|
|
|
53
54
|
async def main():
|
|
54
55
|
# Create client
|
|
55
56
|
client = AdsClient.create_tcp(
|
|
56
57
|
src=AmsAddress(net_id="192.168.1.100.1.1", port=1234),
|
|
57
|
-
dst=AmsAddress(net_id="192.168.1.200.1.1", port=
|
|
58
|
+
dst=AmsAddress(net_id="192.168.1.200.1.1", port=AmsServicePort.TC3_RUNTIME_1),
|
|
58
59
|
ip="192.168.1.200",
|
|
59
60
|
port=48898,
|
|
60
61
|
)
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
try:
|
|
63
64
|
# Connect to PLC
|
|
64
65
|
await client.connect()
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
# Read device state
|
|
67
68
|
state = await client.read_state()
|
|
68
69
|
print(f"PLC State: {state.ads_state.name}")
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
# Read single symbol
|
|
71
72
|
value = await client.read_symbol_by_name("MAIN.MyVariable")
|
|
72
73
|
print(f"Value: {value}")
|
|
73
|
-
|
|
74
|
+
|
|
74
75
|
# Read multiple symbols efficiently
|
|
75
76
|
symbols = ["MAIN.Var1", "MAIN.Var2", "MAIN.Var3"]
|
|
76
77
|
values = await client.read_symbols_by_names(symbols)
|
|
77
78
|
print(f"Values: {values}")
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
finally:
|
|
80
81
|
await client.disconnect()
|
|
81
82
|
|
|
@@ -121,7 +122,7 @@ Efficiently read multiple symbols in a single operation:
|
|
|
121
122
|
# Prepare symbol list
|
|
122
123
|
symbols_to_read = {
|
|
123
124
|
"MAIN.Temperature",
|
|
124
|
-
"MAIN.Pressure",
|
|
125
|
+
"MAIN.Pressure",
|
|
125
126
|
"MAIN.FlowRate",
|
|
126
127
|
"MAIN.Status.Running",
|
|
127
128
|
"MAIN.Recipe.CurrentStep"
|
|
@@ -191,7 +192,7 @@ async def read_diagnostics(client, diag_symbols):
|
|
|
191
192
|
async def main():
|
|
192
193
|
client = AdsClient.create_tcp(...)
|
|
193
194
|
await client.connect()
|
|
194
|
-
|
|
195
|
+
|
|
195
196
|
try:
|
|
196
197
|
# Run multiple concurrent tasks
|
|
197
198
|
async with asyncio.TaskGroup() as tg:
|
|
@@ -223,14 +224,12 @@ Full support for TwinCAT data types:
|
|
|
223
224
|
- **Complex Types**: Custom user-defined types
|
|
224
225
|
- **Arrays**: Multi-dimensional arrays with proper indexing
|
|
225
226
|
|
|
226
|
-
|
|
227
227
|
## Requirements
|
|
228
228
|
|
|
229
229
|
- Python 3.11+
|
|
230
230
|
- asyncio
|
|
231
231
|
- aiomqtt (optional)
|
|
232
232
|
|
|
233
|
-
|
|
234
233
|
## Disclaimer
|
|
235
234
|
|
|
236
235
|
This project is an independent, open‑source implementation of the ADS (Automation Device Specification) protocol. It is not affiliated with, endorsed by, or supported by Beckhoff Automation GmbH & Co. KG, the developer of TwinCAT and the ADS protocol.
|
|
@@ -252,7 +251,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|
|
252
251
|
|
|
253
252
|
1. **PLC State**: Ensure PLC is in RUN mode for symbol access
|
|
254
253
|
|
|
255
|
-
|
|
256
254
|
## Contributing guidelines
|
|
257
255
|
|
|
258
256
|
AIOADS is a personal hobby project, developed and maintained in my spare time.
|
|
@@ -35,33 +35,34 @@ pip install aioads[aiomqtt]
|
|
|
35
35
|
```python
|
|
36
36
|
import asyncio
|
|
37
37
|
from aioads import AdsClient, AmsAddress
|
|
38
|
+
from aioads.ams_service_port import AmsServicePort
|
|
38
39
|
|
|
39
40
|
async def main():
|
|
40
41
|
# Create client
|
|
41
42
|
client = AdsClient.create_tcp(
|
|
42
43
|
src=AmsAddress(net_id="192.168.1.100.1.1", port=1234),
|
|
43
|
-
dst=AmsAddress(net_id="192.168.1.200.1.1", port=
|
|
44
|
+
dst=AmsAddress(net_id="192.168.1.200.1.1", port=AmsServicePort.TC3_RUNTIME_1),
|
|
44
45
|
ip="192.168.1.200",
|
|
45
46
|
port=48898,
|
|
46
47
|
)
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
try:
|
|
49
50
|
# Connect to PLC
|
|
50
51
|
await client.connect()
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
# Read device state
|
|
53
54
|
state = await client.read_state()
|
|
54
55
|
print(f"PLC State: {state.ads_state.name}")
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
# Read single symbol
|
|
57
58
|
value = await client.read_symbol_by_name("MAIN.MyVariable")
|
|
58
59
|
print(f"Value: {value}")
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
# Read multiple symbols efficiently
|
|
61
62
|
symbols = ["MAIN.Var1", "MAIN.Var2", "MAIN.Var3"]
|
|
62
63
|
values = await client.read_symbols_by_names(symbols)
|
|
63
64
|
print(f"Values: {values}")
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
finally:
|
|
66
67
|
await client.disconnect()
|
|
67
68
|
|
|
@@ -107,7 +108,7 @@ Efficiently read multiple symbols in a single operation:
|
|
|
107
108
|
# Prepare symbol list
|
|
108
109
|
symbols_to_read = {
|
|
109
110
|
"MAIN.Temperature",
|
|
110
|
-
"MAIN.Pressure",
|
|
111
|
+
"MAIN.Pressure",
|
|
111
112
|
"MAIN.FlowRate",
|
|
112
113
|
"MAIN.Status.Running",
|
|
113
114
|
"MAIN.Recipe.CurrentStep"
|
|
@@ -177,7 +178,7 @@ async def read_diagnostics(client, diag_symbols):
|
|
|
177
178
|
async def main():
|
|
178
179
|
client = AdsClient.create_tcp(...)
|
|
179
180
|
await client.connect()
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
try:
|
|
182
183
|
# Run multiple concurrent tasks
|
|
183
184
|
async with asyncio.TaskGroup() as tg:
|
|
@@ -209,14 +210,12 @@ Full support for TwinCAT data types:
|
|
|
209
210
|
- **Complex Types**: Custom user-defined types
|
|
210
211
|
- **Arrays**: Multi-dimensional arrays with proper indexing
|
|
211
212
|
|
|
212
|
-
|
|
213
213
|
## Requirements
|
|
214
214
|
|
|
215
215
|
- Python 3.11+
|
|
216
216
|
- asyncio
|
|
217
217
|
- aiomqtt (optional)
|
|
218
218
|
|
|
219
|
-
|
|
220
219
|
## Disclaimer
|
|
221
220
|
|
|
222
221
|
This project is an independent, open‑source implementation of the ADS (Automation Device Specification) protocol. It is not affiliated with, endorsed by, or supported by Beckhoff Automation GmbH & Co. KG, the developer of TwinCAT and the ADS protocol.
|
|
@@ -238,7 +237,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|
|
238
237
|
|
|
239
238
|
1. **PLC State**: Ensure PLC is in RUN mode for symbol access
|
|
240
239
|
|
|
241
|
-
|
|
242
240
|
## Contributing guidelines
|
|
243
241
|
|
|
244
242
|
AIOADS is a personal hobby project, developed and maintained in my spare time.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""ADS Client for communicating with ADS devices asynchronously."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import logging
|
|
4
|
-
import os
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
6
4
|
from contextlib import asynccontextmanager
|
|
7
5
|
from dataclasses import dataclass
|
|
8
6
|
from typing import Any
|
|
@@ -59,9 +57,6 @@ class AdsClient:
|
|
|
59
57
|
self.parser = parser
|
|
60
58
|
self._cache = cache
|
|
61
59
|
self._notification = notification
|
|
62
|
-
self.parser_pool = ThreadPoolExecutor(
|
|
63
|
-
max_workers=(os.cpu_count() or 1) * 2
|
|
64
|
-
)
|
|
65
60
|
|
|
66
61
|
@classmethod
|
|
67
62
|
def create_tcp(
|
|
@@ -93,7 +88,9 @@ class AdsClient:
|
|
|
93
88
|
)
|
|
94
89
|
|
|
95
90
|
@classmethod
|
|
96
|
-
def create_from_transport(
|
|
91
|
+
def create_from_transport(
|
|
92
|
+
cls, dst: AmsAddress, transport: ITransport
|
|
93
|
+
) -> "AdsClient":
|
|
97
94
|
"""
|
|
98
95
|
Create a new ADS client with an existing transport instance.
|
|
99
96
|
:param src: The source AMS address
|
|
@@ -151,7 +148,7 @@ class AdsClient:
|
|
|
151
148
|
|
|
152
149
|
async def enable_route(self, route_name: str, enabled: bool):
|
|
153
150
|
"""
|
|
154
|
-
Enable or disable a ads route.
|
|
151
|
+
Enable or disable a ads route.
|
|
155
152
|
Example route name for `ads over mqtt`: `MQTT:192.168.178.12.1.1:ads` (MQTT:<NetID>:<Topic>)
|
|
156
153
|
Example with defined name: `MQTT:MyBroker`
|
|
157
154
|
"""
|
|
@@ -160,7 +157,7 @@ class AdsClient:
|
|
|
160
157
|
self.transport,
|
|
161
158
|
self.dst_address,
|
|
162
159
|
route_name,
|
|
163
|
-
RouteSwitch.ROUTE_ENABLE_TMP if enabled else RouteSwitch.ROUTE_DISABLE_TMP
|
|
160
|
+
RouteSwitch.ROUTE_ENABLE_TMP if enabled else RouteSwitch.ROUTE_DISABLE_TMP,
|
|
164
161
|
)
|
|
165
162
|
response = await request.execute()
|
|
166
163
|
if not response.error_code.ok:
|
|
@@ -258,8 +255,7 @@ class AdsClient:
|
|
|
258
255
|
exceptions.append(AdsCommandError(error_code, symbol_path))
|
|
259
256
|
|
|
260
257
|
if exceptions:
|
|
261
|
-
raise ExceptionGroup(
|
|
262
|
-
"One or more symbol read errors occurred", exceptions)
|
|
258
|
+
raise ExceptionGroup("One or more symbol read errors occurred", exceptions)
|
|
263
259
|
|
|
264
260
|
async def read_symbols_by_names(
|
|
265
261
|
self, symbol_names: set[str], raise_errors: bool = True
|
|
@@ -274,64 +270,53 @@ class AdsClient:
|
|
|
274
270
|
if raise_errors:
|
|
275
271
|
self._raise_if_error(symbol_infos)
|
|
276
272
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
273
|
+
# Pre-seed the result with lookup errors so the output preserves the
|
|
274
|
+
# requested order; successful entries are overwritten after the read.
|
|
275
|
+
# Symbols with a failed lookup cannot be read: their idx_group/offset
|
|
276
|
+
# would misalign the bulk response.
|
|
277
|
+
output: dict[str, SymbolReadResult] = {
|
|
278
|
+
name: SymbolReadResult(error_code, None)
|
|
279
|
+
for name, (error_code, _) in symbol_infos.items()
|
|
280
|
+
}
|
|
281
|
+
readable = [
|
|
282
|
+
(name, info)
|
|
283
|
+
for name, (error_code, info) in symbol_infos.items()
|
|
284
|
+
if error_code.ok
|
|
285
|
+
]
|
|
289
286
|
if not readable:
|
|
290
287
|
return output
|
|
291
288
|
|
|
292
|
-
|
|
289
|
+
sum_read = AdsSumRead(
|
|
293
290
|
transport=self.transport,
|
|
294
291
|
ams_address=self.dst_address,
|
|
295
292
|
commands=[
|
|
296
293
|
AdsReadCommand(
|
|
297
294
|
transport=self.transport,
|
|
298
295
|
ams_address=self.dst_address,
|
|
299
|
-
idx_group=
|
|
300
|
-
idx_offset=
|
|
301
|
-
length=
|
|
296
|
+
idx_group=info.idx_group,
|
|
297
|
+
idx_offset=info.idx_offset,
|
|
298
|
+
length=info.idx_length,
|
|
302
299
|
)
|
|
303
|
-
for _,
|
|
300
|
+
for _, info in readable
|
|
304
301
|
],
|
|
305
302
|
)
|
|
306
|
-
response = await
|
|
303
|
+
response = await sum_read.execute()
|
|
307
304
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
tasks = []
|
|
311
|
-
for (requested_name, symbol_info), (read_response, resp_payload) in zip(
|
|
312
|
-
readable, response
|
|
305
|
+
for (name, info), (read_response, resp_payload) in zip(
|
|
306
|
+
readable, response, strict=True
|
|
313
307
|
):
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
output[requested_name] = SymbolReadResult(
|
|
317
|
-
read_response.error_code, None)
|
|
308
|
+
if not read_response.error_code.ok:
|
|
309
|
+
output[name] = SymbolReadResult(read_response.error_code, None)
|
|
318
310
|
continue
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
resp_payload,
|
|
327
|
-
)
|
|
311
|
+
output[name] = SymbolReadResult(
|
|
312
|
+
AdsErrorCode(0),
|
|
313
|
+
self.parser.parse(
|
|
314
|
+
data_type=info.data_type,
|
|
315
|
+
type_name=info.type_name,
|
|
316
|
+
raw_data=resp_payload,
|
|
317
|
+
),
|
|
328
318
|
)
|
|
329
319
|
|
|
330
|
-
parsed = await asyncio.gather(*tasks)
|
|
331
|
-
for requested_name, symbol_data in zip(parse_names, parsed):
|
|
332
|
-
output[requested_name] = SymbolReadResult(
|
|
333
|
-
AdsErrorCode(0), symbol_data)
|
|
334
|
-
|
|
335
320
|
return output
|
|
336
321
|
|
|
337
322
|
@asynccontextmanager
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""AMS service port constants used to address well-known endpoints on a target."""
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AmsServicePort(IntEnum):
|
|
7
|
+
"""Well-known AMS service port numbers used in AMS/ADS communication."""
|
|
8
|
+
|
|
9
|
+
# --- Core / Router ---
|
|
10
|
+
ADS_ROUTER = 1
|
|
11
|
+
AMS_DEBUGGER = 2
|
|
12
|
+
LICENSE_SERVER = 30
|
|
13
|
+
|
|
14
|
+
# --- Logging / Events ---
|
|
15
|
+
LOGGER = 100
|
|
16
|
+
EVENT_LOGGER = 110
|
|
17
|
+
EVENT_LOGGER_USER_V2 = 130
|
|
18
|
+
EVENT_LOGGER_RT_V2 = 131
|
|
19
|
+
EVENT_LOGGER_PUBLISHER_V2 = 132
|
|
20
|
+
|
|
21
|
+
# --- System / Core Services ---
|
|
22
|
+
SYSTEM_SERVICE = 10000
|
|
23
|
+
TCPIP_SERVER = 10201
|
|
24
|
+
SYSTEM_MANAGER = 10300
|
|
25
|
+
SMS_SERVER = 10400
|
|
26
|
+
MODBUS_SERVER = 10500
|
|
27
|
+
AMS_LOGGER = 10502
|
|
28
|
+
XML_DATA_SERVER = 10600
|
|
29
|
+
AUTO_CONFIGURATION = 10700
|
|
30
|
+
PLC_CONTROL = 10800
|
|
31
|
+
FTP_CLIENT = 10900
|
|
32
|
+
|
|
33
|
+
# --- NC / Motion / Automation ---
|
|
34
|
+
NC_CONTROL = 11000
|
|
35
|
+
NC_INTERPRETER = 11500
|
|
36
|
+
GST_INTERPRETER = 11600
|
|
37
|
+
TRACK_CONTROL = 12000
|
|
38
|
+
CAM_CONTROL = 13000
|
|
39
|
+
|
|
40
|
+
# --- Monitoring / Diagnostics ---
|
|
41
|
+
SCOPE_SERVER = 14000
|
|
42
|
+
CONDITION_MONITORING = 14100
|
|
43
|
+
|
|
44
|
+
# --- Communication / Integration ---
|
|
45
|
+
CONTROL_NET = 16000
|
|
46
|
+
OPC_SERVER = 17000
|
|
47
|
+
OPC_CLIENT = 17500
|
|
48
|
+
MAIL_SERVER = 18000
|
|
49
|
+
|
|
50
|
+
# --- Management / Infrastructure ---
|
|
51
|
+
MGMT_SERVER = 19100
|
|
52
|
+
HMI_SERVER = 19800
|
|
53
|
+
DATABASE_SERVER = 21372
|
|
54
|
+
|
|
55
|
+
# --- PLC Runtime (TwinCAT 2) ---
|
|
56
|
+
TC2_PLC = 800
|
|
57
|
+
TC2_RUNTIME_1 = 801
|
|
58
|
+
TC2_RUNTIME_2 = 811
|
|
59
|
+
TC2_RUNTIME_3 = 821
|
|
60
|
+
TC2_RUNTIME_4 = 831
|
|
61
|
+
|
|
62
|
+
# --- PLC Runtime (TwinCAT 3) ---
|
|
63
|
+
TC3_PLC_BASE = 850
|
|
64
|
+
TC3_RUNTIME_1 = 851
|
|
65
|
+
TC3_RUNTIME_2 = 852
|
|
66
|
+
TC3_RUNTIME_3 = 853
|
|
67
|
+
TC3_RUNTIME_4 = 854
|
|
68
|
+
|
|
69
|
+
# --- Real-time / Ring 0 ---
|
|
70
|
+
R0_REALTIME = 200
|
|
71
|
+
R0_TRACE = 290
|
|
72
|
+
R0_IO = 300
|
|
73
|
+
R0_PLC_LEGACY = 400
|
|
74
|
+
R0_NC = 500
|
|
75
|
+
R0_NC_SAF = 501
|
|
76
|
+
NC_INSTANCE = 520
|
|
77
|
+
R0_CNC = 600
|
|
78
|
+
R0_LINE = 700
|
|
79
|
+
|
|
80
|
+
# --- Misc / Optional services ---
|
|
81
|
+
CAM_CONTROLLER = 900
|
|
82
|
+
CAM_TOOL = 950
|
|
83
|
+
FTP = 10900 # alias
|
|
@@ -9,7 +9,7 @@ config:
|
|
|
9
9
|
rowHeight: 40
|
|
10
10
|
bitWidth: 80
|
|
11
11
|
bitsPerRow: 8
|
|
12
|
-
showBits:
|
|
12
|
+
showBits: true
|
|
13
13
|
---
|
|
14
14
|
packet
|
|
15
15
|
+4: "IndexGroup = 808 (4 bytes)"
|
|
@@ -25,7 +25,7 @@ IndexOffset values:
|
|
|
25
25
|
4 = ADS_ROUTE_ENABLE_TMP (temporary)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
Example of a route config `/3.1/Target/Routes/<route.xml>`
|
|
28
|
+
Example of a route config `/3.1/Target/Routes/<route.xml>`
|
|
29
29
|
```
|
|
30
30
|
<!-- Route configuration example -->
|
|
31
31
|
<Mqtt Disabled="true">
|
|
@@ -35,12 +35,13 @@ Example of a route config `/3.1/Target/Routes/<route.xml>`
|
|
|
35
35
|
</Mqtt>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Expected route name string: `MQTT:MyBroker`
|
|
38
|
+
Expected route name string: `MQTT:MyBroker`
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
from enum import IntEnum
|
|
42
42
|
|
|
43
43
|
from aioads.ams_address import AmsAddress
|
|
44
|
+
from aioads.ams_service_port import AmsServicePort
|
|
44
45
|
from aioads.commands.ads_write import AdsWriteCommand, AdsWriteResponse
|
|
45
46
|
from aioads.functions.ads_function import AdsFunctionSymbolGroup, IAdsFunction
|
|
46
47
|
from aioads.transport import ITransport
|
|
@@ -48,9 +49,10 @@ from aioads.transport import ITransport
|
|
|
48
49
|
|
|
49
50
|
class RouteSwitch(IntEnum):
|
|
50
51
|
"""
|
|
51
|
-
Enum (index offset) to control the state of a route.
|
|
52
|
-
A route can temporarily enabled / disabled and gets reset after a restart of the runtime.
|
|
52
|
+
Enum (index offset) to control the state of a route.
|
|
53
|
+
A route can temporarily enabled / disabled and gets reset after a restart of the runtime.
|
|
53
54
|
"""
|
|
55
|
+
|
|
54
56
|
ROUTE_DISABLE = 1
|
|
55
57
|
ROUTE_ENABLE = 2
|
|
56
58
|
ROUTE_DISABLE_TMP = 3
|
|
@@ -69,16 +71,19 @@ class AdsEnableRoute(IAdsFunction[AdsWriteResponse]):
|
|
|
69
71
|
or if a name in the mqtt route config section is defined
|
|
70
72
|
`MQTT:<RouteName>`
|
|
71
73
|
|
|
72
|
-
I think that this interface allows the same for the default router but i have not tested it yet.
|
|
74
|
+
I think that this interface allows the same for the default router but i have not tested it yet.
|
|
73
75
|
"""
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
transport: ITransport,
|
|
80
|
+
ams_address: AmsAddress,
|
|
81
|
+
route: str,
|
|
82
|
+
switch: RouteSwitch,
|
|
83
|
+
) -> None:
|
|
78
84
|
# We modify the port here as this function only works on the system service
|
|
79
85
|
self.modified_address = AmsAddress(
|
|
80
|
-
net_id=ams_address.net_id,
|
|
81
|
-
port=self.SYSTEM_SERVICE_PORT
|
|
86
|
+
net_id=ams_address.net_id, port=AmsServicePort.SYSTEM_SERVICE
|
|
82
87
|
)
|
|
83
88
|
self.transport = transport
|
|
84
89
|
self.route = route
|
|
@@ -94,6 +99,6 @@ class AdsEnableRoute(IAdsFunction[AdsWriteResponse]):
|
|
|
94
99
|
ams_address=self.modified_address,
|
|
95
100
|
idx_group=AdsFunctionSymbolGroup.ADSIGRP_TOGGLE_ROUTE_ENABLE,
|
|
96
101
|
idx_offset=self.switch.value,
|
|
97
|
-
payload=self.serialize()
|
|
102
|
+
payload=self.serialize(),
|
|
98
103
|
)
|
|
99
104
|
return await command.request()
|
|
@@ -5,13 +5,13 @@ sending multiple `AdsReadCommand` in a single command
|
|
|
5
5
|
https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_adsdll2/124830987.html&id=
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
from struct import Struct
|
|
10
9
|
from typing import Final
|
|
11
10
|
from aioads.ads_error_codes import AdsErrorCode
|
|
12
11
|
from aioads.ams_address import AmsAddress
|
|
13
12
|
from aioads.commands.ads_read import AdsReadCommand, AdsReadResponse
|
|
14
13
|
from aioads.commands.ads_read_write import AdsReadWriteCommand
|
|
14
|
+
from aioads.commands.errors import AdsCommandError
|
|
15
15
|
from aioads.functions.ads_function import AdsFunctionSymbolGroup, IAdsFunction
|
|
16
16
|
from aioads.stream import AdsStream
|
|
17
17
|
from aioads.transport import ITransport
|
|
@@ -19,7 +19,7 @@ from aioads.transport import ITransport
|
|
|
19
19
|
|
|
20
20
|
class AdsSumRead(IAdsFunction[list[tuple[AdsReadResponse, AdsStream]]]):
|
|
21
21
|
"""
|
|
22
|
-
Ads sum read command that allows batching up to 500 `AdsReadCommand` in a single call
|
|
22
|
+
Ads sum read command that allows batching up to 500 `AdsReadCommand` in a single call
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
ERROR_CODE_STRUCT_DEF: Final[Struct] = Struct("<I")
|
|
@@ -36,17 +36,15 @@ class AdsSumRead(IAdsFunction[list[tuple[AdsReadResponse, AdsStream]]]):
|
|
|
36
36
|
|
|
37
37
|
def serialize(self) -> bytes:
|
|
38
38
|
"""
|
|
39
|
-
serialize all commands to bytes
|
|
39
|
+
serialize all commands to bytes
|
|
40
40
|
"""
|
|
41
41
|
return b"".join(command.serialize() for command in self.commands)
|
|
42
42
|
|
|
43
43
|
async def execute(self) -> list[tuple[AdsReadResponse, AdsStream]]:
|
|
44
44
|
if len(self.commands) == 0:
|
|
45
|
-
raise ValueError(
|
|
46
|
-
"At least one command is required for ADS Sum Read")
|
|
45
|
+
raise ValueError("At least one command is required for ADS Sum Read")
|
|
47
46
|
if len(self.commands) > 500:
|
|
48
|
-
raise ValueError(
|
|
49
|
-
"Too many commands for ADS Sum Read, maximum is 500")
|
|
47
|
+
raise ValueError("Too many commands for ADS Sum Read, maximum is 500")
|
|
50
48
|
|
|
51
49
|
payload = self.serialize()
|
|
52
50
|
command = AdsReadWriteCommand(
|
|
@@ -60,20 +58,27 @@ class AdsSumRead(IAdsFunction[list[tuple[AdsReadResponse, AdsStream]]]):
|
|
|
60
58
|
write_length=len(payload),
|
|
61
59
|
write_data=payload,
|
|
62
60
|
)
|
|
63
|
-
|
|
61
|
+
header, read_payload = await command.request()
|
|
62
|
+
if not header.error_code.ok:
|
|
63
|
+
raise AdsCommandError(header.error_code, "Failed to execute ADS Sum Read")
|
|
64
64
|
error_stream = read_payload.sub_stream(
|
|
65
65
|
# 4 bytes per error code
|
|
66
|
-
len(self.commands)
|
|
66
|
+
len(self.commands)
|
|
67
|
+
* self.ERROR_CODE_STRUCT_DEF.size
|
|
68
|
+
)
|
|
67
69
|
|
|
68
70
|
response: list[tuple[AdsReadResponse, AdsStream]] = []
|
|
69
71
|
for cmd in self.commands:
|
|
70
72
|
error_code = AdsErrorCode(
|
|
71
|
-
error_stream.read_struct(self.ERROR_CODE_STRUCT_DEF)[0]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
error_stream.read_struct(self.ERROR_CODE_STRUCT_DEF)[0]
|
|
74
|
+
)
|
|
75
|
+
response.append(
|
|
76
|
+
(
|
|
77
|
+
AdsReadResponse(
|
|
78
|
+
error_code=error_code,
|
|
79
|
+
length=cmd.length,
|
|
80
|
+
),
|
|
81
|
+
read_payload.sub_stream(cmd.length),
|
|
82
|
+
)
|
|
78
83
|
)
|
|
79
84
|
return response
|