From 103da16bd996b3588523516725edbf3f0faa3f93 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Mon, 17 Nov 2025 17:34:17 +0000 Subject: [PATCH] Update examples to 334bc637ffe76144215e0a7cf5ca78e62a70c3fb Signed-off-by: Michael Carroll --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/flowstate-ci.yml | 11 +- MODULE.bazel | 27 +- notebooks/002_solution_building_library.ipynb | 351 +--------- notebooks/002b_executive_and_processes.ipynb | 643 ++++++++++++++++++ .../010_parameterizable_behavior_trees.ipynb | 53 +- .../configurable_service.py | 1 - services/hmi_python/server.py | 10 +- services/platform_http_server/BUILD | 85 +++ services/platform_http_server/README.md | 156 +++++ .../config/default_config_values.textproto | 6 + .../platform_http_server/data_asset_utils.py | 68 ++ .../platform_http_server.proto | 8 + .../platform_http_server_manifest.textproto | 30 + .../platform_http_server/requirements.txt | 10 + services/platform_http_server/server.py | 314 +++++++++ .../testdata/image_files.yaml | 7 + services/random_number/random_number.py | 4 +- services/random_number/random_number_main.py | 4 +- services/random_number/random_number_test.py | 3 +- services/stopwatch/stopwatch_service.py | 2 - skills/get_random_number/get_random_number.py | 2 - .../get_random_number_test.py | 6 +- ...ad_joint_positions_from_opcua_equipment.py | 8 +- ...int_positions_from_opcua_equipment_test.py | 13 +- skills/say_skill/say_skill.py | 1 - skills/say_skill/say_skill_test.py | 1 - skills/scan_barcodes/BUILD | 2 +- skills/scan_barcodes/scan_barcodes.cc | 4 +- skills/scan_barcodes/scan_barcodes.py | 19 +- skills/scan_barcodes/scan_barcodes_test.py | 3 +- skills/start_stopwatch/start_stopwatch.py | 6 +- .../start_stopwatch/start_stopwatch_test.py | 5 +- skills/stop_stopwatch/BUILD | 1 + skills/stop_stopwatch/stop_stopwatch.cc | 35 +- skills/stop_stopwatch/stop_stopwatch.py | 6 +- skills/stop_stopwatch/stop_stopwatch_test.py | 5 +- skills/use_world/use_world.py | 2 - skills/use_world/use_world_test.py | 1 - skills/validate_pose/validate_pose.py | 5 +- skills/validate_pose/validate_pose_test.py | 1 - skills/wiggle_joint/wiggle_joint.py | 2 - skills/wiggle_joint/wiggle_joint_test.py | 1 - ...rite_joint_positions_to_opcua_equipment.py | 10 +- ...joint_positions_to_opcua_equipment_test.py | 15 +- tests/README.md | 19 +- tests/run_ci.sh | 212 ++---- tests/sbl_ci.py | 3 +- 48 files changed, 1520 insertions(+), 663 deletions(-) create mode 100644 notebooks/002b_executive_and_processes.ipynb create mode 100644 services/platform_http_server/BUILD create mode 100644 services/platform_http_server/README.md create mode 100644 services/platform_http_server/config/default_config_values.textproto create mode 100644 services/platform_http_server/data_asset_utils.py create mode 100644 services/platform_http_server/platform_http_server.proto create mode 100644 services/platform_http_server/platform_http_server_manifest.textproto create mode 100644 services/platform_http_server/requirements.txt create mode 100644 services/platform_http_server/server.py create mode 100644 services/platform_http_server/testdata/image_files.yaml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6b4e429..d42a15b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -31,7 +31,7 @@ jobs: # Share repository cache between workflows. repository-cache: true - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Update SDK to latest diff --git a/.github/workflows/flowstate-ci.yml b/.github/workflows/flowstate-ci.yml index 7d4bb77..ebf1d4a 100644 --- a/.github/workflows/flowstate-ci.yml +++ b/.github/workflows/flowstate-ci.yml @@ -15,11 +15,6 @@ on: description: 'Version of Intrinsic tools to use' required: false default: 'latest' - INTRINSIC_VM_DURATION: - description: 'Duration time (hours) for request the VM' - required: false - default: '1' - type: string jobs: ci: @@ -29,7 +24,6 @@ jobs: INTRINSIC_SOLUTION: ${{ github.event.inputs.INTRINSIC_SOLUTION }} INTRINSIC_API_KEY: ${{ secrets.INTRINSIC_API_KEY }} INTRINSIC_TOOL_VERSION: ${{ github.event.inputs.INTRINSIC_TOOL_VERSION }} - INTRINSIC_VM_DURATION: ${{ github.event.inputs.INTRINSIC_VM_DURATION }} SKILLS_UNDER_TEST: | //skills/start_stopwatch:start_stopwatch_skill //skills/stop_stopwatch:stop_stopwatch_py_skill @@ -59,7 +53,7 @@ jobs: # Share repository cache between workflows. repository-cache: true - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Intrinsic Tools uses: ./.github/actions/setup-intrinsic-tools @@ -107,9 +101,10 @@ jobs: - name: Run CI Bash Script shell: bash + env: + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{ github.workspace }}/bin run: | . ./tests/run_ci.sh --skill=${{ steps.prep_targets.outputs.skill_list }} \ --org=${INTRINSIC_ORGANIZATION} \ --solution=${INTRINSIC_SOLUTION} \ - --vm-duration="${INTRINSIC_VM_DURATION}" \ --service=${{ steps.prep_targets.outputs.service_list }}\ diff --git a/MODULE.bazel b/MODULE.bazel index eaeac85..8de889f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -11,16 +11,16 @@ archive_override( ) # Direct dependencies -bazel_dep(name = "abseil-cpp", version = "20250814.0", repo_name = "com_google_absl") +bazel_dep(name = "abseil-cpp", version = "20250814.1", repo_name = "com_google_absl") bazel_dep(name = "abseil-py", version = "2.1.0", repo_name = "com_google_absl_py") -bazel_dep(name = "bazel_skylib", version = "1.8.1") -bazel_dep(name = "googletest", version = "1.17.0", repo_name = "com_google_googletest") +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "googletest", version = "1.17.0.bcr.1", repo_name = "com_google_googletest") bazel_dep(name = "grpc", version = "1.74.1", repo_name = "com_github_grpc_grpc") bazel_dep(name = "platforms", version = "1.0.0") -bazel_dep(name = "protobuf", version = "32.0", repo_name = "com_google_protobuf") +bazel_dep(name = "protobuf", version = "32.1", repo_name = "com_google_protobuf") bazel_dep(name = "rules_go", version = "0.57.0", repo_name = "io_bazel_rules_go") bazel_dep(name = "rules_pkg", version = "1.1.0") -bazel_dep(name = "rules_python", version = "1.6.1") +bazel_dep(name = "rules_python", version = "1.6.3") # C++ toolchain bazel_dep(name = "toolchains_llvm", version = "1.5.0") @@ -41,11 +41,11 @@ use_repo(llvm, "llvm_toolchain") register_toolchains("@llvm_toolchain//:all") -bazel_dep(name = "rules_cc", version = "0.2.4") -bazel_dep(name = "rules_foreign_cc", version = "0.15.0") +bazel_dep(name = "rules_cc", version = "0.2.13") +bazel_dep(name = "rules_foreign_cc", version = "0.15.1") # Google API bindings -bazel_dep(name = "googleapis", version = "0.0.0-20250826-a92cee39", repo_name = "com_google_googleapis") +bazel_dep(name = "googleapis", version = "0.0.0-20251003-2193a2bf", repo_name = "com_google_googleapis") switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") switched_rules.use_languages( @@ -69,7 +69,7 @@ use_repo( ########## # Go ########## -bazel_dep(name = "gazelle", version = "0.45.0", repo_name = "bazel_gazelle") +bazel_dep(name = "gazelle", version = "0.46.0", repo_name = "bazel_gazelle") go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") use_repo( @@ -110,11 +110,18 @@ pip.parse( ) use_repo(pip, "random_number_pip_deps") +pip.parse( + hub_name = "platform_http_server_pip_deps", + python_version = "3.11", + requirements_lock = "//services/platform_http_server:requirements.txt", +) +use_repo(pip, "platform_http_server_pip_deps") + ########## # Containers ########## -bazel_dep(name = "container_structure_test", version = "1.19.1") +bazel_dep(name = "container_structure_test", version = "1.21.1") ########## # Non-bzlmod dependencies diff --git a/notebooks/002_solution_building_library.ipynb b/notebooks/002_solution_building_library.ipynb index 22233e9..db457da 100644 --- a/notebooks/002_solution_building_library.ipynb +++ b/notebooks/002_solution_building_library.ipynb @@ -11,7 +11,7 @@ "\n", "This example notebook demonstrates the basics of using the Intrinsic Solution Building Library.\n", "\n", - "\u003cdiv class=\"alert alert-info\"\u003e\n", + "
\n", "\n", "**Important**\n", "\n", @@ -31,7 +31,7 @@ "\n", "1. Recommended: Keep the browser tab with the Flowstate solution editor open to watch the effect of notebook actions such as running a skill. You can simultaneously interact with the solution through the web UI and the notebook.\n", "\n", - "\u003c/div\u003e" + "
" ] }, { @@ -79,7 +79,7 @@ "\n", "```\n", "Connecting to deployed solution...\n", - "Connected successfully to \"\u003csolution_name\u003e(\u003cbuild\u003e)\" at \"\u003chost\u003e\".\n", + "Connected successfully to \"()\" at \"\".\n", "```\n", "\n", "Here you can check again that you are connected to the correct solution. If all looks correct, you can move on to exploring the solution." @@ -184,7 +184,7 @@ "id": "870ZFWMKN3" }, "source": [ - "And now try typing `intrinsic_skills.` or `resources.` (followed by \u003ckbd\u003eCtrl\u003c/kbd\u003e + \u003ckbd\u003eSpace\u003c/kbd\u003e if necessary):" + "And now try typing `intrinsic_skills.` or `resources.` (followed by Ctrl + Space if necessary):" ] }, { @@ -317,7 +317,7 @@ "source": [ "Some IDEs have functionality to access this information in a more direct way - without manually calling `help()`. In VS Code, you can use the **Jupyter PowerToys** extension:\n", "\n", - "1. Click on the **Jupyter** icon \u003cimg src=\"https://raw.githubusercontent.com/microsoft/vscode-codicons/6ceb091d5c40da3e5836e3d80b08d3f74efc4cbf/src/icons/notebook.svg\" width=\"25\"\u003e in the activity bar (to the far left side of the VS Code window).\n", + "1. Click on the **Jupyter** icon in the activity bar (to the far left side of the VS Code window).\n", "1. Place your cursor, e.g., on `move_robot` in the cell below or select any other code in a code cell.\n", "1. See the `help` output shown live under **CONTEXTUAL HELP** in the sidebar (to the left side of the VS Code window)." ] @@ -357,13 +357,13 @@ "\n", "To work with the Solution Building Library in a more comfortable way we highly recommend that you create custom [Python stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) for your solution and configure your IDE or type checker to use them. In the following steps we use the example of VS Code but the process is similar for other IDEs and type checkers.\n", "\n", - "\u003cdiv class=\"alert alert-info\"\u003e\n", + "
\n", "\n", "**Important**\n", "\n", "The stubs generated by the Solution Building Library are specific to a solution. They match the skills installed in the solution at their respective version. The stubs need to be updated everytime you connect to a different solution and everytime you install a new or modified skill in the solution.\n", "\n", - "\u003c/div\u003e\n", + "
\n", "\n", "**Step 1 - Find/configure stub location**\n", "\n", @@ -417,7 +417,7 @@ "\n", "**Step 4 - Verify that stubs are working**\n", "\n", - "There are various quick ways to confirm that the stubs are being found and used. In VS Code, e.g., in the following cell right-click on `move_robot` and choose **Go to declaration**. If the file `providers.pyi` inside of the stubs folder is opened, everything is setup correctly. Also, you should get a pretty tooltip with parameter hints if you place your cursor inside of the parentheses of `move_robot()` and press \u003ckbd\u003eCTRL\u003c/kbd\u003e + \u003ckbd\u003eSPACE\u003c/kbd\u003e." + "There are various quick ways to confirm that the stubs are being found and used. In VS Code, e.g., in the following cell right-click on `move_robot` and choose **Go to declaration**. If the file `providers.pyi` inside of the stubs folder is opened, everything is setup correctly. Also, you should get a pretty tooltip with parameter hints if you place your cursor inside of the parentheses of `move_robot()` and press CTRL + SPACE." ] }, { @@ -432,339 +432,6 @@ "a = skills.ai.intrinsic.move_robot()" ] }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1QOR6GO8T5" - }, - "source": [ - "## Executive" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6CAEMJGI8O" - }, - "source": [ - "The `executive` is the main entrypoint for running skills and processes in the solution. To demonstrate its usage we first create a few sample skills so that we have *something* to execute. The following example skills here move the robot, so it will be easy to see the effect of running them in the Flowstate solution editor. Creating and parameterizing skills is explained in detail in the \"skills\" example notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "VTF04TG2S5" - }, - "outputs": [], - "source": [ - "move_robot = skills.ai.intrinsic.move_robot\n", - "\n", - "# Moves the robot to the 'home' pose.\n", - "move_skill_1 = move_robot(\n", - " motion_segments=[\n", - " move_robot.intrinsic_proto.skills.MotionSegment(\n", - " joint_position=world.robot.joint_configurations.home,\n", - " motion_type=move_robot.intrinsic_proto.skills.MotionSegment.MotionType.JOINT,\n", - " )\n", - " ],\n", - " arm_part=world.robot,\n", - ")\n", - "\n", - "# Moves the robot to 'view_pose_left'.\n", - "move_skill_2 = move_robot(\n", - " motion_segments=[\n", - " move_robot.intrinsic_proto.skills.MotionSegment(\n", - " joint_position=world.robot.joint_configurations.view_pose_left,\n", - " motion_type=move_robot.intrinsic_proto.skills.MotionSegment.MotionType.JOINT,\n", - " )\n", - " ],\n", - " arm_part=world.robot,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "SIM171NJN3" - }, - "source": [ - "### Synchronous execution\n", - "\n", - "You can run a single skill like this:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "MHU818JGGS" - }, - "outputs": [], - "source": [ - "executive.run(move_skill_2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "IH6Q8TWIIU" - }, - "source": [ - " While the cell is executing, you can watch the robot move in the Flowstate solution editor.\n", - "\n", - " You can also run a sequence of skills:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "KGSZ7GHQUX" - }, - "outputs": [], - "source": [ - "executive.run([move_skill_1, move_skill_2])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "TLOGADIC8E" - }, - "source": [ - "After the execution has started you will be able to see the sequence of skills that are being executed in the process editor of the Flowstate solution editor. Because we have passed skill instances directly to `executive.run()`, observe that the skills are unnamed and that the process is called `(untitled)`. This is useful for testing, but usually you should wrap skill instances inside of appropriate behavior tree nodes wrapped by a `BehaviorTree` instance at the top-level:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "2Z79HPY43E" - }, - "outputs": [], - "source": [ - "from intrinsic.solutions import behavior_tree as bt\n", - "\n", - "tree = bt.BehaviorTree(\n", - " name=\"My first behavior tree\",\n", - " root=bt.Sequence(\n", - " [\n", - " bt.Task(action=move_skill_1, name=\"Some move\"),\n", - " bt.Task(action=move_skill_2, name=\"Another move\"),\n", - " ]\n", - " ),\n", - ")\n", - "\n", - "executive.run(tree)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "P7V0OA13UD" - }, - "source": [ - "This gives you the same capabilities as the process editor of the Flowstate solution editor. E.g., it allows for naming nodes and opens up the possibility to use flow control nodes such as `Branch` or `Loop`. You can find more complex behavior trees in the other example notebooks.\n", - "\n", - "### Asynchronous execution\n", - "\n", - "Executing a behavior tree can take a while and `executive.run()` will block until the execution has finished. If you want to do something during execution, you can use `run_async`. E.g., you can observe the different state transitions inside the executive:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "GDRUQ0S7FZ" - }, - "outputs": [], - "source": [ - "from intrinsic.executive.proto import behavior_tree_pb2\n", - "\n", - "\n", - "def print_executive_state():\n", - " print(\n", - " \"Executive state:\",\n", - " behavior_tree_pb2.BehaviorTree.State.Name(\n", - " executive.operation.metadata.behavior_tree_state\n", - " ),\n", - " )\n", - "\n", - "\n", - "def print_is_succeeded():\n", - " print(\n", - " \"Is succeeded:\",\n", - " executive.operation.metadata.behavior_tree_state\n", - " == behavior_tree_pb2.BehaviorTree.SUCCEEDED,\n", - " )\n", - "\n", - "\n", - "print_executive_state()\n", - "\n", - "executive.run_async(tree)\n", - "print_executive_state()\n", - "\n", - "executive.block_until_completed()\n", - "print_executive_state()\n", - "print_is_succeeded()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "P4S138COJY" - }, - "source": [ - "Note that `executive.operation.metadata` is a \"Protocol Buffer\" (proto) message. You can find all about using protos in Python in the official [Python Generated Code Guide]( https://protobuf.dev/reference/python/python-generated/).\n", - "\n", - "Here you can see the first transition to `RUNNING` and, after calling `executive.block_until_completed()`, you can see the transition to `SUCCEEDED`.\n", - "\n", - "You can also interrupt the execution of the behavior tree and resume it. This is done by using `executive.suspend()` and `executive.resume()` as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "M0TZV05TAV" - }, - "outputs": [], - "source": [ - "executive.run_async(tree)\n", - "print_executive_state()\n", - "\n", - "executive.suspend()\n", - "print_executive_state()\n", - "\n", - "executive.resume()\n", - "print_executive_state()\n", - "\n", - "executive.block_until_completed()\n", - "print_executive_state()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6MWHPBISMR" - }, - "source": [ - "`executive.suspend()` waits for the first skill in the behavior tree to finish and then stops the executive. When `executive.resume()` is called the second skill get executed.\n", - "Calling `executive.suspend()` while an action is running leads to the executive being in state `SUSPENDING` until the execution of the skill has finished.\n", - "Only afterwards does the executive transition to `SUSPENDED` and therefore succeeds the `executive.suspend()` operation and continues with the program.\n", - "\n", - "If your executive ends up in `FAILED` state the errors are displayed automatically inside the notebook.\n", - "\n", - "You can cancel execution immediately (without the option to resume) by using `executive.cancel()` or `executive.cancel_async()`.\n", - "\n", - "Calling `executive.cancel()` while an action is running leads to the executive being in state `CANCELING` until the running skill finishes cancelling (or, if it does not support cancellation, finishes execution as usual). Afterwards, the executive ends in either the state `CANCELED` (if the cancellation was processed) or in `SUCCEEDED`/`FAILED` (if it finished in success/failure before processing the cancellation)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "E1ROSPTZ12" - }, - "outputs": [], - "source": [ - "executive.run_async(tree)\n", - "print_executive_state()\n", - "\n", - "executive.cancel()\n", - "print_executive_state()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "GONZ1CP3SV" - }, - "source": [ - "## Resetting\n", - "\n", - "Various components of the solution can be reset separately from each other.\n", - "\n", - "If you have unsaved world modifications as a result of running certain skills or because you edited the belief world you can restore the belief world to its last saved state like this:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "V6K6WRFIBO" - }, - "outputs": [], - "source": [ - "world.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "XUJV3B93BU" - }, - "source": [ - "For more ways to interact with the belief world see the `003_world.ipynb` example.\n", - "\n", - "You can reset the simulation manually which is the same as clicking **Reset** in the **Simulator** tab of the [workcell designer](https://developers.intrinsic.ai/guides/workcell_design/workcell_overview) of the Flowstate solution editor. The simulation state will be reset to the state of the **Belief** world." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "R23CCOAXZF" - }, - "outputs": [], - "source": [ - "simulator.reset()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "XN5LTGU0WF" - }, - "source": [ - "\n", - "If you want to restore the initial state of the executive you can reset it. This restores the initial plan and the executive ends up in state `ACCEPTED` after this." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab_type": "code", - "id": "PYTYKX73K4" - }, - "outputs": [], - "source": [ - "executive.reset()\n", - "print_executive_state()" - ] - }, { "cell_type": "markdown", "metadata": { @@ -799,7 +466,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/notebooks/002b_executive_and_processes.ipynb b/notebooks/002b_executive_and_processes.ipynb new file mode 100644 index 0000000..bfee137 --- /dev/null +++ b/notebooks/002b_executive_and_processes.ipynb @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "76dde994", + "metadata": { + "colab_type": "text", + "id": "YH721V5NIO" + }, + "source": [ + "# The executive and processes in the Solution Building Library\n", + "\n", + "This example notebook demonstrates how to use the executive and how to create processes in the Solution Building Library.\n", + "\n", + "
\n", + "\n", + "**Important**\n", + "\n", + "This notebook requires a running Flowstate solution to connect to. To start a solution:\n", + "\n", + "1. Navigate to [flowstate.intrinsic.ai](https://flowstate.intrinsic.ai/) and sign in\n", + " using your registered Flowstate account.\n", + "\n", + "1. Do **one** of the following:\n", + " - Create a new solution:\n", + " 1. Click \"Create new solution\" and choose \"From an example\".\n", + " 1. Select `pick_and_place:pick_and_place_module2`\n", + " 1. Click \"Create\".\n", + " - Or open an existing solution that was created from the `pick_and_place:pick_and_place_module2` example:\n", + " 1. Hover over the solution in the list.\n", + " 1. Click \"Open solution\" or \"Start solution\".\n", + "\n", + "1. Recommended: Keep the browser tab with the Flowstate solution editor open to watch the effect of notebook actions such as running a skill. You can simultaneously interact with the solution through the web UI and the notebook.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "6ae9c743", + "metadata": { + "colab_type": "text", + "id": "PY8M2M46JW" + }, + "source": [ + "## Connect to solution\n", + "\n", + "Let's start with the typical preamble:\n", + "\n", + "- Import the relevant modules.\n", + "- Connect to the deployed solution.\n", + "- Define some shortcut variables for convenience." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e9d0561", + "metadata": { + "colab_type": "code", + "id": "OJYY1HLATB" + }, + "outputs": [], + "source": [ + "from intrinsic.solutions import deployments\n", + "from intrinsic.solutions import behavior_tree as bt\n", + "\n", + "solution = deployments.connect_to_selected_solution()\n", + "\n", + "executive = solution.executive\n", + "skills = solution.skills\n", + "world = solution.world\n", + "simulator = solution.simulator" + ] + }, + { + "cell_type": "markdown", + "id": "edde7c8e", + "metadata": { + "colab_type": "text", + "id": "BPPIM3OOXG" + }, + "source": [ + "## Executive" + ] + }, + { + "cell_type": "markdown", + "id": "465fe914", + "metadata": { + "colab_type": "text", + "id": "JLS7D271GD" + }, + "source": [ + "The `executive` is the main entrypoint for running skills and processes in the solution. To demonstrate its usage we first create a few sample skills so that we have *something* to execute. The following example skills here move the robot, so it will be easy to see the effect of running them in the Flowstate solution editor. Creating and parameterizing skills is explained in detail in the \"skills\" example notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d45136e7", + "metadata": { + "colab_type": "code", + "id": "T037909OFW" + }, + "outputs": [], + "source": [ + "move_robot = skills.ai.intrinsic.move_robot\n", + "\n", + "# Moves the robot to the 'home' pose.\n", + "move_skill_1 = move_robot(\n", + " motion_segments=[\n", + " move_robot.intrinsic_proto.skills.MotionSegment(\n", + " joint_position=world.robot.joint_configurations.home,\n", + " motion_type=move_robot.intrinsic_proto.skills.MotionSegment.MotionType.JOINT,\n", + " )\n", + " ],\n", + " arm_part=world.robot,\n", + ")\n", + "\n", + "# Moves the robot to 'view_pose_left'.\n", + "move_skill_2 = move_robot(\n", + " motion_segments=[\n", + " move_robot.intrinsic_proto.skills.MotionSegment(\n", + " joint_position=world.robot.joint_configurations.view_pose_left,\n", + " motion_type=move_robot.intrinsic_proto.skills.MotionSegment.MotionType.JOINT,\n", + " )\n", + " ],\n", + " arm_part=world.robot,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6f051179", + "metadata": { + "colab_type": "text", + "id": "G8OVSO4AQJ" + }, + "source": [ + "### Synchronous execution\n", + "\n", + "You can run a single skill like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f9d2968", + "metadata": { + "colab_type": "code", + "id": "URHJVIEHDV" + }, + "outputs": [], + "source": [ + "executive.run(move_skill_2)" + ] + }, + { + "cell_type": "markdown", + "id": "17689bbd", + "metadata": { + "colab_type": "text", + "id": "CGY2Y5EC4E" + }, + "source": [ + " While the cell is executing, you can watch the robot move in the Flowstate solution editor (make sure that the 3D scene view is set to the \"Execute\" tab).\n", + "\n", + " You can also run a sequence of skills:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5bc66f6", + "metadata": { + "colab_type": "code", + "id": "D5J2HPOC3C" + }, + "outputs": [], + "source": [ + "executive.run([move_skill_1, move_skill_2])" + ] + }, + { + "cell_type": "markdown", + "id": "a6bb1a20", + "metadata": { + "colab_type": "text", + "id": "755RQ32QOU" + }, + "source": [ + "After the execution has finished, you can choose \"Process\" --> \"Load\" --> \"From executive\" from the menu to view, edit or replay the last executed process in the process editor. Because we have passed skill instances directly to `executive.run()`, observe that the skills are unnamed and that the process is called `(untitled)`. This is useful for testing, but usually you should wrap skill instances in nodes of a `BehaviorTree`. The `BehaviorTree` class represents processes in the Solution Building Library:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b4c9ee0", + "metadata": { + "colab_type": "code", + "id": "2C6QJVKUM8" + }, + "outputs": [], + "source": [ + "from intrinsic.solutions import behavior_tree as bt\n", + "\n", + "tree = bt.BehaviorTree(\n", + " name=\"My first behavior tree\",\n", + " root=bt.Sequence([\n", + " bt.Task(action=move_skill_1, name=\"Some move\"),\n", + " bt.Task(action=move_skill_2, name=\"Another move\"),\n", + " ]),\n", + ")\n", + "\n", + "executive.run(tree)" + ] + }, + { + "cell_type": "markdown", + "id": "2cfb15fc", + "metadata": { + "colab_type": "text", + "id": "RXLLQHPWAI" + }, + "source": [ + "This gives you the same capabilities as the process editor of the Flowstate solution editor. E.g., it allows for naming nodes and opens up the possibility to use flow control nodes such as `Branch` or `Loop`. Here is one more example tree with an endless loop which will be useful in the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c3dd843", + "metadata": { + "colab_type": "code", + "id": "V49PVLBMFM" + }, + "outputs": [], + "source": [ + "tree_with_loop = bt.BehaviorTree(\n", + " name=\"Move back and forth forever\",\n", + " root=bt.Loop(\n", + " while_condition=bt.Blackboard(\"true\"),\n", + " do_child=bt.Sequence([\n", + " bt.Task(action=move_skill_1, name=\"Some move\"),\n", + " bt.Task(action=move_skill_2, name=\"Another move\"),\n", + " ]),\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ec63470c", + "metadata": { + "colab_type": "text", + "id": "B2BHOG86GI" + }, + "source": [ + "You can find more complex behavior trees in the other example notebooks.\n", + "\n", + "### Asynchronous execution\n", + "\n", + "Executing a behavior tree can take a while and `executive.run()` will block until the execution has finished. If you want to do something during execution, you can use `run_async`. E.g., you can observe the different state transitions inside the executive:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b94c42a", + "metadata": { + "colab_type": "code", + "id": "04CTI1IQK3" + }, + "outputs": [], + "source": [ + "from intrinsic.executive.proto import behavior_tree_pb2\n", + "\n", + "\n", + "def print_executive_state():\n", + " print(\n", + " \"Executive state:\",\n", + " behavior_tree_pb2.BehaviorTree.State.Name(\n", + " executive.operation.metadata.behavior_tree_state\n", + " ),\n", + " )\n", + "\n", + "\n", + "def print_is_succeeded():\n", + " print(\n", + " \"Is succeeded:\",\n", + " executive.operation.metadata.behavior_tree_state\n", + " == behavior_tree_pb2.BehaviorTree.SUCCEEDED,\n", + " )\n", + "\n", + "\n", + "print_executive_state()\n", + "\n", + "executive.run_async(tree)\n", + "print_executive_state()\n", + "\n", + "# ... do something here ...\n", + "\n", + "executive.block_until_completed()\n", + "print_executive_state()\n", + "print_is_succeeded()" + ] + }, + { + "cell_type": "markdown", + "id": "9874583e", + "metadata": { + "colab_type": "text", + "id": "AN0BG9P133" + }, + "source": [ + "Note that `executive.operation.metadata` is a \"Protocol Buffer\" (proto) message. You can find all about using protos in Python in the official [Python Generated Code Guide]( https://protobuf.dev/reference/python/python-generated/).\n", + "\n", + "Here you can see the first transition to `RUNNING` and, after calling `executive.block_until_completed()`, you can see the transition to `SUCCEEDED`.\n", + "\n", + "You can also interrupt the execution of the behavior tree and resume it. This is done by using `executive.suspend()` and `executive.resume()` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc354958", + "metadata": { + "colab_type": "code", + "id": "J8MJF5A9JG" + }, + "outputs": [], + "source": [ + "# This tree has an endless loop and will not finish on its own.\n", + "executive.run_async(tree_with_loop)\n", + "print_executive_state()\n", + "\n", + "# ... do something here ...\n", + "\n", + "executive.suspend()\n", + "print_executive_state()\n", + "\n", + "# ... do something here ...\n", + "\n", + "executive.resume()\n", + "print_executive_state()" + ] + }, + { + "cell_type": "markdown", + "id": "ce8e7e08", + "metadata": { + "colab_type": "text", + "id": "E39YIMTCU5" + }, + "source": [ + "`executive.suspend()` waits for the currently running skills in the behavior tree to finish and then stops the executive. When `executive.resume()` is called the next skill gets executed.\n", + "Calling `executive.suspend()` while an action is running leads to the executive being in state `SUSPENDING` until the execution of the skill has finished.\n", + "Only afterwards does the executive transition to `SUSPENDED` and therefore succeeds the `executive.suspend()` operation and continues with the program.\n", + "\n", + "If your executive ends up in `FAILED` state the errors are displayed automatically inside the notebook.\n", + "\n", + "You can cancel execution immediately (without the option to resume) by using `executive.cancel()` or `executive.cancel_async()`.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69de4d10", + "metadata": { + "colab_type": "code", + "id": "12PJF8SBZX" + }, + "outputs": [], + "source": [ + "executive.cancel()\n", + "print_executive_state()" + ] + }, + { + "cell_type": "markdown", + "id": "24eca1b0", + "metadata": { + "colab_type": "text", + "id": "AIW9MI8LM7" + }, + "source": [ + "Calling `executive.cancel()` while an action is running leads to the executive being in state `CANCELING` until the running skill finishes cancelling (or, if it does not support cancellation, finishes execution as usual). Afterwards, the executive ends in either the state `CANCELED` (if the cancellation was processed) or in `SUCCEEDED`/`FAILED` (if it finished in success/failure before processing the cancellation)." + ] + }, + { + "cell_type": "markdown", + "id": "c50452da", + "metadata": { + "colab_type": "text", + "id": "Q16X50GBTT" + }, + "source": [ + "## Resetting\n", + "\n", + "Various components of the solution can be reset separately from each other.\n", + "\n", + "If you have unsaved world modifications as a result of running certain skills or because you edited the belief world you can restore the belief world to its last saved state like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7ece532", + "metadata": { + "colab_type": "code", + "id": "LZPD8SU7J1" + }, + "outputs": [], + "source": [ + "world.reset()" + ] + }, + { + "cell_type": "markdown", + "id": "fedb419f", + "metadata": { + "colab_type": "text", + "id": "ZPBF6LUNRH" + }, + "source": [ + "For more ways to interact with the belief world see the `003_world.ipynb` example.\n", + "\n", + "You can reset the simulation manually which is the same as clicking **Reset** in the **Simulator** tab of the [workcell designer](https://developers.intrinsic.ai/guides/workcell_design/workcell_overview) of the Flowstate solution editor. The simulation state will be reset to the state of the **Belief** world." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ecdc2aa", + "metadata": { + "colab_type": "code", + "id": "3P66ZQ5Y27" + }, + "outputs": [], + "source": [ + "simulator.reset()" + ] + }, + { + "cell_type": "markdown", + "id": "1f3eb57f", + "metadata": { + "colab_type": "text", + "id": "LU7G7MY5FG" + }, + "source": [ + "## Processes\n", + "\n", + "Processes are represented as assets in Flowstate - similar to skills or services. Process assets which are installed in a solution can be accessed in the Solution Building Library through `solution.processes`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36838aff", + "metadata": { + "colab_type": "code", + "id": "LSLW65H54V" + }, + "outputs": [], + "source": [ + "processes = solution.processes" + ] + }, + { + "cell_type": "markdown", + "id": "2859a059", + "metadata": { + "colab_type": "text", + "id": "34FXK3BZ3L" + }, + "source": [ + "Process assets are represented by `BehaviorTree` objects. However, a `BehaviorTree` instance by default only has a display name and no further asset metadata. It is a local, \"anonymous\" process which can be sent directly to the executive for execution as we have done above. To be able to save the process as a Process asset to the solution you need to first setup the asset metadata (at least the required fields)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "640becf6", + "metadata": { + "colab_type": "code", + "id": "COL4MLSWC6" + }, + "outputs": [], + "source": [ + "tree = bt.BehaviorTree(\n", + " name=\"My first process\",\n", + " root=bt.Sequence([\n", + " bt.Task(action=move_skill_1, name=\"Some move\"),\n", + " bt.Task(action=move_skill_2, name=\"Another move\"),\n", + " ]),\n", + ")\n", + "tree.set_asset_metadata(\n", + " id=\"com.myorg.my_first_process\",\n", + " vendor=\"MyOrg\",\n", + ")\n", + "\n", + "# Save 'tree' as Process asset with the ID 'com.myorg.my_first_process'\n", + "processes.save(tree)" + ] + }, + { + "cell_type": "markdown", + "id": "a272da53", + "metadata": { + "colab_type": "text", + "id": "O49LEEAJW8" + }, + "source": [ + "Now you can verify that your process was saved by listing all processes in the solution. `solution.processes` supports dict-like read access so you can use `keys()`, `items()` and `values()` on it. For example, the following printout should include the ID of our newly saved process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a6a9da8", + "metadata": { + "colab_type": "code", + "id": "AOADIS3RAJ" + }, + "outputs": [], + "source": [ + "print(processes.keys())\n" + ] + }, + { + "cell_type": "markdown", + "id": "14038b15", + "metadata": { + "colab_type": "text", + "id": "V9RBCQ3C50" + }, + "source": [ + "A Process asset can also be loaded from the solution. This yields an instance of `BehaviorTree` representing the process. For example, we can load a copy of a saved process and save that copy under a different asset ID." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0132ce51", + "metadata": { + "colab_type": "code", + "id": "X7AQMAWBOB" + }, + "outputs": [], + "source": [ + "other_tree = processes['com.myorg.my_first_process']\n", + "# Change asset metadata to a different ID\n", + "other_tree.set_asset_metadata(id='com.myorg.my_second_process', vendor='MyOrg')\n", + "processes.save(other_tree)\n", + "\n", + "# Should include 'com.myorg.my_first_process' and 'com.myorg.my_second_process'\n", + "print(processes.keys())" + ] + }, + { + "cell_type": "markdown", + "id": "7b5be74a", + "metadata": { + "colab_type": "text", + "id": "CTMZHUMCD4" + }, + "source": [ + "We can also delete a Process asset from the solution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b88c0b0", + "metadata": { + "colab_type": "code", + "id": "CFTEFW54BI" + }, + "outputs": [], + "source": [ + "del solution.processes['com.myorg.my_second_process']\n", + "\n", + "# Should print only 'com.myorg.my_first_process'\n", + "print(processes.keys())" + ] + }, + { + "cell_type": "markdown", + "id": "77e5d73d", + "metadata": { + "colab_type": "text", + "id": "JW94BZ9Z7P" + }, + "source": [ + "### Advanced\n", + "\n", + "Last but not least, you can export the behavior tree of a process as a proto file. This is only required for some advandced use cases, e.g., if you want to use your behavior tree as input to Bazel build rules from the Intrinsic SDK." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03d0998b", + "metadata": { + "colab_type": "code", + "id": "0OC9V74BHN" + }, + "outputs": [], + "source": [ + "from google.protobuf import text_format\n", + "\n", + "file_content = text_format.MessageToString(tree.proto)\n", + "with open('tree.txtpb', 'w') as file:\n", + " file.write(file_content)" + ] + } + ], + "metadata": { + "colab": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/010_parameterizable_behavior_trees.ipynb b/notebooks/010_parameterizable_behavior_trees.ipynb index a29a717..d73941c 100644 --- a/notebooks/010_parameterizable_behavior_trees.ipynb +++ b/notebooks/010_parameterizable_behavior_trees.ipynb @@ -18,9 +18,9 @@ "- Creating parameterizable behavior trees\n", " - Creating tree parameters\n", " - Using tree parameters as skill parameters\n", - "- Sideloading and using parameterizable behavior trees\n", + "- Saving parameterizable behavior trees as Process assets and using them\n", "\n", - "\u003cdiv class=\"alert alert-info\"\u003e\n", + "
\n", "\n", "**Important**\n", "\n", @@ -40,7 +40,7 @@ "\n", "1. Recommended: Keep the browser tab with the Flowstate solution editor open to watch the effect of notebook actions such as running a skill. You can simultaneously interact with the solution through the web UI and the notebook.\n", "\n", - "\u003c/div\u003e" + "
" ] }, { @@ -211,9 +211,7 @@ "id": "2N8EM04GZM" }, "source": [ - "Create a parameterizable behavior tree named `grasp_tree`. This is just a `BehaviorTree` instance that is initialized as a parameterizable tree.\n", - "\n", - "The `skill_id` make this parameterizable tree re-usable in a process like a skill. In addition to that the `grasp_param_message` defined above defines what parameters the tree has." + "Create a parameterizable behavior tree named `grasp_tree`. This is just a `BehaviorTree` instance for which we set asset metadata (so that we can save and reference it later) and which we initialize as a parameterizable tree using the `grasp_param_message` created above to define what parameters the tree has." ] }, { @@ -227,11 +225,9 @@ "outputs": [], "source": [ "grasp_pbt = bt.BehaviorTree(name=\"grasp_tree\")\n", - "grasp_pbt.initialize_pbt_with_protos(\n", - " skill_id=\"ai.intrinsic.grasp_tree\",\n", - " display_name=\"Grasp Tree\",\n", - " parameter_proto=grasp_param_message,\n", - ")" + "# Note: subprocess=True is required to make the process accessible via solution.skills below\n", + "grasp_pbt.set_asset_metadata(id=\"com.myorg.grasp_tree\", vendor=\"MyOrg\", subprocess=True)\n", + "grasp_pbt.initialize_pbt_with_protos(parameter_proto=grasp_param_message)" ] }, { @@ -244,7 +240,7 @@ "source": [ "Create the skill calls for the `grasp_pbt` as for any other tree.\n", "\n", - "Note that the parameters of the behavior tree are available at `grasp_pbt.params.\u003cparam\u003e`. These can be used in the same way that one would use a skill return value." + "Note that the parameters of the behavior tree are available at `grasp_pbt.params.`. These can be used in the same way that one would use a skill return value." ] }, { @@ -399,11 +395,9 @@ "outputs": [], "source": [ "place_pbt = bt.BehaviorTree(name=\"place_tree\")\n", - "place_pbt.initialize_pbt_with_protos(\n", - " skill_id=\"ai.intrinsic.place_tree\",\n", - " display_name=\"Place Tree\",\n", - " parameter_proto=place_param_message,\n", - ")" + "# Note: subprocess=True is required to make the process accessible via solution.skills below\n", + "place_pbt.set_asset_metadata(id=\"com.myorg.place_tree\", vendor=\"MyOrg\", subprocess=True)\n", + "place_pbt.initialize_pbt_with_protos(parameter_proto=place_param_message)" ] }, { @@ -469,9 +463,9 @@ "id": "6GH2SY4Z3J" }, "source": [ - "Sideload the created trees. Afterwards these will be available as new skills with the given parameters.\n", + "Save the created trees to the solution as Process assets. Because we have called `set_asset_metadata(..., subprocess=True)` on those trees, they will be accessible via `solution.skills` as \"skills\" with the previously defined parameters.\n", "\n", - "Thus call `update_skills()` here to refresh the list of skills. If you are using the frontend to show the process then reload the frontend, so that it can read the new skill definitions." + "After saving, call `update_skills()` to refresh the list of skills. If you are using the frontend to show the process then reload the frontend, so that it can read the new skill definitions." ] }, { @@ -484,9 +478,12 @@ }, "outputs": [], "source": [ - "solution.pbt_registry.sideload_behavior_tree(grasp_pbt)\n", - "solution.pbt_registry.sideload_behavior_tree(place_pbt)\n", - "solution.update_skills()" + "solution.processes.save(grasp_pbt)\n", + "solution.processes.save(place_pbt)\n", + "solution.update_skills()\n", + "\n", + "# Should include 'com.myorg.grasp_tree' and 'com.myorg.place_tree'\n", + "list(solution.skills.get_skill_ids())" ] }, { @@ -516,13 +513,13 @@ " initialize,\n", " bt.Task(\n", " name=\"Grasp block0\",\n", - " action=skills.ai.intrinsic.grasp_tree(\n", + " action=skills.com.myorg.grasp_tree(\n", " object=world.building_block0\n", " ),\n", " ),\n", " bt.Task(\n", " name=\"Place block0\",\n", - " action=skills.ai.intrinsic.place_tree(\n", + " action=skills.com.myorg.place_tree(\n", " object=world.building_block0, place_frame=world.target_right\n", " ),\n", " ),\n", @@ -558,13 +555,13 @@ " initialize,\n", " bt.Task(\n", " name=\"Grasp block0\",\n", - " action=skills.ai.intrinsic.grasp_tree(\n", + " action=skills.com.myorg.grasp_tree(\n", " object=world.building_block0\n", " ),\n", " ),\n", " bt.Task(\n", " name=\"Place block0\",\n", - " action=skills.ai.intrinsic.place_tree(\n", + " action=skills.com.myorg.place_tree(\n", " object=world.building_block0, place_frame=world.target_left\n", " ),\n", " ),\n", @@ -593,7 +590,7 @@ "metadata": { "colab": {}, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -607,7 +604,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.14" } }, "nbformat": 4, diff --git a/services/configurable_service/configurable_service.py b/services/configurable_service/configurable_service.py index 6f48e29..ea50e7f 100644 --- a/services/configurable_service/configurable_service.py +++ b/services/configurable_service/configurable_service.py @@ -6,7 +6,6 @@ import time from intrinsic.resources.proto import runtime_context_pb2 - from services.configurable_service import configurable_service_pb2 diff --git a/services/hmi_python/server.py b/services/hmi_python/server.py index fd89a24..c194be8 100644 --- a/services/hmi_python/server.py +++ b/services/hmi_python/server.py @@ -2,15 +2,17 @@ #!/usr/bin/env python3 +from http.server import HTTPServer +from http.server import SimpleHTTPRequestHandler import logging +import pathlib import sys -from intrinsic.resources.proto import runtime_context_pb2 -from intrinsic.executive.proto import executive_service_pb2_grpc + from google.longrunning.operations_pb2 import ListOperationsRequest # type: ignore from google.protobuf import json_format import grpc -from http.server import HTTPServer, SimpleHTTPRequestHandler -import pathlib +from intrinsic.executive.proto import executive_service_pb2_grpc +from intrinsic.resources.proto import runtime_context_pb2 logger = logging.getLogger(__name__) diff --git a/services/platform_http_server/BUILD b/services/platform_http_server/BUILD new file mode 100644 index 0000000..4ed5cf6 --- /dev/null +++ b/services/platform_http_server/BUILD @@ -0,0 +1,85 @@ +load("@ai_intrinsic_sdks//bazel:python_oci_image.bzl", "python_oci_image") +load("@ai_intrinsic_sdks//intrinsic/assets/services/build_defs:services.bzl", "intrinsic_service") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@platform_http_server_pip_deps//:requirements.bzl", "requirement") +load("@rules_pkg//:pkg.bzl", "pkg_tar") +load("@rules_python//python:defs.bzl", "py_binary") + +proto_library( + name = "platform_http_server_proto", + srcs = ["platform_http_server.proto"], +) + +py_proto_library( + name = "platform_http_server_py_pb2", + visibility = ["//visibility:public"], + deps = [":platform_http_server_proto"], +) + +py_binary( + name = "server", + srcs = [ + "data_asset_utils.py", + "server.py", + ], + main = "server.py", + deps = [ + ":platform_http_server_py_pb2", + "@ai_intrinsic_sdks//intrinsic/assets/data/proto/v1:data_asset_py_pb2", + "@ai_intrinsic_sdks//intrinsic/assets/data/proto/v1:data_assets_py_pb2", + "@ai_intrinsic_sdks//intrinsic/assets/data/proto/v1:data_assets_py_pb2_grpc", + "@ai_intrinsic_sdks//intrinsic/assets/data/proto/v1:referenced_data_struct_py_pb2", + "@ai_intrinsic_sdks//intrinsic/assets/services/proto/v1:service_state_py_pb2", + "@ai_intrinsic_sdks//intrinsic/assets/services/proto/v1:service_state_py_pb2_grpc", + "@ai_intrinsic_sdks//intrinsic/executive/proto:executive_service_py_pb2_grpc", + "@ai_intrinsic_sdks//intrinsic/resources/proto:runtime_context_py_pb2", + requirement("flask"), + requirement("waitress"), + requirement("grpcio"), + ], +) + +exports_files(["requirements.txt"]) + +pkg_tar( + name = "server_layer", + srcs = [":server"], + extension = "tar.gz", + include_runfiles = True, + strip_prefix = "/", +) + +python_oci_image( + name = "platform_http_server_image", + base = "@distroless_python3", + binary = "server", + entrypoint = [ + "python3", + "-u", + "/services/platform_http_server/server", + ], + extra_tars = [ + ":server_layer", + ], +) + +container_structure_test( + name = "platform_http_server_image_test", + configs = ["testdata/image_files.yaml"], + driver = "tar", + image = ":platform_http_server_image.tar", +) + +intrinsic_service( + name = "platform_http_server", + default_config = "config/default_config_values.textproto", + images = [ + ":platform_http_server_image.tar", + ], + manifest = ":platform_http_server_manifest.textproto", + deps = [ + ":platform_http_server_proto", + ], +) diff --git a/services/platform_http_server/README.md b/services/platform_http_server/README.md new file mode 100644 index 0000000..daf3dcd --- /dev/null +++ b/services/platform_http_server/README.md @@ -0,0 +1,156 @@ +# Platform HTTP Server Service (Python) + +This service is a reusable, generic web server for the Flowstate platform. +Its purpose is to host static web content (like an HMI, dashboard, or documentation) that is packaged and delivered as an Intrinsic Data Asset. + +This approach decouples the web content from the server binary, allowing HMI developers to update their user interfaces without needing to rebuild or reinstall the entire service. + +The workflow is separated into two parts: a one-time installation of this service, and the ongoing management of the HMI content via data assets. + +## Content management workflow & usage + +Once the service is running, your primary workflow is managing the HMI content through the `inctl data` command. You do not need to rebuild or reinstall the service to update the HMI. + +### Step 0: Setup your organization and solution + +Export some variables related to the organization in order to build and install you data asset. + +```bash +export INTRINSIC_ORG=intrinsic@intrinsic-prod-us +export INTRINSIC_SOLUTION=9999ffff-9999-ffff-9999-ffff9999ffff_BRANCH +``` + +### Step 1: Build and install your HMI as a data asset + +Follow the guide under the *data_assets/platform_http_server* directory, [here](../../data_assets/platform_http_server/README.md) to build and install your data asset. + +### Step 2: Configure the service (for initial Load) + +The service's `config/default_config_values.textproto` file determines which asset to load on startup. +To load your HMI for the first time, you would set the initial asset ID. + +`config/default_config_values.textproto:` +```bash +[type.googleapis.com/platform_http_server.PlatformHttpServerConfig] { + data_asset_id: "ai.intrinsic.hello_world" + } +``` + +When the platform http server starts, it will read this config and immediately serve the content from that asset. + +## Service installation (one-time setup) + +Before you can serve content, you need to build and install this platform http server into your solution. + + 1. **Build the service bundle:** + This command packages the Python server into a deployable `.tar` bundle. + + ```bash + bazel build //services/platform_http_server:platform_http_server + ``` + + 2. **Install the service into your solution:** + Use inctl to install the service bundle into a running solution. + + ```bash + inctl service install bazel-bin/services/platform_http_server/platform_http_server.bundle.tar --org=ORGANIZATION_NAME + ``` + + 3. **Add your service:** + + * Find the *Services* tab on the right side. + * Select *Add service*. The Platform HTTP Server service you just installed should be shown in the list with the display name from metadata in the service manifest. + * Select the Platform HTTP Server service and click Add. + * You will be prompted for a service name. This can be any unique identifier you like. Use the name *platform_http_server*. + * Select Apply to add the Platform HTTP Server to the solution. This should be very quick. + + You can add it too with inctl: + + ```bash + inctl service add ai.intrinsic.platform_http_server --org=ORGANIZATION_NAME + ``` + +The platform http server will start up and should now be available. + +Once installed, the service will be running and ready to serve content. + +### Access the platform http server + + 1. On your browser, paste the desired url. It should follow this format: + ```bash + https://flowstate.intrinsic.ai/content/projects//uis/onprem/clusters//api/resourceinstances// + ``` + +## Updating the platform http server content + +There are two primary workflows for updating the HMI content live without rebuilding the service. + +### Approach 1: In-Place update with Disable/Enable + +This is the recommended approach for deploying a new version of the same HMI. +The server will automatically detect and load the new content when it is re-enabled. + +#### Step 1: Disable the Service + +Temporarily disable the service to prepare for the update. +You can do it through the Service manager dialog (File -> Service Manager) and untoggle the enable button. + +#### Step 2: Build and install the updated data asset + +Build the new version of your HMI data asset and install it. +Note that the asset name (e.g., `ai.intrinsic.hello_world`) remains the same, but its content is updated. + + ```bash + bazel build //data_assets/platform_http_server/frontend_1:hello_world_data + ``` + + ```bash + inctl data install bazel-bin/data_assets/platform_http_server/frontend_1/hello_world_data.bundle.tar --solution YOUR_SOLUTION_ID + ``` +#### Step 3: Enable the service + +Re-enable the service. This action triggers the server to automatically re-scan the disk, discover the newly installed asset version, and load its content into memory. You can do it through the Service manager dialog (File -> Service Manager) and toggle the enable button. + +#### Step 4: Verify the update + +Refresh your browser. The HMI should now be serving the updated content. + +### Approach 2: Live hot-reload with a new asset + +This approach is useful when you want to switch between completely different HMIs (e.g., for A/B testing or diagnostics) without an intermediate disabled state. + +#### Step 1: Build and install a new, different date asset. + +Build and install a second data asset with a unique name (e.g., `ai.intrinsic.hello_world_2`). Follow the guide under the *data_assets/platform_http_server* directory, [here](../../data_assets/platform_http_server/README.md) + +#### Step 2: Restart your service. + +In order to get the previous data asset updated, the server needs to be restarted. Run the following command and wait for a minute. Or restart it from the Service Manager. + +```bash + inctl service state restart hmi +``` + +#### Step 3: Update the live HMI (hot reload) + +To update the HMI, send a POST request to the service's /reconfigure endpoint to trigger a live update. + +**NOTE**: This is only available with the intermediate step of `inctl cluster port-forward`. + +1. Run the following command for getting the port: + ```bash + inctl cluster port-forward --cluster="vmp-0123-abc4d56e" + ``` +2. On your browser, run: + ```bash + http://localhost:17081/api/resourceinstances/hmi/ + ``` + +3. *Use curl to tell the service to load the new version* +```bash +curl -X POST http://:/reconfigure \ + -H "Content-Type: application/json" \ + -d '{"data_asset_id": "ai.intrinsic.hello_world_2"}' +``` + +Then, manually refresh the page of your localhost and the one from the flowstate, e.g: `https://flowstate.intrinsic.ai/content/projects/giza-workcells/uis/onprem/clusters/vmp-0123-abc4d56e/api/resourceinstances/hmi/` and the server will immediately switch to serving the new content without reinstalling. diff --git a/services/platform_http_server/config/default_config_values.textproto b/services/platform_http_server/config/default_config_values.textproto new file mode 100644 index 0000000..9c08cfe --- /dev/null +++ b/services/platform_http_server/config/default_config_values.textproto @@ -0,0 +1,6 @@ +# proto-file: google/protobuf/any.proto +# proto-message: Any +[type.googleapis.com/platform_http_server.PlatformHttpServerConfig] { + data_asset_id: "ai.intrinsic.hello_world" + + } diff --git a/services/platform_http_server/data_asset_utils.py b/services/platform_http_server/data_asset_utils.py new file mode 100644 index 0000000..b21f5f1 --- /dev/null +++ b/services/platform_http_server/data_asset_utils.py @@ -0,0 +1,68 @@ +import logging +from typing import Any +from typing import Callable +from typing import List +from typing import Optional +from typing import Tuple + +import grpc +from intrinsic.assets.data.proto.v1 import data_asset_pb2 +from intrinsic.assets.data.proto.v1 import data_assets_pb2 +from intrinsic.assets.data.proto.v1 import data_assets_pb2_grpc +from intrinsic.assets.proto import id_pb2 + + +def create_insecure_channel( + server_address: str, + server_port: str, + grpc_options: Optional[List[Tuple[str, Any]]] = None, +) -> grpc.Channel: + """Creates an insecure gRPC channel.""" + server_endpoint = f"{server_address}:{server_port}" + logging.info("Creating insecure channel at: %s", server_endpoint) + channel = grpc.insecure_channel(server_endpoint, options=grpc_options) + grpc.channel_ready_future(channel).result(timeout=10.0) + return channel + + +class DataAssetsService: + """Client for the gRPC Data Assets service. + + This class provides a Python interface to interact with the Data Assets + service, allowing users to list and retrieve data assets. + + Attributes: + _channel: The insecure gRPC channel used for communication. + _stub: The gRPC stub to make API calls. + """ + + def __init__( + self, + address: str = "istio-ingressgateway.app-ingress.svc.cluster.local", + port: str = "80", + grpc_options: Optional[List[Tuple[str, Any]]] = None, + ): + self._channel = create_insecure_channel(address, port, grpc_options) + self._stub = data_assets_pb2_grpc.DataAssetsStub(self._channel) + + def list_data_assets( + self, proto_name: str | None = None + ) -> List[data_asset_pb2.DataAsset]: + if proto_name is None: + list_data_assets_request = data_assets_pb2.ListDataAssetsRequest() + else: + list_data_assets_request = data_assets_pb2.ListDataAssetsRequest( + strict_filter=data_assets_pb2.DataAssetFilter( + proto_name=proto_name, + ) + ) + response = self._stub.ListDataAssets(list_data_assets_request) + return response.data_assets + + +def get_data_asset(self, package: str, name: str) -> data_asset_pb2.DataAsset: + return self._stub.GetDataAsset( + data_assets_pb2.GetDataAssetRequest( + id=id_pb2.Id(package=package, name=name) + ) + ) diff --git a/services/platform_http_server/platform_http_server.proto b/services/platform_http_server/platform_http_server.proto new file mode 100644 index 0000000..7c9901a --- /dev/null +++ b/services/platform_http_server/platform_http_server.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package platform_http_server; + +message PlatformHttpServerConfig { + // Determines which asset to load on startup. + string data_asset_id = 1; +} diff --git a/services/platform_http_server/platform_http_server_manifest.textproto b/services/platform_http_server/platform_http_server_manifest.textproto new file mode 100644 index 0000000..b644920 --- /dev/null +++ b/services/platform_http_server/platform_http_server_manifest.textproto @@ -0,0 +1,30 @@ +# proto-file: https://github.com/intrinsic-ai/sdk/blob/main/intrinsic/assets/services/proto/service_manifest.proto +# proto-message: intrinsic_proto.services.ServiceManifest + +metadata { + id { + package: "ai.intrinsic" + name: "platform_http_server" + } + vendor { + display_name: "Intrinsic" + } + documentation { + description: "A web server for static content." + } + display_name: "Platform HTTP Server" +} +service_def { + http_config: {} + supports_service_state: true + real_spec { + image { + archive_filename: "platform_http_server_image.tar" + } + } + sim_spec { + image { + archive_filename: "platform_http_server_image.tar" + } + } +} diff --git a/services/platform_http_server/requirements.txt b/services/platform_http_server/requirements.txt new file mode 100644 index 0000000..218a82c --- /dev/null +++ b/services/platform_http_server/requirements.txt @@ -0,0 +1,10 @@ +flask==3.0.0 +waitress==2.1.2 +grpcio==1.65.0 +# The following are dependencies of flask +blinker==1.7.0 +click==8.1.7 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +Werkzeug==3.0.1 diff --git a/services/platform_http_server/server.py b/services/platform_http_server/server.py new file mode 100644 index 0000000..f3f321e --- /dev/null +++ b/services/platform_http_server/server.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +""" +This script works as the binary for a generic, data-asset-driven HMI server. +It discovers installed data assets, unpacks their content into memory, serves one +specified in a config file, and supports hot-reloading by swapping which +in-memory asset is active. It uses the Flask web framework to provide robust request handling +and to enforce security-enhancing HTTP headers on all response. + +This server also implements the gRPC ServiceState servicer, allowing its lifecycle +(enable/disable) to be managed by Flowstate, in addition to HTTP endpoints. +""" + +from concurrent import futures +import json +import logging +import mimetypes +import os +import pathlib +import sys +import threading + +from flask import Flask +from flask import jsonify +from flask import request +from flask import Response +import grpc +from intrinsic.assets.data.proto.v1 import referenced_data_struct_pb2 +# Intrinsic-specific imports +from intrinsic.assets.services.proto.v1 import service_state_pb2 as state_proto +from intrinsic.assets.services.proto.v1 import service_state_pb2_grpc as state_grpc +from intrinsic.resources.proto import runtime_context_pb2 +from services.platform_http_server import data_asset_utils +from services.platform_http_server import platform_http_server_pb2 +from waitress import serve + +app = Flask(__name__) +update_lock = threading.Lock() +_SERVICE_STATE = {} + + +def get_runtime_context(): + """Reads the runtime context protobuf to get dynamic configuration like the port.""" + if not os.path.exists("/etc/intrinsic/runtime_config.pb"): + logging.warning( + "Runtime context not found. Using default port 8080 for local testing." + ) + return None + with open("/etc/intrinsic/runtime_config.pb", "rb") as fin: + return runtime_context_pb2.RuntimeContext.FromString(fin.read()) + + +def load_assets_to_memory(): + """Discovers all installed data assets and unpacks them into a dictionary.""" + data_asset_service = data_asset_utils.DataAssetsService() + available_assets = data_asset_service.list_data_assets() + + if not available_assets: + logging.critical("No installed data assets found. Server cannot start.") + sys.exit(1) + + logging.info( + f"Found {len(available_assets)} installed data assets. Unpacking to" + " memory..." + ) + + all_assets_content = {} + for asset in available_assets: + asset_id = f"{asset.metadata.id_version.id.package}.{asset.metadata.id_version.id.name}" + + # Check if the asset's data is of the expected type. + if "ReferencedDataStruct" in asset.data.type_url: + rds = referenced_data_struct_pb2.ReferencedDataStruct() + asset.data.Unpack(rds) + + content_map = { + filename: data_value.referenced_data_value.inlined + for filename, data_value in rds.fields.items() + } + + for filename, content in content_map.items(): + logging.info( + f" - Loaded '{filename}' ({len(content)} bytes) into memory for" + f" asset '{asset_id}'." + ) + + all_assets_content[asset_id] = content_map + else: + logging.warning( + f"Skipping asset '{asset_id}' with unexpected data type:" + f" {asset.data.type_url}" + ) + return all_assets_content + + +def _reload_assets_and_enable_service(): + """ + Helper to reload all data assets from disk and set the service state to ENABLED. + """ + logging.info("Reloading all data assets from disk...") + all_assets_content = load_assets_to_memory() + app.config["ALL_ASSETS_CONTENT"] = all_assets_content + _SERVICE_STATE["state_code"] = state_proto.SelfState.STATE_CODE_ENABLED + logging.info("Asset reload complete. Service is now ENABLED.") + + +class PlatformHttpServicer(state_grpc.ServiceStateServicer): + """Implements the gRPC ServiceState servicer for the HMI server.""" + + def GetState(self, request, context): + """Returns the current state of the service.""" + with update_lock: + return state_proto.SelfState(state_code=_SERVICE_STATE["state_code"]) + + def Enable(self, request, context): + """Enables the service via gRPC call.""" + with update_lock: + _reload_assets_and_enable_service() + logging.info("Service has been enabled via gRPC.") + return state_proto.EnableResponse() + + def Disable(self, request, context): + """Disables the service via gRPC call.""" + with update_lock: + _SERVICE_STATE["state_code"] = state_proto.SelfState.STATE_CODE_DISABLED + logging.info("Service has been disabled via gRPC.") + return state_proto.DisableResponse() + + +@app.after_request +def add_security_headers(response): + """ + Applies security-enhancing headers to every outgoing response. + This helps to mitigate common web vulnerabilities. + """ + # Prevents clickjacking attacks. + response.headers["X-Frame-Options"] = "SAMEORIGIN" + # Prevents browsers from MIME-sniffing the content type. + response.headers["X-Content-Type-Options"] = "nosniff" + # A robust Content Security Policy to prevent XSS. + response.headers["Content-Security-Policy"] = "default-src 'self'" + # Controls how much referrer information is sent. + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + return response + + +@app.route("/enable", methods=["POST"]) +def enable_service(): + """Enables the service, allowing it to serve files.""" + with update_lock: + _reload_assets_and_enable_service() + logging.info("Service has been enabled via HTTP.") + return jsonify({"status": "ENABLED"}), 200 + + +@app.route("/disable", methods=["POST"]) +def disable_service(): + """Disables the service, preventing it from serving files.""" + with update_lock: + _SERVICE_STATE["state_code"] = state_proto.SelfState.STATE_CODE_DISABLED + logging.info("Service has been disabled via HTTP.") + return jsonify({"status": "DISABLED"}), 200 + + +@app.route("/status", methods=["GET"]) +def get_status(): + """Returns the current state of the service (ENABLED or DISABLED).""" + with update_lock: + state_code = _SERVICE_STATE.get("state_code") + status_str = state_proto.SelfState.StateCode.Name(state_code) + return jsonify({"status": status_str}), 200 + + +@app.route("/reconfigure", methods=["POST"]) +def handle_reconfigure(): + """ + Handles POST requests for hot-reloading the active data asset. + Expects a JSON payload: {"data_asset_id": "new.asset.id"} + """ + try: + data = request.get_json() + if not data: + return jsonify({"error": "Request must be valid JSON"}), 400 + + new_asset_id = data.get("data_asset_id") + if not new_asset_id: + return jsonify({"error": "Missing 'data_asset_id' in request body"}), 400 + + logging.info(f"Hot reload triggered for asset: {new_asset_id}") + + all_assets = app.config["ALL_ASSETS_CONTENT"] + if new_asset_id not in all_assets: + logging.error(f"Asset '{new_asset_id}' not found in memory.") + return jsonify({"error": f"Asset '{new_asset_id}' not found"}), 404 + + # Atomically swap the active asset ID using the lock. + with update_lock: + app.config["ACTIVE_ASSET_ID"] = new_asset_id + + logging.info(f"Successfully reconfigured to serve asset '{new_asset_id}'.") + return jsonify({"status": "ok"}), 200 + + except Exception as e: + logging.error(f"Reconfiguration failed: {e}", exc_info=True) + return jsonify({"error": "Internal Server Error"}), 500 + + +@app.route("/") +@app.route("/") +def serve_file(filepath=None): + """ + Handles GET requests by looking up the path in the active in-memory asset. + If the root path '/' is requested, it serves 'index.html' if available. + """ + path = filepath + + with update_lock: + if ( + _SERVICE_STATE.get("state_code") + == state_proto.SelfState.STATE_CODE_DISABLED + ): + logging.warning("Request received while service is disabled.") + return jsonify({"error": "Service is disabled."}), 503 + active_id = app.config["ACTIVE_ASSET_ID"] + active_content = app.config["ALL_ASSETS_CONTENT"].get(active_id, {}) + + if not path: + for index_file in ["index.html", "hello_world.html"]: + if index_file in active_content: + path = index_file + break + + if not path: + logging.warning("No index file found to serve for root request.") + return "File Not Found", 404 + + content_bytes = active_content.get(path) + + if content_bytes: + mime_type, _ = mimetypes.guess_type(path) + if mime_type and ("\r" in mime_type or "\n" in mime_type): + logging.error( + f"Invalid characters detected in mime type for path: {path}" + ) + return "Bad Request", 400 + + # Create a Flask Response object to send the file content. + return Response( + content_bytes, mimetype=mime_type or "application/octet-stream" + ) + else: + logging.warning(f"File not found in memory: {path}") + return "File Not Found", 404 + + +def main(): + """Main function to discover assets, unpack them to memory, and run the server.""" + # Discover all installed data assets using the utility service. + all_assets_content = load_assets_to_memory() + # Read the configuration to determine which asset to load initially. + context = get_runtime_context() + config = platform_http_server_pb2.PlatformHttpServerConfig() + context.config.Unpack(config) + initial_asset_id = config.data_asset_id + + if not initial_asset_id: + logging.critical("Config file is missing 'data_asset_id'.") + sys.exit(1) + + # Validate that the configured initial asset was found and unpacked. + if initial_asset_id not in all_assets_content: + logging.critical( + f"Initial asset '{initial_asset_id}' from config was not found " + "or failed to unpack." + ) + sys.exit(1) + + http_port = context.http_port if context else 8080 + if context and hasattr(context, "grpc_port"): + grpc_port = context.grpc_port + else: + grpc_port = 9090 + logging.warning( + "gRPC port not found in runtime context. Defaulting to 9090." + ) + logging.info(f"HTTP port set to: {http_port}") + + _SERVICE_STATE["state_code"] = state_proto.SelfState.STATE_CODE_ENABLED + + # Set the initial configuration for the Flask app. + app.config["ALL_ASSETS_CONTENT"] = all_assets_content + app.config["ACTIVE_ASSET_ID"] = initial_asset_id + + grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + state_grpc.add_ServiceStateServicer_to_server( + PlatformHttpServicer(), grpc_server + ) + grpc_server.add_insecure_port(f"[::]:{grpc_port}") + grpc_thread = threading.Thread(target=grpc_server.start, daemon=True) + grpc_thread.start() + logging.info(f"gRPC ServiceState server started on port {grpc_port}.") + + logging.info(f"Starting in-memory HMI server on port {http_port}...") + logging.info(f"Serving initial content from asset '{initial_asset_id}'") + logging.info(f"Service state is initially 'ENABLED'") + serve(app, host="0.0.0.0", port=http_port) + + +if __name__ == "__main__": + logging.basicConfig( + stream=sys.stderr, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + ) + main() diff --git a/services/platform_http_server/testdata/image_files.yaml b/services/platform_http_server/testdata/image_files.yaml new file mode 100644 index 0000000..8474d16 --- /dev/null +++ b/services/platform_http_server/testdata/image_files.yaml @@ -0,0 +1,7 @@ +schemaVersion: "2.0.0" + +fileExistenceTests: +- name: 'Server binary' + path: '/services/platform_http_server/server' + shouldExist: true + isExecutableBy: 'group' diff --git a/services/random_number/random_number.py b/services/random_number/random_number.py index 4e067bc..8a72c03 100644 --- a/services/random_number/random_number.py +++ b/services/random_number/random_number.py @@ -3,14 +3,12 @@ import logging import random -import grpc from google.protobuf import timestamp_pb2 as timestamp_proto - +import grpc from intrinsic.assets.services.proto.v1 import service_state_pb2 as state_proto from intrinsic.assets.services.proto.v1 import service_state_pb2_grpc as state_grpc from intrinsic.util.grpc import error_handling from intrinsic.util.status import extended_status_pb2 as ext_status_proto - from services.random_number import random_number_pb2 as random_num_proto from services.random_number import random_number_pb2_grpc as random_num_grpc diff --git a/services/random_number/random_number_main.py b/services/random_number/random_number_main.py index 8002b22..f837d78 100644 --- a/services/random_number/random_number_main.py +++ b/services/random_number/random_number_main.py @@ -4,10 +4,8 @@ import sys import grpc - -from intrinsic.resources.proto import runtime_context_pb2 from intrinsic.assets.services.proto.v1 import service_state_pb2_grpc as state_grpc - +from intrinsic.resources.proto import runtime_context_pb2 from services.random_number import random_number from services.random_number import random_number_pb2_grpc as random_num_grpc diff --git a/services/random_number/random_number_test.py b/services/random_number/random_number_test.py index 1c9bb3d..b29e71e 100644 --- a/services/random_number/random_number_test.py +++ b/services/random_number/random_number_test.py @@ -1,9 +1,8 @@ import unittest -import portpicker import grpc from grpc.framework.foundation import logging_pool - +import portpicker from services.random_number import random_number from services.random_number import random_number_pb2 as rand_num_proto from services.random_number import random_number_pb2_grpc as random_num_grpc diff --git a/services/stopwatch/stopwatch_service.py b/services/stopwatch/stopwatch_service.py index 0433fa8..a37a09a 100644 --- a/services/stopwatch/stopwatch_service.py +++ b/services/stopwatch/stopwatch_service.py @@ -7,11 +7,9 @@ import grpc from intrinsic.resources.proto import runtime_context_pb2 - from services.stopwatch import stopwatch_service_pb2 as stopwatch_proto from services.stopwatch import stopwatch_service_pb2_grpc as stopwatch_grpc - logger = logging.getLogger(__name__) diff --git a/skills/get_random_number/get_random_number.py b/skills/get_random_number/get_random_number.py index 5ce28a7..e90fc45 100644 --- a/skills/get_random_number/get_random_number.py +++ b/skills/get_random_number/get_random_number.py @@ -2,12 +2,10 @@ from absl import logging import grpc - from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides from intrinsic.util.grpc import connection from intrinsic.util.grpc import interceptor - from services.random_number import random_number_pb2 as rand_num_proto from services.random_number import random_number_pb2_grpc as rand_num_grpc from skills.get_random_number import get_random_number_pb2 as get_rand_num_proto diff --git a/skills/get_random_number/get_random_number_test.py b/skills/get_random_number/get_random_number_test.py index 0839172..948b4ee 100644 --- a/skills/get_random_number/get_random_number_test.py +++ b/skills/get_random_number/get_random_number_test.py @@ -1,13 +1,11 @@ import unittest import grpc - from intrinsic.skills.testing import skill_test_utils as stu - -from skills.get_random_number import get_random_number -from skills.get_random_number import get_random_number_pb2 as get_rand_num_proto from services.random_number import random_number_pb2 as rand_num_proto from services.random_number import random_number_pb2_grpc as rand_num_grpc +from skills.get_random_number import get_random_number +from skills.get_random_number import get_random_number_pb2 as get_rand_num_proto class FakeRandomNumberServicer(rand_num_grpc.RandomNumberServiceServicer): diff --git a/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment.py b/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment.py index 4fbd517..7511078 100644 --- a/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment.py +++ b/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment.py @@ -20,14 +20,12 @@ from absl import logging import grpc +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc from intrinsic.icon.proto import joint_space_pb2 from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides -from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 -from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc -from skills.read_joint_positions_from_opcua_equipment import ( - read_joint_positions_from_opcua_equipment_pb2, -) +from skills.read_joint_positions_from_opcua_equipment import read_joint_positions_from_opcua_equipment_pb2 _EQUIPMENT_SLOT = "opcua_equipment" diff --git a/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment_test.py b/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment_test.py index 92ddad6..e400890 100644 --- a/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment_test.py +++ b/skills/read_joint_positions_from_opcua_equipment/read_joint_positions_from_opcua_equipment_test.py @@ -4,18 +4,13 @@ import grpc from grpc.framework.foundation import logging_pool - -from intrinsic.skills.python import skill_interface from intrinsic.hardware.gpio.v1.signal_pb2 import SignalValue from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 -from intrinsic.hardware.opcua_equipment import ( - opcua_equipment_service_pb2_grpc, -) +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc from intrinsic.resources.proto import resource_handle_pb2 -from skills.read_joint_positions_from_opcua_equipment import ( - read_joint_positions_from_opcua_equipment, - read_joint_positions_from_opcua_equipment_pb2, -) +from intrinsic.skills.python import skill_interface +from skills.read_joint_positions_from_opcua_equipment import read_joint_positions_from_opcua_equipment +from skills.read_joint_positions_from_opcua_equipment import read_joint_positions_from_opcua_equipment_pb2 ReadJointPositionsFromOpcuaEquipment = ( read_joint_positions_from_opcua_equipment.ReadJointPositionsFromOpcuaEquipment diff --git a/skills/say_skill/say_skill.py b/skills/say_skill/say_skill.py index 94a71c3..29c0008 100644 --- a/skills/say_skill/say_skill.py +++ b/skills/say_skill/say_skill.py @@ -1,7 +1,6 @@ """Contains the skill say_skill_py.""" from absl import logging - from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides diff --git a/skills/say_skill/say_skill_test.py b/skills/say_skill/say_skill_test.py index 9b6ad66..b613bb6 100644 --- a/skills/say_skill/say_skill_test.py +++ b/skills/say_skill/say_skill_test.py @@ -4,7 +4,6 @@ from intrinsic.skills.python import skill_canceller from intrinsic.skills.python import skill_interface - from skills.say_skill.say_skill import SaySkill from skills.say_skill.say_skill_pb2 import SaySkillParams diff --git a/skills/scan_barcodes/BUILD b/skills/scan_barcodes/BUILD index a5b316c..e2a7bb5 100644 --- a/skills/scan_barcodes/BUILD +++ b/skills/scan_barcodes/BUILD @@ -82,12 +82,12 @@ cc_library( hdrs = ["scan_barcodes.h"], deps = [ ":scan_barcodes_cc_proto", + "@ai_intrinsic_sdks//intrinsic/connect/cc/grpc:channel", "@ai_intrinsic_sdks//intrinsic/perception/proto/v1:camera_config_cc_proto", "@ai_intrinsic_sdks//intrinsic/perception/proto/v1:camera_service_cc_grpc", "@ai_intrinsic_sdks//intrinsic/skills/cc:equipment_pack", "@ai_intrinsic_sdks//intrinsic/skills/cc:skill_interface", "@ai_intrinsic_sdks//intrinsic/skills/cc:skill_utils", - "@ai_intrinsic_sdks//intrinsic/util/grpc", "@ai_intrinsic_sdks//intrinsic/util/status:status_conversion_grpc", "@ai_intrinsic_sdks//intrinsic/util/status:status_macros", "@com_google_absl//absl/log", diff --git a/skills/scan_barcodes/scan_barcodes.cc b/skills/scan_barcodes/scan_barcodes.cc index 356682a..d4c6251 100644 --- a/skills/scan_barcodes/scan_barcodes.cc +++ b/skills/scan_barcodes/scan_barcodes.cc @@ -10,11 +10,11 @@ #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "intrinsic/connect/cc/grpc/channel.h" #include "intrinsic/perception/proto/v1/camera_config.pb.h" #include "intrinsic/perception/proto/v1/camera_service.grpc.pb.h" #include "intrinsic/skills/cc/skill_utils.h" #include "intrinsic/skills/proto/skill_service.pb.h" -#include "intrinsic/util/grpc/grpc.h" #include "intrinsic/util/status/status_conversion_grpc.h" #include "intrinsic/util/status/status_macros.h" #include "opencv2/core/mat.hpp" @@ -26,7 +26,7 @@ using ::com::example::BarcodeType; using ::com::example::ScanBarcodesParams; using ::com::example::ScanBarcodesResult; -using ::intrinsic::WaitForChannelConnected; +using ::intrinsic::connect::WaitForChannelConnected; using ::intrinsic::skills::EquipmentPack; using ::intrinsic::skills::ExecuteContext; using ::intrinsic::skills::ExecuteRequest; diff --git a/skills/scan_barcodes/scan_barcodes.py b/skills/scan_barcodes/scan_barcodes.py index a37861d..786e599 100644 --- a/skills/scan_barcodes/scan_barcodes.py +++ b/skills/scan_barcodes/scan_barcodes.py @@ -3,27 +3,24 @@ # [START import_typing] from typing import List -# [END import_typing] - from absl import logging - # [START import_cv2] import cv2 - +# [START import_cameras] +from intrinsic.perception.python.camera import cameras +# [END import_cameras] +from intrinsic.skills.python import skill_interface +from intrinsic.util.decorators import overrides # [END import_cv2] # [START import_numpy] import numpy as np +from skills.scan_barcodes import scan_barcodes_pb2 -# [END import_numpy] +# [END import_typing] -# [START import_cameras] -from intrinsic.perception.python.camera import cameras -# [END import_cameras] -from intrinsic.skills.python import skill_interface -from intrinsic.util.decorators import overrides +# [END import_numpy] -from skills.scan_barcodes import scan_barcodes_pb2 # [START camera_slot_constant] # Camera slot name; make sure this matches the skill manifest. diff --git a/skills/scan_barcodes/scan_barcodes_test.py b/skills/scan_barcodes/scan_barcodes_test.py index 9e9c4d9..40911fa 100644 --- a/skills/scan_barcodes/scan_barcodes_test.py +++ b/skills/scan_barcodes/scan_barcodes_test.py @@ -14,9 +14,8 @@ from intrinsic.perception.python.camera.cameras import Camera from intrinsic.perception.python.camera.data_classes import SensorImage from intrinsic.skills.python import skill_interface - -from skills.scan_barcodes.scan_barcodes import ScanBarcodes from skills.scan_barcodes import scan_barcodes_pb2 +from skills.scan_barcodes.scan_barcodes import ScanBarcodes class ScanBarcodesTest(unittest.TestCase): diff --git a/skills/start_stopwatch/start_stopwatch.py b/skills/start_stopwatch/start_stopwatch.py index 957425e..62655f0 100644 --- a/skills/start_stopwatch/start_stopwatch.py +++ b/skills/start_stopwatch/start_stopwatch.py @@ -1,17 +1,15 @@ """Contains the skill start_stopwatch.""" from absl import logging - +import grpc from intrinsic.skills.python import proto_utils from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides - -from skills.start_stopwatch import start_stopwatch_pb2 -import grpc from intrinsic.util.grpc import connection from intrinsic.util.grpc import interceptor from services.stopwatch import stopwatch_service_pb2 as stopwatch_proto from services.stopwatch import stopwatch_service_pb2_grpc as stopwatch_grpc +from skills.start_stopwatch import start_stopwatch_pb2 def make_grpc_stub(resource_handle): diff --git a/skills/start_stopwatch/start_stopwatch_test.py b/skills/start_stopwatch/start_stopwatch_test.py index 2accb52..62e6126 100644 --- a/skills/start_stopwatch/start_stopwatch_test.py +++ b/skills/start_stopwatch/start_stopwatch_test.py @@ -1,11 +1,10 @@ import unittest from intrinsic.skills.testing import skill_test_utils as stu - -from skills.start_stopwatch.start_stopwatch import StartStopwatch -from skills.start_stopwatch.start_stopwatch_pb2 import StartStopwatchParams from services.stopwatch import stopwatch_service_pb2 as stopwatch_proto from services.stopwatch import stopwatch_service_pb2_grpc as stopwatch_grpc +from skills.start_stopwatch.start_stopwatch import StartStopwatch +from skills.start_stopwatch.start_stopwatch_pb2 import StartStopwatchParams class FakeStopwatchServicer(stopwatch_grpc.StopwatchServiceServicer): diff --git a/skills/stop_stopwatch/BUILD b/skills/stop_stopwatch/BUILD index 3fba15b..2370f1b 100644 --- a/skills/stop_stopwatch/BUILD +++ b/skills/stop_stopwatch/BUILD @@ -92,6 +92,7 @@ cc_library( "@ai_intrinsic_sdks//intrinsic/skills/proto:equipment_cc_proto", "@ai_intrinsic_sdks//intrinsic/util/status:status_macros", "@ai_intrinsic_sdks//intrinsic/util/status:status_macros_grpc", + "@com_github_grpc_grpc//:grpc++", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/log", "@com_google_absl//absl/status", diff --git a/skills/stop_stopwatch/stop_stopwatch.cc b/skills/stop_stopwatch/stop_stopwatch.cc index c4d3996..ce4b0bc 100644 --- a/skills/stop_stopwatch/stop_stopwatch.cc +++ b/skills/stop_stopwatch/stop_stopwatch.cc @@ -7,14 +7,15 @@ #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "skills/stop_stopwatch/stop_stopwatch.pb.h" #include "google/protobuf/message.h" +#include "grpcpp/channel.h" +#include "grpcpp/create_channel.h" #include "intrinsic/skills/cc/skill_utils.h" #include "intrinsic/util/status/status_macros.h" #include "intrinsic/util/status/status_macros_grpc.h" - -#include "services/stopwatch/stopwatch_service.pb.h" #include "services/stopwatch/stopwatch_service.grpc.pb.h" +#include "services/stopwatch/stopwatch_service.pb.h" +#include "skills/stop_stopwatch/stop_stopwatch.pb.h" namespace skills::stop_stopwatch { @@ -29,16 +30,18 @@ using ::intrinsic::skills::PreviewRequest; using ::intrinsic::skills::SkillInterface; using ::intrinsic::skills::SkillProjectInterface; - -std::unique_ptr<::stopwatch::StopwatchService::Stub> MakeGrpcStub(intrinsic_proto::resources::ResourceHandle handle) { +std::unique_ptr<::stopwatch::StopwatchService::Stub> MakeGrpcStub( + intrinsic_proto::resources::ResourceHandle handle) { const std::string& address = handle.connection_info().grpc().address(); - std::shared_ptr channel = ::grpc::CreateChannel( - address, grpc::InsecureChannelCredentials()); + std::shared_ptr channel = + ::grpc::CreateChannel(address, grpc::InsecureChannelCredentials()); return ::stopwatch::StopwatchService::NewStub(channel); } -std::unique_ptr<::grpc::ClientContext> MakeClientContext(intrinsic_proto::resources::ResourceHandle handle) { - const std::string& instance = handle.connection_info().grpc().server_instance(); +std::unique_ptr<::grpc::ClientContext> MakeClientContext( + intrinsic_proto::resources::ResourceHandle handle) { + const std::string& instance = + handle.connection_info().grpc().server_instance(); auto ctx = std::make_unique<::grpc::ClientContext>(); ctx->AddMetadata("x-resource-instance-name", instance); return ctx; @@ -55,14 +58,13 @@ absl::StatusOr StopStopwatch::GetFootprint( return std::move(result); } -absl::StatusOr> StopStopwatch::Preview( - const PreviewRequest& request, PreviewContext& context) { - return absl::UnimplementedError("Skill has not implemented `Preview`."); +absl::StatusOr> +StopStopwatch::Preview(const PreviewRequest& request, PreviewContext& context) { + return absl::UnimplementedError("Skill has not implemented `Preview`."); } -absl::StatusOr> StopStopwatch::Execute( - const ExecuteRequest& request, ExecuteContext& context) { - +absl::StatusOr> +StopStopwatch::Execute(const ExecuteRequest& request, ExecuteContext& context) { INTR_ASSIGN_OR_RETURN(intrinsic_proto::resources::ResourceHandle handle, context.equipment().GetHandle("stopwatch_service")); @@ -70,7 +72,8 @@ absl::StatusOr> StopStopwatch::Execut auto ctx = MakeClientContext(handle); ::stopwatch::StopRequest stop_request; ::stopwatch::StopResponse stop_response; - INTR_RETURN_IF_ERROR_GRPC(stub->Stop(ctx.get(), stop_request, &stop_response)); + INTR_RETURN_IF_ERROR_GRPC( + stub->Stop(ctx.get(), stop_request, &stop_response)); LOG(INFO) << "Time elapsed: " << stop_response.time_elapsed(); diff --git a/skills/stop_stopwatch/stop_stopwatch.py b/skills/stop_stopwatch/stop_stopwatch.py index e023f83..1c284cb 100644 --- a/skills/stop_stopwatch/stop_stopwatch.py +++ b/skills/stop_stopwatch/stop_stopwatch.py @@ -1,17 +1,15 @@ """Contains the skill stop_stopwatch.""" from absl import logging - +import grpc from intrinsic.skills.python import proto_utils from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides - -from skills.stop_stopwatch import stop_stopwatch_pb2 -import grpc from intrinsic.util.grpc import connection from intrinsic.util.grpc import interceptor from services.stopwatch import stopwatch_service_pb2 as stopwatch_proto from services.stopwatch import stopwatch_service_pb2_grpc as stopwatch_grpc +from skills.stop_stopwatch import stop_stopwatch_pb2 def make_grpc_stub(resource_handle): diff --git a/skills/stop_stopwatch/stop_stopwatch_test.py b/skills/stop_stopwatch/stop_stopwatch_test.py index c014a2e..a692365 100644 --- a/skills/stop_stopwatch/stop_stopwatch_test.py +++ b/skills/stop_stopwatch/stop_stopwatch_test.py @@ -1,11 +1,10 @@ import unittest from intrinsic.skills.testing import skill_test_utils as stu - -from skills.stop_stopwatch.stop_stopwatch import StopStopwatch -from skills.stop_stopwatch.stop_stopwatch_pb2 import StopStopwatchParams from services.stopwatch import stopwatch_service_pb2 as stopwatch_proto from services.stopwatch import stopwatch_service_pb2_grpc as stopwatch_grpc +from skills.stop_stopwatch.stop_stopwatch import StopStopwatch +from skills.stop_stopwatch.stop_stopwatch_pb2 import StopStopwatchParams class FakeStopwatchServicer(stopwatch_grpc.StopwatchServiceServicer): diff --git a/skills/use_world/use_world.py b/skills/use_world/use_world.py index 56c1ac2..1164682 100644 --- a/skills/use_world/use_world.py +++ b/skills/use_world/use_world.py @@ -3,7 +3,6 @@ from typing import cast from absl import logging - from intrinsic.math.python import data_types from intrinsic.skills.proto import skill_service_pb2 from intrinsic.skills.python import proto_utils @@ -11,7 +10,6 @@ from intrinsic.util.decorators import overrides from intrinsic.world.python import object_world_client from intrinsic.world.python import object_world_resources - from skills.use_world import use_world_pb2 ROBOT_EQUIPMENT_SLOT: str = "robot" diff --git a/skills/use_world/use_world_test.py b/skills/use_world/use_world_test.py index b307cdb..ce2a3d8 100644 --- a/skills/use_world/use_world_test.py +++ b/skills/use_world/use_world_test.py @@ -2,7 +2,6 @@ from unittest.mock import create_autospec from intrinsic.skills.python import skill_interface - from skills.use_world.use_world import UseWorld from skills.use_world.use_world_pb2 import UseWorldParams diff --git a/skills/validate_pose/validate_pose.py b/skills/validate_pose/validate_pose.py index 9737eea..cf1a31b 100644 --- a/skills/validate_pose/validate_pose.py +++ b/skills/validate_pose/validate_pose.py @@ -1,13 +1,10 @@ """Contains the skill validate_pose.""" -import numpy - from absl import logging - from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides from intrinsic.world.python import object_world_client - +import numpy from skills.validate_pose import validate_pose_pb2 diff --git a/skills/validate_pose/validate_pose_test.py b/skills/validate_pose/validate_pose_test.py index 55d099c..3c5bae9 100644 --- a/skills/validate_pose/validate_pose_test.py +++ b/skills/validate_pose/validate_pose_test.py @@ -5,7 +5,6 @@ from intrinsic.math.python.quaternion import Quaternion from intrinsic.math.python.rotation3 import Rotation3 from intrinsic.skills.python import skill_interface - from skills.validate_pose.validate_pose import ValidatePose from skills.validate_pose.validate_pose_pb2 import ValidatePoseParams diff --git a/skills/wiggle_joint/wiggle_joint.py b/skills/wiggle_joint/wiggle_joint.py index e520ee1..431a98f 100644 --- a/skills/wiggle_joint/wiggle_joint.py +++ b/skills/wiggle_joint/wiggle_joint.py @@ -1,5 +1,4 @@ from absl import logging - from intrinsic.icon.actions import point_to_point_move_pb2 from intrinsic.icon.equipment import equipment_utils from intrinsic.icon.proto import joint_space_pb2 @@ -9,7 +8,6 @@ from intrinsic.skills.python import proto_utils from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides - from skills.wiggle_joint import wiggle_joint_pb2 ROBOT_EQUIPMENT_SLOT: str = "robot" diff --git a/skills/wiggle_joint/wiggle_joint_test.py b/skills/wiggle_joint/wiggle_joint_test.py index 36b9f8b..426651e 100644 --- a/skills/wiggle_joint/wiggle_joint_test.py +++ b/skills/wiggle_joint/wiggle_joint_test.py @@ -5,7 +5,6 @@ from intrinsic.icon.proto.v1 import service_pb2 from intrinsic.icon.python import icon_api from intrinsic.skills.python import skill_interface - from skills.wiggle_joint.wiggle_joint import WiggleJoint from skills.wiggle_joint.wiggle_joint_pb2 import WiggleJointParams diff --git a/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment.py b/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment.py index 27d4b94..8110d77 100644 --- a/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment.py +++ b/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment.py @@ -21,15 +21,13 @@ from absl import logging import grpc +from intrinsic.hardware.gpio.v1.signal_pb2 import SignalValue +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc from intrinsic.icon.equipment import equipment_utils from intrinsic.skills.python import skill_interface from intrinsic.util.decorators import overrides -from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 -from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc -from intrinsic.hardware.gpio.v1.signal_pb2 import SignalValue -from skills.write_joint_positions_to_opcua_equipment import ( - write_joint_positions_to_opcua_equipment_pb2, -) +from skills.write_joint_positions_to_opcua_equipment import write_joint_positions_to_opcua_equipment_pb2 _EQUIPMENT_SLOT = "opcua_equipment" _ROBOT_SLOT = "robot" diff --git a/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment_test.py b/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment_test.py index 7825da7..191214d 100644 --- a/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment_test.py +++ b/skills/write_joint_positions_to_opcua_equipment/write_joint_positions_to_opcua_equipment_test.py @@ -4,19 +4,14 @@ import grpc from grpc.framework.foundation import logging_pool - +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 +from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2_grpc from intrinsic.icon.proto.v1 import service_pb2 from intrinsic.icon.python import icon_api -from intrinsic.skills.python import skill_interface -from intrinsic.hardware.opcua_equipment import opcua_equipment_service_pb2 -from intrinsic.hardware.opcua_equipment import ( - opcua_equipment_service_pb2_grpc, -) from intrinsic.resources.proto import resource_handle_pb2 -from skills.write_joint_positions_to_opcua_equipment import ( - write_joint_positions_to_opcua_equipment, - write_joint_positions_to_opcua_equipment_pb2, -) +from intrinsic.skills.python import skill_interface +from skills.write_joint_positions_to_opcua_equipment import write_joint_positions_to_opcua_equipment +from skills.write_joint_positions_to_opcua_equipment import write_joint_positions_to_opcua_equipment_pb2 WriteJointPositionsToOpcuaEquipment = ( write_joint_positions_to_opcua_equipment.WriteJointPositionsToOpcuaEquipment diff --git a/tests/README.md b/tests/README.md index 433cbd7..8a4d7a7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -34,17 +34,14 @@ In that way it will use the following skills: `//skills/start_stopwatch:start_st The worklow contains one main bash script which is an end-to-end journey for performing continuous integration. This bash goes through the following steps: -1. Check Intrinsic organization. -2. Build the skill(s). -3. Build the service(s). -4. Lease a virtual machine (vm). -5. Deploy an existing solution. -6. Install the skill(s). -7. Install the service(s). -8. Add the service(s). -9. Add a process that uses the skill and service. -10. Stop your solution. -11. Return your vm. +1. Check Intrinsic Organization. +2. Deploy an existing solution. +3. Build the skill(s). +4. Install the skill(s). +5. Build the service(s). +6. Install the service(s). +7. Add the service(s). +8. Add a process that uses the skill and service. For running the github action: diff --git a/tests/run_ci.sh b/tests/run_ci.sh index b325e44..1c9e36e 100755 --- a/tests/run_ci.sh +++ b/tests/run_ci.sh @@ -5,7 +5,6 @@ echo "---This bash will go through all the CI journey ---" # Variables setup INTRINSIC_ORGANIZATION="" -INTRINSIC_VM_DURATION="" SKILL_BAZEL="" INTRINSIC_SOLUTION="" SERVICE_BAZEL="" @@ -19,10 +18,6 @@ while [[ "$#" -gt 0 ]]; do INTRINSIC_ORGANIZATION="${1#*=}" echo "Argument --org received: $INTRINSIC_ORGANIZATION" ;; - --vm-duration=*) - INTRINSIC_VM_DURATION="${1#*=}" - echo "Argument --vm-duration received: $INTRINSIC_VM_DURATION" - ;; --skill=*) SKILL_BAZEL="${1#*=}" echo "Argument --skill received: $SKILL_BAZEL (comma-separated if multiple)" @@ -63,130 +58,62 @@ export INTRINSIC_ORGANIZATION ORG echo "" -echo "2. Build the skill(s)." -echo "NOTE: You should have a skill created in order to build it in this step." +echo "2. Deploy an existing solution." -SKILL_BAZEL=$(echo "$SKILL_BAZEL" | xargs) +echo "NOTE: You should have your solution running with the corresponding id." -if [ -n "$SKILL_BAZEL" ]; then - SKILL_TARGETS=$(echo "$SKILL_BAZEL" | tr ',' ' ') - - echo "Building all skills with the targets: $SKILL_TARGETS" +echo "2.1. Checking the solution id." - bazel build $SKILL_TARGETS - - if [ $? -ne 0 ]; then - echo "Error: Bazel build for skills failed. Exiting." +if [ -z "$INTRINSIC_SOLUTION" ]; then + inctl solution list --filter running_in_sim,running_on_hw --org "$INTRINSIC_ORGANIZATION" + echo "" + read -p "Please, copy the solution id: " INTRINSIC_SOLUTION + if [ -z "$INTRINSIC_SOLUTION" ]; then + echo "Error! Solution id wasn't set." exit 1 + else + export INTRINSIC_SOLUTION="$INTRINSIC_SOLUTION" + echo "Done! The INTRINSIC_SOLUTION has been set to: $INTRINSIC_SOLUTION" fi - echo "Successfully built all skills." else - echo "No skill targets were provided. Skipping build step." + export INTRINSIC_SOLUTION="$INTRINSIC_SOLUTION" + echo "Done! The INTRINSIC_SOLUTION has been set to: $INTRINSIC_SOLUTION" fi echo "" -echo "3. Build the service(s)" -echo "NOTE: You should have a service created in order to build it in this step." - -SERVICE_BAZEL=$(echo "$SERVICE_BAZEL" | xargs) - -if [ -n "$SERVICE_BAZEL" ]; then - SERVICE_TARGETS=$(echo "$SERVICE_BAZEL" | tr ',' ' ') - - echo "Building all services with the targets: $SERVICE_TARGETS" - - bazel build $SERVICE_TARGETS - - if [ $? -ne 0 ]; then - echo "Error: Bazel build for service failed. Exiting." - exit 1 - fi - echo "Successfully built all services." -else - echo "No services targets were provided. Skipping build step." -fi +echo "2.2. Get the solution from the id." -echo "" - -echo "4.Lease a VM" - -if [ -n "$INTRINSIC_VM_DURATION" ]; then - echo "Requesting VM for $INTRINSIC_VM_DURATION hours..." - lease_output=$(inctl vm lease --silent -d "${INTRINSIC_VM_DURATION}h" --org "$INTRINSIC_ORGANIZATION") - lease_status=$? - if [ $lease_status -eq 0 ]; then - echo "" - echo "VM lease request successful!" - extracted_vm_instance=$lease_output - - if [ -n "$extracted_vm_instance" ]; then - VM_INSTANCE="$extracted_vm_instance" - echo "Auto-captured VM instance ID: $VM_INSTANCE" - export VM_INSTANCE - else - echo "Warning: Could not auto-capture VM instance ID from command output." - echo "Output was: $lease_output" - read -p "Please, copy and paste the VM instance ID to return it later: " VM_INSTANCE < /dev/tty - if [ -z "$VM_INSTANCE" ]; then - echo "Warning: VM instance ID was not provided. You will need to return it manually." - fi - fi - else - echo "There was an error leasing the VM." - echo "Command output: $lease_output" - echo "Please, verify the command and its output." - fi -else - echo "VM lease skipped due to missing time duration." -fi +inctl solution get $INTRINSIC_SOLUTION --org "$INTRINSIC_ORGANIZATION" echo "" -echo "5. Deploy an existing solution." - -echo "5.1. Starting the solution." - -if [ -n "$INTRINSIC_SOLUTION" ]; then - echo "Starting solution '$INTRINSIC_SOLUTION'..." - (INTRINSIC_SOLUTION="" inctl solution start "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION" --cluster "$lease_output") +echo "3. Build the skill(s)." +echo "NOTE: You should have a skill created in order to build it in this step." - timeout_seconds=300 # 5 minutes - check_interval_seconds=10 - elapsed_seconds=0 +SKILL_BAZEL=$(echo "$SKILL_BAZEL" | xargs) - while true; do - status_output=$(inctl solution get "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION") +if [ -n "$SKILL_BAZEL" ]; then + SKILL_TARGETS=$(echo "$SKILL_BAZEL" | tr ',' ' ') - if echo "$status_output" | grep -q "is running"; then - echo "Solution '$INTRINSIC_SOLUTION' is now running." - break - fi + echo "Building all skills with the targets: $SKILL_TARGETS" - # Check for timeout - if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then - echo "Error: Timed out after $timeout_seconds seconds." - echo "Last status check output:" - echo "$status_output" - exit 1 - fi + bazel build $SKILL_TARGETS - echo "Not ready yet. Retrying in $check_interval_seconds seconds." - sleep "$check_interval_seconds" - elapsed_seconds=$((elapsed_seconds + check_interval_seconds)) - done - echo "Waiting 3 minutes for the propagation of the solution..." - sleep 180 + if [ $? -ne 0 ]; then + echo "Error: Bazel build for skills failed. Exiting." + exit 1 + fi + echo "Successfully built all skills." else - echo "Warning: '$INTRINSIC_SOLUTION' is not set. Skipping start and wait logic." - exit 1 + echo "No skill targets were provided. Skipping build step." fi echo "" -echo "6. Install the skill(s)." +echo "4. Install the skill(s)." -INSTALLED_SKILLS=$(inctl skill list --org "$INTRINSIC_ORGANIZATION" --solution "$INTRINSIC_SOLUTION" --filter "sideloaded" 2>&1) +INSTALLED_SKILLS=$(inctl skill list --org "$INTRINSIC_ORGANIZATION" --solution "$INTRINSIC_SOLUTION") IFS=',' read -ra SKILL_ARRAY <<< "$SKILL_BAZEL" @@ -211,14 +138,36 @@ for SKILL in "${SKILL_ARRAY[@]}"; do inctl skill install bazel-bin/"$skill_package"/"$skill_target".bundle.tar --solution "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION" if [ $? -ne 0 ]; then echo "Error: Skill installation for '$SKILL' failed. Exiting." - exit 1 fi fi done echo "" -echo "7. Install the service(s)." +echo "5. Build the service(s)" +echo "NOTE: You should have a service created in order to build it in this step." + +SERVICE_BAZEL=$(echo "$SERVICE_BAZEL" | xargs) + +if [ -n "$SERVICE_BAZEL" ]; then + SERVICE_TARGETS=$(echo "$SERVICE_BAZEL" | tr ',' ' ') + + echo "Building all services with the targets: $SERVICE_TARGETS" + + bazel build $SERVICE_TARGETS + + if [ $? -ne 0 ]; then + echo "Error: Bazel build for service failed. Exiting." + exit 1 + fi + echo "Successfully built all services." +else + echo "No services targets were provided. Skipping build step." +fi + +echo "" + +echo "6. Install the service(s)." INSTALLED_SERVICES=$(inctl asset list --asset_types="service" --org "$INTRINSIC_ORGANIZATION" --solution "$INTRINSIC_SOLUTION") @@ -233,7 +182,7 @@ for SERVICE in "${SERVICE_ARRAY[@]}"; do else echo "Warning: No colon found in SERVICE ('$SERVICE'). Assuming it's a target within the current package." service_package="" - service_target="$SERVICE" + service_target="$SERVICE" fi SERVICES_TARGET+=("$service_target") @@ -248,7 +197,6 @@ for SERVICE in "${SERVICE_ARRAY[@]}"; do echo "Installing service: $SERVICE" inctl service install bazel-bin/"$service_package"/"$service_target".bundle.tar --solution "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION" - if [ $? -ne 0 ]; then echo "Error: Service installation for '$SERVICE' failed. Exiting." fi @@ -257,9 +205,9 @@ done echo "" -echo "8. Add the service(s)." +echo "7. Add the service(s)." -echo "8.1 Listing your installed assets for services" +echo "7.1 Listing your installed assets for services" INSTALLED_SERVICES=() @@ -283,7 +231,7 @@ while IFS= read -r full_service_name; do done done < <(inctl asset list --org "$INTRINSIC_ORGANIZATION" --solution "$INTRINSIC_SOLUTION") -echo "8.2 Add the service(s)" +echo "7.2 Add the service(s)" if [ ${#INSTALLED_SERVICES[@]} -eq 0 ]; then echo "No matching installed services found to add. Skipping this step." @@ -312,7 +260,7 @@ else fi echo "" -echo "9. Add a process that uses the skill and service." +echo "8. Add a process that uses the skill and service." PYTHON_SCRIPT_PATH="./tests/sbl_ci.py" @@ -335,46 +283,6 @@ fi echo "" -echo "10. Stopping your solution" - -echo "Stopping the solution '$INTRINSIC_SOLUTION' started in the first steps." -(INTRINSIC_SOLUTION="" inctl solution stop "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION" --cluster "$lease_output") - -echo "Waiting for solution to enter a not running state." - -timeout_seconds=300 # 5 minutes -check_interval_seconds=10 -elapsed_seconds=0 - -while true; do - status_output=$(inctl solution get "$INTRINSIC_SOLUTION" --org "$INTRINSIC_ORGANIZATION") - - if echo "$status_output" | grep -q "not running"; then - echo "Solution '$INTRINSIC_SOLUTION' is not running." - break - fi - - # Check for timeout - if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then - echo "Error: Timed out after $timeout_seconds seconds." - echo "Last status check output:" - echo "$status_output" - exit 1 - fi - - echo "Not stopped yet. Retrying in $check_interval_seconds seconds." - sleep "$check_interval_seconds" - elapsed_seconds=$((elapsed_seconds + check_interval_seconds)) -done - -echo "" - -echo "11. Return your VM" - -echo "Returning your VM requested in the first steps." - -inctl vm return "$lease_output" --org "$INTRINSIC_ORGANIZATION" - echo "---------------------------" echo "CI Journey finished" echo "---------------------------" diff --git a/tests/sbl_ci.py b/tests/sbl_ci.py index 4a5cda7..fd39a24 100644 --- a/tests/sbl_ci.py +++ b/tests/sbl_ci.py @@ -1,6 +1,7 @@ +import argparse + from intrinsic.solutions import behavior_tree as bt from intrinsic.solutions import deployments -import argparse def run_stopwatch_sequence(