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.
- aioads-0.1.0.dev5/.github/dependabot.yml +20 -0
- aioads-0.1.0.dev5/.github/workflows/ci.yml +51 -0
- aioads-0.1.0.dev5/.github/workflows/publish.yml +62 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/.gitignore +3 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/.vscode/settings.json +10 -1
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/PKG-INFO +10 -12
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/README.md +9 -11
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_client.py +49 -41
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_address.py +1 -1
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_header.py +1 -1
- aioads-0.1.0.dev5/aioads/ams_service_port.py +83 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ams_tcp_header.py +1 -1
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_add_notification.py +1 -1
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/errors.py +4 -1
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_enable_route.py +17 -12
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read_write.py +2 -2
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_by_name.py +32 -24
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_info_by_name_ex.py +35 -27
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/stream.py +19 -11
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/transport.py +22 -7
- aioads-0.1.0.dev5/docs/unittest_style_guide.html +1739 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cmd_reuse_mqtt.py +6 -9
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cycles.py +3 -4
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_cycles_mqtt.py +5 -6
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_multiple.py +9 -7
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_multiple_mqtt.py +11 -8
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/examples/read_single.py +2 -3
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/pyproject.toml +6 -1
- aioads-0.1.0.dev5/tests/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/builders.py +63 -0
- aioads-0.1.0.dev5/tests/integration/README.md +36 -0
- aioads-0.1.0.dev5/tests/integration/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/integration/base.py +27 -0
- aioads-0.1.0.dev5/tests/integration/config.example.toml +37 -0
- aioads-0.1.0.dev5/tests/integration/config.py +58 -0
- aioads-0.1.0.dev5/tests/integration/test_connection.py +20 -0
- aioads-0.1.0.dev5/tests/integration/test_performance.py +49 -0
- aioads-0.1.0.dev5/tests/integration/test_read_symbols.py +32 -0
- aioads-0.1.0.dev5/tests/unit/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/unit/commands/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_add_notification.py +131 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_command.py +63 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_delete_notification.py +83 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_read.py +124 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_device_info.py +147 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_state.py +137 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_read_write.py +98 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_write.py +117 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_ads_write_state.py +85 -0
- aioads-0.1.0.dev5/tests/unit/commands/test_errors.py +50 -0
- aioads-0.1.0.dev5/tests/unit/functions/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_enable_route.py +80 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_function.py +32 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_sum_read.py +102 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_sum_read_write.py +96 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_datatype_by_name.py +137 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_datatype_upload.py +82 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_info_by_name_ex.py +150 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_table_version.py +74 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_upload.py +78 -0
- aioads-0.1.0.dev5/tests/unit/functions/test_ads_symbol_upload_info.py +77 -0
- aioads-0.1.0.dev5/tests/unit/test_ads_client.py +184 -0
- aioads-0.1.0.dev5/tests/unit/test_ads_error_codes.py +77 -0
- aioads-0.1.0.dev5/tests/unit/test_ads_notifications.py +161 -0
- aioads-0.1.0.dev5/tests/unit/test_ads_symbol_cache.py +182 -0
- aioads-0.1.0.dev5/tests/unit/test_ads_symbol_parser.py +244 -0
- aioads-0.1.0.dev5/tests/unit/test_ams_address.py +61 -0
- aioads-0.1.0.dev5/tests/unit/test_ams_header.py +84 -0
- aioads-0.1.0.dev5/tests/unit/test_ams_tcp_header.py +62 -0
- aioads-0.1.0.dev5/tests/unit/test_errors.py +51 -0
- aioads-0.1.0.dev5/tests/unit/test_stream.py +167 -0
- aioads-0.1.0.dev5/tests/unit/test_transport.py +220 -0
- aioads-0.1.0.dev5/tests/unit/utils/__init__.py +0 -0
- aioads-0.1.0.dev5/tests/unit/utils/test_local_ip.py +60 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/LICENSE +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_error_codes.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_notifications.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_symbol_cache.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/ads_symbol_parser.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_command.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_delete_notification.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_device_info.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_state.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_read_write.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_write.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/commands/ads_write_state.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/errors.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_function.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_sum_read.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_datatype_upload.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_table_version.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/functions/ads_symbol_upload_info.py +0 -0
- /aioads-0.1.0.dev3/tests/__init__.py → /aioads-0.1.0.dev5/aioads/py.typed +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/aioads/utils/local_ip.py +0 -0
- {aioads-0.1.0.dev3 → aioads-0.1.0.dev5}/docs/transmission_mode.md +0 -0
- {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.
|
|
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=
|
|
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,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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
|
@@ -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
|
|
@@ -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("<
|
|
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
|
|
30
|
+
return (
|
|
31
|
+
f"AdsCommandError(error_code={self.error_code!r}, "
|
|
32
|
+
f"error_message:{self.error_code.description} args={self.args!r})"
|
|
33
|
+
)
|