Collection of samples to help developers build their own IoT solution for Omniverse
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

import omni.ext
import omni.ui as ui
from pxr import Usd, Sdf, Tf, UsdGeom
import omni.ui.color_utils as cl

TRANSLATE_OFFSET = "xformOp:translate:offset"
ROTATE_SPIN = "xformOp:rotateX:spin"


class uiTextStyles:
    title = {"margin": 10, "color": 0xFFFFFFFF, "font_size": 18, "alignment": ui.Alignment.LEFT_CENTER}
    title2 = {"margin": 10, "color": 0xFFFFFFFF, "font_size": 18, "alignment": ui.Alignment.LEFT_CENTER}


class uiElementStyles:
    mainWindow = {"Window": {"background_color": cl.color(32, 42, 87, 100), "width": 350}}


class uiButtonStyles:
    mainButton = {
        "Button": {"background_color": cl.color(32, 42, 87, 125), "width": 175, "height": 80},
        "Button:hovered": {"background_color": cl.color(32, 42, 87, 200)},
    }

    # geometry manipulation


class LiveCube:
    def __init__(self, stage: Usd.Stage, path: str):
        self._prim = stage.GetPrimAtPath(path)
        self._op = self._prim.HasProperty(TRANSLATE_OFFSET)
        if self._prim:
            self._xform = UsdGeom.Xformable(self._prim)

    def resume(self):
        if self._xform and not self._op:
            op = self._xform.AddTranslateOp(opSuffix="offset")
            op.Set(time=1, value=(0, -20.0, 0))
            op.Set(time=192, value=(0, -950, 0))
            self._op = True

    def pause(self):
        if self._xform and self._op:
            default_ops = []
            for op in self._xform.GetOrderedXformOps():
                if op.GetOpName() != TRANSLATE_OFFSET:
                    default_ops.append(op)
            self._xform.SetXformOpOrder(default_ops)
            self._prim.RemoveProperty(TRANSLATE_OFFSET)
            self._op = False


class LiveRoller:
    def __init__(self, stage: Usd.Stage, path: str):
        self._prim = stage.GetPrimAtPath(path)
        self._op = self._prim.HasProperty(ROTATE_SPIN)
        if self._prim:
            self._xform = UsdGeom.Xformable(self._prim)

    def resume(self):
        if self._xform and not self._op:
            op = self._xform.AddRotateXOp(opSuffix="spin")
            op.Set(time=1, value=0)
            op.Set(time=192, value=1440)
            self._op = True

    def pause(self):
        if self._xform and self._op:
            default_ops = []
            for op in self._xform.GetOrderedXformOps():
                if op.GetOpName() != ROTATE_SPIN:
                    default_ops.append(op)
            self._xform.SetXformOpOrder(default_ops)
            self._prim.RemoveProperty(ROTATE_SPIN)
            self._op = False


# Any class derived from `omni.ext.IExt` in top level module (defined in `python.modules` of `extension.toml`) will be
# instantiated when extension gets enabled and `on_startup(ext_id)` will be called. Later when extension gets disabled
# on_shutdown() is called.
class OmniIotSamplePanelExtension(omni.ext.IExt):
    # ext_id is current extension id. It can be used with extension manager to query additional information, like where
    # this extension is located on filesystem.
    def on_startup(self, ext_id):
        print("[omni.iot.sample.panel] startup")

        self._usd_context = omni.usd.get_context()
        self._stage = self._usd_context.get_stage()
        self._selected_prim = None

        live_layer_path = None
        root_layer = self._stage.GetRootLayer()
        subLayerPaths = root_layer.subLayerPaths
        for subLayerPath in subLayerPaths:
            if subLayerPath.endswith(".live"):
                live_layer_path = subLayerPath

        self._window = ui.Window("Sample IoT Data", width=350, height=380)
        self._window.frame.set_style(uiElementStyles.mainWindow)
        self._stage.SetEditTarget(root_layer)
        root_layer.startTimeCode = 1
        root_layer.endTimeCode = 192
        UsdGeom.SetStageUpAxis(self._stage, UsdGeom.Tokens.z)

        if live_layer_path:
            live_layer = None
            for sub_layer in self._stage.GetLayerStack():
                if sub_layer.realPath == live_layer_path:
                    live_layer = sub_layer

            if live_layer:
                live_layer.startTimeCode = 1
                live_layer.endTimeCode = 192
                # self._stage.SetEditTarget(live_layer)
                self._iot_prim = self._stage.GetPrimAtPath("/iot")
                self._cube = LiveCube(self._stage, "/World/Cube")
                self._rollers = []

                for x in range(38):
                    self._rollers.append(
                        LiveRoller(self._stage, f"/World/Geometry/SM_ConveyorBelt_A08_Roller{x+1:02d}_01")
                    )

                # this will capture when the select changes in the stage_selected_iot_prim_label
                self._stage_event_sub = self._usd_context.get_stage_event_stream().create_subscription_to_pop(
                    self._on_stage_event, name="Stage Update"
                )

                # this will capture changes to the IoT data
                self.listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._on_objects_changed, self._stage)

                # create an simple window with empty VStack for the IoT data
                with self._window.frame:
                    with ui.VStack():
                        with ui.HStack(height=22):
                            ui.Label("IoT Prim:", style=uiTextStyles.title, width=75)
                            self._selected_iot_prim_label = ui.Label(" ", style=uiTextStyles.title)
                        self._property_stack = ui.VStack(height=22)

                if self._iot_prim:
                    self._on_selected_prim_changed()

    def on_shutdown(self):
        print("[omni.iot.sample.panel] shutdown")

    def _on_velocity_changed(self, speed):
        print(f"[omni.iot.sample.panel] _on_velocity_changed: {speed}")
        if speed is not None and speed > 0.0:
            with Sdf.ChangeBlock():
                self._cube.resume()
                for roller in self._rollers:
                    roller.resume()
        else:
            with Sdf.ChangeBlock():
                self._cube.pause()
                for roller in self._rollers:
                    roller.pause()

    def _update_frame(self):
        if self._selected_prim is not None:
            self._property_stack.clear()
            properties = self._selected_prim.GetProperties()
            button_height = uiButtonStyles.mainButton["Button"]["height"]
            self._property_stack.height.value = (round(len(properties) / 2) + 1) * button_height
            x = 0
            hStack = ui.HStack()
            self._property_stack.add_child(hStack)
            # repopulate the VStack with the IoT data attributes
            for prop in properties:
                if x > 0 and x % 2 == 0:
                    hStack = ui.HStack()
                    self._property_stack.add_child(hStack)
                prop_name = prop.GetName()
                prop_value = prop.Get()
                ui_button = ui.Button(f"{prop_name}\n{str(prop_value)}", style=uiButtonStyles.mainButton)
                hStack.add_child(ui_button)
                if prop_name == "Velocity":
                    self._on_velocity_changed(prop_value)
                x += 1

            if x % 2 != 0:
                with hStack:
                    ui.Button("", style=uiButtonStyles.mainButton)

    def _on_selected_prim_changed(self):
        print("[omni.iot.sample.panel] _on_selected_prim_changed")
        selected_prim = self._usd_context.get_selection()
        selected_paths = selected_prim.get_selected_prim_paths()
        if selected_paths and len(selected_paths):
            sdf_path = Sdf.Path(selected_paths[0])

            # only handle data that resides under the /iot prim
            if (
                sdf_path.IsPrimPath()
                and sdf_path.HasPrefix(self._iot_prim.GetPath())
                and sdf_path != self._iot_prim.GetPath()
            ):
                self._selected_prim = self._stage.GetPrimAtPath(sdf_path)
                self._selected_iot_prim_label.text = str(sdf_path)
                self._update_frame()

    # ===================== stage events START =======================
    def _on_selection_changed(self):
        print("[omni.iot.sample.panel] _on_selection_changed")
        if self._iot_prim:
            self._on_selected_prim_changed()

    def _on_asset_opened(self):
        print("[omni.iot.sample.panel] on_asset_opened")

    def _on_stage_event(self, event):
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            self._on_selection_changed()
        elif event.type == int(omni.usd.StageEventType.OPENED):
            self._on_asset_opened()

    def _on_objects_changed(self, notice, stage):
        updated_objects = []
        for p in notice.GetChangedInfoOnlyPaths():
            if p.IsPropertyPath() and p.GetParentPath() == self._selected_prim.GetPath():
                updated_objects.append(p)

        if len(updated_objects) > 0:
            self._update_frame()

    # ===================== stage events END =======================

exts/omni.iot.sample.panel/omni/iot/sample/panel/extension.py
NVIDIA-Omniverse/iot-samples
from .test_hello_world import *
exts/omni.iot.sample.panel/omni/iot/sample/panel/tests/__init__.py
NVIDIA-Omniverse/iot-samples
# NOTE:
#   omni.kit.test - std python's unittest module with additional wrapping to add suport for async/await tests
#   For most things refer to unittest docs: https://docs.python.org/3/library/unittest.html
import omni.kit.test

# Extnsion for writing UI tests (simulate UI interaction)
import omni.kit.ui_test as ui_test

# Import extension python module we are testing with absolute import path, as if we are external user (other extension)
import omni.iot.sample.panel


# Having a test class dervived from omni.kit.test.AsyncTestCase declared on the root of module will make it auto-discoverable by omni.kit.test
class Test(omni.kit.test.AsyncTestCase):
    # Before running each test
    async def setUp(self):
        pass

    # After running each test
    async def tearDown(self):
        pass

    # Actual test, notice it is "async" function, so "await" can be used if needed
    @omni.kit.test.omni_test_registry(guid="f898a949-bacc-41f5-be56-b4eb8923f54e")
    async def test_hello_public_function(self):
        result = omni.iot.sample.panel.some_public_function(4)
        self.assertEqual(result, 256)


    @omni.kit.test.omni_test_registry(guid="4626d574-659f-4a85-8958-9fa8588fbce3")
    async def test_window_button(self):

        # Find a label in our window
        label = ui_test.find("My Window//Frame/**/Label[*]")

        # Find buttons in our window
        add_button = ui_test.find("My Window//Frame/**/Button[*].text=='Add'")
        reset_button = ui_test.find("My Window//Frame/**/Button[*].text=='Reset'")

        # Click reset button
        await reset_button.click()
        self.assertEqual(label.widget.text, "empty")

        await add_button.click()
        self.assertEqual(label.widget.text, "count: 1")

        await add_button.click()
        self.assertEqual(label.widget.text, "count: 2")

exts/omni.iot.sample.panel/omni/iot/sample/panel/tests/test_hello_world.py
NVIDIA-Omniverse/iot-samples
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

# pip install pandas

import asyncio
import os
import omni.client
from pxr import Usd, Sdf
from pathlib import Path
import pandas as pd
import time

OMNI_HOST = os.environ.get("OMNI_HOST", "localhost")
BASE_URL = "omniverse://" + OMNI_HOST + "/Projects/IoT/Samples/HeadlessApp"
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
CONTENT_DIR = Path(SCRIPT_DIR).resolve().parents[1].joinpath("content")

messages = []


def log_handler(thread, component, level, message):
    # print(message)
    messages.append((thread, component, level, message))


def initialize_device_prim(live_layer, iot_topic):
    iot_root = live_layer.GetPrimAtPath("/iot")
    if not iot_root:
        iot_root = Sdf.PrimSpec(live_layer, "iot", Sdf.SpecifierDef, "IoT Root")

    iot_spec = live_layer.GetPrimAtPath(f"/iot/{iot_topic}")
    if not iot_spec:
        iot_spec = Sdf.PrimSpec(iot_root, iot_topic, Sdf.SpecifierDef, "ConveyorBelt Type")
    if not iot_spec:
        raise Exception("Failed to create the IoT Spec.")

    # clear out any attrubutes that may be on the spec
    for attrib in iot_spec.attributes:
        iot_spec.RemoveProperty(attrib)

    IOT_TOPIC_DATA = f"{CONTENT_DIR}/{iot_topic}_iot_data.csv"
    data = pd.read_csv(IOT_TOPIC_DATA)
    data.head()

    # create all the IoT attributes that will be written
    attr = Sdf.AttributeSpec(iot_spec, "_ts", Sdf.ValueTypeNames.Double)
    if not attr:
        raise Exception(f"Could not define the attribute: {attrName}")

    # infer the unique data points in the CSV.
    # The values may be known in advance and can be hard coded
    grouped = data.groupby("Id")
    for attrName, group in grouped:
        attr = Sdf.AttributeSpec(iot_spec, attrName, Sdf.ValueTypeNames.Double)
        if not attr:
            raise Exception(f"Could not define the attribute: {attrName}")


def create_live_layer(iot_topic):
    LIVE_URL = f"{BASE_URL}/{iot_topic}.live"

    live_layer = Sdf.Layer.CreateNew(LIVE_URL)
    if not live_layer:
        raise Exception(f"Could load the live layer {LIVE_URL}.")

    Sdf.PrimSpec(live_layer, "iot", Sdf.SpecifierDef, "IoT Root")
    live_layer.Save()
    return live_layer


async def initialize_async(iot_topic):
    # copy a the Conveyor Belt to the target nucleus server
    LOCAL_URL = f"file:{CONTENT_DIR}/ConveyorBelt_{iot_topic}.usd"
    STAGE_URL = f"{BASE_URL}/ConveyorBelt_{iot_topic}.usd"
    LIVE_URL = f"{BASE_URL}/{iot_topic}.live"
    result = await omni.client.copy_async(
        LOCAL_URL,
        STAGE_URL,
        behavior=omni.client.CopyBehavior.ERROR_IF_EXISTS,
        message="Copy Conveyor Belt",
    )

    stage = Usd.Stage.Open(STAGE_URL)
    if not stage:
        raise Exception(f"Could load the stage {STAGE_URL}.")

    root_layer = stage.GetRootLayer()
    live_layer = Sdf.Layer.FindOrOpen(LIVE_URL)
    if not live_layer:
        live_layer = create_live_layer(iot_topic)

    found = False
    subLayerPaths = root_layer.subLayerPaths
    for subLayerPath in subLayerPaths:
        if subLayerPath == live_layer.identifier:
            found = True

    if not found:
        root_layer.subLayerPaths.append(live_layer.identifier)
        root_layer.Save()

    # set the live layer as the edit target
    stage.SetEditTarget(live_layer)
    initialize_device_prim(live_layer, iot_topic)
    omni.client.live_process()
    return stage, live_layer


def write_to_live(live_layer, iot_topic, group, ts):
    # write the iot values to the usd prim attributes
    print(group.iloc[0]["TimeStamp"])
    ts_attribute = live_layer.GetAttributeAtPath(f"/iot/{iot_topic}._ts")
    ts_attribute.default = ts
    with Sdf.ChangeBlock():
        for index, row in group.iterrows():
            id = row["Id"]
            value = row["Value"]
            attr = live_layer.GetAttributeAtPath(f"/iot/{iot_topic}.{id}")
            if not attr:
                raise Exception(f"Could not find attribute /iot/{iot_topic}.{id}.")
            attr.default = value
    omni.client.live_process()


def run(stage, live_layer, iot_topic):
    # we assume that the file contains the data for single device
    IOT_TOPIC_DATA = f"{CONTENT_DIR}/{iot_topic}_iot_data.csv"
    data = pd.read_csv(IOT_TOPIC_DATA)
    data.head()

    # Converting to DateTime Format and drop ms
    data["TimeStamp"] = pd.to_datetime(data["TimeStamp"])
    data["TimeStamp"] = data["TimeStamp"].dt.floor("s")

    data.set_index("TimeStamp")
    start_time = data.min()["TimeStamp"]
    last_time = start_time
    grouped = data.groupby("TimeStamp")

    # play back the data in real-time
    for next_time, group in grouped:
        diff = (next_time - last_time).total_seconds()
        if diff > 0:
            time.sleep(diff)
        write_to_live(live_layer, iot_topic, group, (next_time - start_time).total_seconds())
        last_time = next_time


if __name__ == "__main__":
    IOT_TOPIC = "A08_PR_NVD_01"
    omni.client.initialize()
    omni.client.set_log_level(omni.client.LogLevel.DEBUG)
    omni.client.set_log_callback(log_handler)
    try:
        stage, live_layer = asyncio.run(initialize_async(IOT_TOPIC))
        run(stage, live_layer, IOT_TOPIC)
    finally:
        omni.client.shutdown()

source/ingest_app_csv/app.py
NVIDIA-Omniverse/iot-samples
# Copyright (c) 2023, 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.

import os
import argparse
import platform
import subprocess
from pathlib import Path

PLATFORM_SYSTEM = platform.system().lower()
PLATFORM_MACHINE = platform.machine()

if PLATFORM_MACHINE == "i686" or PLATFORM_MACHINE == "AMD64":
    PLATFORM_MACHINE = "x86_64"

CURRENT_PLATFORM = f"{PLATFORM_SYSTEM}-{PLATFORM_MACHINE}"

parser = argparse.ArgumentParser()
parser.add_argument("--server", "-s", default="localhost")
parser.add_argument("--username", "-u")
parser.add_argument("--password", "-p")
parser.add_argument("--config", "-c", choices=["debug", "release"], default="release")
parser.add_argument("--platform", default=CURRENT_PLATFORM)
args = parser.parse_args()

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
ROOT_DIR = Path(SCRIPT_DIR).resolve().parents[1]

BUILD_DIR = ROOT_DIR.joinpath("_build", args.platform, args.config)
DEPS_DIR = ROOT_DIR.joinpath("_build", "target-deps")
APP_DIR = ROOT_DIR.joinpath("app")
PYTHON_LIB = DEPS_DIR.joinpath("python")
USD_BIN_DIR = DEPS_DIR.joinpath("usd", args.config, "bin")
USD_LIB_DIR = DEPS_DIR.joinpath("usd", args.config, "lib")
CL_LIB_DIR = DEPS_DIR.joinpath("omni_client_library", args.config)

EXTRA_PATHS = [str(CL_LIB_DIR), str(USD_BIN_DIR), str(USD_LIB_DIR), str(BUILD_DIR), str(PYTHON_LIB)]
EXTRA_PYTHON_PATHS = [
    str(USD_LIB_DIR.joinpath("python")),
    str(CL_LIB_DIR.joinpath("bindings-python")),
    str(BUILD_DIR.joinpath("bindings-python")),
]

if PLATFORM_SYSTEM == "windows":
    os.environ["PATH"] += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    ot_bin = "carb.omnitrace.plugin.dll"
else:
    p = os.environ.get("LD_LIBRARY_PATH", "")
    p += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    os.environ["LD_LIBRARY_PATH"] = p
    ot_bin = "libcarb.omnitrace.plugin.so"

os.environ["OMNI_TRACE_LIB"] = os.path.join(str(DEPS_DIR), "omni-trace", "bin", ot_bin)
os.environ["PYTHONPATH"] = os.pathsep + os.pathsep.join(EXTRA_PYTHON_PATHS)
os.environ["OMNI_USER"] = args.username
os.environ["OMNI_PASS"] = args.password
os.environ["OMNI_HOST"] = args.server

if PLATFORM_SYSTEM == "windows":
    PYTHON_EXE = APP_DIR.joinpath("kit", "python", "python")
else:
    PYTHON_EXE = APP_DIR.joinpath("kit" "python", "bin", "python3")

plugin_paths = DEPS_DIR.joinpath("omni_usd_resolver", args.config, "usd", "omniverse", "resources")
os.environ["PXR_PLUGINPATH_NAME"] = str(plugin_paths)
REQ_FILE = ROOT_DIR.joinpath("requirements.txt")
subprocess.run(f"{PYTHON_EXE} -m pip install -r {REQ_FILE}", shell=True)
result = subprocess.run(
    [PYTHON_EXE, os.path.join(SCRIPT_DIR, "app.py")],
    stderr=subprocess.STDOUT,
)

source/ingest_app_csv/run_app.py
NVIDIA-Omniverse/iot-samples
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

# pip install pandas
# pip install paho-mqtt

import asyncio
import os
import omni.client
from pxr import Usd, Sdf
from pathlib import Path
import pandas as pd
import time
from paho.mqtt import client as mqtt_client
import random
import json

OMNI_HOST = os.environ.get("OMNI_HOST", "localhost")
BASE_URL = "omniverse://" + OMNI_HOST + "/Projects/IoT/Samples/HeadlessApp"
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
CONTENT_DIR = Path(SCRIPT_DIR).resolve().parents[1].joinpath("content")

messages = []


def log_handler(thread, component, level, message):
    # print(message)
    messages.append((thread, component, level, message))


def initialize_device_prim(live_layer, iot_topic):
    iot_root = live_layer.GetPrimAtPath("/iot")
    iot_spec = live_layer.GetPrimAtPath(f"/iot/{iot_topic}")
    if not iot_spec:
        iot_spec = Sdf.PrimSpec(iot_root, iot_topic, Sdf.SpecifierDef, "ConveyorBelt Type")
    if not iot_spec:
        raise Exception("Failed to create the IoT Spec.")

    # clear out any attrubutes that may be on the spec
    for attrib in iot_spec.attributes:
        iot_spec.RemoveProperty(attrib)

    IOT_TOPIC_DATA = f"{CONTENT_DIR}/{iot_topic}_iot_data.csv"
    data = pd.read_csv(IOT_TOPIC_DATA)
    data.head()

    # create all the IoT attributes that will be written
    attr = Sdf.AttributeSpec(iot_spec, "_ts", Sdf.ValueTypeNames.Double)
    if not attr:
        raise Exception(f"Could not define the attribute: {attrName}")

    # infer the unique data points in the CSV.
    # The values may be known in advance and can be hard coded
    grouped = data.groupby("Id")
    for attrName, group in grouped:
        attr = Sdf.AttributeSpec(iot_spec, attrName, Sdf.ValueTypeNames.Double)
        if not attr:
            raise Exception(f"Could not define the attribute: {attrName}")


def create_live_layer(iot_topic):
    LIVE_URL = f"{BASE_URL}/{iot_topic}.live"

    live_layer = Sdf.Layer.CreateNew(LIVE_URL)
    if not live_layer:
        raise Exception(f"Could load the live layer {LIVE_URL}.")

    iot_root = Sdf.PrimSpec(live_layer, "iot", Sdf.SpecifierDef, "IoT Root")
    live_layer.Save()
    return live_layer


async def initialize_async(iot_topic):
    # copy a the Conveyor Belt to the target nucleus server
    LOCAL_URL = f"file:{CONTENT_DIR}/ConveyorBelt_{iot_topic}.usd"
    STAGE_URL = f"{BASE_URL}/ConveyorBelt_{iot_topic}.usd"
    LIVE_URL = f"{BASE_URL}/{iot_topic}.live"
    result = await omni.client.copy_async(
        LOCAL_URL,
        STAGE_URL,
        behavior=omni.client.CopyBehavior.ERROR_IF_EXISTS,
        message="Copy Conveyor Belt",
    )

    stage = Usd.Stage.Open(STAGE_URL)
    if not stage:
        raise Exception(f"Could load the stage {STAGE_URL}.")

    root_layer = stage.GetRootLayer()
    live_layer = Sdf.Layer.FindOrOpen(LIVE_URL)
    if not live_layer:
        live_layer = create_live_layer(iot_topic)

    found = False
    subLayerPaths = root_layer.subLayerPaths
    for subLayerPath in subLayerPaths:
        if subLayerPath == live_layer.identifier:
            found = True

    if not found:
        root_layer.subLayerPaths.append(live_layer.identifier)
        root_layer.Save()

    initialize_device_prim(live_layer, iot_topic)

    # set the live layer as the edit target
    stage.SetEditTarget(live_layer)
    return stage, live_layer


def write_to_live(live_layer, iot_topic, msg_content):
    # write the iot values to the usd prim attributes
    payload = json.loads(msg_content)
    with Sdf.ChangeBlock():
        for i, (id, value) in enumerate(payload.items()):
            attr = live_layer.GetAttributeAtPath(f"/iot/{iot_topic}.{id}")
            if not attr:
                raise Exception(f"Could not find attribute /iot/{iot_topic}.{id}.")
            attr.default = value
    omni.client.live_process()


# publish to mqtt broker
def write_to_mqtt(mqtt_client, iot_topic, group, ts):
    # write the iot values to the usd prim attributes
    topic = f"iot/{iot_topic}"
    print(group.iloc[0]["TimeStamp"])
    payload = {"_ts": ts}
    for index, row in group.iterrows():
        payload[row["Id"]] = row["Value"]
    mqtt_client.publish(topic, json.dumps(payload, indent=2).encode("utf-8"))


# connect to mqtt broker
def connect_mqtt(iot_topic):
    topic = f"iot/{iot_topic}"

    # called when a message arrives
    def on_message(client, userdata, msg):
        msg_content = msg.payload.decode()
        write_to_live(live_layer, iot_topic, msg_content)
        print(f"Received `{msg_content}` from `{msg.topic}` topic")

    # called when connection to mqtt broker has been established
    def on_connect(client, userdata, flags, rc):
        if rc == 0:
            # connect to our topic
            print(f"Subscribing to topic: {topic}")
            client.subscribe(topic)
        else:
            print(f"Failed to connect, return code {rc}")

    # let us know when we've subscribed
    def on_subscribe(client, userdata, mid, granted_qos):
        print(f"subscribed {mid} {granted_qos}")

    # Set Connecting Client ID
    client = mqtt_client.Client(f"python-mqtt-{random.randint(0, 1000)}")

    client.on_connect = on_connect
    client.on_message = on_message
    client.on_subscribe = on_subscribe
    client.connect("test.mosquitto.org", 1883)
    client.loop_start()
    return client


def run(stage, live_layer, iot_topic):
    # we assume that the file contains the data for single device
    IOT_TOPIC_DATA = f"{CONTENT_DIR}/{iot_topic}_iot_data.csv"
    data = pd.read_csv(IOT_TOPIC_DATA)
    data.head()

    # Converting to DateTime Format and drop ms
    data["TimeStamp"] = pd.to_datetime(data["TimeStamp"])
    data["TimeStamp"] = data["TimeStamp"].dt.floor("s")

    data.set_index("TimeStamp")
    start_time = data.min()["TimeStamp"]
    last_time = start_time
    grouped = data.groupby("TimeStamp")

    mqtt_client = connect_mqtt(iot_topic)

    # play back the data in real-time
    for next_time, group in grouped:
        diff = (next_time - last_time).total_seconds()
        if diff > 0:
            time.sleep(diff)
            write_to_mqtt(mqtt_client, iot_topic, group, (next_time - start_time).total_seconds())
        last_time = next_time

    mqtt_client = None


if __name__ == "__main__":
    IOT_TOPIC = "A08_PR_NVD_01"
    omni.client.initialize()
    omni.client.set_log_level(omni.client.LogLevel.DEBUG)
    omni.client.set_log_callback(log_handler)
    try:
        stage, live_layer = asyncio.run(initialize_async(IOT_TOPIC))
        run(stage, live_layer, IOT_TOPIC)
    finally:
        omni.client.shutdown()

source/ingest_app_mqtt/app.py
NVIDIA-Omniverse/iot-samples
# Copyright (c) 2023, 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.

import os
import argparse
import platform
import subprocess
from pathlib import Path

PLATFORM_SYSTEM = platform.system().lower()
PLATFORM_MACHINE = platform.machine()

if PLATFORM_MACHINE == "i686" or PLATFORM_MACHINE == "AMD64":
    PLATFORM_MACHINE = "x86_64"

CURRENT_PLATFORM = f"{PLATFORM_SYSTEM}-{PLATFORM_MACHINE}"

parser = argparse.ArgumentParser()
parser.add_argument("--server", "-s", default="localhost")
parser.add_argument("--username", "-u")
parser.add_argument("--password", "-p")
parser.add_argument("--config", "-c", choices=["debug", "release"], default="release")
parser.add_argument("--platform", default=CURRENT_PLATFORM)
args = parser.parse_args()

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
ROOT_DIR = Path(SCRIPT_DIR).resolve().parents[1]

BUILD_DIR = ROOT_DIR.joinpath("_build", args.platform, args.config)
DEPS_DIR = ROOT_DIR.joinpath("_build", "target-deps")
APP_DIR = ROOT_DIR.joinpath("app")
PYTHON_LIB = DEPS_DIR.joinpath("python")
USD_BIN_DIR = DEPS_DIR.joinpath("usd", args.config, "bin")
USD_LIB_DIR = DEPS_DIR.joinpath("usd", args.config, "lib")
CL_LIB_DIR = DEPS_DIR.joinpath("omni_client_library", args.config)

EXTRA_PATHS = [str(CL_LIB_DIR), str(USD_BIN_DIR), str(USD_LIB_DIR), str(BUILD_DIR), str(PYTHON_LIB)]
EXTRA_PYTHON_PATHS = [
    str(USD_LIB_DIR.joinpath("python")),
    str(CL_LIB_DIR.joinpath("bindings-python")),
    str(BUILD_DIR.joinpath("bindings-python")),
]

if PLATFORM_SYSTEM == "windows":
    os.environ["PATH"] += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    ot_bin = "carb.omnitrace.plugin.dll"
else:
    p = os.environ.get("LD_LIBRARY_PATH", "")
    p += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    os.environ["LD_LIBRARY_PATH"] = p
    ot_bin = "libcarb.omnitrace.plugin.so"

os.environ["OMNI_TRACE_LIB"] = os.path.join(str(DEPS_DIR), "omni-trace", "bin", ot_bin)
os.environ["PYTHONPATH"] = os.pathsep + os.pathsep.join(EXTRA_PYTHON_PATHS)
os.environ["OMNI_USER"] = args.username
os.environ["OMNI_PASS"] = args.password
os.environ["OMNI_HOST"] = args.server

if PLATFORM_SYSTEM == "windows":
    PYTHON_EXE = APP_DIR.joinpath("kit", "python", "python")
else:
    PYTHON_EXE = APP_DIR.joinpath("kit" "python", "bin", "python3")

plugin_paths = DEPS_DIR.joinpath("omni_usd_resolver", args.config, "usd", "omniverse", "resources")
os.environ["PXR_PLUGINPATH_NAME"] = str(plugin_paths)
REQ_FILE = ROOT_DIR.joinpath("requirements.txt")
subprocess.run(f"{PYTHON_EXE} -m pip install -r {REQ_FILE}", shell=True)
result = subprocess.run(
    [PYTHON_EXE, os.path.join(SCRIPT_DIR, "app.py")],
    stderr=subprocess.STDOUT,
)

source/ingest_app_mqtt/run_app.py
NVIDIA-Omniverse/iot-samples
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

# pip install openpyxl
# pip install pandas

import asyncio
import os
import omni.client
from pxr import Usd, Sdf, Gf, UsdGeom
from pathlib import Path
import time
import random

OMNI_HOST = os.environ.get("OMNI_HOST", "localhost")
BASE_URL = "omniverse://" + OMNI_HOST + "/Projects/IoT/Samples/HeadlessApp"
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
CONTENT_DIR = Path(SCRIPT_DIR).resolve().parents[1].joinpath("content")

messages = []


class LivePrim:
    def __init__(self, stage: Usd.Stage):
        points = [
            (50, 50, 50),
            (-50, 50, 50),
            (-50, -50, 50),
            (50, -50, 50),
            (-50, -50, -50),
            (-50, 50, -50),
            (50, 50, -50),
            (50, -50, -50),
        ]
        faceVertexIndices = [0, 1, 2, 3, 4, 5, 6, 7, 0, 6, 5, 1, 4, 7, 3, 2, 0, 3, 7, 6, 4, 2, 1, 5]
        faceVertexCounts = [4, 4, 4, 4, 4, 4]

        cube = stage.GetPrimAtPath("/World/cube")
        if not cube:
            cube = stage.DefinePrim("/World/cube", "Cube")

        if not cube:
            raise Exception("Could load the cube: /World/cube.")

        # cube.GetAttribute("size").Set(2.0)

        mesh = stage.GetPrimAtPath("/World/cube/mesh")
        if not mesh:
            mesh = UsdGeom.Mesh.Define(stage, "/World/cube/mesh")
            mesh.CreatePointsAttr().Set(points)
            mesh.CreateFaceVertexIndicesAttr().Set(faceVertexIndices)
            mesh.CreateFaceVertexCountsAttr().Set(faceVertexCounts)
            mesh.CreateDoubleSidedAttr().Set(False)
            mesh.CreateSubdivisionSchemeAttr("bilinear")
            mesh.CreateDisplayColorAttr().Set([(0.463, 0.725, 0.0)])
            mesh.AddTranslateOp().Set(Gf.Vec3d(0.0))
            mesh.AddScaleOp().Set(Gf.Vec3f(0.8535))
            mesh.AddTransformOp().Set(Gf.Matrix4d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1))

        self._rotation_increment = Gf.Vec3f(
            random.uniform(-1.0, 1.0) * 10.0, random.uniform(-1.0, 1.0) * 10.0, random.uniform(-1.0, 1.0) * 10.0
        )

        self._rotateXYZOp = None
        xformable = UsdGeom.Xformable(cube)
        for op in xformable.GetOrderedXformOps():
            if op.GetOpType() == UsdGeom.XformOp.TypeRotateXYZ:
                self._rotateXYZOp = op

        if self._rotateXYZOp is None:
            self._rotateXYZOp = xformable.AddRotateXYZOp()
        self._rotation = Gf.Vec3f(0.0, 0.0, 0.0)
        self._rotateXYZOp.Set(self._rotation)

    def _increment(self):
        if abs(self._rotation[0] + self._rotation_increment[0]) > 360.0:
            self._rotation_increment[0] *= -1.0
        if abs(self._rotation[1] + self._rotation_increment[1]) > 360.0:
            self._rotation_increment[1] *= -1.0
        if abs(self._rotation[2] + self._rotation_increment[2]) > 360.0:
            self._rotation_increment[2] *= -1.0

        self._rotation[0] += self._rotation_increment[0]
        self._rotation[1] += self._rotation_increment[1]
        self._rotation[2] += self._rotation_increment[2]

    def write_to_live(self, live_layer):
        # write the transformation the usd prim attributes
        self._increment()
        self._rotateXYZOp.Set(self._rotation)


def log_handler(thread, component, level, message):
    # print(message)
    messages.append((thread, component, level, message))


async def initialize_async():
    # copy a the Conveyor Belt to the target nucleus server
    IOT_TOPIC = "Dancing_Cubes"
    STAGE_URL = f"{BASE_URL}/{IOT_TOPIC}.usd"
    LIVE_URL = f"{BASE_URL}/{IOT_TOPIC}.live"

    try:
        stage = Usd.Stage.Open(STAGE_URL)
    except:
        stage = Usd.Stage.CreateNew(STAGE_URL)

    if not stage:
        raise Exception(f"Could load the stage {STAGE_URL}.")

    root_layer = stage.GetRootLayer()
    live_layer = Sdf.Layer.FindOrOpen(LIVE_URL)
    if not live_layer:
        live_layer = Sdf.Layer.CreateNew(LIVE_URL)

    if not live_layer:
        raise Exception(f"Could load the live layer {LIVE_URL}.")

    found = False
    subLayerPaths = root_layer.subLayerPaths
    for subLayerPath in subLayerPaths:
        if subLayerPath == live_layer.identifier:
            found = True

    if not found:
        root_layer.subLayerPaths.append(live_layer.identifier)
        root_layer.Save()

    # set the live layer as the edit target
    stage.SetEditTarget(live_layer)

    stage.DefinePrim("/World", "Xform")

    omni.client.live_process()
    return stage, live_layer


def run(stage, live_layer):
    # we assume that the file contains the data for single device

    # play back the data in at 30fps for 20 seconds
    delay = 0.033
    iterations = 600
    live_prim = LivePrim(stage)
    omni.client.live_process()

    for x in range(iterations):
        with Sdf.ChangeBlock():
            live_prim.write_to_live(live_layer)
        omni.client.live_process()
        time.sleep(delay)


if __name__ == "__main__":
    omni.client.initialize()
    omni.client.set_log_level(omni.client.LogLevel.DEBUG)
    omni.client.set_log_callback(log_handler)
    try:
        stage, live_layer = asyncio.run(initialize_async())
        run(stage, live_layer)
    finally:
        omni.client.shutdown()

source/transform_geometry/app.py
NVIDIA-Omniverse/iot-samples
# Copyright (c) 2023, 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.

import os
import argparse
import platform
import subprocess
from pathlib import Path

PLATFORM_SYSTEM = platform.system().lower()
PLATFORM_MACHINE = platform.machine()

if PLATFORM_MACHINE == "i686" or PLATFORM_MACHINE == "AMD64":
    PLATFORM_MACHINE = "x86_64"

CURRENT_PLATFORM = f"{PLATFORM_SYSTEM}-{PLATFORM_MACHINE}"

parser = argparse.ArgumentParser()
parser.add_argument("--server", "-s", default="localhost")
parser.add_argument("--username", "-u")
parser.add_argument("--password", "-p")
parser.add_argument("--config", "-c", choices=["debug", "release"], default="release")
parser.add_argument("--platform", default=CURRENT_PLATFORM)
args = parser.parse_args()

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
ROOT_DIR = Path(SCRIPT_DIR).resolve().parents[1]

BUILD_DIR = ROOT_DIR.joinpath("_build", args.platform, args.config)
DEPS_DIR = ROOT_DIR.joinpath("_build", "target-deps")
APP_DIR = ROOT_DIR.joinpath("app")
PYTHON_LIB = DEPS_DIR.joinpath("python")
USD_BIN_DIR = DEPS_DIR.joinpath("usd", args.config, "bin")
USD_LIB_DIR = DEPS_DIR.joinpath("usd", args.config, "lib")
CL_LIB_DIR = DEPS_DIR.joinpath("omni_client_library", args.config)

EXTRA_PATHS = [str(CL_LIB_DIR), str(USD_BIN_DIR), str(USD_LIB_DIR), str(BUILD_DIR), str(PYTHON_LIB)]
EXTRA_PYTHON_PATHS = [
    str(USD_LIB_DIR.joinpath("python")),
    str(CL_LIB_DIR.joinpath("bindings-python")),
    str(BUILD_DIR.joinpath("bindings-python")),
]

if PLATFORM_SYSTEM == "windows":
    os.environ["PATH"] += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    ot_bin = "carb.omnitrace.plugin.dll"
else:
    p = os.environ.get("LD_LIBRARY_PATH", "")
    p += os.pathsep + os.pathsep.join(EXTRA_PATHS)
    os.environ["LD_LIBRARY_PATH"] = p
    ot_bin = "libcarb.omnitrace.plugin.so"

os.environ["OMNI_TRACE_LIB"] = os.path.join(str(DEPS_DIR), "omni-trace", "bin", ot_bin)
os.environ["PYTHONPATH"] = os.pathsep + os.pathsep.join(EXTRA_PYTHON_PATHS)
os.environ["OMNI_USER"] = args.username
os.environ["OMNI_PASS"] = args.password
os.environ["OMNI_HOST"] = args.server

if PLATFORM_SYSTEM == "windows":
    PYTHON_EXE = APP_DIR.joinpath("kit", "python", "python")
else:
    PYTHON_EXE = APP_DIR.joinpath("kit" "python", "bin", "python3")

plugin_paths = DEPS_DIR.joinpath("omni_usd_resolver", args.config, "usd", "omniverse", "resources")
os.environ["PXR_PLUGINPATH_NAME"] = str(plugin_paths)
REQ_FILE = ROOT_DIR.joinpath("requirements.txt")
subprocess.run(f"{PYTHON_EXE} -m pip install -r {REQ_FILE}", shell=True)
result = subprocess.run(
    [PYTHON_EXE, os.path.join(SCRIPT_DIR, "app.py")],
    stderr=subprocess.STDOUT,
)

source/transform_geometry/run_app.py
NVIDIA-Omniverse/iot-samples
# Copyright 2019 NVIDIA CORPORATION

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     https://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import zipfile
import tempfile
import sys
import os
import stat
import time
from typing import Any, Callable


RENAME_RETRY_COUNT = 100
RENAME_RETRY_DELAY = 0.1

logging.basicConfig(level=logging.WARNING, format="%(message)s")
logger = logging.getLogger("install_package")


def remove_directory_item(path):
    if os.path.islink(path) or os.path.isfile(path):
        try:
            os.remove(path)
        except PermissionError:
            # make sure we have access and try again:
            os.chmod(path, stat.S_IRWXU)
            os.remove(path)
    else:
        # try first to delete the dir because this will work for folder junctions, otherwise we would follow the junctions and cause destruction!
        clean_out_folder = False
        try:
            # make sure we have access preemptively - this is necessary because recursing into a directory without permissions
            # will only lead to heart ache
            os.chmod(path, stat.S_IRWXU)
            os.rmdir(path)
        except OSError:
            clean_out_folder = True

        if clean_out_folder:
            # we should make sure the directory is empty
            names = os.listdir(path)
            for name in names:
                fullname = os.path.join(path, name)
                remove_directory_item(fullname)
            # now try to again get rid of the folder - and not catch if it raises:
            os.rmdir(path)


class StagingDirectory:
    def __init__(self, staging_path):
        self.staging_path = staging_path
        self.temp_folder_path = None
        os.makedirs(staging_path, exist_ok=True)

    def __enter__(self):
        self.temp_folder_path = tempfile.mkdtemp(prefix="ver-", dir=self.staging_path)
        return self

    def get_temp_folder_path(self):
        return self.temp_folder_path

    # this function renames the temp staging folder to folder_name, it is required that the parent path exists!
    def promote_and_rename(self, folder_name):
        abs_dst_folder_name = os.path.join(self.staging_path, folder_name)
        os.rename(self.temp_folder_path, abs_dst_folder_name)

    def __exit__(self, type, value, traceback):
        # Remove temp staging folder if it's still there (something went wrong):
        path = self.temp_folder_path
        if os.path.isdir(path):
            remove_directory_item(path)


def rename_folder(staging_dir: StagingDirectory, folder_name: str):
    try:
        staging_dir.promote_and_rename(folder_name)
    except OSError as exc:
        # if we failed to rename because the folder now exists we can assume that another packman process
        # has managed to update the package before us - in all other cases we re-raise the exception
        abs_dst_folder_name = os.path.join(staging_dir.staging_path, folder_name)
        if os.path.exists(abs_dst_folder_name):
            logger.warning(
                f"Directory {abs_dst_folder_name} already present, package installation already completed"
            )
        else:
            raise


def call_with_retry(
    op_name: str, func: Callable, retry_count: int = 3, retry_delay: float = 20
) -> Any:
    retries_left = retry_count
    while True:
        try:
            return func()
        except (OSError, IOError) as exc:
            logger.warning(f"Failure while executing {op_name} [{str(exc)}]")
            if retries_left:
                retry_str = "retry" if retries_left == 1 else "retries"
                logger.warning(
                    f"Retrying after {retry_delay} seconds"
                    f" ({retries_left} {retry_str} left) ..."
                )
                time.sleep(retry_delay)
            else:
                logger.error("Maximum retries exceeded, giving up")
                raise
            retries_left -= 1


def rename_folder_with_retry(staging_dir: StagingDirectory, folder_name):
    dst_path = os.path.join(staging_dir.staging_path, folder_name)
    call_with_retry(
        f"rename {staging_dir.get_temp_folder_path()} -> {dst_path}",
        lambda: rename_folder(staging_dir, folder_name),
        RENAME_RETRY_COUNT,
        RENAME_RETRY_DELAY,
    )


def install_package(package_path, install_path):
    staging_path, version = os.path.split(install_path)
    with StagingDirectory(staging_path) as staging_dir:
        output_folder = staging_dir.get_temp_folder_path()
        with zipfile.ZipFile(package_path, allowZip64=True) as zip_file:
            zip_file.extractall(output_folder)

            # attempt the rename operation
            rename_folder_with_retry(staging_dir, version)

    print(f"Package successfully installed to {install_path}")


if __name__ == "__main__":
    executable_paths = os.getenv("PATH")
    paths_list = executable_paths.split(os.path.pathsep) if executable_paths else []
    target_path_np = os.path.normpath(sys.argv[2])
    target_path_np_nc = os.path.normcase(target_path_np)
    for exec_path in paths_list:
        if os.path.normcase(os.path.normpath(exec_path)) == target_path_np_nc:
            raise RuntimeError(f"packman will not install to executable path '{exec_path}'")
    install_package(sys.argv[1], target_path_np)

tools/packman/bootstrap/install_package.py
NVIDIA-Omniverse/iot-samples
# Use this file to bootstrap packman into your Python environment (3.7.x). Simply
# add the path by doing sys.insert to where packmanconf.py is located and then execute:
#
# >>> import packmanconf
# >>> packmanconf.init()
#
# It will use the configured remote(s) and the version of packman in the same folder,
# giving you full access to the packman API via the following module
#
# >> import packmanapi
# >> dir(packmanapi)

import os
import platform
import sys


def init():
    """Call this function to initialize the packman configuration.

    Calls to the packman API will work after successfully calling this function.

    Note:
        This function only needs to be called once during the execution of your
        program. Calling it repeatedly is harmless but wasteful.
        Compatibility with your Python interpreter is checked and upon failure
        the function will report what is required.

    Example:
        >>> import packmanconf
        >>> packmanconf.init()
        >>> import packmanapi
        >>> packmanapi.set_verbosity_level(packmanapi.VERBOSITY_HIGH)
    """
    major = sys.version_info[0]
    minor = sys.version_info[1]
    if major != 3 or minor != 10:
        raise RuntimeError(
            f"This version of packman requires Python 3.10.x, but {major}.{minor} was provided"
        )
    conf_dir = os.path.dirname(os.path.abspath(__file__))
    os.environ["PM_INSTALL_PATH"] = conf_dir
    packages_root = get_packages_root(conf_dir)
    version = get_version(conf_dir)
    module_dir = get_module_dir(conf_dir, packages_root, version)
    sys.path.insert(1, module_dir)


def get_packages_root(conf_dir: str) -> str:
    root = os.getenv("PM_PACKAGES_ROOT")
    if not root:
        platform_name = platform.system()
        if platform_name == "Windows":
            drive, _ = os.path.splitdrive(conf_dir)
            root = os.path.join(drive, "packman-repo")
        elif platform_name == "Darwin":
            # macOS
            root = os.path.join(
                os.path.expanduser("~"), "Library/Application Support/packman-cache"
            )
        elif platform_name == "Linux":
            try:
                cache_root = os.environ["XDG_HOME_CACHE"]
            except KeyError:
                cache_root = os.path.join(os.path.expanduser("~"), ".cache")
            return os.path.join(cache_root, "packman")
        else:
            raise RuntimeError(f"Unsupported platform '{platform_name}'")
    # make sure the path exists:
    os.makedirs(root, exist_ok=True)
    return root


def get_module_dir(conf_dir, packages_root: str, version: str) -> str:
    module_dir = os.path.join(packages_root, "packman-common", version)
    if not os.path.exists(module_dir):
        import tempfile

        tf = tempfile.NamedTemporaryFile(delete=False)
        target_name = tf.name
        tf.close()
        url = f"https://bootstrap.packman.nvidia.com/packman-common@{version}.zip"
        print(f"Downloading '{url}' ...")
        import urllib.request

        urllib.request.urlretrieve(url, target_name)
        from importlib.machinery import SourceFileLoader

        # import module from path provided
        script_path = os.path.join(conf_dir, "bootstrap", "install_package.py")
        ip = SourceFileLoader("install_package", script_path).load_module()
        print("Unpacking ...")
        ip.install_package(target_name, module_dir)
        os.unlink(tf.name)
    return module_dir


def get_version(conf_dir: str):
    path = os.path.join(conf_dir, "packman")
    if not os.path.exists(path):  # in dev repo fallback
        path += ".sh"
    with open(path, "rt", encoding="utf8") as launch_file:
        for line in launch_file.readlines():
            if line.startswith("PM_PACKMAN_VERSION"):
                _, value = line.split("=")
                return value.strip()
    raise RuntimeError(f"Unable to find 'PM_PACKMAN_VERSION' in '{path}'")

tools/packman/packmanconf.py
NVIDIA-Omniverse/iot-samples
import argparse
import json
import os
import sys

import packmanapi
import urllib3


def find_omniverse_apps():
    http = urllib3.PoolManager()
    try:
        r = http.request("GET", "http://127.0.0.1:33480/components")
    except Exception as e:
        print(f"Failed retrieving apps from an Omniverse Launcher, maybe it is not installed?\nError: {e}")
        sys.exit(1)

    apps = {}
    for x in json.loads(r.data.decode("utf-8")):
        latest = x.get("installedVersions", {}).get("latest", "")
        if latest:
            for s in x.get("settings", []):
                if s.get("version", "") == latest:
                    root = s.get("launch", {}).get("root", "")
                    apps[x["slug"]] = (x["name"], root)
                    break
    return apps


def create_link(src, dst):
    print(f"Creating a link '{src}' -> '{dst}'")
    packmanapi.link(src, dst)


APP_PRIORITIES = ["code", "create", "view"]

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description="Create folder link to Kit App installed from Omniverse Launcher")
    parser.add_argument(
        "--path",
        help="Path to Kit App installed from Omniverse Launcher, e.g.: 'C:/Users/bob/AppData/Local/ov/pkg/create-2021.3.4'",
        required=False,
    )
    parser.add_argument(
        "--app", help="Name of Kit App installed from Omniverse Launcher, e.g.: 'code', 'create'", required=False
    )
    args = parser.parse_args()

    path = args.path
    if not path:
        print("Path is not specified, looking for Omniverse Apps...")
        apps = find_omniverse_apps()
        if len(apps) == 0:
            print(
                "Can't find any Omniverse Apps. Use Omniverse Launcher to install one. 'Code' is the recommended app for developers."
            )
            sys.exit(0)

        print("\nFound following Omniverse Apps:")
        for i, slug in enumerate(apps):
            name, root = apps[slug]
            print(f"{i}: {name} ({slug}) at: '{root}'")

        if args.app:
            selected_app = args.app.lower()
            if selected_app not in apps:
                choices = ", ".join(apps.keys())
                print(f"Passed app: '{selected_app}' is not found. Specify one of the following found Apps: {choices}")
                sys.exit(0)
        else:
            selected_app = next((x for x in APP_PRIORITIES if x in apps), None)
            if not selected_app:
                selected_app = next(iter(apps))

        print(f"\nSelected app: {selected_app}")
        _, path = apps[selected_app]

    if not os.path.exists(path):
        print(f"Provided path doesn't exist: {path}")
    else:
        SCRIPT_ROOT = os.path.dirname(os.path.realpath(__file__))
        create_link(f"{SCRIPT_ROOT}/../../app", path)
        print("Success!")

tools/scripts/link_app.py
NVIDIA-Omniverse/iot-samples