uphy-controller 0.1.0.dev2809__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.
Files changed (16) hide show
  1. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/LICENSE.txt +5 -5
  2. {uphy_controller-0.1.0.dev2809/src/uphy_controller.egg-info → uphy_controller-0.1.0.dev3125}/PKG-INFO +51 -43
  3. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/README.md +27 -20
  4. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/pyproject.toml +31 -27
  5. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/setup.cfg +4 -4
  6. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy/__init__.py +2 -2
  7. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__init__.py +7 -8
  8. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/__main__.py +190 -187
  9. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/gui.py +175 -157
  10. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy/controller/modbus.py +108 -103
  11. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125/src/uphy_controller.egg-info}/PKG-INFO +51 -43
  12. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/requires.txt +2 -1
  13. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/SOURCES.txt +0 -0
  14. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/dependency_links.txt +0 -0
  15. {uphy_controller-0.1.0.dev2809 → uphy_controller-0.1.0.dev3125}/src/uphy_controller.egg-info/entry_points.txt +0 -0
  16. {uphy_controller-0.1.0.dev2809 → 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,43 +1,51 @@
1
- Metadata-Version: 2.2
2
- Name: uphy-controller
3
- Version: 0.1.0.dev2809
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.7.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-tcp
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-tcp --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
43
- ```
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,20 +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-tcp
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-tcp --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
20
- ```
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.dev2809"
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.7.3",
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.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__)
@@ -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,187 +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()
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
- models = [RootModel.parse_file(file) for file in model]
46
-
47
- devices_by_id = {
48
- device.id: (root, device) for root in models for device in root.devices
49
- }
50
-
51
- devices_by_name = {
52
- device.name: (root, device) for root in models for device in root.devices
53
- }
54
-
55
- if target is None:
56
- table = Table(title="Available devices")
57
- table.add_column("ID")
58
- table.add_column("Name")
59
-
60
- for _, device in devices_by_id.values():
61
- table.add_row(str(device.id), device.name)
62
-
63
- print(table)
64
- print()
65
- print(
66
- "Please add targets to connect to using the --target ID:HOST:PORT argument."
67
- )
68
- print()
69
- exit(-1)
70
-
71
- gui.setup()
72
-
73
- stop = Event()
74
- try:
75
- for target_obj in target:
76
- root, device = devices_by_id.get(target_obj.id, (None, None))
77
- if not device:
78
- root, device = devices_by_name.get(target_obj.id, (None, None))
79
- if not device:
80
- LOG.warning("Unable to find device %s", target_obj.id)
81
-
82
- device_gui = gui.add_device(
83
- root, device, f"{target_obj.host}:{target_obj.port}"
84
- )
85
-
86
- thread = Thread(
87
- target=partial(modbus.target_runner, stop, target_obj, device_gui),
88
- name=device.name,
89
- )
90
- thread.start()
91
-
92
- gui.run()
93
- finally:
94
- stop.set()
95
-
96
-
97
- @app.command()
98
- def mdns():
99
- from zeroconf import (
100
- Zeroconf,
101
- ServiceBrowser,
102
- ServiceStateChange,
103
- IPVersion,
104
- ServiceInfo,
105
- )
106
- import requests
107
-
108
- zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
109
- services = ["_modbus._tcp.local."]
110
-
111
- device_runners: dict[tuple, Event] = {}
112
-
113
- def on_device_added(device_key: tuple, info: ServiceInfo):
114
- addresses = info.parsed_addresses()
115
- properties = info.decoded_properties
116
- model_port = properties.get("model-port")
117
- model_path = properties.get("model-path")
118
- device_id = properties.get("device-id")
119
- if not model_port or not model_path or not device_id:
120
- LOG.error("Missing model info for %s", info.name)
121
- return
122
-
123
- device_stop = Event()
124
- device_runners[device_key] = device_stop
125
-
126
- def _run():
127
- for address in info.parsed_addresses(IPVersion.V4Only):
128
- model_url = f"http://{address}:{model_port}{model_path}"
129
- target_obj = Target(device_id, address, info.port)
130
- try:
131
- model_raw = requests.get(model_url).content
132
- break
133
- except Exception as exception:
134
- LOG.debug("Failed to connect to %s:%r", model_url, exception)
135
- continue
136
- else:
137
- LOG.error("Unable to get model for %s", info.name)
138
- return
139
-
140
-
141
- model = RootModel.parse_raw(model_raw)
142
- for device in model.devices:
143
- if str(device.id) == device_id:
144
- break
145
- else:
146
- LOG.error("Can't find device %s in model", device_id)
147
- return
148
-
149
- LOG.info("Adding device %s", info.name)
150
- device_gui = gui.add_device(model, device, address)
151
- modbus.target_runner(device_stop, target_obj, device_gui)
152
- device_gui.close()
153
-
154
- thread = Thread(
155
- target=_run,
156
- )
157
- thread.start()
158
-
159
- def on_service_state_change(
160
- zeroconf: Zeroconf,
161
- service_type: str,
162
- name: str,
163
- state_change: ServiceStateChange,
164
- ) -> None:
165
- print(f"Service {name}: {state_change}")
166
-
167
- device_key = (service_type, name)
168
-
169
- if state_change is ServiceStateChange.Added:
170
- info = zeroconf.get_service_info(service_type, name)
171
- if info and info.port:
172
- on_device_added(device_key, info)
173
-
174
- if state_change is ServiceStateChange.Removed:
175
- device_stop = device_runners.pop(device_key, None)
176
- if device_stop:
177
- device_stop.set()
178
-
179
- gui.setup()
180
-
181
- ServiceBrowser(zeroconf, services, handlers=[on_service_state_change])
182
-
183
- gui.run()
184
-
185
-
186
- if __name__ == "__main__":
187
- 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()
@@ -1,157 +1,175 @@
1
- import math
2
- from dataclasses import dataclass
3
-
4
- import dearpygui.dearpygui as dpg
5
- from upgen.model.uphy import DataType, Device, Signal, Slot, Parameter
6
- from upgen.model.uphy import Root as RootModel
7
-
8
- datatype_to_min_max = {
9
- DataType.INT8: (-(2**7), 2**7 - 1),
10
- DataType.INT16: (-(2**15), 2**15 - 1),
11
- DataType.INT32: (-(2**31), 2**31 - 1),
12
- DataType.UINT8: (0, 2**8 - 1),
13
- DataType.UINT16: (0, 2**16 - 1),
14
- DataType.UINT32: (0, 2**32 - 1),
15
- DataType.REAL32: (-math.inf, math.inf),
16
- }
17
-
18
-
19
- @dataclass
20
- class InputGUI:
21
- signal: Signal
22
- value_id: list[str | int]
23
-
24
- def __init__(self, signal: Signal):
25
- self.signal = signal
26
- dpg.add_text(signal.name)
27
- with dpg.group():
28
- self.value_id = [
29
- dpg.add_text("N/A") for _ in range(signal.array_length or 1)
30
- ]
31
-
32
- def update(self, value: list[int | float] | None):
33
- if value is None:
34
- for ix in range(self.signal.array_length or 1):
35
- dpg.set_value(self.value_id[ix], "N/A")
36
- else:
37
- for ix in range(self.signal.array_length or 1):
38
- dpg.set_value(self.value_id[ix], value[ix])
39
-
40
-
41
- @dataclass
42
- class OutputGUI:
43
- signal: Signal
44
- value_id: list[str | int]
45
- value: list[int] | None = None
46
-
47
- def __init__(self, signal: Signal):
48
- self.signal = signal
49
- self.value = [0] * (signal.array_length or 1)
50
- dpg.add_text(signal.name)
51
- min, max = datatype_to_min_max[signal.datatype]
52
- with dpg.group():
53
- self.value_id = [
54
- dpg.add_input_int(
55
- width=100,
56
- callback=lambda sender, app_data, user_data: self.callback(
57
- ix, app_data
58
- ),
59
- min_value=min,
60
- max_value=max,
61
- )
62
- for ix in range(signal.array_length or 1)
63
- ]
64
-
65
- def callback(self, ix: int, value: int):
66
- self.value[ix] = value
67
-
68
-
69
- class ParamGUI:
70
- def __init__(self, signal: Parameter):
71
- self.signal = signal
72
-
73
-
74
- @dataclass
75
- class SlotGUI:
76
- inputs: dict[str, InputGUI]
77
- outputs: dict[str, OutputGUI]
78
- params: dict[str, ParamGUI]
79
-
80
-
81
- @dataclass
82
- class DeviceGUI:
83
- widget: int | str
84
- slots: dict[str, SlotGUI]
85
- status_id: str | int
86
-
87
- def update_status(self, status: str):
88
- dpg.set_value(self.status_id, status)
89
-
90
- def close(self):
91
- dpg.delete_item(self.widget)
92
-
93
-
94
- def add_device(model: RootModel, device: Device, suffix: str) -> DeviceGUI:
95
- width = 300
96
- windows = dpg.get_windows()
97
- for ix, window in enumerate(windows):
98
- dpg.set_item_pos(window, [ix * width, 0])
99
- ix = len(windows)
100
-
101
- with dpg.window(
102
- width=width,
103
- pos=[ix * width, 0],
104
- autosize=True,
105
- label=f"{device.name} - {suffix}",
106
- ) as window:
107
- with dpg.group(horizontal=True):
108
- dpg.add_text("Status")
109
- status_id = dpg.add_text()
110
-
111
- slot_guis = {}
112
- for slot in device.slots:
113
- with dpg.child_window(auto_resize_y=True):
114
- slot_gui = _add_slot(model, slot)
115
- slot_guis[slot.name] = slot_gui
116
-
117
- return DeviceGUI(window, slot_guis, status_id)
118
-
119
-
120
- def _add_slot(model: RootModel, slot: Slot):
121
- module = model.get_module(slot.module)
122
- dpg.add_text(f"{slot.name}")
123
-
124
- dpg.add_separator()
125
-
126
- with dpg.group(indent=10), dpg.table(label=slot.name, header_row=False):
127
- dpg.add_table_column()
128
- dpg.add_table_column()
129
-
130
- inputs: dict[str, InputGUI] = {}
131
- for signal in module.inputs:
132
- with dpg.table_row():
133
- inputs[signal.id] = InputGUI(signal)
134
-
135
- outputs: dict[str, OutputGUI] = {}
136
- for signal in module.outputs:
137
- with dpg.table_row():
138
- outputs[signal.id] = OutputGUI(signal)
139
-
140
- params: dict[str, ParamGUI] = {}
141
- for signal in module.parameters:
142
- with dpg.table_row():
143
- params[signal.id] = ParamGUI(signal)
144
-
145
- return SlotGUI(inputs=inputs, outputs=outputs, params=params)
146
-
147
-
148
- def setup():
149
- dpg.create_context()
150
- dpg.create_viewport(title="U-Phy Controller", width=800)
151
- dpg.setup_dearpygui()
152
-
153
-
154
- def run():
155
- dpg.show_viewport()
156
- dpg.start_dearpygui()
157
- dpg.destroy_context()
1
+ import math
2
+ from dataclasses import dataclass
3
+
4
+ import dearpygui.dearpygui as dpg
5
+ from upgen.model.uphy import DataType
6
+ from upgen.model.runtime import Root as RootModel, Device, Slot, Signal, Parameter
7
+
8
+ datatype_to_min_max = {
9
+ DataType.INT8: (-(2**7), 2**7 - 1),
10
+ DataType.INT16: (-(2**15), 2**15 - 1),
11
+ DataType.INT32: (-(2**31), 2**31 - 1),
12
+ DataType.UINT8: (0, 2**8 - 1),
13
+ DataType.UINT16: (0, 2**16 - 1),
14
+ DataType.UINT32: (0, 2**31 - 1), # Incorrect max but larger values do not work with dpg
15
+ DataType.REAL32: (-math.inf, math.inf),
16
+ }
17
+
18
+
19
+ @dataclass
20
+ class InputGUI:
21
+ signal: Signal
22
+ value_id: list[str | int]
23
+
24
+ def __init__(self, signal: Signal):
25
+ self.signal = signal
26
+ dpg.add_text(signal.name)
27
+ with dpg.group():
28
+ self.value_id = [
29
+ dpg.add_text("N/A") for _ in range(signal.array_length or 1)
30
+ ]
31
+
32
+ def update(self, value: list[int | float] | None):
33
+ if value is None:
34
+ for ix in range(self.signal.array_length or 1):
35
+ dpg.set_value(self.value_id[ix], "N/A")
36
+ else:
37
+ for ix in range(self.signal.array_length or 1):
38
+ dpg.set_value(self.value_id[ix], value[ix])
39
+
40
+
41
+ @dataclass
42
+ class OutputGUI:
43
+ signal: Signal
44
+ value_id: list[str | int]
45
+ value: list[int] | None = None
46
+
47
+ def __init__(self, signal: Signal):
48
+ self.signal = signal
49
+ self.value = [0] * (signal.array_length or 1)
50
+ dpg.add_text(signal.name)
51
+ min, max = datatype_to_min_max[signal.datatype]
52
+ widget = dpg.add_input_float if signal.datatype == DataType.REAL32 else dpg.add_input_int
53
+ with dpg.group():
54
+ self.value_id = [
55
+ widget(
56
+ width=100,
57
+ callback=lambda sender, app_data, user_data: self.callback(
58
+ ix, app_data
59
+ ),
60
+ min_value=min,
61
+ max_value=max,
62
+ min_clamped=True,
63
+ max_clamped=True,
64
+ )
65
+ for ix in range(signal.array_length or 1)
66
+ ]
67
+
68
+ def callback(self, ix: int, value: int):
69
+ self.value[ix] = value
70
+
71
+
72
+ class ParamGUI:
73
+ def __init__(self, signal: Parameter):
74
+ self.signal = signal
75
+
76
+
77
+ @dataclass
78
+ class SlotGUI:
79
+ inputs: dict[str, InputGUI]
80
+ outputs: dict[str, OutputGUI]
81
+ params: dict[str, ParamGUI]
82
+
83
+
84
+ @dataclass
85
+ class DeviceGUI:
86
+ widget: int | str
87
+ slots: dict[str, SlotGUI]
88
+ status_id: str | int
89
+
90
+ def update_status(self, status: str):
91
+ dpg.set_value(self.status_id, status)
92
+
93
+ def close(self):
94
+ dpg.delete_item(self.widget)
95
+
96
+
97
+ def add_device(model: RootModel, device: Device, suffix: str) -> DeviceGUI:
98
+ width = 300
99
+ windows = [
100
+ window
101
+ for window in dpg.get_windows()
102
+ if dpg.get_item_user_data(window) == "device"
103
+ ]
104
+ for ix, window in enumerate(windows):
105
+ dpg.set_item_pos(window, [ix * width, 0])
106
+ ix = len(windows)
107
+
108
+ with dpg.window(
109
+ width=width,
110
+ pos=[ix * width, 0],
111
+ autosize=True,
112
+ label=f"{device.name} - {suffix}",
113
+ user_data="device"
114
+ ) as window:
115
+ with dpg.group(horizontal=True):
116
+ dpg.add_text("Status")
117
+ status_id = dpg.add_text()
118
+
119
+ slot_guis = {}
120
+ for slot in device.slots:
121
+ with dpg.child_window(auto_resize_y=True):
122
+ slot_gui = _add_slot(model, slot)
123
+ slot_guis[slot.name] = slot_gui
124
+
125
+ return DeviceGUI(window, slot_guis, status_id)
126
+
127
+
128
+ def _add_slot(model: RootModel, slot: Slot):
129
+ dpg.add_text(f"{slot.name}")
130
+
131
+ dpg.add_separator()
132
+
133
+ with dpg.group(indent=10), dpg.table(label=slot.name, header_row=False):
134
+ dpg.add_table_column()
135
+ dpg.add_table_column()
136
+
137
+ inputs: dict[str, InputGUI] = {}
138
+ for signal in slot.inputs:
139
+ with dpg.table_row():
140
+ inputs[signal.id] = InputGUI(signal)
141
+
142
+ outputs: dict[str, OutputGUI] = {}
143
+ for signal in slot.outputs:
144
+ with dpg.table_row():
145
+ outputs[signal.id] = OutputGUI(signal)
146
+
147
+ params: dict[str, ParamGUI] = {}
148
+ for signal in slot.parameters:
149
+ with dpg.table_row():
150
+ params[signal.id] = ParamGUI(signal)
151
+
152
+ return SlotGUI(inputs=inputs, outputs=outputs, params=params)
153
+
154
+
155
+ def setup():
156
+ dpg.create_context()
157
+ def on_key_q(s, a, u):
158
+ if dpg.is_key_down(dpg.mvKey_LControl):
159
+ dpg.stop_dearpygui()
160
+
161
+ def on_key_esc(s, a, u):
162
+ dpg.stop_dearpygui()
163
+
164
+ with dpg.handler_registry():
165
+ dpg.add_key_press_handler(key=dpg.mvKey_Q, callback=on_key_q)
166
+ dpg.add_key_press_handler(key=dpg.mvKey_Escape, callback=on_key_esc)
167
+
168
+ dpg.create_viewport(title="U-Phy Controller", width=800)
169
+ dpg.setup_dearpygui()
170
+
171
+
172
+ def run():
173
+ dpg.show_viewport()
174
+ dpg.start_dearpygui()
175
+ dpg.destroy_context()
@@ -1,103 +1,108 @@
1
- import logging
2
- from threading import Event
3
- from time import sleep
4
-
5
- import pymodbus.client
6
- from pymodbus.client import ModbusBaseClient, ModbusTcpClient
7
- import pymodbus.exceptions
8
- from upgen.model.uphy import DataType, Signal
9
-
10
- from . import Target, gui
11
-
12
- datatype_to_modbus_type = {
13
- DataType.INT8: ModbusBaseClient.DATATYPE.INT16,
14
- DataType.INT16: ModbusBaseClient.DATATYPE.INT16,
15
- DataType.INT32: ModbusBaseClient.DATATYPE.INT32,
16
- DataType.UINT8: ModbusBaseClient.DATATYPE.UINT16,
17
- DataType.UINT16: ModbusBaseClient.DATATYPE.UINT16,
18
- DataType.UINT32: ModbusBaseClient.DATATYPE.UINT32,
19
- DataType.REAL32: ModbusBaseClient.DATATYPE.FLOAT32,
20
- }
21
-
22
-
23
- def get_type(datatype: DataType) -> tuple[ModbusBaseClient.DATATYPE, int]:
24
- data_type = datatype_to_modbus_type[datatype]
25
- data_type_bits = data_type.value[1] * 16
26
- return data_type, data_type_bits
27
-
28
-
29
- def signal_to_regs_and_type(signal: Signal):
30
- data_type, data_bits = get_type(signal.datatype)
31
- data_regs = bits_to_regs(data_bits) * (signal.array_length or 1)
32
- return data_regs, data_type
33
-
34
-
35
- def bits_to_regs(bits: int) -> None:
36
- return (bits + 15) // 16
37
-
38
-
39
- def target_runner(stop: Event, target: Target, device_gui: gui.DeviceGUI):
40
- device_gui.update_status("CONNECTING")
41
- client = ModbusTcpClient(target.host, port=target.port)
42
-
43
- while not stop.is_set():
44
- try:
45
- client.connect()
46
-
47
- base = 0
48
- for slot in device_gui.slots.values():
49
- for input in slot.inputs.values():
50
- count, data_type = signal_to_regs_and_type(input.signal)
51
- index = base
52
- base += count
53
- try:
54
- response = client.read_holding_registers(index, count)
55
- if response.isError():
56
- raise pymodbus.ModbusException("Error")
57
- except pymodbus.exceptions.ConnectionException:
58
- raise
59
- except pymodbus.ModbusException as exception:
60
- logging.error("Error on read %d - %r", index, exception)
61
- input.update(None)
62
- continue
63
-
64
- values = [
65
- client.convert_from_registers(
66
- response.registers[ix : ix + data_type.value[1]], data_type
67
- )
68
- for ix in range(0, count, data_type.value[1])
69
- ]
70
- input.update(values)
71
-
72
- for output in slot.outputs.values():
73
- count, data_type = signal_to_regs_and_type(input.signal)
74
- index = base
75
- base += count
76
-
77
- if output.value is None:
78
- continue
79
-
80
- payload = []
81
- for value in output.value:
82
- payload.extend(client.convert_to_registers(value, data_type))
83
-
84
- try:
85
- response = client.write_registers(index, payload)
86
- if response.isError():
87
- raise pymodbus.ModbusException("Error")
88
- except pymodbus.exceptions.ConnectionException:
89
- raise
90
- except pymodbus.ModbusException as exception:
91
- logging.error("Error on read %d - %r", index, exception)
92
-
93
- for param in slot.params.values():
94
- count = bits_to_regs(param.signal.bitlen)
95
- index = base
96
- base += count
97
- except pymodbus.ModbusException as exception:
98
- device_gui.update_status("ERROR")
99
- logging.error("%r", exception)
100
- sleep(2)
101
- else:
102
- device_gui.update_status("CONNECTED")
103
- sleep(0.5)
1
+ import logging
2
+ from threading import Event
3
+ from time import sleep
4
+
5
+ import pymodbus.client
6
+ from pymodbus.client import ModbusBaseClient, ModbusTcpClient
7
+ import pymodbus.exceptions
8
+ from upgen.model.uphy import DataType, Signal
9
+ import struct
10
+
11
+ from . import Target, gui
12
+
13
+ datatype_to_modbus_type = {
14
+ DataType.INT8: ModbusBaseClient.DATATYPE.INT16,
15
+ DataType.INT16: ModbusBaseClient.DATATYPE.INT16,
16
+ DataType.INT32: ModbusBaseClient.DATATYPE.INT32,
17
+ DataType.UINT8: ModbusBaseClient.DATATYPE.UINT16,
18
+ DataType.UINT16: ModbusBaseClient.DATATYPE.UINT16,
19
+ DataType.UINT32: ModbusBaseClient.DATATYPE.UINT32,
20
+ DataType.REAL32: ModbusBaseClient.DATATYPE.FLOAT32,
21
+ }
22
+
23
+
24
+ def get_type(datatype: DataType) -> tuple[ModbusBaseClient.DATATYPE, int]:
25
+ data_type = datatype_to_modbus_type[datatype]
26
+ data_type_bits = data_type.value[1] * 16
27
+ return data_type, data_type_bits
28
+
29
+
30
+ def signal_to_regs_and_type(signal: Signal):
31
+ data_type, data_bits = get_type(signal.datatype)
32
+ data_regs = bits_to_regs(data_bits) * (signal.array_length or 1)
33
+ return data_regs, data_type
34
+
35
+
36
+ def bits_to_regs(bits: int) -> None:
37
+ return (bits + 15) // 16
38
+
39
+
40
+ def target_runner(stop: Event, target: Target, device_gui: gui.DeviceGUI):
41
+ device_gui.update_status("CONNECTING")
42
+ client = ModbusTcpClient(target.host, port=target.port)
43
+
44
+ while not stop.is_set():
45
+ try:
46
+ client.connect()
47
+
48
+ base = 0
49
+ for slot in device_gui.slots.values():
50
+ for input in slot.inputs.values():
51
+ count, data_type = signal_to_regs_and_type(input.signal)
52
+ index = base
53
+ base += count
54
+ try:
55
+ response = client.read_holding_registers(index, count=count)
56
+ if response.isError():
57
+ raise pymodbus.ModbusException("Error")
58
+ except pymodbus.exceptions.ConnectionException:
59
+ raise
60
+ except pymodbus.ModbusException as exception:
61
+ logging.error("Error on read %d - %r", index, exception)
62
+ input.update(None)
63
+ continue
64
+
65
+ values = [
66
+ client.convert_from_registers(
67
+ response.registers[ix : ix + data_type.value[1]], data_type
68
+ )
69
+ for ix in range(0, count, data_type.value[1])
70
+ ]
71
+ input.update(values)
72
+
73
+ for output in slot.outputs.values():
74
+ count, data_type = signal_to_regs_and_type(output.signal)
75
+ index = base
76
+ base += count
77
+
78
+ if output.value is None:
79
+ continue
80
+
81
+ try:
82
+ payload = []
83
+ for value in output.value:
84
+ payload.extend(client.convert_to_registers(value, data_type))
85
+ except struct.error:
86
+ logging.error("Bad payload")
87
+ continue
88
+
89
+ try:
90
+ response = client.write_registers(index, payload)
91
+ if response.isError():
92
+ raise pymodbus.ModbusException("Error")
93
+ except pymodbus.exceptions.ConnectionException:
94
+ raise
95
+ except pymodbus.ModbusException as exception:
96
+ logging.error("Error on read %d - %r", index, exception)
97
+
98
+ for param in slot.params.values():
99
+ count = bits_to_regs(param.signal.bitlen)
100
+ index = base
101
+ base += count
102
+ except pymodbus.ModbusException as exception:
103
+ device_gui.update_status("ERROR")
104
+ logging.error("%r", exception)
105
+ sleep(2)
106
+ else:
107
+ device_gui.update_status("CONNECTED")
108
+ sleep(0.5)
@@ -1,43 +1,51 @@
1
- Metadata-Version: 2.2
2
- Name: uphy-controller
3
- Version: 0.1.0.dev2809
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.7.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-tcp
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-tcp --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
43
- ```
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,6 +1,7 @@
1
1
  dearpygui>=2.0.0
2
- pymodbus>=3.7.3
2
+ pymodbus<3.9.0,>=3.8.3
3
3
  requests>=2.32.3
4
4
  rich>=13.9.2
5
5
  typer>=0.12.5
6
+ uphy
6
7
  zeroconf>=0.135.0