diff --git a/pyproject.toml b/pyproject.toml index a31be185b..3721f8663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ ignore = [ ] [tool.ruff.lint.isort] -known-first-party = ["mars_nav", "mars_control", "mars_cam", "mars_arm", "brain_client", "manipulation"] +known-first-party = ["mars_nav", "mars_control", "mars_cam", "mars_arm", "brain_client", "manipulation", "workspace"] [tool.ruff.format] quote-style = "double" diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..220abfbd2 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,14 @@ +{ + "pythonVersion": "3.10", + "pythonPlatform": "Linux", + "extraPaths": [ + "ros2_ws/src/brain/brain_client", + "ros2_ws/src/cloud/clients/proxy-client", + "ros2_ws/install/brain_client/local/lib/python3.10/dist-packages", + "ros2_ws/install/brain_messages/local/lib/python3.10/dist-packages", + "ros2_ws/install/innate_cloud_msgs/local/lib/python3.10/dist-packages", + "ros2_ws/install/mars_msgs/local/lib/python3.10/dist-packages", + "/opt/ros/humble/lib/python3.10/site-packages", + "/opt/ros/humble/local/lib/python3.10/dist-packages" + ] +} diff --git a/ros2_ws/src/brain/brain_client/brain_client/skills/catalog.py b/ros2_ws/src/brain/brain_client/brain_client/skills/catalog.py index 9c69cb555..7fa60f138 100644 --- a/ros2_ws/src/brain/brain_client/brain_client/skills/catalog.py +++ b/ros2_ws/src/brain/brain_client/brain_client/skills/catalog.py @@ -15,6 +15,7 @@ import os import re import shutil +import sys import threading import time import types @@ -269,8 +270,22 @@ def _compute_skill_id(self, path: str | Path) -> str: return f"{prefix}/{basename}" # --- reload --- + @staticmethod + def _evict_skill_lib() -> None: + """Drop cached workspace.skill_lib modules so a skills reload picks up + lib edits too — skill files re-import the lib as they load. Without + this, sys.modules keeps serving the pre-edit lib to reloaded skills. + + After a selective reload, skills NOT on the reload list keep the module + objects they imported at their own load time — two lib copies coexist. + Fine while the lib is stateless helpers; revisit if it ever holds state + shared across skills.""" + for name in [m for m in sys.modules if m.startswith("workspace.skill_lib")]: + del sys.modules[name] + def reload_all(self) -> None: self._logger.info("Reloading skills...") + self._evict_skill_lib() self._skills_directories = self._resolve_skills_directories() new_code_skills = self._load_code_skills(self._skills_directories) new_physical, new_in_training = self._load_physical_skills(self._skills_directories) @@ -291,6 +306,7 @@ def reload_selective(self, skill_ids: list[str]) -> list[str]: return list(self._code_skills.keys()) + list(self._physical_skills.keys()) self._logger.info(f"Selectively reloading skills: {skill_ids}") + self._evict_skill_lib() reloaded = [] for skill_id in skill_ids: basename = skill_id.split("/", 1)[-1] if "/" in skill_id else skill_id diff --git a/ros2_ws/src/brain/brain_client/brain_client/skills/types.py b/ros2_ws/src/brain/brain_client/brain_client/skills/types.py index d7dfe1e73..7a9e23904 100644 --- a/ros2_ws/src/brain/brain_client/brain_client/skills/types.py +++ b/ros2_ws/src/brain/brain_client/brain_client/skills/types.py @@ -208,7 +208,7 @@ def __init__(self, logger): @property @abstractmethod - def name(self): + def name(self) -> str: """ The name of the skill. Must be defined by every subclass. @@ -216,7 +216,7 @@ def name(self): pass @abstractmethod - def execute(self, *args, **kwargs): + def execute(self, *args, **kwargs) -> tuple: """ Execute the skill. @@ -389,14 +389,14 @@ def inject_interface(self, interface_type: InterfaceType, interface_instance): return True return False - def guidelines(self): + def guidelines(self) -> str | None: """ Optionally provide guidelines for this skill. Subclasses may override this method if guidelines are available. """ return None - def guidelines_when_running(self): + def guidelines_when_running(self) -> str | None: """ Optionally provide guidelines for this skill when it is running. Subclasses may override this method if guidelines are available. diff --git a/scripts/update/post_update.sh b/scripts/update/post_update.sh index 0401695d6..890b14c0a 100755 --- a/scripts/update/post_update.sh +++ b/scripts/update/post_update.sh @@ -228,6 +228,19 @@ if [ -f "$ENV_FILE" ]; then fi fi +# Enforce ownership/mode on an existing /etc/innate.env no matter how it got there: +# a hand-created file (sudo redirect/tee) ends up 600 root:root, which the non-root +# launch readers can't open — print_runtime_env.py then treats it as absent and the +# service key silently drops out of the runtime env (proxy "not configured"). +# Idempotent; matches the seeded state above. Contents are never touched. +if [ -f "$SYSTEM_ENV_FILE" ]; then + if [ "$(stat -c '%U:%G %a' "$SYSTEM_ENV_FILE")" != "root:$ACTUAL_USER 640" ]; then + chown "root:$ACTUAL_USER" "$SYSTEM_ENV_FILE" + chmod 640 "$SYSTEM_ENV_FILE" + log "Fixed $SYSTEM_ENV_FILE ownership/mode to root:$ACTUAL_USER 640 so launch readers can read the service key" + fi +fi + # ----------------------------------------------------------------------------- # 0a. Migrate user-created data into the post-refactor layout. # The refactor moved agents/skills/inputs under workspace/ and maps + nav-state diff --git a/webapp/css/app.css b/webapp/css/app.css index bf5ae931f..ef6d9a775 100644 --- a/webapp/css/app.css +++ b/webapp/css/app.css @@ -872,6 +872,96 @@ button { color: var(--accent); } +/* ---- pick overlays (Teleop) ---------------------------------------------- */ + +/* Grasp reticle over the live head video: the skill's grasp pixel, centered + on (left,top) set in JS. Lives in .video-stage, drawn above the frame but + below the glass overlays. */ +.picktune-grab { + position: absolute; + z-index: 1; + transform: translate(-50%, -50%); + color: var(--accent); + pointer-events: none; + filter: drop-shadow(0 0 2px rgb(0 0 0 / 70%)); +} + +.picktune-grab-tag { + position: absolute; + top: 50%; + left: calc(100% + 4px); + transform: translateY(-50%); + font-size: 10px; + color: inherit; + white-space: nowrap; +} + +/* Detection marker (box corners at Gemini's pixel) — green to read apart + from the amber grasp target. Its tag hangs left so the two labels don't + collide when detection and grasp target sit centimeters apart (the usual + case: they differ only by the fingertip offset). */ +.picktune-seen { + color: var(--ok); +} + +.picktune-seen .picktune-grab-tag { + left: auto; + right: calc(100% + 4px); +} + +/* The positioning goal square on the video: put the detection inside this and + the base stops. Green while the skill reports the detection inside. */ +.picktune-boxgoal { + position: absolute; + z-index: 1; + transform: translate(-50%, -50%); + border: 1.5px dashed var(--accent-dim); + border-radius: 2px; + pointer-events: none; +} + +/* Inner accept box: the point must land in THIS to stop positioning. Solid + amber guide; greens (with the tag) when the skill reports the point inside. */ +.picktune-boxaccept { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 1.5px solid var(--accent-dim); + border-radius: 2px; + pointer-events: none; +} + +.picktune-boxgoal.inside .picktune-boxaccept { + border-color: var(--ok); +} + +.picktune-boxgoal-tag { + position: absolute; + bottom: calc(100% + 3px); + left: 0; + font-size: 10px; + color: var(--accent-dim); + white-space: nowrap; +} + +.picktune-boxgoal.inside .picktune-boxgoal-tag { + color: var(--ok); +} + +/* The wrist goal box is the one interactive overlay: drag it to re-aim the + wrist servo (publishes wrist_box_u/v as a live tuning override). */ +.picktune-wristbox { + pointer-events: auto; + cursor: grab; + touch-action: none; /* pointer events own the gesture on touch screens */ +} + +.picktune-wristbox.dragging { + cursor: grabbing; + border-style: solid; +} + /* ---- record HUD (Collect page) ------------------------------------------ */ .overlay-record { diff --git a/webapp/js/constants.js b/webapp/js/constants.js index c52d6e2d1..bdae70131 100644 --- a/webapp/js/constants.js +++ b/webapp/js/constants.js @@ -132,6 +132,13 @@ export const PINNED_SKILLS = ["navigate with vision", "navigate with position", export const EXECUTE_SKILL_ACTION = "/execute_skill"; export const EXECUTE_SKILL_ACTION_TYPE = "brain_messages/action/ExecuteSkill"; +// pick_any_object stage events (std_msgs/String JSON: {ev, t, ...}) — drive +// the Teleop aim overlays. The tuning panel and draggable wrist box publish +// partial TUNABLE dicts (String JSON) back on the tuning topic; the running +// skill applies them mid-run and acks with a params event. +export const PICK_DEBUG_TOPIC = "/pick_any_object/debug"; +export const PICK_TUNING_TOPIC = "/pick_any_object/tuning"; + export const HEAD_MIN_DEG = -40; export const HEAD_MAX_DEG = 70; diff --git a/webapp/js/teleop/main.js b/webapp/js/teleop/main.js index 37d636718..8bb373456 100644 --- a/webapp/js/teleop/main.js +++ b/webapp/js/teleop/main.js @@ -23,6 +23,7 @@ import { createTtsBar } from "./ttsBar.js"; import { createTelemetry } from "./telemetry.js"; import { createArmPanel } from "./armPanel.js"; import { createProfilingPanel } from "./profilingPanel.js"; +import { createPickOverlay } from "./pickOverlay.js"; import { createSkillsMenu } from "./skillsMenu.js"; import { createCameraSwitch } from "./cameraSwitch.js"; @@ -92,6 +93,7 @@ function buildCockpit(root) { createSkillsMenu(ttsOverlay, ros), createArmPanel(armOverlay, ros, { hideServices: !!config.simControls }), ...(config.simControls ? [] : [createProfilingPanel(root, session)]), + createPickOverlay(root, ros, session), createCameraSwitch(root, session, ros), keyboard, ); diff --git a/webapp/js/teleop/pickOverlay.js b/webapp/js/teleop/pickOverlay.js new file mode 100644 index 000000000..9d5b3929e --- /dev/null +++ b/webapp/js/teleop/pickOverlay.js @@ -0,0 +1,357 @@ +// @ts-check +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Innate Inc +// Pick overlays — aim feedback for the pick_any_object skill, drawn over the +// video stage between run_start and run_end on /pick_any_object/debug. +// Head-camera overlays (grasp reticle, detection marker, pick box) only make +// sense while the "main" stream is on the stage; the wrist-align overlays only +// while "arm" is. The wrist goal box is draggable — dropping it publishes +// wrist_box_u/v on the tuning topic, so aiming the servo is literally dragging +// the target on the video. + +import { PICK_DEBUG_TOPIC, PICK_TUNING_TOPIC } from "../constants.js"; + +// Live copies of the skill's wrist-aim params, seeded with the TUNABLE +// defaults from pick_any_object.py and resynced from every run_start/params +// debug event — live tuning overrides survive in the running skill between +// runs, so the defaults alone would draw the box where the skill no longer +// aims. The head-camera pick box needs no seed and no mirrored camera model: +// the skill projects it and sends it ready to draw ([cu, cv, half, accept] +// image px, the `box` event field). +const P = { + wrist_box_u: 320, + wrist_box_v: 240, + wrist_half_px: 60, +}; + +// Fallback only — the live