uphy-device 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_device-0.1.0.dev2794/LICENSE.txt +5 -0
- uphy_device-0.1.0.dev2794/PKG-INFO +33 -0
- uphy_device-0.1.0.dev2794/README.md +7 -0
- uphy_device-0.1.0.dev2794/pyproject.toml +42 -0
- uphy_device-0.1.0.dev2794/setup.cfg +4 -0
- uphy_device-0.1.0.dev2794/setup.py +77 -0
- uphy_device-0.1.0.dev2794/uphy/device/__init__.py +251 -0
- uphy_device-0.1.0.dev2794/uphy/device/__main__.py +231 -0
- uphy_device-0.1.0.dev2794/uphy/device/api/__init__.cpp +102625 -0
- uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pxd +546 -0
- uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pyi +629 -0
- uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pyx +1740 -0
- uphy_device-0.1.0.dev2794/uphy/device/gui/__init__.py +44 -0
- uphy_device-0.1.0.dev2794/uphy/device/gui/dear.py +196 -0
- uphy_device-0.1.0.dev2794/uphy/device/gui/rich.py +40 -0
- uphy_device-0.1.0.dev2794/uphy/device/handler.py +49 -0
- uphy_device-0.1.0.dev2794/uphy/device/server/__init__.py +38 -0
- uphy_device-0.1.0.dev2794/uphy/device/server/bin/sample +0 -0
- uphy_device-0.1.0.dev2794/uphy/device/server/bin/server +0 -0
- uphy_device-0.1.0.dev2794/uphy/device/server/mdns.py +88 -0
- uphy_device-0.1.0.dev2794/uphy/device/share/digio.json +234 -0
- uphy_device-0.1.0.dev2794/uphy/device/share/set_network_parameters +95 -0
- uphy_device-0.1.0.dev2794/uphy/device/share/set_profinet_leds +36 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/PKG-INFO +33 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/SOURCES.txt +27 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/dependency_links.txt +1 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/entry_points.txt +5 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/requires.txt +9 -0
- uphy_device-0.1.0.dev2794/uphy_device.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: uphy-device
|
|
3
|
+
Version: 0.1.0.dev2794
|
|
4
|
+
Author-email: RT-Labs <support@rt-labs.com>
|
|
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: yarl>=1.15.2
|
|
18
|
+
Requires-Dist: typer>=0.12.5
|
|
19
|
+
Requires-Dist: rich>=13.9.2
|
|
20
|
+
Requires-Dist: dearpygui>=2.0.0
|
|
21
|
+
Requires-Dist: uphy-upgen>=0.1.0
|
|
22
|
+
Requires-Dist: yarl>=1.12.1
|
|
23
|
+
Requires-Dist: pyserial>=3.5
|
|
24
|
+
Requires-Dist: psutil>=6.1.0
|
|
25
|
+
Requires-Dist: zeroconf>=0.136.0
|
|
26
|
+
|
|
27
|
+
# U-Phy Device Tools
|
|
28
|
+
|
|
29
|
+
## Introduction
|
|
30
|
+
|
|
31
|
+
U-Phy Device tools support controlling and executing a U-Phy server to simulation and run a fieldbus device inside a python program.
|
|
32
|
+
|
|
33
|
+
See documentation at [https://docs.rt-labs.com/u-phy](https://docs.rt-labs.com/u-phy)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel", "Cython"]
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name="uphy-device"
|
|
6
|
+
version="0.1.0.dev2794"
|
|
7
|
+
authors = [
|
|
8
|
+
{name="RT-Labs", email="support@rt-labs.com"}
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">= 3.10"
|
|
11
|
+
|
|
12
|
+
license = {file = "LICENSE.txt"}
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"yarl>=1.15.2",
|
|
17
|
+
"typer>=0.12.5",
|
|
18
|
+
"rich>=13.9.2",
|
|
19
|
+
"dearpygui>=2.0.0",
|
|
20
|
+
"uphy-upgen>=0.1.0",
|
|
21
|
+
"yarl>=1.12.1",
|
|
22
|
+
"pyserial>=3.5",
|
|
23
|
+
"psutil>=6.1.0",
|
|
24
|
+
"zeroconf>=0.136.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
RT-Labs = "https://rt-labs.com"
|
|
29
|
+
U-Phy = "https://rt-labs.com/u-phy"
|
|
30
|
+
Documentation = "https://docs.rt-labs.com/u-phy"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
uphy-device = "uphy.device.__main__:app"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[tool.uv]
|
|
37
|
+
dev-dependencies = [
|
|
38
|
+
"ruff>=0.6.8",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.entry-points.'uphy.cli']
|
|
42
|
+
device = 'uphy.device.__main__:app'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# setup.py
|
|
2
|
+
|
|
3
|
+
from setuptools import setup, Extension
|
|
4
|
+
from Cython.Build import cythonize
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import shutil
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Copy the uphy install into package data
|
|
13
|
+
package_data = {}
|
|
14
|
+
if prefix := os.environ.get("LIBUPHY_INSTALL_PATH", None):
|
|
15
|
+
target = Path("uphy/device/server/bin")
|
|
16
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
17
|
+
shutil.copytree(Path(prefix) / "bin", target)
|
|
18
|
+
package_data["uphy.device.server"] = [
|
|
19
|
+
str(file.relative_to("uphy/device/server")) for file in target.rglob("*")
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
target = Path("uphy/device/share")
|
|
23
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
24
|
+
shutil.copytree(Path(prefix) / "share" / "uphy", target)
|
|
25
|
+
package_data["uphy.device"] = [
|
|
26
|
+
str(file.relative_to("uphy/device")) for file in target.rglob("*")
|
|
27
|
+
]
|
|
28
|
+
else:
|
|
29
|
+
logging.error("LIBUPHY_INSTALL_PATH not specified")
|
|
30
|
+
exit(-1)
|
|
31
|
+
|
|
32
|
+
include_dirs = [f"{prefix}/include", "src"]
|
|
33
|
+
library_dirs = [f"{prefix}/lib"]
|
|
34
|
+
libraries = ["uphy", "erpc", "osal", "upi-host"]
|
|
35
|
+
|
|
36
|
+
if sys.platform == "win32":
|
|
37
|
+
libraries.extend(["wsock32", "ws2_32", "winmm"])
|
|
38
|
+
server_name = "server.exe"
|
|
39
|
+
else:
|
|
40
|
+
server_name = "server"
|
|
41
|
+
|
|
42
|
+
extensions = [
|
|
43
|
+
Extension(
|
|
44
|
+
name="uphy.device.api",
|
|
45
|
+
sources=["uphy/device/api/*.pyx"],
|
|
46
|
+
include_dirs=include_dirs,
|
|
47
|
+
libraries=libraries,
|
|
48
|
+
library_dirs=library_dirs,
|
|
49
|
+
language="c++",
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
packages = [
|
|
54
|
+
"uphy.device",
|
|
55
|
+
"uphy.device.api",
|
|
56
|
+
"uphy.device.gui",
|
|
57
|
+
"uphy.device.server",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
package_data["uphy.device.api"] = ["__init__.pyx", "__init__.pxd"]
|
|
61
|
+
|
|
62
|
+
setup(
|
|
63
|
+
packages=packages,
|
|
64
|
+
package_data=package_data,
|
|
65
|
+
include_package_data=True,
|
|
66
|
+
ext_modules=cythonize(
|
|
67
|
+
extensions,
|
|
68
|
+
gdb_debug=True,
|
|
69
|
+
language_level="3",
|
|
70
|
+
compiler_directives={
|
|
71
|
+
"c_string_type": "bytes",
|
|
72
|
+
"c_string_encoding": "utf-8",
|
|
73
|
+
"embedsignature": "true",
|
|
74
|
+
"embedsignature.format": "python",
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from functools import partial
|
|
4
|
+
import importlib
|
|
5
|
+
from threading import Thread
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from uphy.device.api import (
|
|
9
|
+
Up,
|
|
10
|
+
ApiError,
|
|
11
|
+
Device,
|
|
12
|
+
SignalInfos,
|
|
13
|
+
ProfinetConfig,
|
|
14
|
+
EthercatDevice,
|
|
15
|
+
ModbusDevice,
|
|
16
|
+
)
|
|
17
|
+
from yarl import URL
|
|
18
|
+
import logging
|
|
19
|
+
from time import sleep
|
|
20
|
+
from .server import server_binary
|
|
21
|
+
from .gui import Gui, GuiExit, UpdateProtocol
|
|
22
|
+
import subprocess
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
|
|
25
|
+
from uphy.device import api
|
|
26
|
+
import upgen.model.uphy as uphy_model
|
|
27
|
+
from enum import Enum
|
|
28
|
+
import importlib.util
|
|
29
|
+
import importlib.resources
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Protocol(str, Enum):
|
|
36
|
+
ETHERCAT = "ethercat"
|
|
37
|
+
PROFINET = "profinet"
|
|
38
|
+
MODBUS = "modbus"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DeviceError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnsupportedError(DeviceError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DeviceHandlerBase(Up, ABC):
|
|
50
|
+
_update: UpdateProtocol
|
|
51
|
+
_status: str
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
device: Device,
|
|
56
|
+
vars: SignalInfos,
|
|
57
|
+
busconf: ProfinetConfig | EthercatDevice | ModbusDevice,
|
|
58
|
+
):
|
|
59
|
+
LOGGER.debug(device)
|
|
60
|
+
LOGGER.debug(vars)
|
|
61
|
+
LOGGER.debug(busconf)
|
|
62
|
+
|
|
63
|
+
super().__init__(device=device, vars=vars, busconf=busconf)
|
|
64
|
+
self._init()
|
|
65
|
+
|
|
66
|
+
def set_update_callback(self, update: UpdateProtocol):
|
|
67
|
+
self._update = update
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def gui(self, gui_arg: Gui) -> Generator[None, None, None]:
|
|
71
|
+
with gui.get(gui_arg, self.device) as update:
|
|
72
|
+
self.set_update_callback(update)
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def _init():
|
|
77
|
+
"""Handler started"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _set_outputs():
|
|
82
|
+
"""System initialized."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def _get_inputs():
|
|
87
|
+
"""Update data."""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _error_ind(self, error_code) -> None:
|
|
91
|
+
LOGGER.error("ERROR: error_code=%s", error_code)
|
|
92
|
+
self._status = f"Error: {error_code}"
|
|
93
|
+
|
|
94
|
+
def _avail(self) -> None:
|
|
95
|
+
LOGGER.debug("AVAIL")
|
|
96
|
+
self.read_outputs()
|
|
97
|
+
self._set_outputs()
|
|
98
|
+
|
|
99
|
+
def _sync(self) -> None:
|
|
100
|
+
LOGGER.debug("SYNC")
|
|
101
|
+
self._get_inputs()
|
|
102
|
+
self.write_inputs()
|
|
103
|
+
|
|
104
|
+
def _status_ind(self, status: int) -> None:
|
|
105
|
+
LOGGER.info("STATUS: %s", status)
|
|
106
|
+
self._status = str(status)
|
|
107
|
+
|
|
108
|
+
def _profinet_signal_led_ind(self) -> None:
|
|
109
|
+
LOGGER.info("PROFINET LED SIGNAL")
|
|
110
|
+
|
|
111
|
+
def _poll_ind(self) -> None:
|
|
112
|
+
LOGGER.debug("POLL")
|
|
113
|
+
|
|
114
|
+
self.read_outputs()
|
|
115
|
+
self._set_outputs()
|
|
116
|
+
|
|
117
|
+
self._get_inputs()
|
|
118
|
+
self.write_inputs()
|
|
119
|
+
|
|
120
|
+
if self._update:
|
|
121
|
+
self._update(status=self._status)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_sample_model() -> uphy_model.Root:
|
|
125
|
+
try:
|
|
126
|
+
with importlib.resources.as_file(importlib.resources.files(__name__)) as base:
|
|
127
|
+
if (path := base / "share" / "digio.json").exists():
|
|
128
|
+
return uphy_model.Root.parse_file(str(path))
|
|
129
|
+
except Exception as exception:
|
|
130
|
+
raise DeviceError(str(exception)) from exception
|
|
131
|
+
else:
|
|
132
|
+
raise DeviceError("Can't find sample model")
|
|
133
|
+
|
|
134
|
+
def _get_handler(handler: Optional[str]):
|
|
135
|
+
if handler:
|
|
136
|
+
spec = importlib.util.spec_from_file_location("uphy.device.handler", handler)
|
|
137
|
+
device_handler_module = importlib.util.module_from_spec(spec)
|
|
138
|
+
sys.modules["uphy.device.handler"] = device_handler_module
|
|
139
|
+
spec.loader.exec_module(device_handler_module)
|
|
140
|
+
else:
|
|
141
|
+
from . import handler as device_handler_module
|
|
142
|
+
|
|
143
|
+
return device_handler_module.DeviceHandler
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_device_config(model: uphy_model.Root, protocol: Protocol):
|
|
147
|
+
root = model
|
|
148
|
+
device = api.Device.from_model(root, root.devices[0])
|
|
149
|
+
vars = api.SignalInfos.from_slots(device.slots)
|
|
150
|
+
|
|
151
|
+
if protocol == Protocol.ETHERCAT:
|
|
152
|
+
config = api.EthercatDevice(root, root.devices[0])
|
|
153
|
+
elif protocol == Protocol.PROFINET:
|
|
154
|
+
config = api.ProfinetConfig.from_model(root, root.devices[0])
|
|
155
|
+
elif protocol == Protocol.MODBUS:
|
|
156
|
+
config = api.ModbusDevice(root, root.devices[0])
|
|
157
|
+
else:
|
|
158
|
+
raise UnsupportedError(f"Unsupported protocol: {protocol}")
|
|
159
|
+
|
|
160
|
+
return device, config, vars
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_device_handler(
|
|
164
|
+
protocol: Protocol, model: uphy_model.Root = None, handler: Optional[str] = None
|
|
165
|
+
) -> DeviceHandlerBase:
|
|
166
|
+
"""Create a device handler from protocol and model information."""
|
|
167
|
+
device_handler = _get_handler(handler)
|
|
168
|
+
device, config, vars = get_device_config(model, protocol)
|
|
169
|
+
return device_handler(device=device, vars=vars, busconf=config)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def init_transport_url(up: Up, transport: str):
|
|
173
|
+
url = URL(transport)
|
|
174
|
+
if url.scheme == "tcp":
|
|
175
|
+
up.tcp_transport_init(
|
|
176
|
+
url.host if url.host else "localhost", url.port if url.port else 5150
|
|
177
|
+
)
|
|
178
|
+
elif url.scheme == "":
|
|
179
|
+
up.serial_transport_init(transport)
|
|
180
|
+
else:
|
|
181
|
+
raise ApiError(f"Unknown transport {transport}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_client_blocking(up: Up, transport: str):
|
|
185
|
+
try:
|
|
186
|
+
LOGGER.info("Starting transport")
|
|
187
|
+
init_transport_url(up, transport)
|
|
188
|
+
|
|
189
|
+
up.rpc_init()
|
|
190
|
+
|
|
191
|
+
while True:
|
|
192
|
+
LOGGER.info("Starting application")
|
|
193
|
+
|
|
194
|
+
up.rpc_start()
|
|
195
|
+
up.init_device()
|
|
196
|
+
|
|
197
|
+
up.start_device()
|
|
198
|
+
|
|
199
|
+
up.write_inputs()
|
|
200
|
+
|
|
201
|
+
up.enable_watchdog(True)
|
|
202
|
+
|
|
203
|
+
while up.worker():
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
LOGGER.info("Restarting application")
|
|
207
|
+
except ApiError as exception:
|
|
208
|
+
LOGGER.error(exception)
|
|
209
|
+
except GuiExit:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def run_client(
|
|
214
|
+
up: Up,
|
|
215
|
+
transport: str,
|
|
216
|
+
):
|
|
217
|
+
# Run off main thread to make sure main thread can handle signals
|
|
218
|
+
runner = Thread(
|
|
219
|
+
target=partial(run_client_blocking, up, transport),
|
|
220
|
+
name="Device Runner",
|
|
221
|
+
daemon=True,
|
|
222
|
+
)
|
|
223
|
+
runner.start()
|
|
224
|
+
while runner.is_alive():
|
|
225
|
+
sleep(1)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def run_client_and_server(up: Up, interface: str):
|
|
229
|
+
server = server_binary()
|
|
230
|
+
|
|
231
|
+
server_runner = subprocess.Popen([server, interface])
|
|
232
|
+
time.sleep(1)
|
|
233
|
+
try:
|
|
234
|
+
client_runner = Thread(
|
|
235
|
+
target=partial(run_client_blocking, up, "tcp://localhost"),
|
|
236
|
+
name="Device Runner",
|
|
237
|
+
daemon=True,
|
|
238
|
+
)
|
|
239
|
+
client_runner.start()
|
|
240
|
+
|
|
241
|
+
while client_runner.is_alive():
|
|
242
|
+
sleep(1)
|
|
243
|
+
|
|
244
|
+
finally:
|
|
245
|
+
server_runner.kill()
|
|
246
|
+
server_runner.wait()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run_server(interface: str):
|
|
250
|
+
server = server_binary()
|
|
251
|
+
subprocess.run([server, interface])
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from contextlib import nullcontext
|
|
2
|
+
import faulthandler
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from typing import Optional, Annotated
|
|
5
|
+
|
|
6
|
+
import typer.rich_utils
|
|
7
|
+
from uphy.device import gui
|
|
8
|
+
from uphy.device import api
|
|
9
|
+
import logging
|
|
10
|
+
import typer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from rich.logging import RichHandler
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich import print
|
|
15
|
+
import importlib.metadata
|
|
16
|
+
import importlib.util
|
|
17
|
+
import importlib.resources
|
|
18
|
+
import psutil
|
|
19
|
+
import upgen.model.uphy as uphy_model
|
|
20
|
+
|
|
21
|
+
from . import (
|
|
22
|
+
get_device_handler,
|
|
23
|
+
get_sample_model,
|
|
24
|
+
DeviceError,
|
|
25
|
+
run_client,
|
|
26
|
+
run_client_and_server,
|
|
27
|
+
run_server,
|
|
28
|
+
Protocol,
|
|
29
|
+
LOGGER,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .server import mdns
|
|
33
|
+
|
|
34
|
+
faulthandler.enable()
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
pretty_exceptions_enable=False,
|
|
38
|
+
no_args_is_help=True,
|
|
39
|
+
name="device",
|
|
40
|
+
help="Run a U-Phy server from python.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _model_parser(path: Path) -> uphy_model.Root:
|
|
45
|
+
try:
|
|
46
|
+
return uphy_model.Root.parse_file(str(path))
|
|
47
|
+
except Exception as exception:
|
|
48
|
+
raise typer.BadParameter(str(exception)) from exception
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _interface_parser(interface: str) -> str:
|
|
52
|
+
interfaces = psutil.net_if_addrs()
|
|
53
|
+
if interface not in interfaces:
|
|
54
|
+
raise typer.BadParameter(
|
|
55
|
+
f"Interface '{interface}' not found in {list(interfaces)}"
|
|
56
|
+
)
|
|
57
|
+
return interface
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
INTERFACE_HELP = "The network interface to run the server on. NOTE: This should not be your main network card, but a secondary card used for protocol data."
|
|
61
|
+
INTERFACE_OPTION = typer.Option(
|
|
62
|
+
help=INTERFACE_HELP,
|
|
63
|
+
parser=_interface_parser,
|
|
64
|
+
prompt="Enter network interface to use",
|
|
65
|
+
)
|
|
66
|
+
TRANSPORT_HELP = "The target transport to connect to the running server. 'tcp://' for network localhost access, '/dev/uphyX' or 'COMX') for serial connection to device."
|
|
67
|
+
MODEL_HELP = "Path to a U-Phy device model json file."
|
|
68
|
+
MODEL_OPTION = typer.Option(help=MODEL_HELP, parser=_model_parser)
|
|
69
|
+
HANDLER_HELP = "Path to custom device handler python script. A template file can be generated using 'uphy-device export-handler'."
|
|
70
|
+
MDNS_HELP = "Expose device and model for discovery over mdns/zeroconf, useful to use with a supported controller."
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _set_logging(level, force=False):
|
|
74
|
+
logging.basicConfig(
|
|
75
|
+
level=level,
|
|
76
|
+
format="%(message)s",
|
|
77
|
+
datefmt="[%X]",
|
|
78
|
+
handlers=[RichHandler(markup=True)],
|
|
79
|
+
force=force,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_set_logging(level=logging.INFO)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command(no_args_is_help=True)
|
|
87
|
+
def client(
|
|
88
|
+
protocol: Protocol,
|
|
89
|
+
transport: Annotated[
|
|
90
|
+
str,
|
|
91
|
+
typer.Option(help=TRANSPORT_HELP),
|
|
92
|
+
],
|
|
93
|
+
model: Annotated[Optional[uphy_model.Root], MODEL_OPTION] = None,
|
|
94
|
+
verbose: bool = False,
|
|
95
|
+
handler: Annotated[Optional[str], typer.Option(help=HANDLER_HELP)] = None,
|
|
96
|
+
gui_arg: Annotated[gui.Gui, typer.Option("--gui")] = gui.Gui.dear,
|
|
97
|
+
):
|
|
98
|
+
"""Run a model from source XML file"""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
if verbose:
|
|
102
|
+
_set_logging(level=logging.DEBUG, force=True)
|
|
103
|
+
|
|
104
|
+
if model is None:
|
|
105
|
+
model = get_sample_model()
|
|
106
|
+
|
|
107
|
+
up = get_device_handler(protocol, model, handler)
|
|
108
|
+
with up.gui(gui_arg):
|
|
109
|
+
run_client(up, transport)
|
|
110
|
+
|
|
111
|
+
except (DeviceError, api.ApiError) as exception:
|
|
112
|
+
LOGGER.error(str(exception))
|
|
113
|
+
LOGGER.debug("", exc_info=True)
|
|
114
|
+
except gui.GuiExit:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def mono(
|
|
120
|
+
protocol: Protocol,
|
|
121
|
+
interface: Annotated[
|
|
122
|
+
str,
|
|
123
|
+
INTERFACE_OPTION,
|
|
124
|
+
],
|
|
125
|
+
model: Annotated[Optional[uphy_model.Root], MODEL_OPTION] = None,
|
|
126
|
+
verbose: bool = False,
|
|
127
|
+
handler: Annotated[Optional[str], typer.Option(help=HANDLER_HELP)] = None,
|
|
128
|
+
gui_arg: Annotated[gui.Gui, typer.Option("--gui")] = gui.Gui.dear,
|
|
129
|
+
run_mdns: Annotated[bool, typer.Option("--mdns", help=MDNS_HELP)] = False,
|
|
130
|
+
):
|
|
131
|
+
"""Run a client and server on same system."""
|
|
132
|
+
try:
|
|
133
|
+
if verbose:
|
|
134
|
+
_set_logging(level=logging.DEBUG, force=True)
|
|
135
|
+
|
|
136
|
+
if model is None:
|
|
137
|
+
model = get_sample_model()
|
|
138
|
+
|
|
139
|
+
if run_mdns:
|
|
140
|
+
mdns_ctx = mdns.run(
|
|
141
|
+
model=model,
|
|
142
|
+
device=model.devices[0],
|
|
143
|
+
interface=interface,
|
|
144
|
+
protocol=protocol,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
mdns_ctx = nullcontext()
|
|
148
|
+
|
|
149
|
+
with mdns_ctx:
|
|
150
|
+
up = get_device_handler(protocol, model, handler)
|
|
151
|
+
with up.gui(gui_arg):
|
|
152
|
+
run_client_and_server(up, interface)
|
|
153
|
+
|
|
154
|
+
except (DeviceError, api.ApiError) as exception:
|
|
155
|
+
LOGGER.error(str(exception))
|
|
156
|
+
LOGGER.debug("", exc_info=True)
|
|
157
|
+
except gui.GuiExit:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command()
|
|
162
|
+
def build():
|
|
163
|
+
"""Start building your device model"""
|
|
164
|
+
|
|
165
|
+
print("To start building your device you need to create a model of your device describing its inputs and outputs.")
|
|
166
|
+
print("This is done using RT-Labs device builder at https://devicebuilder.rt-labs.com/")
|
|
167
|
+
print()
|
|
168
|
+
print("After you have configured your model, download the model file into a known location")
|
|
169
|
+
print()
|
|
170
|
+
print()
|
|
171
|
+
if typer.confirm(
|
|
172
|
+
"Start a web browser navigating to this location?"
|
|
173
|
+
):
|
|
174
|
+
typer.launch("https://devicebuilder.rt-labs.com/")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def export_handler(file: Annotated[typer.FileBinaryWrite, typer.Argument(mode="xb")]):
|
|
179
|
+
"""Export a template handler to file"""
|
|
180
|
+
with importlib.resources.open_binary(__package__, "handler.py") as template:
|
|
181
|
+
file.write(template.read())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.command()
|
|
185
|
+
def server(
|
|
186
|
+
interface: Annotated[
|
|
187
|
+
str,
|
|
188
|
+
INTERFACE_OPTION,
|
|
189
|
+
],
|
|
190
|
+
):
|
|
191
|
+
"""Start a u-phy server on your local system. This will listen to connections from client instances to run the u-phy system."""
|
|
192
|
+
try:
|
|
193
|
+
run_server(interface)
|
|
194
|
+
except (DeviceError, api.ApiError) as exception:
|
|
195
|
+
LOGGER.error(str(exception))
|
|
196
|
+
LOGGER.debug("", exc_info=True)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command()
|
|
200
|
+
def discover():
|
|
201
|
+
"""Tries to discovery locally attached u-phy servers"""
|
|
202
|
+
import serial.tools.list_ports
|
|
203
|
+
|
|
204
|
+
table = Table("ID", "Serial Number", "Subsystem", title="Serial ports")
|
|
205
|
+
for port in serial.tools.list_ports.comports():
|
|
206
|
+
print(port.usb_info())
|
|
207
|
+
if port.vid != 0x04D8 or port.pid != 0x1301:
|
|
208
|
+
continue
|
|
209
|
+
index = port.location.split(".")[-1]
|
|
210
|
+
if index == "0":
|
|
211
|
+
system = "server"
|
|
212
|
+
elif index == "2":
|
|
213
|
+
system = "console"
|
|
214
|
+
else:
|
|
215
|
+
system = "unkown"
|
|
216
|
+
table.add_row(port.name, port.serial_number, system)
|
|
217
|
+
|
|
218
|
+
print(table)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command()
|
|
222
|
+
def readme():
|
|
223
|
+
print("The main documentation site for U-Phy is located at https://docs.rt-labs.com/u-phy.")
|
|
224
|
+
if typer.confirm(
|
|
225
|
+
"Start a web browser navigating to site?"
|
|
226
|
+
):
|
|
227
|
+
typer.launch("https://docs.rt-labs.com/u-phy")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
app()
|