aioads 0.1.0.dev3__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.dev5/.github/dependabot.yml +20 -0
  2. aioads-0.1.0.dev5/.github/workflows/ci.yml +51 -0
  3. aioads-0.1.0.dev5/.github/workflows/publish.yml +62 -0
  4. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/.gitignore +3 -0
  5. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/.vscode/settings.json +10 -1
  6. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/PKG-INFO +10 -12
  7. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/README.md +9 -11
  8. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_client.py +49 -41
  9. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_address.py +1 -1
  10. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_header.py +1 -1
  11. aioads-0.1.0.dev5/aioads/ams_service_port.py +83 -0
  12. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_tcp_header.py +1 -1
  13. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_add_notification.py +1 -1
  14. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/errors.py +4 -1
  15. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_enable_route.py +17 -12
  16. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read_write.py +2 -2
  17. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_by_name.py +32 -24
  18. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_info_by_name_ex.py +35 -27
  19. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/stream.py +19 -11
  20. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/transport.py +22 -7
  21. aioads-0.1.0.dev5/docs/unittest_style_guide.html +1739 -0
  22. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cmd_reuse_mqtt.py +6 -9
  23. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cycles.py +3 -4
  24. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cycles_mqtt.py +5 -6
  25. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_multiple.py +9 -7
  26. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_multiple_mqtt.py +11 -8
  27. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_single.py +2 -3
  28. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/pyproject.toml +6 -1
  29. aioads-0.1.0.dev5/tests/__init__.py +0 -0
  30. aioads-0.1.0.dev5/tests/builders.py +63 -0
  31. aioads-0.1.0.dev5/tests/integration/README.md +36 -0
  32. aioads-0.1.0.dev5/tests/integration/__init__.py +0 -0
  33. aioads-0.1.0.dev5/tests/integration/base.py +27 -0
  34. aioads-0.1.0.dev5/tests/integration/config.example.toml +37 -0
  35. aioads-0.1.0.dev5/tests/integration/config.py +58 -0
  36. aioads-0.1.0.dev5/tests/integration/test_connection.py +20 -0
  37. aioads-0.1.0.dev5/tests/integration/test_performance.py +49 -0
  38. aioads-0.1.0.dev5/tests/integration/test_read_symbols.py +32 -0
  39. aioads-0.1.0.dev5/tests/unit/__init__.py +0 -0
  40. aioads-0.1.0.dev5/tests/unit/commands/__init__.py +0 -0
  41. aioads-0.1.0.dev5/tests/unit/commands/test_ads_add_notification.py +131 -0
  42. aioads-0.1.0.dev5/tests/unit/commands/test_ads_command.py +63 -0
  43. aioads-0.1.0.dev5/tests/unit/commands/test_ads_delete_notification.py +83 -0
  44. aioads-0.1.0.dev5/tests/unit/commands/test_ads_read.py +124 -0
  45. aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_device_info.py +147 -0
  46. aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_state.py +137 -0
  47. aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_write.py +98 -0
  48. aioads-0.1.0.dev5/tests/unit/commands/test_ads_write.py +117 -0
  49. aioads-0.1.0.dev5/tests/unit/commands/test_ads_write_state.py +85 -0
  50. aioads-0.1.0.dev5/tests/unit/commands/test_errors.py +50 -0
  51. aioads-0.1.0.dev5/tests/unit/functions/__init__.py +0 -0
  52. aioads-0.1.0.dev5/tests/unit/functions/test_ads_enable_route.py +80 -0
  53. aioads-0.1.0.dev5/tests/unit/functions/test_ads_function.py +32 -0
  54. aioads-0.1.0.dev5/tests/unit/functions/test_ads_sum_read.py +102 -0
  55. aioads-0.1.0.dev5/tests/unit/functions/test_ads_sum_read_write.py +96 -0
  56. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_datatype_by_name.py +137 -0
  57. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_datatype_upload.py +82 -0
  58. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_info_by_name_ex.py +150 -0
  59. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_table_version.py +74 -0
  60. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_upload.py +78 -0
  61. aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_upload_info.py +77 -0
  62. aioads-0.1.0.dev5/tests/unit/test_ads_client.py +184 -0
  63. aioads-0.1.0.dev5/tests/unit/test_ads_error_codes.py +77 -0
  64. aioads-0.1.0.dev5/tests/unit/test_ads_notifications.py +161 -0
  65. aioads-0.1.0.dev5/tests/unit/test_ads_symbol_cache.py +182 -0
  66. aioads-0.1.0.dev5/tests/unit/test_ads_symbol_parser.py +244 -0
  67. aioads-0.1.0.dev5/tests/unit/test_ams_address.py +61 -0
  68. aioads-0.1.0.dev5/tests/unit/test_ams_header.py +84 -0
  69. aioads-0.1.0.dev5/tests/unit/test_ams_tcp_header.py +62 -0
  70. aioads-0.1.0.dev5/tests/unit/test_errors.py +51 -0
  71. aioads-0.1.0.dev5/tests/unit/test_stream.py +167 -0
  72. aioads-0.1.0.dev5/tests/unit/test_transport.py +220 -0
  73. aioads-0.1.0.dev5/tests/unit/utils/__init__.py +0 -0
  74. aioads-0.1.0.dev5/tests/unit/utils/test_local_ip.py +60 -0
  75. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/LICENSE +0 -0
  76. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_error_codes.py +0 -0
  77. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_notifications.py +0 -0
  78. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_symbol_cache.py +0 -0
  79. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_symbol_parser.py +0 -0
  80. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_command.py +0 -0
  81. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_delete_notification.py +0 -0
  82. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read.py +0 -0
  83. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_device_info.py +0 -0
  84. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_state.py +0 -0
  85. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_write.py +0 -0
  86. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_write.py +0 -0
  87. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_write_state.py +0 -0
  88. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/errors.py +0 -0
  89. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_function.py +0 -0
  90. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read.py +0 -0
  91. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_upload.py +0 -0
  92. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_table_version.py +0 -0
  93. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload.py +0 -0
  94. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload_info.py +0 -0
  95. /aioads-0.1.0.dev3/tests/__init__.py → /aioads-0.1.0.dev5/aioads/py.typed +0 -0
  96. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/utils/local_ip.py +0 -0
  97. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/docs/transmission_mode.md +0 -0
  98. {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/pdm.lock +0 -0
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ # Keep the SHA-pinned GitHub Actions current (Dependabot updates both the
4
+ # pinned SHA and the trailing version comment).
5
+ - package-ecosystem: github-actions
6
+ directory: "/"
7
+ schedule:
8
+ interval: weekly
9
+ groups:
10
+ actions:
11
+ patterns: ["*"]
12
+
13
+ # Python dev/runtime dependencies tracked in pdm.lock / pyproject.toml.
14
+ - package-ecosystem: pip
15
+ directory: "/"
16
+ schedule:
17
+ interval: weekly
18
+ groups:
19
+ python:
20
+ patterns: ["*"]
@@ -0,0 +1,51 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ci-${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ name: Test (Python ${{ matrix.python-version }})
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.12", "3.13"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - name: Set up PDM
31
+ uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+ cache: true
35
+
36
+ - name: Install dependencies
37
+ run: pdm install
38
+
39
+ - name: Lint
40
+ run: pdm run lint
41
+
42
+ - name: Run tests with coverage
43
+ run: pdm run coverage
44
+
45
+ - name: Upload coverage artifact
46
+ if: matrix.python-version == '3.12'
47
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
48
+ with:
49
+ name: coverage-xml
50
+ path: coverage.xml
51
+ if-no-files-found: ignore
@@ -0,0 +1,62 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ # Avoid two releases racing to publish the same version.
12
+ concurrency:
13
+ group: publish-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ build:
18
+ name: Build distribution
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22
+ with:
23
+ persist-credentials: false
24
+
25
+ - name: Set up PDM
26
+ uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Build sdist and wheel
31
+ run: pdm build
32
+
33
+ - name: Upload build artifacts
34
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ publish:
40
+ name: Publish to PyPI
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ # Only publish for real releases, not manual test runs.
44
+ if: github.event_name == 'release'
45
+ environment:
46
+ name: pypi
47
+ url: https://pypi.org/project/aioads/
48
+ permissions:
49
+ # IMPORTANT: required for OIDC trusted publishing.
50
+ id-token: write
51
+ steps:
52
+ - name: Download build artifacts
53
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
54
+ with:
55
+ name: dist
56
+ path: dist/
57
+
58
+ - name: Publish to PyPI
59
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
60
+ with:
61
+ # Sign the published distributions with PEP 740 build provenance.
62
+ attestations: true
@@ -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.dev3
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,56 +255,67 @@ 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
266
262
  ) -> dict[str, SymbolReadResult]:
267
263
  """
268
264
  Read multiple symbols by their names from the ADS device.
265
+
266
+ The returned dict is keyed by the symbol names exactly as requested,
267
+ preserving their original casing.
269
268
  """
270
269
  symbol_infos = await self._cache.read_symbol_infos_by_names(symbol_names)
271
270
  if raise_errors:
272
271
  self._raise_if_error(symbol_infos)
273
272
 
274
- read_commands = [
275
- AdsReadCommand(
276
- transport=self.transport,
277
- ams_address=self.dst_address,
278
- idx_group=symbol_info.idx_group,
279
- idx_offset=symbol_info.idx_offset,
280
- length=symbol_info.idx_length,
281
- )
282
- for _, symbol_info in symbol_infos.values()
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
283
285
  ]
284
- function = AdsSumRead(
286
+ if not readable:
287
+ return output
288
+
289
+ sum_read = AdsSumRead(
285
290
  transport=self.transport,
286
291
  ams_address=self.dst_address,
287
- commands=read_commands,
292
+ commands=[
293
+ AdsReadCommand(
294
+ transport=self.transport,
295
+ ams_address=self.dst_address,
296
+ idx_group=info.idx_group,
297
+ idx_offset=info.idx_offset,
298
+ length=info.idx_length,
299
+ )
300
+ for _, info in readable
301
+ ],
288
302
  )
303
+ response = await sum_read.execute()
289
304
 
290
- response = await function.execute()
291
- output: dict[str, SymbolReadResult] = {}
292
-
293
- tasks = []
294
- for (_, resp_payload), (error_code, symbol_info) in zip(
295
- response, symbol_infos.values()
305
+ for (name, info), (read_response, resp_payload) in zip(
306
+ readable, response, strict=True
296
307
  ):
297
- tasks.append(
298
- asyncio.get_running_loop().run_in_executor(
299
- self.parser_pool,
300
- self.parser.parse,
301
- symbol_info.data_type,
302
- symbol_info.type_name,
303
- resp_payload,
304
- )
308
+ if not read_response.error_code.ok:
309
+ output[name] = SymbolReadResult(read_response.error_code, None)
310
+ continue
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
+ ),
305
318
  )
306
- parsed = await asyncio.gather(*tasks)
307
-
308
- for symbol_data, (error_code, symbol_info) in zip(parsed, symbol_infos.values()):
309
- output[symbol_info.symbol_name] = SymbolReadResult(
310
- error_code, symbol_data)
311
319
 
312
320
  return output
313
321
 
@@ -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)"
@@ -77,7 +77,7 @@ class AdsAddNotificationCommand(ICommand[AdsAddNotificationResponse]):
77
77
  Ads command to create a ads notification (subscription to a remote resource)
78
78
  """
79
79
 
80
- SERIALIZE_STRUCT_DEF: Final[Struct] = Struct("<IIIIIII16s")
80
+ SERIALIZE_STRUCT_DEF: Final[Struct] = Struct("<IIIIII16s")
81
81
 
82
82
  def __init__(
83
83
  self,
@@ -27,4 +27,7 @@ class AdsCommandError(Exception):
27
27
 
28
28
  def __repr__(self) -> str:
29
29
  """Returns a detailed representation of the error for debugging."""
30
- return f"AdsCommandError(error_code={self.error_code!r}, error_message:{self.error_code.description} args={self.args!r})"
30
+ return (
31
+ f"AdsCommandError(error_code={self.error_code!r}, "
32
+ f"error_message:{self.error_code.description} args={self.args!r})"
33
+ )