uphy-controller 1.0.2__tar.gz → 1.1.0__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.
@@ -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,47 +1,47 @@
1
- Metadata-Version: 2.4
2
- Name: uphy-controller
3
- Version: 1.0.2
4
- Summary: U-Phy Controller
5
- License-Expression: LicenseRef-RT-Labs-Commercial-Agreement
6
- Project-URL: RT-Labs, https://rt-labs.com
7
- Project-URL: U-Phy, https://rt-labs.com/u-phy
8
- Project-URL: Documentation, https://docs.rt-labs.com/u-phy
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE.txt
12
- Requires-Dist: dearpygui>=2.0.0
13
- Requires-Dist: pymodbus<3.9.0,>=3.8.3
14
- Requires-Dist: requests>=2.32.3
15
- Requires-Dist: rich>=13.9.2
16
- Requires-Dist: typer>=0.12.5
17
- Requires-Dist: uphy
18
- Requires-Dist: zeroconf>=0.135.0
19
- Dynamic: license-file
20
-
21
- # U-Phy Controller
22
-
23
- This contain a python based U-Phy device controller that act as a fieldbus master/controller.
24
-
25
- Supported fieldbuses:
26
-
27
- - modbus
28
-
29
- ## Install
30
-
31
- ```sh
32
- uv pip install uphy-controller
33
- ```
34
-
35
- ## Workflow
36
-
37
- ```sh
38
- # Start up controller against an existing device
39
- uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
40
- ```
41
-
42
-
43
- ```sh
44
- # Start up controller against discovered devices that expose their model via
45
- # mdns.
46
- uphy-controller mdns
47
- ```
1
+ Metadata-Version: 2.4
2
+ Name: uphy-controller
3
+ Version: 1.1.0
4
+ Summary: U-Phy Controller
5
+ License-Expression: LicenseRef-RT-Labs-Commercial-Agreement
6
+ Project-URL: RT-Labs, https://rt-labs.com
7
+ Project-URL: U-Phy, https://rt-labs.com/u-phy
8
+ Project-URL: Documentation, https://docs.rt-labs.com/u-phy
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: dearpygui>=2.0.0
13
+ Requires-Dist: pymodbus<3.9.0,>=3.8.3
14
+ Requires-Dist: requests>=2.32.3
15
+ Requires-Dist: rich>=13.9.2
16
+ Requires-Dist: typer>=0.12.5
17
+ Requires-Dist: uphy
18
+ Requires-Dist: zeroconf>=0.135.0
19
+ Dynamic: license-file
20
+
21
+ # U-Phy Controller
22
+
23
+ This contain a python based U-Phy device controller that act as a fieldbus master/controller.
24
+
25
+ Supported fieldbuses:
26
+
27
+ - modbus
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ uv pip install uphy-controller
33
+ ```
34
+
35
+ ## Workflow
36
+
37
+ ```sh
38
+ # Start up controller against an existing device
39
+ uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
40
+ ```
41
+
42
+
43
+ ```sh
44
+ # Start up controller against discovered devices that expose their model via
45
+ # mdns.
46
+ uphy-controller mdns
47
+ ```
@@ -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,33 +1,33 @@
1
- [project]
2
- name = "uphy-controller"
3
- version = "1.0.2"
4
- description = "U-Phy Controller"
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 = "LicenseRef-RT-Labs-Commercial-Agreement"
17
- license-files = [
18
- "LICENSE.txt"
19
- ]
20
-
21
- [project.urls]
22
- RT-Labs = "https://rt-labs.com"
23
- U-Phy = "https://rt-labs.com/u-phy"
24
- Documentation = "https://docs.rt-labs.com/u-phy"
25
-
26
- [project.scripts]
27
- uphy-controller = "uphy.controller.__main__:app"
28
-
29
- [project.entry-points.'uphy.cli']
30
- controller = 'uphy.controller.__main__:app'
31
-
32
- [tool.uv.sources]
33
- uphy = { workspace = true }
1
+ [project]
2
+ name = "uphy-controller"
3
+ version = "1.1.0"
4
+ description = "U-Phy Controller"
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 = "LicenseRef-RT-Labs-Commercial-Agreement"
17
+ license-files = [
18
+ "LICENSE.txt"
19
+ ]
20
+
21
+ [project.urls]
22
+ RT-Labs = "https://rt-labs.com"
23
+ U-Phy = "https://rt-labs.com/u-phy"
24
+ Documentation = "https://docs.rt-labs.com/u-phy"
25
+
26
+ [project.scripts]
27
+ uphy-controller = "uphy.controller.__main__:app"
28
+
29
+ [project.entry-points.'uphy.cli']
30
+ controller = 'uphy.controller.__main__:app'
31
+
32
+ [tool.uv.sources]
33
+ 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,7 +1,7 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- @dataclass
5
- class Target:
6
- host: str
7
- 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.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
+ 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,175 +1,175 @@
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.name] = InputGUI(signal)
141
-
142
- outputs: dict[str, OutputGUI] = {}
143
- for signal in slot.outputs:
144
- with dpg.table_row():
145
- outputs[signal.name] = OutputGUI(signal)
146
-
147
- params: dict[str, ParamGUI] = {}
148
- for signal in slot.parameters:
149
- with dpg.table_row():
150
- params[signal.name] = 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
+ 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.name] = InputGUI(signal)
141
+
142
+ outputs: dict[str, OutputGUI] = {}
143
+ for signal in slot.outputs:
144
+ with dpg.table_row():
145
+ outputs[signal.name] = OutputGUI(signal)
146
+
147
+ params: dict[str, ParamGUI] = {}
148
+ for signal in slot.parameters:
149
+ with dpg.table_row():
150
+ params[signal.name] = 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,108 +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
- 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
+ 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,47 +1,47 @@
1
- Metadata-Version: 2.4
2
- Name: uphy-controller
3
- Version: 1.0.2
4
- Summary: U-Phy Controller
5
- License-Expression: LicenseRef-RT-Labs-Commercial-Agreement
6
- Project-URL: RT-Labs, https://rt-labs.com
7
- Project-URL: U-Phy, https://rt-labs.com/u-phy
8
- Project-URL: Documentation, https://docs.rt-labs.com/u-phy
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE.txt
12
- Requires-Dist: dearpygui>=2.0.0
13
- Requires-Dist: pymodbus<3.9.0,>=3.8.3
14
- Requires-Dist: requests>=2.32.3
15
- Requires-Dist: rich>=13.9.2
16
- Requires-Dist: typer>=0.12.5
17
- Requires-Dist: uphy
18
- Requires-Dist: zeroconf>=0.135.0
19
- Dynamic: license-file
20
-
21
- # U-Phy Controller
22
-
23
- This contain a python based U-Phy device controller that act as a fieldbus master/controller.
24
-
25
- Supported fieldbuses:
26
-
27
- - modbus
28
-
29
- ## Install
30
-
31
- ```sh
32
- uv pip install uphy-controller
33
- ```
34
-
35
- ## Workflow
36
-
37
- ```sh
38
- # Start up controller against an existing device
39
- uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
40
- ```
41
-
42
-
43
- ```sh
44
- # Start up controller against discovered devices that expose their model via
45
- # mdns.
46
- uphy-controller mdns
47
- ```
1
+ Metadata-Version: 2.4
2
+ Name: uphy-controller
3
+ Version: 1.1.0
4
+ Summary: U-Phy Controller
5
+ License-Expression: LicenseRef-RT-Labs-Commercial-Agreement
6
+ Project-URL: RT-Labs, https://rt-labs.com
7
+ Project-URL: U-Phy, https://rt-labs.com/u-phy
8
+ Project-URL: Documentation, https://docs.rt-labs.com/u-phy
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: dearpygui>=2.0.0
13
+ Requires-Dist: pymodbus<3.9.0,>=3.8.3
14
+ Requires-Dist: requests>=2.32.3
15
+ Requires-Dist: rich>=13.9.2
16
+ Requires-Dist: typer>=0.12.5
17
+ Requires-Dist: uphy
18
+ Requires-Dist: zeroconf>=0.135.0
19
+ Dynamic: license-file
20
+
21
+ # U-Phy Controller
22
+
23
+ This contain a python based U-Phy device controller that act as a fieldbus master/controller.
24
+
25
+ Supported fieldbuses:
26
+
27
+ - modbus
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ uv pip install uphy-controller
33
+ ```
34
+
35
+ ## Workflow
36
+
37
+ ```sh
38
+ # Start up controller against an existing device
39
+ uphy-controller modbus --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
40
+ ```
41
+
42
+
43
+ ```sh
44
+ # Start up controller against discovered devices that expose their model via
45
+ # mdns.
46
+ uphy-controller mdns
47
+ ```