uphy-controller 0.1.0.dev2895__tar.gz → 0.1.0.dev3137__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 (16) hide show
  1. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/LICENSE.txt +5 -5
  2. {uphy_controller-0.1.0.dev2895/src/uphy_controller.egg-info → uphy_controller-0.1.0.dev3137}/PKG-INFO +51 -50
  3. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/README.md +27 -27
  4. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/pyproject.toml +31 -27
  5. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/setup.cfg +4 -4
  6. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy/__init__.py +2 -2
  7. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy/controller/__init__.py +7 -8
  8. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy/controller/__main__.py +190 -190
  9. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy/controller/gui.py +178 -157
  10. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy/controller/modbus.py +108 -103
  11. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137/src/uphy_controller.egg-info}/PKG-INFO +51 -50
  12. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy_controller.egg-info/requires.txt +1 -0
  13. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy_controller.egg-info/SOURCES.txt +0 -0
  14. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy_controller.egg-info/dependency_links.txt +0 -0
  15. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/src/uphy_controller.egg-info/entry_points.txt +0 -0
  16. {uphy_controller-0.1.0.dev2895 → uphy_controller-0.1.0.dev3137}/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.dev2895
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: zeroconf>=0.135.0
23
-
24
- # U-Phy Controller
25
-
26
- This contain a python based U-Phy device controller that act as a fieldbus master/controller.
27
-
28
- Supported fieldbuses:
29
-
30
- - modbus
31
-
32
- ## Install
33
-
34
- ```sh
35
- uv pip install uphy-controller
36
- ```
37
-
38
- ## Workflow
39
-
40
- ```sh
41
- # Start up controller against an existing device
42
- uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
43
- ```
44
-
45
-
46
- ```sh
47
- # Start up controller against discovered devices that expose their model via
48
- # mdns.
49
- uphy-controller mdns
50
- ```
1
+ Metadata-Version: 2.2
2
+ Name: uphy-controller
3
+ Version: 0.1.0.dev3137
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.dev2895"
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
- "zeroconf>=0.135.0",
14
- ]
15
- license = {file = "LICENSE.txt"}
16
-
17
-
18
- [project.urls]
19
- RT-Labs = "https://rt-labs.com"
20
- U-Phy = "https://rt-labs.com/u-phy"
21
- Documentation = "https://docs.rt-labs.com/u-phy"
22
-
23
- [project.scripts]
24
- uphy-controller = "uphy.controller.__main__:app"
25
-
26
- [project.entry-points.'uphy.cli']
27
- controller = 'uphy.controller.__main__:app'
1
+ [project]
2
+ name = "uphy-controller"
3
+ version = "0.1.0.dev3137"
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__)
@@ -1,8 +1,7 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- @dataclass
5
- class Target:
6
- id: str
7
- host: str
8
- port: str = 502
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Target:
6
+ host: str
7
+ port: str = 502
@@ -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.uphy import Root as RootModel
11
-
12
- from . import Target, gui, modbus
13
-
14
- LOG = logging.getLogger(__name__)
15
-
16
- app = Typer(
17
- pretty_exceptions_enable=False,
18
- no_args_is_help=True,
19
- name="controller",
20
- help="Run a demo modbus controller based on a model",
21
- )
22
-
23
-
24
- @staticmethod
25
- def parse_target(value: str):
26
- values = value.split(":")
27
- if len(values) != 3:
28
- raise BadParameter(f"Expected uuid:host:port, got {value}")
29
-
30
- return Target(*value.split(":"))
31
-
32
-
33
- @app.command(name="modbus")
34
- def modbus_tcp(
35
- model: Annotated[list[Path], Option()],
36
- target: Annotated[
37
- list[Target] | None,
38
- Option(
39
- parser=parse_target,
40
- metavar="ID:HOST:PORT",
41
- 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.",
42
- ),
43
- ] = None,
44
- ):
45
- """Run controller against a set of fixed uphy devices."""
46
- models = [RootModel.parse_file(file) for file in model]
47
-
48
- devices_by_id = {
49
- str(device.id): (root, device) for root in models for device in root.devices
50
- }
51
-
52
- devices_by_name = {
53
- device.name: (root, device) for root in models for device in root.devices
54
- }
55
-
56
- if target is None:
57
- table = Table(title="Available devices")
58
- table.add_column("ID")
59
- table.add_column("Name")
60
-
61
- for id, (_, device) in devices_by_id.items():
62
- table.add_row(id, device.name)
63
-
64
- print(table)
65
- print()
66
- print(
67
- "Please add targets to connect to using the --target ID:HOST:PORT argument."
68
- )
69
- print()
70
- exit(-1)
71
-
72
- gui.setup()
73
-
74
- stop = Event()
75
- try:
76
- for target_obj in target:
77
- root, device = devices_by_id.get(target_obj.id, (None, None))
78
- if not device:
79
- root, device = devices_by_name.get(target_obj.id, (None, None))
80
- if not device:
81
- LOG.warning("Unable to find device %s", target_obj.id)
82
- continue
83
-
84
- device_gui = gui.add_device(
85
- root, device, f"{target_obj.host}:{target_obj.port}"
86
- )
87
-
88
- thread = Thread(
89
- target=partial(modbus.target_runner, stop, target_obj, device_gui),
90
- name=device.name,
91
- )
92
- thread.start()
93
-
94
- gui.run()
95
- finally:
96
- stop.set()
97
-
98
-
99
- @app.command()
100
- def mdns():
101
- """Run controller against devices automatically discovered via mdns."""
102
- from zeroconf import (
103
- Zeroconf,
104
- ServiceBrowser,
105
- ServiceStateChange,
106
- IPVersion,
107
- ServiceInfo,
108
- )
109
- import requests
110
-
111
- zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
112
- services = ["_modbus._tcp.local."]
113
-
114
- device_runners: dict[tuple, Event] = {}
115
-
116
- def on_device_added(device_key: tuple, info: ServiceInfo):
117
- addresses = info.parsed_addresses()
118
- properties = info.decoded_properties
119
- model_port = properties.get("model-port")
120
- model_path = properties.get("model-path")
121
- device_id = properties.get("device-id")
122
- if not model_port or not model_path or not device_id:
123
- LOG.error("Missing model info for %s", info.name)
124
- return
125
-
126
- device_stop = Event()
127
- device_runners[device_key] = device_stop
128
-
129
- def _run():
130
- for address in info.parsed_addresses(IPVersion.V4Only):
131
- model_url = f"http://{address}:{model_port}{model_path}"
132
- target_obj = Target(device_id, address, info.port)
133
- try:
134
- model_raw = requests.get(model_url).content
135
- break
136
- except Exception as exception:
137
- LOG.debug("Failed to connect to %s:%r", model_url, exception)
138
- continue
139
- else:
140
- LOG.error("Unable to get model for %s", info.name)
141
- return
142
-
143
-
144
- model = RootModel.parse_raw(model_raw)
145
- for device in model.devices:
146
- if str(device.id) == device_id:
147
- break
148
- else:
149
- LOG.error("Can't find device %s in model", device_id)
150
- return
151
-
152
- LOG.info("Adding device %s", info.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()
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()