ansys-pyensight-core 0.8.8__py3-none-any.whl → 0.8.10__py3-none-any.whl
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.
Potentially problematic release.
This version of ansys-pyensight-core might be problematic. Click here for more details.
- ansys/pyensight/core/__init__.py +1 -1
- ansys/pyensight/core/common.py +216 -0
- ansys/pyensight/core/dockerlauncher.py +85 -94
- ansys/pyensight/core/enshell_grpc.py +32 -0
- ansys/pyensight/core/launch_ensight.py +120 -19
- ansys/pyensight/core/launcher.py +10 -61
- ansys/pyensight/core/libuserd.py +1832 -0
- ansys/pyensight/core/locallauncher.py +47 -2
- ansys/pyensight/core/renderable.py +30 -0
- ansys/pyensight/core/session.py +6 -1
- ansys/pyensight/core/utils/dsg_server.py +227 -35
- ansys/pyensight/core/utils/omniverse.py +84 -24
- ansys/pyensight/core/utils/omniverse_cli.py +481 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +236 -426
- ansys/pyensight/core/utils/omniverse_glb_server.py +279 -0
- ansys/pyensight/core/utils/readers.py +15 -11
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/METADATA +10 -6
- ansys_pyensight_core-0.8.10.dist-info/RECORD +36 -0
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +0 -407
- ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +0 -59
- ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +0 -18
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +0 -193
- ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +0 -49
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +0 -18
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +0 -54
- ansys_pyensight_core-0.8.8.dist-info/RECORD +0 -52
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.10.dist-info}/WHEEL +0 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import glob
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
4
|
+
import platform
|
|
3
5
|
import subprocess
|
|
4
6
|
import sys
|
|
7
|
+
import tempfile
|
|
5
8
|
from types import ModuleType
|
|
6
9
|
from typing import TYPE_CHECKING, Optional, Union
|
|
10
|
+
import uuid
|
|
7
11
|
|
|
8
12
|
import psutil
|
|
9
13
|
|
|
@@ -13,8 +17,6 @@ if TYPE_CHECKING:
|
|
|
13
17
|
except ImportError:
|
|
14
18
|
from ansys.api.pyensight import ensight_api
|
|
15
19
|
|
|
16
|
-
import ansys.pyensight.core
|
|
17
|
-
|
|
18
20
|
|
|
19
21
|
class Omniverse:
|
|
20
22
|
"""Provides the ``ensight.utils.omniverse`` interface.
|
|
@@ -52,6 +54,7 @@ class Omniverse:
|
|
|
52
54
|
self._ensight = interface
|
|
53
55
|
self._server_pid: Optional[int] = None
|
|
54
56
|
self._interpreter: str = ""
|
|
57
|
+
self._status_filename: str = ""
|
|
55
58
|
|
|
56
59
|
@staticmethod
|
|
57
60
|
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
@@ -129,10 +132,7 @@ class Omniverse:
|
|
|
129
132
|
def _check_modules(self) -> None:
|
|
130
133
|
"""Verify that the Python interpreter is correct
|
|
131
134
|
|
|
132
|
-
Check for
|
|
133
|
-
If pxr is there as well, then we can just use sys.executable.
|
|
134
|
-
If not, check to see if 'kit.bat' or 'kit.sh' can be found and
|
|
135
|
-
arrange to use those instead.
|
|
135
|
+
Check for module dependencies. If not present, raise an exception.
|
|
136
136
|
|
|
137
137
|
Raises
|
|
138
138
|
------
|
|
@@ -144,11 +144,27 @@ class Omniverse:
|
|
|
144
144
|
if len(self._interpreter):
|
|
145
145
|
return
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
147
|
+
# if a module, then we are inside EnSight
|
|
148
|
+
if isinstance(self._ensight, ModuleType): # pragma: no cover
|
|
149
|
+
# in this case, we can just use cpython
|
|
150
|
+
import ceiversion
|
|
151
|
+
import enve
|
|
152
|
+
|
|
153
|
+
cei_home = os.environ.get("CEI_HOME", enve.home())
|
|
154
|
+
self._interpreter = os.path.join(cei_home, "bin", f"cpython{ceiversion.apex_suffix}")
|
|
155
|
+
if platform.system() == "Windows":
|
|
156
|
+
self._interpreter += ".bat"
|
|
150
157
|
return
|
|
151
|
-
|
|
158
|
+
# Using the python interpreter running this code
|
|
159
|
+
self._interpreter = sys.executable
|
|
160
|
+
|
|
161
|
+
# in the future, these will be part of the pyensight wheel
|
|
162
|
+
# dependencies, but for now we include this check.
|
|
163
|
+
try:
|
|
164
|
+
import pxr # noqa: F401
|
|
165
|
+
import pygltflib # noqa: F401
|
|
166
|
+
except Exception:
|
|
167
|
+
raise RuntimeError("Unable to detect omniverse dependencies: usd-core, pygltflib.")
|
|
152
168
|
|
|
153
169
|
def is_running_omniverse(self) -> bool:
|
|
154
170
|
"""Check that an Omniverse connection is active
|
|
@@ -228,30 +244,73 @@ class Omniverse:
|
|
|
228
244
|
port = options.get("port", 12345)
|
|
229
245
|
token = options.get("security", "")
|
|
230
246
|
|
|
231
|
-
# Launch the server via the 'ansys.
|
|
247
|
+
# Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
|
|
232
248
|
dsg_uri = f"grpc://{hostname}:{port}"
|
|
233
|
-
kit_dir = os.path.join(os.path.dirname(ansys.pyensight.core.__file__), "exts")
|
|
234
249
|
cmd = [self._interpreter]
|
|
235
|
-
cmd.extend(["
|
|
236
|
-
cmd.
|
|
250
|
+
cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
|
|
251
|
+
cmd.append(omniverse_path)
|
|
237
252
|
if token:
|
|
238
|
-
cmd.
|
|
253
|
+
cmd.extend(["--security_token", token])
|
|
239
254
|
if temporal:
|
|
240
|
-
cmd.
|
|
255
|
+
cmd.extend(["--temporal", "true"])
|
|
241
256
|
if not include_camera:
|
|
242
|
-
cmd.
|
|
257
|
+
cmd.extend(["--include_camera", "false"])
|
|
243
258
|
if normalize_geometry:
|
|
244
|
-
cmd.
|
|
259
|
+
cmd.extend(["--normalize_geometry", "true"])
|
|
245
260
|
if time_scale != 1.0:
|
|
246
|
-
cmd.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cmd.
|
|
261
|
+
cmd.extend(["--time_scale", str(time_scale)])
|
|
262
|
+
if not live:
|
|
263
|
+
cmd.extend(["--oneshot", "1"])
|
|
264
|
+
cmd.extend(["--dsg_uri", dsg_uri])
|
|
250
265
|
env_vars = os.environ.copy()
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
# we are launching the kit from EnSight or PyEnSight. In these cases, we
|
|
267
|
+
# inform the kit instance of:
|
|
268
|
+
# (1) the name of the "server status" file, if any
|
|
269
|
+
self._new_status_file()
|
|
270
|
+
env_vars["ANSYS_OV_SERVER_STATUS_FILENAME"] = self._status_filename
|
|
271
|
+
process = subprocess.Popen(cmd, close_fds=True, env=env_vars)
|
|
253
272
|
self._server_pid = process.pid
|
|
254
273
|
|
|
274
|
+
def _new_status_file(self, new=True) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Remove any existing status file and create a new one if requested.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
new : bool
|
|
281
|
+
If True, create a new status file.
|
|
282
|
+
"""
|
|
283
|
+
if self._status_filename:
|
|
284
|
+
try:
|
|
285
|
+
os.remove(self._status_filename)
|
|
286
|
+
except OSError:
|
|
287
|
+
pass
|
|
288
|
+
self._status_filename = ""
|
|
289
|
+
if new:
|
|
290
|
+
self._status_filename = os.path.join(
|
|
291
|
+
tempfile.gettempdir(), str(uuid.uuid1()) + "_gs_status.txt"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def read_status_file(self) -> dict:
|
|
295
|
+
"""Read the status file and return its contents as a dictionary.
|
|
296
|
+
|
|
297
|
+
Note: this can fail if the file is being written to when this call is made, so expect
|
|
298
|
+
failures.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
Optional[dict]
|
|
303
|
+
A dictionary with the fields 'status', 'start_time', 'processed_buffers', 'total_buffers' or empty
|
|
304
|
+
"""
|
|
305
|
+
if not self._status_filename:
|
|
306
|
+
return {}
|
|
307
|
+
try:
|
|
308
|
+
with open(self._status_filename, "r") as status_file:
|
|
309
|
+
data = json.load(status_file)
|
|
310
|
+
except Exception:
|
|
311
|
+
return {}
|
|
312
|
+
return data
|
|
313
|
+
|
|
255
314
|
def close_connection(self) -> None:
|
|
256
315
|
"""Shut down the open EnSight dsg -> omniverse server
|
|
257
316
|
|
|
@@ -275,6 +334,7 @@ class Omniverse:
|
|
|
275
334
|
except psutil.NoSuchProcess:
|
|
276
335
|
pass
|
|
277
336
|
self._server_pid = None
|
|
337
|
+
self._new_status_file(new=False)
|
|
278
338
|
|
|
279
339
|
def update(self, temporal: bool = False) -> None:
|
|
280
340
|
"""Update the geometry in Omniverse
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from functools import partial
|
|
3
|
+
import glob
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, List, Optional
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import ansys.pyensight.core
|
|
13
|
+
import ansys.pyensight.core.utils.dsg_server as dsg_server
|
|
14
|
+
import ansys.pyensight.core.utils.omniverse_dsg_server as ov_dsg_server
|
|
15
|
+
import ansys.pyensight.core.utils.omniverse_glb_server as ov_glb_server
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def str2bool_type(v: Any) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
This function is designed to be a 'type=' filter for an argparse entry returning a boolean.
|
|
21
|
+
It allows for additional, common alternative strings as booleans. These include 'yes','no',
|
|
22
|
+
'true','false','t','f','y','n','1' and '0'. If the value does not meet the requirements,
|
|
23
|
+
the function will raise the argparse.ArgumentTypeError exception.
|
|
24
|
+
:param v: The (potential) boolean argument.
|
|
25
|
+
:return: The actual boolean value.
|
|
26
|
+
:raises: argparse.ArgumentTypeError
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(v, bool):
|
|
29
|
+
return v
|
|
30
|
+
if v.lower() in ("yes", "true", "t", "y", "1"):
|
|
31
|
+
return True
|
|
32
|
+
elif v.lower() in ("no", "false", "f", "n", "0"):
|
|
33
|
+
return False
|
|
34
|
+
else:
|
|
35
|
+
raise argparse.ArgumentTypeError("Boolean value expected.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def int_range_type(
|
|
39
|
+
v: Any, min_value: int = 0, max_value: int = 100, allow: Optional[List[int]] = None
|
|
40
|
+
) -> int:
|
|
41
|
+
"""
|
|
42
|
+
This function is designed to be a 'type=' filter for an argparse entry returning an integer value within
|
|
43
|
+
a specified range. If the value does not meet the requirements, the function will raise the
|
|
44
|
+
argparse.ArgumentTypeError exception. This function is normally used with functools.partial to bind
|
|
45
|
+
the minimum and maximum values. For example: type=partial(int_range_type, min_value=0, max_value=65535)
|
|
46
|
+
:param v: The (potential) integer argument.
|
|
47
|
+
:param min_value: The minimum legal integer value.
|
|
48
|
+
:param max_value: The maximum legal integer value.
|
|
49
|
+
:param allow:A list of additional, legal values
|
|
50
|
+
:return: The validated integer value.
|
|
51
|
+
:raises: argparse.ArgumentTypeError
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
value = int(v)
|
|
55
|
+
except ValueError:
|
|
56
|
+
raise argparse.ArgumentTypeError("Integer value expected.")
|
|
57
|
+
if allow is None:
|
|
58
|
+
allow = []
|
|
59
|
+
if (value >= min_value) and (value <= max_value):
|
|
60
|
+
return value
|
|
61
|
+
elif value in allow:
|
|
62
|
+
return value
|
|
63
|
+
else:
|
|
64
|
+
msg = f"Integer value is not in the range [{min_value},{max_value}]"
|
|
65
|
+
if allow:
|
|
66
|
+
msg += f" or in the list {allow}"
|
|
67
|
+
raise argparse.ArgumentTypeError(msg + ".")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OmniverseGeometryServer(object):
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
security_token: str = "",
|
|
74
|
+
destination: str = "",
|
|
75
|
+
temporal: bool = False,
|
|
76
|
+
vrmode: bool = False,
|
|
77
|
+
time_scale: float = 1.0,
|
|
78
|
+
normalize_geometry: bool = False,
|
|
79
|
+
dsg_uri: str = "",
|
|
80
|
+
monitor_directory: str = "",
|
|
81
|
+
) -> None:
|
|
82
|
+
self._dsg_uri = dsg_uri
|
|
83
|
+
self._destination = destination
|
|
84
|
+
self._security_token = security_token
|
|
85
|
+
if not self._security_token:
|
|
86
|
+
self._security_token = os.environ.get("ENSIGHT_SECURITY_TOKEN", "")
|
|
87
|
+
self._temporal = temporal
|
|
88
|
+
self._vrmode = vrmode
|
|
89
|
+
self._time_scale = time_scale
|
|
90
|
+
self._normalize_geometry = normalize_geometry
|
|
91
|
+
self._version = "unknown"
|
|
92
|
+
self._shutdown = False
|
|
93
|
+
self._server_process = None
|
|
94
|
+
self._status_filename: str = ""
|
|
95
|
+
self._monitor_directory: str = monitor_directory
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def monitor_directory(self) -> Optional[str]:
|
|
99
|
+
if self._monitor_directory:
|
|
100
|
+
return self._monitor_directory
|
|
101
|
+
# converts "" -> None
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def pyensight_version(self) -> str:
|
|
106
|
+
"""The ansys.pyensight.core version"""
|
|
107
|
+
return ansys.pyensight.core.VERSION
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def dsg_uri(self) -> str:
|
|
111
|
+
"""The endpoint of a Dynamic Scene Graph service: grpc://{hostname}:{port}"""
|
|
112
|
+
return self._dsg_uri
|
|
113
|
+
|
|
114
|
+
@dsg_uri.setter
|
|
115
|
+
def dsg_uri(self, uri: str) -> None:
|
|
116
|
+
self._dsg_uri = uri
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def destination(self) -> str:
|
|
120
|
+
"""The endpoint of an Omniverse Nucleus service: omniverse://{hostname}/{path}"""
|
|
121
|
+
return self._destination
|
|
122
|
+
|
|
123
|
+
@destination.setter
|
|
124
|
+
def destination(self, value: str) -> None:
|
|
125
|
+
self._destination = value
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def security_token(self) -> str:
|
|
129
|
+
"""The security token of the DSG service instance."""
|
|
130
|
+
return self._security_token
|
|
131
|
+
|
|
132
|
+
@security_token.setter
|
|
133
|
+
def security_token(self, value: str) -> None:
|
|
134
|
+
self._security_token = value
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def temporal(self) -> bool:
|
|
138
|
+
"""If True, the DSG update should include all timesteps."""
|
|
139
|
+
return self._temporal
|
|
140
|
+
|
|
141
|
+
@temporal.setter
|
|
142
|
+
def temporal(self, value: bool) -> None:
|
|
143
|
+
self._temporal = bool(value)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def vrmode(self) -> bool:
|
|
147
|
+
"""If True, the DSG update should not include camera transforms."""
|
|
148
|
+
return self._vrmode
|
|
149
|
+
|
|
150
|
+
@vrmode.setter
|
|
151
|
+
def vrmode(self, value: bool) -> None:
|
|
152
|
+
self._vrmode = bool(value)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def normalize_geometry(self) -> bool:
|
|
156
|
+
"""If True, the DSG geometry should be remapped into normalized space."""
|
|
157
|
+
return self._normalize_geometry
|
|
158
|
+
|
|
159
|
+
@normalize_geometry.setter
|
|
160
|
+
def normalize_geometry(self, val: bool) -> None:
|
|
161
|
+
self._normalize_geometry = val
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def time_scale(self) -> float:
|
|
165
|
+
"""Value to multiply DSG time values by before passing to Omniverse"""
|
|
166
|
+
return self._time_scale
|
|
167
|
+
|
|
168
|
+
@time_scale.setter
|
|
169
|
+
def time_scale(self, value: float) -> None:
|
|
170
|
+
self._time_scale = value
|
|
171
|
+
|
|
172
|
+
def run_server(self, one_shot: bool = False) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Run a DSG to Omniverse server in process.
|
|
175
|
+
|
|
176
|
+
Note: this method does not return until the DSG connection is dropped or
|
|
177
|
+
self.stop_server() has been called.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
one_shot : bool
|
|
182
|
+
If True, only run the server to transfer a single scene and
|
|
183
|
+
then return.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# Build the Omniverse connection
|
|
187
|
+
omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
|
|
188
|
+
logging.info("Omniverse connection established.")
|
|
189
|
+
|
|
190
|
+
# parse the DSG USI
|
|
191
|
+
parsed = urlparse(self.dsg_uri)
|
|
192
|
+
port = parsed.port
|
|
193
|
+
host = parsed.hostname
|
|
194
|
+
|
|
195
|
+
# link it to a DSG session
|
|
196
|
+
update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
|
|
197
|
+
dsg_link = dsg_server.DSGSession(
|
|
198
|
+
port=port,
|
|
199
|
+
host=host,
|
|
200
|
+
vrmode=self.vrmode,
|
|
201
|
+
security_code=self.security_token,
|
|
202
|
+
verbose=1,
|
|
203
|
+
normalize_geometry=self.normalize_geometry,
|
|
204
|
+
time_scale=self.time_scale,
|
|
205
|
+
handler=update_handler,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Start the DSG link
|
|
209
|
+
logging.info(f"Making DSG connection to: {self.dsg_uri}")
|
|
210
|
+
err = dsg_link.start()
|
|
211
|
+
if err < 0:
|
|
212
|
+
logging.error("Omniverse connection failed.")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Initial pull request
|
|
216
|
+
dsg_link.request_an_update(animation=self.temporal)
|
|
217
|
+
|
|
218
|
+
# until the link is dropped, continue
|
|
219
|
+
while not dsg_link.is_shutdown() and not self._shutdown:
|
|
220
|
+
dsg_link.handle_one_update()
|
|
221
|
+
if one_shot:
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
logging.info("Shutting down DSG connection")
|
|
225
|
+
dsg_link.end()
|
|
226
|
+
omni_link.shutdown()
|
|
227
|
+
|
|
228
|
+
def run_monitor(self):
|
|
229
|
+
"""
|
|
230
|
+
Run monitor and upload GLB files to Omniverse in process. There are two cases:
|
|
231
|
+
|
|
232
|
+
1) the "directory name" is actually a .glb file. In this case, simply push
|
|
233
|
+
the glb file contents to Omniverse.
|
|
234
|
+
|
|
235
|
+
2) If a directory, then we periodically scan the directory for files named "*.upload".
|
|
236
|
+
If this file is found, there are two cases:
|
|
237
|
+
|
|
238
|
+
a) The file is empty. In this case, for a file named ABC.upload, the file
|
|
239
|
+
ABC.glb will be read and uploaded before both files are deleted.
|
|
240
|
+
|
|
241
|
+
b) The file contains valid json. In this case, the json object is parsed with
|
|
242
|
+
the following format (two glb files for the first timestep and one for the second):
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
"version": 1,
|
|
246
|
+
"destination": "",
|
|
247
|
+
"files": ["a.glb", "b.glb", "c.glb"],
|
|
248
|
+
"times": [0.0, 0.0, 1.0]
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
"times" is optional and defaults to [0*len("files")]. Once processed,
|
|
252
|
+
all the files referenced in the json and the json file itself are deleted.
|
|
253
|
+
"omniuri" is optional and defaults to the passed Omniverse path.
|
|
254
|
+
|
|
255
|
+
Note: In this mode, the method does not return until a "shutdown" file or
|
|
256
|
+
an error is encountered.
|
|
257
|
+
|
|
258
|
+
TODO: add "push" mechanism to trigger a DSG push from the connected session. This
|
|
259
|
+
can be done via the monitor mechanism and used by the Omniverse kit to implement
|
|
260
|
+
a "pull".
|
|
261
|
+
"""
|
|
262
|
+
the_dir = self.monitor_directory
|
|
263
|
+
single_file_upload = False
|
|
264
|
+
if os.path.isfile(the_dir) and the_dir.lower().endswith(".glb"):
|
|
265
|
+
single_file_upload = True
|
|
266
|
+
else:
|
|
267
|
+
if not os.path.isdir(the_dir):
|
|
268
|
+
logging.error(f"The monitor directory {the_dir} does not exist.")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# Build the Omniverse connection
|
|
272
|
+
omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
|
|
273
|
+
logging.info("Omniverse connection established.")
|
|
274
|
+
|
|
275
|
+
# use an OmniverseUpdateHandler
|
|
276
|
+
update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
|
|
277
|
+
|
|
278
|
+
# Link it to the GLB file monitoring service
|
|
279
|
+
glb_link = ov_glb_server.GLBSession(
|
|
280
|
+
verbose=1,
|
|
281
|
+
handler=update_handler,
|
|
282
|
+
)
|
|
283
|
+
if single_file_upload:
|
|
284
|
+
start_time = time.time()
|
|
285
|
+
logging.info(f"Uploading file: {the_dir}.")
|
|
286
|
+
try:
|
|
287
|
+
glb_link.upload_file(the_dir)
|
|
288
|
+
except Exception as error:
|
|
289
|
+
logging.warning(f"Error uploading file: {the_dir}: {error}")
|
|
290
|
+
logging.info(f"Uploaded in {(time.time() - start_time):.2f}")
|
|
291
|
+
else:
|
|
292
|
+
logging.info(f"Starting file monitoring for {the_dir}.")
|
|
293
|
+
the_dir_path = pathlib.Path(the_dir)
|
|
294
|
+
try:
|
|
295
|
+
stop_file = os.path.join(the_dir, "shutdown")
|
|
296
|
+
orig_destination = omni_link.destination
|
|
297
|
+
while not os.path.exists(stop_file):
|
|
298
|
+
loop_time = time.time()
|
|
299
|
+
files_to_remove = []
|
|
300
|
+
for filename in glob.glob(os.path.join(the_dir, "*", "*.upload")):
|
|
301
|
+
# reset to the launch URI/directory
|
|
302
|
+
omni_link.destination = orig_destination
|
|
303
|
+
# Keep track of the files and time values
|
|
304
|
+
files_to_remove.append(filename)
|
|
305
|
+
files_to_process = []
|
|
306
|
+
file_timestamps = []
|
|
307
|
+
if os.path.getsize(filename) == 0:
|
|
308
|
+
# replace the ".upload" extension with ".glb"
|
|
309
|
+
glb_file = os.path.splitext(filename)[0] + ".glb"
|
|
310
|
+
if os.path.exists(glb_file):
|
|
311
|
+
files_to_process.append(glb_file)
|
|
312
|
+
file_timestamps.append(0.0)
|
|
313
|
+
files_to_remove.append(glb_file)
|
|
314
|
+
else:
|
|
315
|
+
# read the .upload file json content
|
|
316
|
+
try:
|
|
317
|
+
with open(filename, "r") as fp:
|
|
318
|
+
glb_info = json.load(fp)
|
|
319
|
+
except Exception:
|
|
320
|
+
logging.warning(f"Error reading file: {filename}")
|
|
321
|
+
continue
|
|
322
|
+
# if specified, set the URI/directory target
|
|
323
|
+
omni_link.destination = glb_info.get("destination", orig_destination)
|
|
324
|
+
# Get the GLB files to process
|
|
325
|
+
the_files = glb_info.get("files", [])
|
|
326
|
+
files_to_remove.extend(the_files)
|
|
327
|
+
# Times not used for now, but parse them anyway
|
|
328
|
+
the_times = glb_info.get("times", [0.0] * len(the_files))
|
|
329
|
+
# Validate a few things
|
|
330
|
+
if len(the_files) != len(the_times):
|
|
331
|
+
logging.warning(
|
|
332
|
+
f"Number of times and files are not the same in: {filename}"
|
|
333
|
+
)
|
|
334
|
+
continue
|
|
335
|
+
if len(set(the_times)) != 1:
|
|
336
|
+
logging.warning("Time values not currently supported.")
|
|
337
|
+
if len(the_files) > 1:
|
|
338
|
+
logging.warning("Multiple glb files not currently fully supported.")
|
|
339
|
+
files_to_process.extend(the_files)
|
|
340
|
+
# Upload the files
|
|
341
|
+
for glb_file in files_to_process:
|
|
342
|
+
start_time = time.time()
|
|
343
|
+
logging.info(
|
|
344
|
+
f"Uploading file: {glb_file} to {omni_link.destination}."
|
|
345
|
+
)
|
|
346
|
+
try:
|
|
347
|
+
glb_link.upload_file(glb_file)
|
|
348
|
+
except Exception as error:
|
|
349
|
+
logging.warning(f"Error uploading file: {glb_file}: {error}")
|
|
350
|
+
logging.info(f"Uploaded in {(time.time() - start_time):%.2f}")
|
|
351
|
+
for filename in files_to_remove:
|
|
352
|
+
try:
|
|
353
|
+
# Only delete the file if it is in the_dir_path
|
|
354
|
+
filename_path = pathlib.Path(filename)
|
|
355
|
+
if filename_path.is_relative_to(the_dir_path):
|
|
356
|
+
os.remove(filename)
|
|
357
|
+
except IOError:
|
|
358
|
+
pass
|
|
359
|
+
if time.time() - loop_time < 0.1:
|
|
360
|
+
time.sleep(0.25)
|
|
361
|
+
except Exception as error:
|
|
362
|
+
logging.error(f"Error encountered while monitoring: {error}")
|
|
363
|
+
logging.info("Stopping file monitoring.")
|
|
364
|
+
os.remove(stop_file)
|
|
365
|
+
|
|
366
|
+
omni_link.shutdown()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
parser = argparse.ArgumentParser(description="PyEnSight Omniverse Geometry Service")
|
|
371
|
+
parser.add_argument(
|
|
372
|
+
"destination", default="", type=str, help="The directory to save the USD scene graph into."
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
"--verbose",
|
|
376
|
+
metavar="verbose_level",
|
|
377
|
+
default=0,
|
|
378
|
+
type=partial(int_range_type, min_value=0, max_value=3),
|
|
379
|
+
help="Enable logging information (0-3). Default: 0",
|
|
380
|
+
)
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"--log_file",
|
|
383
|
+
metavar="log_filename",
|
|
384
|
+
default="",
|
|
385
|
+
type=str,
|
|
386
|
+
help="Save logging output to the named log file instead of stdout.",
|
|
387
|
+
)
|
|
388
|
+
parser.add_argument(
|
|
389
|
+
"--dsg_uri",
|
|
390
|
+
default="grpc://127.0.0.1:5234",
|
|
391
|
+
type=str,
|
|
392
|
+
help="The URI of the EnSight Dynamic Scene Graph server. Default: grpc://127.0.0.1:5234",
|
|
393
|
+
)
|
|
394
|
+
parser.add_argument(
|
|
395
|
+
"--security_token",
|
|
396
|
+
metavar="token",
|
|
397
|
+
default="",
|
|
398
|
+
type=str,
|
|
399
|
+
help="Dynamic scene graph API security token. Default: none",
|
|
400
|
+
)
|
|
401
|
+
parser.add_argument(
|
|
402
|
+
"--monitor_directory",
|
|
403
|
+
metavar="glb_directory",
|
|
404
|
+
default="",
|
|
405
|
+
type=str,
|
|
406
|
+
help="Monitor specified directory for GLB files to be exported. Default: none",
|
|
407
|
+
)
|
|
408
|
+
parser.add_argument(
|
|
409
|
+
"--time_scale",
|
|
410
|
+
metavar="time_scale",
|
|
411
|
+
default=1.0,
|
|
412
|
+
type=float,
|
|
413
|
+
help="Scaling factor to be applied to input time values. Default: 1.0",
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--normalize_geometry",
|
|
417
|
+
metavar="yes|no|true|false|1|0",
|
|
418
|
+
default=False,
|
|
419
|
+
type=str2bool_type,
|
|
420
|
+
help="Enable mapping of geometry to a normalized Cartesian space. Default: false",
|
|
421
|
+
)
|
|
422
|
+
parser.add_argument(
|
|
423
|
+
"--include_camera",
|
|
424
|
+
metavar="yes|no|true|false|1|0",
|
|
425
|
+
default=True,
|
|
426
|
+
type=str2bool_type,
|
|
427
|
+
help="Include the camera in the output USD scene graph. Default: true",
|
|
428
|
+
)
|
|
429
|
+
parser.add_argument(
|
|
430
|
+
"--temporal",
|
|
431
|
+
metavar="yes|no|true|false|1|0",
|
|
432
|
+
default=False,
|
|
433
|
+
type=str2bool_type,
|
|
434
|
+
help="Export a temporal scene graph. Default: false",
|
|
435
|
+
)
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"--oneshot",
|
|
438
|
+
metavar="yes|no|true|false|1|0",
|
|
439
|
+
default=False,
|
|
440
|
+
type=str2bool_type,
|
|
441
|
+
help="Convert a single geometry into USD and exit. Default: false",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# parse the command line
|
|
445
|
+
args = parser.parse_args()
|
|
446
|
+
|
|
447
|
+
# set up logging
|
|
448
|
+
level = logging.ERROR
|
|
449
|
+
if args.verbose == 1:
|
|
450
|
+
level = logging.WARN
|
|
451
|
+
elif args.verbose == 2:
|
|
452
|
+
level = logging.INFO
|
|
453
|
+
elif args.verbose == 3:
|
|
454
|
+
level = logging.DEBUG
|
|
455
|
+
log_args = dict(format="GeometryService:%(levelname)s:%(message)s", level=level)
|
|
456
|
+
if args.log_file:
|
|
457
|
+
log_args["filename"] = args.log_file
|
|
458
|
+
# start with a clean logging instance
|
|
459
|
+
while logging.root.hasHandlers():
|
|
460
|
+
logging.root.removeHandler(logging.root.handlers[0])
|
|
461
|
+
logging.basicConfig(**log_args) # type: ignore
|
|
462
|
+
|
|
463
|
+
# Build the server object
|
|
464
|
+
server = OmniverseGeometryServer(
|
|
465
|
+
destination=args.destination,
|
|
466
|
+
dsg_uri=args.dsg_uri,
|
|
467
|
+
security_token=args.security_token,
|
|
468
|
+
monitor_directory=args.monitor_directory,
|
|
469
|
+
time_scale=args.time_scale,
|
|
470
|
+
normalize_geometry=args.normalize_geometry,
|
|
471
|
+
vrmode=not args.include_camera,
|
|
472
|
+
temporal=args.temporal,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# run the server
|
|
476
|
+
logging.info("Server startup.")
|
|
477
|
+
if server.monitor_directory:
|
|
478
|
+
server.run_monitor()
|
|
479
|
+
else:
|
|
480
|
+
server.run_server(one_shot=args.oneshot)
|
|
481
|
+
logging.info("Server shutdown.")
|