uphy-controller 0.1.0.dev2794__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uphy_controller-0.1.0.dev2794/LICENSE.txt +5 -0
- uphy_controller-0.1.0.dev2794/PKG-INFO +42 -0
- uphy_controller-0.1.0.dev2794/README.md +20 -0
- uphy_controller-0.1.0.dev2794/pyproject.toml +26 -0
- uphy_controller-0.1.0.dev2794/setup.cfg +4 -0
- uphy_controller-0.1.0.dev2794/src/uphy/__init__.py +2 -0
- uphy_controller-0.1.0.dev2794/src/uphy/controller/__init__.py +8 -0
- uphy_controller-0.1.0.dev2794/src/uphy/controller/__main__.py +187 -0
- uphy_controller-0.1.0.dev2794/src/uphy/controller/gui.py +157 -0
- uphy_controller-0.1.0.dev2794/src/uphy/controller/modbus.py +103 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/PKG-INFO +42 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/SOURCES.txt +14 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/dependency_links.txt +1 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/entry_points.txt +5 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/requires.txt +5 -0
- uphy_controller-0.1.0.dev2794/src/uphy_controller.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: uphy-controller
|
|
3
|
+
Version: 0.1.0.dev2794
|
|
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: rich>=13.9.2
|
|
20
|
+
Requires-Dist: typer>=0.12.5
|
|
21
|
+
Requires-Dist: zeroconf>=0.135.0
|
|
22
|
+
|
|
23
|
+
# U-Phy Controller
|
|
24
|
+
|
|
25
|
+
This contain a python based U-Phy device controller that act as a fieldbus master/controller.
|
|
26
|
+
|
|
27
|
+
Supported fieldbuses:
|
|
28
|
+
|
|
29
|
+
- modbus-tcp
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv pip install uphy-controller
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Workflow
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# Start up controller against an existing device
|
|
41
|
+
uphy-controller modbus-tcp --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
|
|
42
|
+
```
|
|
@@ -0,0 +1,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-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
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uphy-controller"
|
|
3
|
+
version = "0.1.0.dev2794"
|
|
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
|
+
"rich>=13.9.2",
|
|
11
|
+
"typer>=0.12.5",
|
|
12
|
+
"zeroconf>=0.135.0",
|
|
13
|
+
]
|
|
14
|
+
license = {file = "LICENSE.txt"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
RT-Labs = "https://rt-labs.com"
|
|
19
|
+
U-Phy = "https://rt-labs.com/u-phy"
|
|
20
|
+
Documentation = "https://docs.rt-labs.com/u-phy"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
uphy-controller = "uphy.controller.__main__:app"
|
|
24
|
+
|
|
25
|
+
[project.entry-points.'uphy.cli']
|
|
26
|
+
controller = 'uphy.controller.__main__:app'
|
|
@@ -0,0 +1,187 @@
|
|
|
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()
|
|
@@ -0,0 +1,157 @@
|
|
|
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()
|
|
@@ -0,0 +1,103 @@
|
|
|
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)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: uphy-controller
|
|
3
|
+
Version: 0.1.0.dev2794
|
|
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: rich>=13.9.2
|
|
20
|
+
Requires-Dist: typer>=0.12.5
|
|
21
|
+
Requires-Dist: zeroconf>=0.135.0
|
|
22
|
+
|
|
23
|
+
# U-Phy Controller
|
|
24
|
+
|
|
25
|
+
This contain a python based U-Phy device controller that act as a fieldbus master/controller.
|
|
26
|
+
|
|
27
|
+
Supported fieldbuses:
|
|
28
|
+
|
|
29
|
+
- modbus-tcp
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv pip install uphy-controller
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Workflow
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# Start up controller against an existing device
|
|
41
|
+
uphy-controller modbus-tcp --model [PATH_TO_MODEL] --target [UUID]:[HOST]:[PORT]
|
|
42
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/uphy/__init__.py
|
|
5
|
+
src/uphy/controller/__init__.py
|
|
6
|
+
src/uphy/controller/__main__.py
|
|
7
|
+
src/uphy/controller/gui.py
|
|
8
|
+
src/uphy/controller/modbus.py
|
|
9
|
+
src/uphy_controller.egg-info/PKG-INFO
|
|
10
|
+
src/uphy_controller.egg-info/SOURCES.txt
|
|
11
|
+
src/uphy_controller.egg-info/dependency_links.txt
|
|
12
|
+
src/uphy_controller.egg-info/entry_points.txt
|
|
13
|
+
src/uphy_controller.egg-info/requires.txt
|
|
14
|
+
src/uphy_controller.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uphy
|