diff --git a/docs/simulation/isaac_sim/ascent_node.png b/docs/simulation/isaac_sim/ascent_node.png new file mode 100644 index 00000000..bb3020b9 Binary files /dev/null and b/docs/simulation/isaac_sim/ascent_node.png differ diff --git a/docs/simulation/isaac_sim/ascent_sitl_extension.md b/docs/simulation/isaac_sim/ascent_sitl_extension.md index e69de29b..5ce4cd31 100644 --- a/docs/simulation/isaac_sim/ascent_sitl_extension.md +++ b/docs/simulation/isaac_sim/ascent_sitl_extension.md @@ -0,0 +1,21 @@ +# AirLab AirStack Extension + +The AirStack extension for IsaacSim does two main things. It creates an Ascent Omnigraph Node which runs the Ascent SITL and updates the position of a drone model in IsaacSim based on the SITL. It also creates a panel for listing, attaching to, and killing tmux sessions. + +## Ascent OmniGraph Node + +The Ascent OmniGraph node takes as input a domain id, node namespace and drone prim. It runs the Ascent SITL, mavproxy, and mavros and takes care of keeping the SITL time synced with IsaacSim's time. Mavros is run using the inputted domain id and node namespace. The drone prim's position is set based off of the position of the drone in the SITL. The drone prim doesn't do collision and will pass through objects in the IsaacSim world. + +The way the SITL is synced with IsaacSim is by running the SITL in gdb with a breakpoint on the functin that advances the SITL time. Every time this function is called, our code is run by injecting a library using the LD_PRELOAD trick. Our code runs a client socket that talks to a server socket running in the AirStack IsaacSim extension which tells it how long to sleep based off the current SITL and IsaacSim time. + +The Ascent OmniGraph node is shown below: + +![Ascent OmniGraph Node](ascent_node.png) + +## TMUX Panel + +This is a panel for listing, attaching to, and killing any running TMUX sessions. The Ascent SITL, mavproxy, and mavros are run in a TMUX sesion, so this is mainly for debugging those and probably doesn't need to be interacted with by most users. A list of TMUX sessions is displayed in the panel. It doesn't auto refresh so you have to manually click the refresh button to display any changes in the list of sessions. For each session, there is an `Attach` button and a `Kill` button. The `Attach` button will bring up an `xterm` window with the TMUX session. The `Kill` button will kill the TMUX session. + +The TMUX panel is shown below: + +![TMUX Panel](tmux_panel.png) \ No newline at end of file diff --git a/docs/simulation/isaac_sim/tmux_panel.png b/docs/simulation/isaac_sim/tmux_panel.png new file mode 100644 index 00000000..3550fd7a Binary files /dev/null and b/docs/simulation/isaac_sim/tmux_panel.png differ diff --git a/simulation/isaac-sim/docker/docker-compose.yaml b/simulation/isaac-sim/docker/docker-compose.yaml index d6168f23..6dd5e68e 100644 --- a/simulation/isaac-sim/docker/docker-compose.yaml +++ b/simulation/isaac-sim/docker/docker-compose.yaml @@ -54,6 +54,7 @@ services: - ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw - ~/docker/isaac-sim/documents:/root/Documents:rw - ./user.config.json:/root/.local/share/ov/data/Kit/Isaac-Sim/4.1/user.config.json:rw + - ./ui.py:/isaac-sim/kit/exts/omni.kit.widget.nucleus_connector/omni/kit/widget/nucleus_connector/ui.py:rw # developer stuff - .dev:/root/.dev:rw # developer config - .bashrc:/root/.bashrc:rw # bash config diff --git a/simulation/isaac-sim/docker/ui.py b/simulation/isaac-sim/docker/ui.py new file mode 100644 index 00000000..f4af59cf --- /dev/null +++ b/simulation/isaac-sim/docker/ui.py @@ -0,0 +1,456 @@ +# Copyright (c) 2018-2020, NVIDIA CORPORATION. All rights reserved. +# +# NVIDIA CORPORATION and its licensors retain all intellectual property +# and proprietary rights in and to this software, related documentation +# and any modifications thereto. Any use, reproduction, disclosure or +# distribution of this software and related documentation without an express +# license agreement from NVIDIA CORPORATION is strictly prohibited. +# +"""This module provides user interface components for establishing and managing Nucleus connections in the Omniverse environment.""" + + +import os +import sys +import tempfile + +import carb +import carb.settings +import omni.ui as ui +import asyncio +import omni.kit.app +from omni.kit.async_engine import run_coroutine + +from typing import Coroutine, Any, Callable, Union +from omni.kit.window.popup_dialog.dialog import PopupDialog +from omni.kit.window.popup_dialog.form_dialog import FormDialog, FormWidget +from .style import UI_STYLES, ICON_PATH + + +class ConnectorDialog(PopupDialog): + """The main connection dialog for establishing Nucleus connections. + + This dialog provides a user interface to add new Omniverse connections with an optional name. It manages the UI elements and tasks associated with the connection process, such as displaying progress, handling cancellations, and showing alerts. + """ + + def __init__(self): + super().__init__(width=400, title="Add Nucleus connection", ok_label="Ok", cancel_label="Cancel", modal=True) + self._form_widget = None + self._progress_bar = None + self._alert_pane = None + self._task = None + self._build_ui() + + def _build_ui(self): + field_defs = [ + FormDialog.FieldDef("url", "Omniverse:// ", ui.StringField, "", True), + FormDialog.FieldDef("name", "Optional Name: ", ui.StringField, ""), + ] + with self._window.frame: + with ui.VStack(style=UI_STYLES, style_type_name_override="Dialog"): + # OM-81873: This is a temporary solution for the authentication dialog; add the form widget in a frame + # so that it could be easily hidden/shown + self._form_frame = ui.Frame() + with self._form_frame: + self._form_widget = FormWidget("Add a new connection with optional name.", field_defs) + with ui.ZStack(): + ui.Spacer(height=60) + with ui.VStack(): + ui.Spacer(height=8) + self._progress_bar = LoopProgressBar(puck_size=40) + ui.Spacer() + # OM-86699: Fix issue with randomly resized connector dialog, specify ZStack width with window width + # OM-98771: Substract padding from window width when specifying AlertPane width + self._alert_pane = AlertPane(width=self._window.width - 24) + self._build_ok_cancel_buttons() + self.hide() + self.set_cancel_clicked_fn(lambda _: self._on_cancel_fn()) + + def _on_cancel_fn(self): + self.cancel_task() + self.hide() + + def get_value(self, name: str) -> str: + if self._form_widget: + return self._form_widget.get_value(name) + return None + + def set_value(self, name: str, value: str): + if self._form_widget: + field = self._form_widget.get_field(name) + if field: + field.model.set_value(value or "") + + def destroy(self): + self.cancel_task() + if self._form_widget: + self._form_widget.destroy() + self._form_widget = None + if self._form_frame: + self._form_frame = None + self._progress_bar = None + if self._alert_pane: + self._alert_pane.destroy() + self._alert_pane = None + super().destroy() + + def __del__(self): + self.destroy() + + async def run_cancellable_task(self, task: Coroutine) -> Any: + """Manages running and cancelling the given task""" + if self._task: + self.cancel_task() + self._task = asyncio.create_task(task) + + try: + return await self._task + except asyncio.CancelledError: + carb.log_info(f"Cancelling task ... {self._task}") + raise + except Exception as e: + raise + finally: + self._task = None + + def cancel_task(self): + """Cancels the managed task""" + if self._task is not None: + self._task.cancel() + self._task = None + + @property + def frame(self): + return self._window.frame + + @property + def visible(self) -> bool: + if self._window: + return self._window.visible + return False + + def show(self, name: str = None, url: str = None): + """Shows the dialog after resetting to a default state""" + broken_url = omni.client.break_url(url or "") + host = broken_url.host if broken_url.scheme == "omniverse" else None + self.set_value("url", host) + self.set_value("name", name) + if self._progress_bar: + self._progress_bar.hide() + if self._alert_pane: + self._alert_pane.hide() + if not self._okay_button: + self._build_ok_cancel_buttons() + self._okay_button.enabled = True + self._cancel_button.enabled = True + self._okay_button.visible = True + if self._form_frame: + self._form_frame.visible = True + + self._window.visible = True + + # focus fields on show + if self._form_widget: + self._form_widget.focus() + + def hide(self): + self._window.visible = False + # reset window title on hide; authentication mode might have reset the window title. + self._window.title = self._title + + def show_waiting(self): + if self._alert_pane: + self._alert_pane.hide() + if self._progress_bar: + self._progress_bar.show() + # Disable apply button + if self._okay_button: + self._okay_button.enabled = False + + def show_alert(self, msg: str = "", alert_type: int = 0): + # hide form widget and OK button + if self._okay_button: + self._okay_button.visible = False + # Disable apply button + self._okay_button.enabled = False + if self._form_frame: + self._form_frame.visible = False + + if self._alert_pane: + self._alert_pane.show(msg, alert_type=alert_type) + if self._progress_bar: + self._progress_bar.hide() + self._window.visible = True + + def show_authenticate(self, name: str, url: str): + """Shows the dialog for authentication, when the name and url is already given.""" + # hide form widget and OK button + if self._okay_button: + self._okay_button.visible = False + if self._form_frame: + self._form_frame.visible = False + + # show login info + broken_url = omni.client.break_url(url or "") + host = broken_url.host if broken_url.scheme == "omniverse" else None + if self._alert_pane: + msg = f"Connecting to ({host}) requires login authentication. Please login from your web browser." + carb.log_warn(msg) + #self._alert_pane.show(msg, alert_type=AlertPane.Info) + + # update window title + self._window.title = "Login Required" + #self._window.visible = True + + +class LoopProgressBar: + """A looping progress bar for visual indication of ongoing processes. + + This class creates a graphical progress bar that loops to indicate an ongoing process without a defined end. It is commonly used in UIs to show that an action is taking place. + + Args: + puck_size (int): Size of the moving element in the progress bar. + period (float): The time it takes for the puck to complete one loop.""" + + def __init__(self, puck_size: int = 20, period: float = 120): + self._puck_size = puck_size + self._period = period + self._widget = None + self._spacer_left = None + self._spacer_right = None + self._loop_task = None + self._build_ui() + + def _build_ui(self): + self._widget = ui.Frame(visible=False) + with self._widget: + with ui.ZStack(height=20): + ui.Rectangle(style_type_name_override="ProgressBar") + with ui.HStack(): + self._spacer_left = ui.Spacer(width=ui.Fraction(0)) + ui.Rectangle(width=self._puck_size, style_type_name_override="ProgressBar.Puck") + self._spacer_right = ui.Spacer(width=ui.Fraction(1)) + + async def _inf_loop(self): + counter = 0 + while True: + await omni.kit.app.get_app().next_update_async() + width = float(counter % self._period) / self._period + # Ping-pong + width = width * 2 + if width > 1.0: + width = 2.0 - width + self._spacer_left.width = ui.Fraction(width) + self._spacer_right.width = ui.Fraction(1.0 - width) + counter += 1 + + def __del__(self): + self.hide() + self._widget = None + + def show(self): + if self._widget: + self._loop_task = asyncio.ensure_future(self._inf_loop()) + self._widget.visible = True + + def hide(self): + if self._widget: + if self._loop_task is not None: + self._loop_task.cancel() + self._loop_task = None + self._widget.visible = False + + +class AlertPane: + """A class representing an alert pane within an application dialog. + + This pane can be used to display informational messages or warnings to the user in a stylized format. + + Args: + width: int + The width of the alert pane in pixels.""" + + Info = 0 + Warn = 1 + + def __init__(self, width: int = 400): + self._widget: ui.Frame = None + self._frame: ui.Frame = None + self._icon_frame: ui.Frame = None + self._label: ui.Label = None + self._build_ui(width=width) + + def destroy(self): + self._label = None + self._frame = None + self._widget = None + + def _build_ui(self, width: int = 400): + self._widget = ui.Frame(visible=False, height=0) + with self._widget: + with ui.ZStack(width=width, style=UI_STYLES): + self._frame = ui.Frame() + with ui.HStack(spacing=4, style_type_name_override="AlertPane.Content"): + self._icon_frame = ui.Frame() + self._label = ui.Label("", height=20, word_wrap=True, style_type_name_override="Label") + + def show(self, msg: str = "", alert_type: int = 0): + if self._widget: + with self._frame: + style_name = "warn" if alert_type == AlertPane.Warn else "info" + ui.Rectangle(style_type_name_override="AlertPane", name=style_name) + with self._icon_frame: + icon = f"{ICON_PATH}/warn.svg" if alert_type == AlertPane.Warn else f"{ICON_PATH}/info.svg" + ui.ImageWithProvider( + icon, + width=16, + height=16, + fill_policy=ui.IwpFillPolicy.IWP_PRESERVE_ASPECT_FIT, + style_type_name_override="Image", + ) + if self._label: + self._label.text = msg + self._widget.visible = True + + def hide(self): + if self._widget: + self._widget.visible = False + + +# OM-98450: Add UI for device auth flow +class DeviceAuthFlowDialog(PopupDialog): + """A dialog for facilitating device authentication flow. + + This class manages the user interface for device authentication, providing a QR code for the user + to scan and input a code for authentication. It handles countdowns for code expiration and + retries for expired codes. + + Args: + params (omni.client.AuthDeviceFlowParams): Parameters for device authentication including URL, server, and expiration. + """ + + TEMP_DIR = os.path.join(tempfile.gettempdir(), "kit_device_auth_qrcode/") + + """The main authentication dialog for device auth flow.""" + + def __init__(self, params: omni.client.AuthDeviceFlowParams): + super().__init__(width=400, title="Device Authentication", ok_label="Retry", cancel_label="Cancel", modal=True) + self._image = None + self._time_remaining = params.expiration + self._server = params.server + self._expiration_frame = None + self._build_ui(params) + self._count_down_task = run_coroutine(self._count_expiration_async()) + self._generate_qrcode(params.url) + + @property + def visible(self) -> bool: + if self._window: + return self._window.visible + return False + + def _build_ui(self, params: omni.client.AuthDeviceFlowParams): + with self._window.frame: + with ui.VStack(style=UI_STYLES, style_type_name_override="Dialog"): + ui.Label(f"Connecting to {params.server}", alignment=ui.Alignment.CENTER) + with ui.HStack(): + ui.Spacer() + self._image = ui.Image(width=128, height=128, style_type_name_override="QrCode") + ui.Spacer() + with ui.HStack(): + ui.Label(f"Please go to ", width=0) + # OM-102092: Change URL to be a string field so it is selectable and could be copied + # TODO: Note that if the URL is too long, currently it could get clipped and will not display the + # entire url; Solution could be: + # 1) Have both a label and a string field, and when mouse pressed, switch to string field for + # selection; which is a bit hacky, since the string field height need to be dynamically adjusted + # 2) have omni.ui.Label support selection of text + ui.StringField( + ui.SimpleStringModel(params.url), + style=UI_STYLES, + style_type_name_override="StringField.Url", + read_only=True, + ) + ui.Label("or scan the qrcode above, and enter the code below:") + ui.Spacer(height=2) + ui.Label(params.code, style_type_name_override="Label.Code") + ui.Spacer(height=2) + self._expiration_frame = ui.Frame() + with self._expiration_frame: + with ui.HStack(spacing=4): + ui.Spacer() + ui.Label("Code expires in", width=0) + self._time_remaining_label = ui.Label( + str(self._time_remaining), style_type_name_override="Label.TimeRemaining", width=0 + ) + ui.Label("seconds", width=0) + ui.Spacer() + ui.Spacer(height=10) + self._build_ok_cancel_buttons() + ui.Spacer(height=30) + self.set_cancel_clicked_fn(lambda _: self._on_cancel_fn()) + self.set_okay_clicked_fn(lambda _: self._on_retry()) + self._okay_button.visible = False + + def _on_cancel_fn(self): + self._count_down_task.cancel() + self.hide() + + def _on_retry(self): + self._on_cancel() + if not self._server.startswith("omniverse://"): + server = "omniverse://" + self._server + else: + server = self._server + omni.client.reconnect(server) + + def destroy(self): + if self._count_down_task: + self._count_down_task.cancel() + self._count_down_task = None + super().destroy() + + def __del__(self): + self.destroy() + + def hide(self): + self._window.visible = False + # reset window title on hide; authentication mode might have reset the window title. + self._window.title = self._title + + def show_expired(self): + """Show the user when the code has expired.""" + with self._expiration_frame: + ui.Label("Code has expired. Please try again.", style_type_name_override="Label.Expired") + self._okay_button.visible = True + + def _generate_qrcode(self, url: str): # pragma: no cover + """Generates a qrcode from the given url.""" + if not os.path.exists(self.TEMP_DIR): + os.mkdir(self.TEMP_DIR) + # Swap out the : && / since the url we are dealing with are usually something like: "https://a.b.c/x/y", + # replacing these special characters with "_" here so we don't end up violating OS naming conventions + img_name = url.replace("/", "_").replace(":", "_") + img_path = os.path.join(self.TEMP_DIR, f"qr_{img_name}.png") + + # we could skip generating the qrcode for the same url, since the image would be the same with the same encoding + if not os.path.exists(img_path): + # try to use qrcode module to generate the image + try: + import qrcode + except ImportError: + carb.log_warn("Package qrcode not installed. Skipping qr code generation.") + return + + img = qrcode.make(url) + img.save(img_path) + + # update image source url + self._image.source_url = img_path + + async def _count_expiration_async(self): + """Counts down the remaining time in seconds before expiration.""" + while self._time_remaining > 0: + await asyncio.sleep(1) + self._time_remaining -= 1 + self._time_remaining_label.text = str(self._time_remaining) + self.show_expired() diff --git a/simulation/isaac-sim/docker/user_TEMPLATE.config.json b/simulation/isaac-sim/docker/user_TEMPLATE.config.json index b0903a52..5addac52 100644 --- a/simulation/isaac-sim/docker/user_TEMPLATE.config.json +++ b/simulation/isaac-sim/docker/user_TEMPLATE.config.json @@ -2702,7 +2702,6 @@ "exts": { "enabled": { "0": "airlab.airstack-1.19.1", - "1": "airlab.tmux_manager-0.1.0" } }, "window": { @@ -2713,4 +2712,4 @@ } } } -} \ No newline at end of file +} diff --git a/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/impl/extension.py b/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/impl/extension.py index 32a7a280..4ff80178 100644 --- a/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/impl/extension.py +++ b/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/impl/extension.py @@ -7,11 +7,13 @@ import socket import struct import time +import subprocess import carb import carb.dictionary import omni import omni.ext +import omni.ui as ui import omni.graph.core as og from omni.kit.app import get_app from omni.isaac.core.world import World @@ -219,6 +221,13 @@ def set_current_sim_time(self, current_sim_time): self.current_sim_time = current_sim_time +def get_tmux_sessions(): + text = subprocess.getoutput("tmux ls") + + if ":" not in text: + return [] + return list(map(lambda x: x.split(":")[0], text.split("\n"))) + # ============================================================================================================== class _PublicExtension(omni.ext.IExt): """Object that tracks the lifetime of the Python part of the extension loading""" @@ -263,6 +272,93 @@ def on_startup(self): # init physics callback self.init_physics() + + + self.window = ui.Window("TMUX Manager", width=300, height=300) + self.window.deferred_dock_in("Property", ui.DockPolicy.DO_NOTHING) + + #''' + with self.window.frame: + self.scroll = ui.ScrollingFrame( + horizontal_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + vertical_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.scroll: + with ui.VStack(): + with ui.HStack(height=50): + ui.Label("TMUX Sessions:") + ui.Button("Refresh", clicked_fn=self.refresh_tmux_sessions) + + ui.Spacer(height=5) + self.sessions_stack = ui.VStack() + #''' + self.sessions_dict = {} + + self.refresh_tmux_sessions() + + def refresh_tmux_sessions(self): + sessions = get_tmux_sessions() + + # even when the hstack is destroyed it still takes up space so it is made not visible + # this probably means that the stack is still around and maybe if enough of them are created it will slow things down + # maybe a better way is to reuse them and if there are less than before make them not visible until more are needed + # for now it doesn't seem like it is impacting performance + for k in self.sessions_dict.keys(): + self.sessions_dict[k]["label"].destroy() + self.sessions_dict[k]["attach_button"].destroy() + self.sessions_dict[k]["kill_button"].destroy() + self.sessions_dict[k]["hstack"].destroy() + self.sessions_dict[k]["hstack"].visible = False + self.sessions_dict = {} + + for s in sessions: + if s in self.sessions_dict.keys(): + continue + self.sessions_dict[s] = {} + + with self.sessions_stack: + self.sessions_dict[s]["hstack"] = ui.HStack(height=50) + with self.sessions_dict[s]["hstack"]: + self.sessions_dict[s]["label"] = ui.Label(s) + + def get_attach(session_name): + def attach(): + # print('xterm -e "tmux a -t ' + session_name + '"') + subprocess.Popen( + 'xterm -bg black -fg white -e "tmux a -t \\"' + + session_name + + '\\""', + shell=True, + ) + + return attach + + def get_kill(session_name): + def kill(): + # print('xterm -e "tmux kill-session -t ' + session_name + '"') + subprocess.Popen( + 'xterm -e "tmux kill-session -t \\"' + + session_name + + '\\""', + shell=True, + ) + + return kill + + self.sessions_dict[s]["attach_button"] = ui.Button( + "Attach", clicked_fn=get_attach(s) + ) + self.sessions_dict[s]["kill_button"] = ui.Button( + "Kill", clicked_fn=get_kill(s) + ) + + keys_to_remove = [] + for k in self.sessions_dict.keys(): + if k not in sessions: + self.sessions_dict[k]["hstack"].destroy() + keys_to_remove.append(k) + for k in keys_to_remove: + del self.sessions_dict[k] def init_physics(self, was_playing=False): self.world = World() @@ -287,6 +383,8 @@ def timeline_callback(self, event): self.init_physics() elif event.type == int(omni.timeline.TimelineEventType.STOP): self.current_sim_time = 0. + + self.refresh_tmux_sessions() def physics_callback(self, sim_time_delta): self.current_sim_time += sim_time_delta @@ -295,6 +393,7 @@ def physics_callback(self, sim_time_delta): def on_shutdown(self): print('SHUTDOWN') + self.refresh_tmux_sessions() self.__stage_subscription = None self.__opt_in_setting_sub = None self.world.remove_physics_callback('airstack_physics_callback') diff --git a/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/ogn/OgnAscentNodeDatabase.py b/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/ogn/OgnAscentNodeDatabase.py index b985c77d..ab72d8ed 100644 --- a/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/ogn/OgnAscentNodeDatabase.py +++ b/simulation/isaac-sim/sitl_integration/kit-app-template/source/extensions/airlab.airstack/airlab/airstack/ogn/OgnAscentNodeDatabase.py @@ -135,30 +135,30 @@ class OgnAscentNodeDatabase(og.Database): False, "", ), - ( - "inputs:deltaSimulationTime", - "double", - 0, - "Simulation Delta Time", - "Description.", - {}, - True, - 0.0, - False, - "", - ), - ( - "inputs:deltaSystemTime", - "double", - 0, - "System Delta Time", - "Description.", - {}, - True, - 0.0, - False, - "", - ), + # ( + # "inputs:deltaSimulationTime", + # "double", + # 0, + # "Simulation Delta Time", + # "Description.", + # {}, + # True, + # 0.0, + # False, + # "", + # ), + # ( + # "inputs:deltaSystemTime", + # "double", + # 0, + # "System Delta Time", + # "Description.", + # {}, + # True, + # 0.0, + # False, + # "", + # ), ( "inputs:dronePrim", "target", @@ -234,8 +234,8 @@ def _populate_role_data(cls): class ValuesForInputs(og.DynamicAttributeAccess): LOCAL_PROPERTY_NAMES = { "execIn", - "deltaSimulationTime", - "deltaSystemTime", + #"deltaSimulationTime", + #"deltaSystemTime", "dronePrim", "domain_id", "nodeNamespace", @@ -259,8 +259,8 @@ def __init__( print("init 3") self._batchedReadAttributes = [ self._attributes.execIn, - self._attributes.deltaSimulationTime, - self._attributes.deltaSystemTime, + #self._attributes.deltaSimulationTime, + #self._attributes.deltaSystemTime, self._attributes.dronePrim, self._attributes.domain_id, self._attributes.nodeNamespace, @@ -278,7 +278,7 @@ def execIn(self): def execIn(self, value): print("execIn 2") self._batchedReadValues[0] = value - + ''' @property def deltaSimulationTime(self): return self._batchedReadValues[1] @@ -294,30 +294,30 @@ def deltaSystemTime(self): @deltaSystemTime.setter def deltaSystemTime(self, value): self._batchedReadValues[2] = value - + ''' @property def dronePrim(self): - return self._batchedReadValues[3] + return self._batchedReadValues[1] @dronePrim.setter def dronePrim(self, value): - self._batchedReadValues[3] = value + self._batchedReadValues[1] = value @property def domain_id(self): - return self._batchedReadValues[4] + return self._batchedReadValues[2] @domain_id.setter def domain_id(self, value): - self._batchedReadValues[4] = value + self._batchedReadValues[2] = value @property def nodeNamespace(self): - return self._batchedReadValues[5] + return self._batchedReadValues[3] @nodeNamespace.setter def nodeNamespace(self, value): - self._batchedReadValues[5] = value + self._batchedReadValues[3] = value def __getattr__(self, item: str): if item in self.LOCAL_PROPERTY_NAMES: @@ -454,20 +454,30 @@ def get_node_type(): @staticmethod def compute(context, node): - # print('compute 1') + #print('compute 1') def database_valid(): return True try: + #print('1') per_node_data = OgnAscentNodeDatabase.PER_NODE_DATA[node.node_id()] + #print('2') db = per_node_data.get("_db") + #print('3') if db is None: + #print('4') db = OgnAscentNodeDatabase(node) + #print('5') per_node_data["_db"] = db + #print('6') if not database_valid(): + #print('7') per_node_data["_db"] = None + #print('8') return False - except: + except Exception as e: + #print('9', e) + #traceback.print_exc() db = OgnAscentNodeDatabase(node) # print('input test', dir(db.inputs)) # print('delta sim time', db.inputs.deltaSimulationTime)