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.
Files changed (29) hide show
  1. uphy_device-0.1.0.dev2794/LICENSE.txt +5 -0
  2. uphy_device-0.1.0.dev2794/PKG-INFO +33 -0
  3. uphy_device-0.1.0.dev2794/README.md +7 -0
  4. uphy_device-0.1.0.dev2794/pyproject.toml +42 -0
  5. uphy_device-0.1.0.dev2794/setup.cfg +4 -0
  6. uphy_device-0.1.0.dev2794/setup.py +77 -0
  7. uphy_device-0.1.0.dev2794/uphy/device/__init__.py +251 -0
  8. uphy_device-0.1.0.dev2794/uphy/device/__main__.py +231 -0
  9. uphy_device-0.1.0.dev2794/uphy/device/api/__init__.cpp +102625 -0
  10. uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pxd +546 -0
  11. uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pyi +629 -0
  12. uphy_device-0.1.0.dev2794/uphy/device/api/__init__.pyx +1740 -0
  13. uphy_device-0.1.0.dev2794/uphy/device/gui/__init__.py +44 -0
  14. uphy_device-0.1.0.dev2794/uphy/device/gui/dear.py +196 -0
  15. uphy_device-0.1.0.dev2794/uphy/device/gui/rich.py +40 -0
  16. uphy_device-0.1.0.dev2794/uphy/device/handler.py +49 -0
  17. uphy_device-0.1.0.dev2794/uphy/device/server/__init__.py +38 -0
  18. uphy_device-0.1.0.dev2794/uphy/device/server/bin/sample +0 -0
  19. uphy_device-0.1.0.dev2794/uphy/device/server/bin/server +0 -0
  20. uphy_device-0.1.0.dev2794/uphy/device/server/mdns.py +88 -0
  21. uphy_device-0.1.0.dev2794/uphy/device/share/digio.json +234 -0
  22. uphy_device-0.1.0.dev2794/uphy/device/share/set_network_parameters +95 -0
  23. uphy_device-0.1.0.dev2794/uphy/device/share/set_profinet_leds +36 -0
  24. uphy_device-0.1.0.dev2794/uphy_device.egg-info/PKG-INFO +33 -0
  25. uphy_device-0.1.0.dev2794/uphy_device.egg-info/SOURCES.txt +27 -0
  26. uphy_device-0.1.0.dev2794/uphy_device.egg-info/dependency_links.txt +1 -0
  27. uphy_device-0.1.0.dev2794/uphy_device.egg-info/entry_points.txt +5 -0
  28. uphy_device-0.1.0.dev2794/uphy_device.egg-info/requires.txt +9 -0
  29. uphy_device-0.1.0.dev2794/uphy_device.egg-info/top_level.txt +1 -0
@@ -0,0 +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.
@@ -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,7 @@
1
+ # U-Phy Device Tools
2
+
3
+ ## Introduction
4
+
5
+ U-Phy Device tools support controlling and executing a U-Phy server to simulation and run a fieldbus device inside a python program.
6
+
7
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()