aioads 0.1.0.dev4__tar.gz → 0.1.0.dev5__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.
Files changed (98) hide show
  1. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/.gitignore +3 -0
  2. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/.vscode/settings.json +10 -1
  3. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/PKG-INFO +10 -12
  4. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/README.md +9 -11
  5. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ads_client.py +37 -52
  6. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ams_address.py +1 -1
  7. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ams_header.py +1 -1
  8. aioads-0.1.0.dev5/aioads/ams_service_port.py +83 -0
  9. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ams_tcp_header.py +1 -1
  10. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_enable_route.py +17 -12
  11. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read_write.py +2 -2
  12. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_by_name.py +32 -24
  13. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_info_by_name_ex.py +35 -27
  14. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/stream.py +19 -11
  15. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_cmd_reuse_mqtt.py +6 -9
  16. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_cycles.py +3 -4
  17. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_cycles_mqtt.py +5 -6
  18. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_multiple.py +9 -7
  19. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_multiple_mqtt.py +11 -8
  20. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/examples/read_single.py +2 -3
  21. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/pyproject.toml +1 -1
  22. aioads-0.1.0.dev5/tests/integration/README.md +36 -0
  23. aioads-0.1.0.dev5/tests/integration/base.py +27 -0
  24. aioads-0.1.0.dev5/tests/integration/config.example.toml +37 -0
  25. aioads-0.1.0.dev5/tests/integration/config.py +58 -0
  26. aioads-0.1.0.dev5/tests/integration/test_connection.py +20 -0
  27. aioads-0.1.0.dev5/tests/integration/test_performance.py +49 -0
  28. aioads-0.1.0.dev5/tests/integration/test_read_symbols.py +32 -0
  29. aioads-0.1.0.dev5/tests/unit/functions/__init__.py +0 -0
  30. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_enable_route.py +12 -3
  31. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_stream.py +34 -0
  32. aioads-0.1.0.dev5/tests/unit/utils/__init__.py +0 -0
  33. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/.github/dependabot.yml +0 -0
  34. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/.github/workflows/ci.yml +0 -0
  35. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/.github/workflows/publish.yml +0 -0
  36. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/LICENSE +0 -0
  37. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ads_error_codes.py +0 -0
  38. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ads_notifications.py +0 -0
  39. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ads_symbol_cache.py +0 -0
  40. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/ads_symbol_parser.py +0 -0
  41. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_add_notification.py +0 -0
  42. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_command.py +0 -0
  43. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_delete_notification.py +0 -0
  44. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_read.py +0 -0
  45. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_read_device_info.py +0 -0
  46. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_read_state.py +0 -0
  47. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_read_write.py +0 -0
  48. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_write.py +0 -0
  49. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/ads_write_state.py +0 -0
  50. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/commands/errors.py +0 -0
  51. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/errors.py +0 -0
  52. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_function.py +0 -0
  53. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read.py +0 -0
  54. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_upload.py +0 -0
  55. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_table_version.py +0 -0
  56. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload.py +0 -0
  57. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload_info.py +0 -0
  58. /aioads-0.1.0.dev4/tests/__init__.py → /aioads-0.1.0.dev5/aioads/py.typed +0 -0
  59. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/transport.py +0 -0
  60. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/aioads/utils/local_ip.py +0 -0
  61. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/docs/transmission_mode.md +0 -0
  62. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/docs/unittest_style_guide.html +0 -0
  63. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/pdm.lock +0 -0
  64. {aioads-0.1.0.dev4/tests/unit → aioads-0.1.0.dev5/tests}/__init__.py +0 -0
  65. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/builders.py +0 -0
  66. {aioads-0.1.0.dev4/tests/unit/commands → aioads-0.1.0.dev5/tests/integration}/__init__.py +0 -0
  67. {aioads-0.1.0.dev4/tests/unit/functions → aioads-0.1.0.dev5/tests/unit}/__init__.py +0 -0
  68. {aioads-0.1.0.dev4/tests/unit/utils → aioads-0.1.0.dev5/tests/unit/commands}/__init__.py +0 -0
  69. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_add_notification.py +0 -0
  70. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_command.py +0 -0
  71. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_delete_notification.py +0 -0
  72. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_read.py +0 -0
  73. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_read_device_info.py +0 -0
  74. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_read_state.py +0 -0
  75. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_read_write.py +0 -0
  76. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_write.py +0 -0
  77. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_ads_write_state.py +0 -0
  78. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/commands/test_errors.py +0 -0
  79. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_function.py +0 -0
  80. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_sum_read.py +0 -0
  81. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_sum_read_write.py +0 -0
  82. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_datatype_by_name.py +0 -0
  83. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_datatype_upload.py +0 -0
  84. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_info_by_name_ex.py +0 -0
  85. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_table_version.py +0 -0
  86. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_upload.py +0 -0
  87. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/functions/test_ads_symbol_upload_info.py +0 -0
  88. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ads_client.py +0 -0
  89. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ads_error_codes.py +0 -0
  90. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ads_notifications.py +0 -0
  91. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ads_symbol_cache.py +0 -0
  92. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ads_symbol_parser.py +0 -0
  93. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ams_address.py +0 -0
  94. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ams_header.py +0 -0
  95. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_ams_tcp_header.py +0 -0
  96. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_errors.py +0 -0
  97. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/tests/unit/test_transport.py +0 -0
  98. {aioads-0.1.0.dev4 → aioads-0.1.0.dev5}/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.dev4
3
+ Version: 0.1.0.dev5
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=851),
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=851),
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
- import asyncio
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(cls, dst: AmsAddress, transport: ITransport) -> "AdsClient":
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
- output: dict[str, SymbolReadResult] = {}
278
-
279
- # Only symbols whose info resolved successfully can be read. Symbols
280
- # with a lookup error are reported as-is; building a read command from
281
- # their (invalid) index group/offset would misalign the bulk response.
282
- readable: list[tuple[str, SymbolInfo]] = []
283
- for requested_name, (error_code, symbol_info) in symbol_infos.items():
284
- if not error_code.ok:
285
- output[requested_name] = SymbolReadResult(error_code, None)
286
- else:
287
- readable.append((requested_name, symbol_info))
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
- function = AdsSumRead(
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=symbol_info.idx_group,
300
- idx_offset=symbol_info.idx_offset,
301
- length=symbol_info.idx_length,
296
+ idx_group=info.idx_group,
297
+ idx_offset=info.idx_offset,
298
+ length=info.idx_length,
302
299
  )
303
- for _, symbol_info in readable
300
+ for _, info in readable
304
301
  ],
305
302
  )
306
- response = await function.execute()
303
+ response = await sum_read.execute()
307
304
 
308
- loop = asyncio.get_running_loop()
309
- parse_names: list[str] = []
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
- # A failed read yields no usable payload, so skip parsing it.
315
- if read_response.error_code != 0:
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
- parse_names.append(requested_name)
320
- tasks.append(
321
- loop.run_in_executor(
322
- self.parser_pool,
323
- self.parser.parse,
324
- symbol_info.data_type,
325
- symbol_info.type_name,
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
@@ -11,7 +11,7 @@ config:
11
11
  rowHeight: 45
12
12
  bitWidth: 32
13
13
  bitsPerRow: 8
14
- showBits: ture
14
+ showBits: true
15
15
  ---
16
16
  packet
17
17
  +6: "AMSNetId (6 bytes)"
@@ -10,7 +10,7 @@ config:
10
10
  rowHeight: 40
11
11
  bitWidth: 80
12
12
  bitsPerRow: 8
13
- showBits: ture
13
+ showBits: true
14
14
  ---
15
15
  packet
16
16
  +6: "AMSNetId Target (6 bytes)"
@@ -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
@@ -10,7 +10,7 @@ config:
10
10
  rowHeight: 40
11
11
  bitWidth: 80
12
12
  bitsPerRow: 8
13
- showBits: ture
13
+ showBits: true
14
14
  ---
15
15
  packet
16
16
  +2: "reserved (2 bytes)"
@@ -9,7 +9,7 @@ config:
9
9
  rowHeight: 40
10
10
  bitWidth: 80
11
11
  bitsPerRow: 8
12
- showBits: ture
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
- SYSTEM_SERVICE_PORT = 10000
76
-
77
- def __init__(self, transport: ITransport, ams_address: AmsAddress, route: str, switch: RouteSwitch) -> None:
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()
@@ -12,7 +12,7 @@ config:
12
12
  rowHeight: 40
13
13
  bitWidth: 80
14
14
  bitsPerRow: 8
15
- showBits: ture
15
+ showBits: true
16
16
  ---
17
17
  packet
18
18
  +4: "1 - idx group (4 bytes)"
@@ -37,7 +37,7 @@ config:
37
37
  rowHeight: 40
38
38
  bitWidth: 80
39
39
  bitsPerRow: 8
40
- showBits: ture
40
+ showBits: true
41
41
  ---
42
42
  packet
43
43
  +4: "1 - error code (4 bytes)"
@@ -10,7 +10,7 @@ config:
10
10
  rowHeight: 40
11
11
  bitWidth: 80
12
12
  bitsPerRow: 8
13
- showBits: ture
13
+ showBits: true
14
14
  ---
15
15
  packet
16
16
  +4: "entry length (4 bytes)"
@@ -37,6 +37,8 @@ packet
37
37
 
38
38
  """
39
39
  from dataclasses import dataclass
40
+ from struct import Struct
41
+ from typing import ClassVar
40
42
  from aioads.ams_address import AmsAddress
41
43
  from aioads.commands.ads_read_write import AdsReadWriteCommand
42
44
  from aioads.functions.ads_function import AdsFunctionSymbolGroup, IAdsFunction
@@ -57,6 +59,8 @@ class AdsDatatypeArrayInfo:
57
59
  l_bound: int
58
60
  e_elements: int
59
61
 
62
+ STRUCT_DEF: ClassVar[Struct] = Struct("<II")
63
+
60
64
  def serialize(self) -> bytes:
61
65
  """
62
66
  serialize the `AdsDatatypeArrayInfo` to bytes
@@ -71,8 +75,8 @@ class AdsDatatypeArrayInfo:
71
75
  """
72
76
  Create `AdsDatatypeArrayInfo` from the `AdsStream`
73
77
  """
74
- l_bound = int.from_bytes(data.read_view(4), byteorder="little")
75
- e_elements = int.from_bytes(data.read_view(4), byteorder="little")
78
+ l_bound, e_elements = data.read_struct(
79
+ AdsDatatypeArrayInfo.STRUCT_DEF)
76
80
  return AdsDatatypeArrayInfo(
77
81
  l_bound=l_bound,
78
82
  e_elements=e_elements,
@@ -98,6 +102,11 @@ class SymbolDataTypeResponse:
98
102
  array: list[AdsDatatypeArrayInfo]
99
103
  sub_items: list["SymbolDataTypeResponse"]
100
104
 
105
+ # entry_length, version, hash_value, type_hash_value, size, offs,
106
+ # data_type, flags, name_length, type_name_length, comment_length,
107
+ # array_dim, sub_items_count
108
+ FIXED_STRUCT: ClassVar[Struct] = Struct("<8I5H")
109
+
101
110
  def serialize(self) -> bytes:
102
111
  """
103
112
  Serialize the SymbolDataTypeResponse into bytes.
@@ -141,25 +150,24 @@ class SymbolDataTypeResponse:
141
150
  """
142
151
 
143
152
  start_pos = data.tell()
144
- entry_length = int.from_bytes(data.read_view(4), byteorder="little")
145
- version = int.from_bytes(data.read_view(4), byteorder="little")
146
- hash_value = int.from_bytes(data.read_view(4), byteorder="little")
147
- type_hash_value = int.from_bytes(data.read_view(4), byteorder="little")
148
- size = int.from_bytes(data.read_view(4), byteorder="little")
149
- offs = int.from_bytes(data.read_view(4), byteorder="little")
150
- data_type = AdsSymbolDataType.from_bytes(
151
- data.read_view(4), byteorder="little")
152
- flags = AdsSymbolFlags.from_bytes(
153
- data.read_view(4), byteorder="little")
154
- name_length = int.from_bytes(data.read_view(2), byteorder="little") + 1
155
- type_length = int.from_bytes(data.read_view(2), byteorder="little") + 1
156
- comment_length = int.from_bytes(
157
- data.read_view(2), byteorder="little") + 1
158
- array_dim = int.from_bytes(data.read_view(2), byteorder="little")
159
- sub_items_cnt = int.from_bytes(data.read_view(2), byteorder="little")
160
- name = data.read(name_length).rstrip(b"\x00").decode("cp1252")
161
- type_name = data.read(type_length).rstrip(b"\x00").decode("cp1252")
162
- comment = data.read(comment_length).rstrip(b"\x00").decode("cp1252")
153
+ (
154
+ entry_length,
155
+ version,
156
+ hash_value,
157
+ type_hash_value,
158
+ size,
159
+ offs,
160
+ data_type,
161
+ flags,
162
+ name_length,
163
+ type_length,
164
+ comment_length,
165
+ array_dim,
166
+ sub_items_cnt,
167
+ ) = data.read_struct(SymbolDataTypeResponse.FIXED_STRUCT)
168
+ name = data.read(name_length + 1).rstrip(b"\x00").decode("cp1252")
169
+ type_name = data.read(type_length + 1).rstrip(b"\x00").decode("cp1252")
170
+ comment = data.read(comment_length + 1).rstrip(b"\x00").decode("cp1252")
163
171
  array_info: list[AdsDatatypeArrayInfo] = []
164
172
  for _ in range(array_dim):
165
173
  array_info.append(AdsDatatypeArrayInfo.deserialize(data))
@@ -176,8 +184,8 @@ class SymbolDataTypeResponse:
176
184
  type_hash_value=type_hash_value,
177
185
  size=size,
178
186
  offs=offs,
179
- data_type=data_type,
180
- flags=flags,
187
+ data_type=AdsSymbolDataType(data_type),
188
+ flags=AdsSymbolFlags(flags),
181
189
  name=name,
182
190
  type_name=type_name,
183
191
  comment=comment,