uphy-controller 0.1.0.dev2895__tar.gz → 0.1.0.dev3125__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.
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/LICENSE.txt +5 -5
- {uphy_controller-0.1.0.dev2895/src/uphy_controller.egg-info → uphy_controller-0.1.0.dev3125}/PKG-INFO +51 -50
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/README.md +27 -27
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/pyproject.toml +31 -27
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/setup.cfg +4 -4
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/__init__.py +2 -2
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__init__.py +7 -8
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__main__.py +190 -190
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/gui.py +175 -157
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/modbus.py +108 -103
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125/src/uphy_controller.egg-info}/PKG-INFO +51 -50
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/requires.txt +1 -0
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/SOURCES.txt +0 -0
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/dependency_links.txt +0 -0
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/entry_points.txt +0 -0
- {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
Copyright rt-labs AB, Sweden.
|
|
2
|
-
All rights reserved.
|
|
3
|
-
|
|
4
|
-
You may not use this software in a commercial product without
|
|
5
|
-
purchasing a license. Contact sales@rt-labs.com for more information.
|
|
1
|
+
Copyright rt-labs AB, Sweden.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
You may not use this software in a commercial product without
|
|
5
|
+
purchasing a license. Contact sales@rt-labs.com for more information.
|
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: uphy-controller
|
|
3
|
-
Version: 0.1.0.
|
|
4
|
-
Summary: Add your description here
|
|
5
|
-
License: Copyright rt-labs AB, Sweden.
|
|
6
|
-
All rights reserved.
|
|
7
|
-
|
|
8
|
-
You may not use this software in a commercial product without
|
|
9
|
-
purchasing a license. Contact sales@rt-labs.com for more information.
|
|
10
|
-
|
|
11
|
-
Project-URL: RT-Labs, https://rt-labs.com
|
|
12
|
-
Project-URL: U-Phy, https://rt-labs.com/u-phy
|
|
13
|
-
Project-URL: Documentation, https://docs.rt-labs.com/u-phy
|
|
14
|
-
Requires-Python: >=3.10
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
License-File: LICENSE.txt
|
|
17
|
-
Requires-Dist: dearpygui>=2.0.0
|
|
18
|
-
Requires-Dist: pymodbus<3.9.0,>=3.8.3
|
|
19
|
-
Requires-Dist: requests>=2.32.3
|
|
20
|
-
Requires-Dist: rich>=13.9.2
|
|
21
|
-
Requires-Dist: typer>=0.12.5
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: uphy-controller
|
|
3
|
+
Version: 0.1.0.dev3125
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
License: Copyright rt-labs AB, Sweden.
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
You may not use this software in a commercial product without
|
|
9
|
+
purchasing a license. Contact sales@rt-labs.com for more information.
|
|
10
|
+
|
|
11
|
+
Project-URL: RT-Labs, https://rt-labs.com
|
|
12
|
+
Project-URL: U-Phy, https://rt-labs.com/u-phy
|
|
13
|
+
Project-URL: Documentation, https://docs.rt-labs.com/u-phy
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.txt
|
|
17
|
+
Requires-Dist: dearpygui>=2.0.0
|
|
18
|
+
Requires-Dist: pymodbus<3.9.0,>=3.8.3
|
|
19
|
+
Requires-Dist: requests>=2.32.3
|
|
20
|
+
Requires-Dist: rich>=13.9.2
|
|
21
|
+
Requires-Dist: typer>=0.12.5
|
|
22
|
+
Requires-Dist: uphy
|
|
23
|
+
Requires-Dist: zeroconf>=0.135.0
|
|
24
|
+
|
|
25
|
+
# U-Phy Controller
|
|
26
|
+
|
|
27
|
+
This contain a python based U-Phy device controller that act as a fieldbus master/controller.
|
|
28
|
+
|
|
29
|
+
Supported fieldbuses:
|
|
30
|
+
|
|
31
|
+
- modbus
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
uv pip install uphy-controller
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# Start up controller against an existing device
|
|
43
|
+
uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# Start up controller against discovered devices that expose their model via
|
|
49
|
+
# mdns.
|
|
50
|
+
uphy-controller mdns
|
|
51
|
+
```
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# U-Phy Controller
|
|
2
|
-
|
|
3
|
-
This contain a python based U-Phy device controller that act as a fieldbus master/controller.
|
|
4
|
-
|
|
5
|
-
Supported fieldbuses:
|
|
6
|
-
|
|
7
|
-
- modbus
|
|
8
|
-
|
|
9
|
-
## Install
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
uv pip install uphy-controller
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Workflow
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
# Start up controller against an existing device
|
|
19
|
-
uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```sh
|
|
24
|
-
# Start up controller against discovered devices that expose their model via
|
|
25
|
-
# mdns.
|
|
26
|
-
uphy-controller mdns
|
|
27
|
-
```
|
|
1
|
+
# U-Phy Controller
|
|
2
|
+
|
|
3
|
+
This contain a python based U-Phy device controller that act as a fieldbus master/controller.
|
|
4
|
+
|
|
5
|
+
Supported fieldbuses:
|
|
6
|
+
|
|
7
|
+
- modbus
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv pip install uphy-controller
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
# Start up controller against an existing device
|
|
19
|
+
uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
# Start up controller against discovered devices that expose their model via
|
|
25
|
+
# mdns.
|
|
26
|
+
uphy-controller mdns
|
|
27
|
+
```
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "uphy-controller"
|
|
3
|
-
version = "0.1.0.
|
|
4
|
-
description = "Add your description here"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.10"
|
|
7
|
-
dependencies = [
|
|
8
|
-
"dearpygui>=2.0.0",
|
|
9
|
-
"pymodbus (>=3.8.3,<3.9.0)",
|
|
10
|
-
"requests>=2.32.3",
|
|
11
|
-
"rich>=13.9.2",
|
|
12
|
-
"typer>=0.12.5",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "uphy-controller"
|
|
3
|
+
version = "0.1.0.dev3125"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"dearpygui>=2.0.0",
|
|
9
|
+
"pymodbus (>=3.8.3,<3.9.0)",
|
|
10
|
+
"requests>=2.32.3",
|
|
11
|
+
"rich>=13.9.2",
|
|
12
|
+
"typer>=0.12.5",
|
|
13
|
+
"uphy",
|
|
14
|
+
"zeroconf>=0.135.0",
|
|
15
|
+
]
|
|
16
|
+
license = {file = "LICENSE.txt"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
RT-Labs = "https://rt-labs.com"
|
|
21
|
+
U-Phy = "https://rt-labs.com/u-phy"
|
|
22
|
+
Documentation = "https://docs.rt-labs.com/u-phy"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
uphy-controller = "uphy.controller.__main__:app"
|
|
26
|
+
|
|
27
|
+
[project.entry-points.'uphy.cli']
|
|
28
|
+
controller = 'uphy.controller.__main__:app'
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
uphy = { workspace = true }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
# This is a namespace package to hold on to uphy namespace
|
|
2
|
-
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
1
|
+
# This is a namespace package to hold on to uphy namespace
|
|
2
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
{uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__init__.py
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
@dataclass
|
|
5
|
-
class Target:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
port: str = 502
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Target:
|
|
6
|
+
host: str
|
|
7
|
+
port: str = 502
|
{uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__main__.py
RENAMED
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from functools import partial
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from threading import Event, Thread
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
from rich.table import Table
|
|
7
|
-
from rich import print
|
|
8
|
-
|
|
9
|
-
from typer import Option, Typer, BadParameter
|
|
10
|
-
from upgen.model.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
print(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
LOG.info("Adding device %s",
|
|
153
|
-
device_gui = gui.add_device(model, device, address)
|
|
154
|
-
modbus.target_runner(device_stop, target_obj, device_gui)
|
|
155
|
-
device_gui.close()
|
|
156
|
-
|
|
157
|
-
thread = Thread(
|
|
158
|
-
target=_run,
|
|
159
|
-
)
|
|
160
|
-
thread.start()
|
|
161
|
-
|
|
162
|
-
def on_service_state_change(
|
|
163
|
-
zeroconf: Zeroconf,
|
|
164
|
-
service_type: str,
|
|
165
|
-
name: str,
|
|
166
|
-
state_change: ServiceStateChange,
|
|
167
|
-
) -> None:
|
|
168
|
-
print(f"Service {name}: {state_change}")
|
|
169
|
-
|
|
170
|
-
device_key = (service_type, name)
|
|
171
|
-
|
|
172
|
-
if state_change is ServiceStateChange.Added:
|
|
173
|
-
info = zeroconf.get_service_info(service_type, name)
|
|
174
|
-
if info and info.port:
|
|
175
|
-
on_device_added(device_key, info)
|
|
176
|
-
|
|
177
|
-
if state_change is ServiceStateChange.Removed:
|
|
178
|
-
device_stop = device_runners.pop(device_key, None)
|
|
179
|
-
if device_stop:
|
|
180
|
-
device_stop.set()
|
|
181
|
-
|
|
182
|
-
gui.setup()
|
|
183
|
-
|
|
184
|
-
ServiceBrowser(zeroconf, services, handlers=[on_service_state_change])
|
|
185
|
-
|
|
186
|
-
gui.run()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if __name__ == "__main__":
|
|
190
|
-
app()
|
|
1
|
+
import logging
|
|
2
|
+
from functools import partial
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from threading import Event, Thread
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich import print
|
|
8
|
+
|
|
9
|
+
from typer import Option, Typer, BadParameter
|
|
10
|
+
from upgen.model.runtime import Root as RootModel
|
|
11
|
+
from uphy.cli import VERBOSE_OPTION, configure_logging
|
|
12
|
+
|
|
13
|
+
from . import Target, gui, modbus
|
|
14
|
+
|
|
15
|
+
LOG = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
app = Typer(
|
|
18
|
+
pretty_exceptions_enable=False,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
name="controller",
|
|
21
|
+
help="Run a demo modbus controller based on a model",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
configure_logging(level=logging.INFO)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def parse_target(value: str):
|
|
28
|
+
values = value.split(":")
|
|
29
|
+
if len(values) != 3:
|
|
30
|
+
raise BadParameter(f"Expected uuid:host:port, got {value}")
|
|
31
|
+
|
|
32
|
+
return Target(*value.split(":"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command(name="modbus")
|
|
36
|
+
def modbus_tcp(
|
|
37
|
+
model: Annotated[list[Path], Option()],
|
|
38
|
+
target: Annotated[
|
|
39
|
+
list[Target] | None,
|
|
40
|
+
Option(
|
|
41
|
+
parser=parse_target,
|
|
42
|
+
metavar="ID:HOST:PORT",
|
|
43
|
+
help="Targets to connect controller to, where ID is a device uuid or name, HOST is hostname to connect to and PORT is the port the modbus-tcp server is listening on.",
|
|
44
|
+
),
|
|
45
|
+
] = None,
|
|
46
|
+
verbose: Annotated[bool, VERBOSE_OPTION] = False,
|
|
47
|
+
):
|
|
48
|
+
"""Run controller against a set of fixed uphy devices."""
|
|
49
|
+
models = [RootModel.parse_file(file) for file in model]
|
|
50
|
+
|
|
51
|
+
devices_by_id = {
|
|
52
|
+
str(device.id): (root, device) for root in models for device in root.devices
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
devices_by_name = {
|
|
56
|
+
device.name: (root, device) for root in models for device in root.devices
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if target is None:
|
|
60
|
+
table = Table(title="Available devices")
|
|
61
|
+
table.add_column("ID")
|
|
62
|
+
table.add_column("Name")
|
|
63
|
+
|
|
64
|
+
for id, (_, device) in devices_by_id.items():
|
|
65
|
+
table.add_row(id, device.name)
|
|
66
|
+
|
|
67
|
+
print(table)
|
|
68
|
+
print()
|
|
69
|
+
print(
|
|
70
|
+
"Please add targets to connect to using the --target ID:HOST:PORT argument."
|
|
71
|
+
)
|
|
72
|
+
print()
|
|
73
|
+
exit(-1)
|
|
74
|
+
|
|
75
|
+
gui.setup()
|
|
76
|
+
|
|
77
|
+
stop = Event()
|
|
78
|
+
try:
|
|
79
|
+
for target_obj in target:
|
|
80
|
+
root, device = devices_by_id.get(target_obj.id, (None, None))
|
|
81
|
+
if not device:
|
|
82
|
+
root, device = devices_by_name.get(target_obj.id, (None, None))
|
|
83
|
+
if not device:
|
|
84
|
+
LOG.warning("Unable to find device %s", target_obj.id)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
device_gui = gui.add_device(
|
|
88
|
+
root, device, f"{target_obj.host}:{target_obj.port}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
thread = Thread(
|
|
92
|
+
target=partial(modbus.target_runner, stop, target_obj, device_gui),
|
|
93
|
+
name=device.name,
|
|
94
|
+
)
|
|
95
|
+
thread.start()
|
|
96
|
+
|
|
97
|
+
gui.run()
|
|
98
|
+
finally:
|
|
99
|
+
stop.set()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command()
|
|
103
|
+
def mdns(
|
|
104
|
+
verbose: Annotated[bool, VERBOSE_OPTION] = False,
|
|
105
|
+
):
|
|
106
|
+
"""Run controller against devices automatically discovered via mdns."""
|
|
107
|
+
from zeroconf import (
|
|
108
|
+
Zeroconf,
|
|
109
|
+
ServiceBrowser,
|
|
110
|
+
ServiceStateChange,
|
|
111
|
+
IPVersion,
|
|
112
|
+
ServiceInfo,
|
|
113
|
+
)
|
|
114
|
+
import requests
|
|
115
|
+
|
|
116
|
+
zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
|
|
117
|
+
services = ["_modbus._tcp.local."]
|
|
118
|
+
|
|
119
|
+
device_runners: dict[tuple, Event] = {}
|
|
120
|
+
|
|
121
|
+
def on_device_added(device_key: tuple, info: ServiceInfo):
|
|
122
|
+
properties = info.decoded_properties
|
|
123
|
+
model_port = properties.get("model-port")
|
|
124
|
+
model_path = properties.get("model-path")
|
|
125
|
+
if not model_port or not model_path:
|
|
126
|
+
LOG.error("Missing model info for %s", info.name)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
device_stop = Event()
|
|
130
|
+
device_runners[device_key] = device_stop
|
|
131
|
+
|
|
132
|
+
def _run():
|
|
133
|
+
for address in info.parsed_addresses(IPVersion.V4Only):
|
|
134
|
+
model_url = f"http://{address}:{model_port}{model_path}"
|
|
135
|
+
target_obj = Target(address, info.port)
|
|
136
|
+
try:
|
|
137
|
+
response = requests.get(model_url)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
model_raw = response.content
|
|
140
|
+
break
|
|
141
|
+
except Exception as exception:
|
|
142
|
+
LOG.warning("Failed to connect to %s:%r", model_url, exception)
|
|
143
|
+
continue
|
|
144
|
+
else:
|
|
145
|
+
LOG.error("Unable to get model for %s", info.name)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
model = RootModel.parse_raw(model_raw)
|
|
150
|
+
device = model.device
|
|
151
|
+
|
|
152
|
+
LOG.info("Adding device %s", device.name)
|
|
153
|
+
device_gui = gui.add_device(model, device, address)
|
|
154
|
+
modbus.target_runner(device_stop, target_obj, device_gui)
|
|
155
|
+
device_gui.close()
|
|
156
|
+
|
|
157
|
+
thread = Thread(
|
|
158
|
+
target=_run,
|
|
159
|
+
)
|
|
160
|
+
thread.start()
|
|
161
|
+
|
|
162
|
+
def on_service_state_change(
|
|
163
|
+
zeroconf: Zeroconf,
|
|
164
|
+
service_type: str,
|
|
165
|
+
name: str,
|
|
166
|
+
state_change: ServiceStateChange,
|
|
167
|
+
) -> None:
|
|
168
|
+
print(f"Service {name}: {state_change}")
|
|
169
|
+
|
|
170
|
+
device_key = (service_type, name)
|
|
171
|
+
|
|
172
|
+
if state_change is ServiceStateChange.Added:
|
|
173
|
+
info = zeroconf.get_service_info(service_type, name)
|
|
174
|
+
if info and info.port:
|
|
175
|
+
on_device_added(device_key, info)
|
|
176
|
+
|
|
177
|
+
if state_change is ServiceStateChange.Removed:
|
|
178
|
+
device_stop = device_runners.pop(device_key, None)
|
|
179
|
+
if device_stop:
|
|
180
|
+
device_stop.set()
|
|
181
|
+
|
|
182
|
+
gui.setup()
|
|
183
|
+
|
|
184
|
+
ServiceBrowser(zeroconf, services, handlers=[on_service_state_change])
|
|
185
|
+
|
|
186
|
+
gui.run()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
app()
|