diff --git a/.github/workflows/install-and-launch.yml b/.github/workflows/install-and-launch.yml index ab8acce..3fa1371 100644 --- a/.github/workflows/install-and-launch.yml +++ b/.github/workflows/install-and-launch.yml @@ -5,6 +5,10 @@ # - CI=true -> install.sh skips the host `xhost` check (no GUI on runners). # - variant=arm runs on `ubuntu-24.04-arm` with `./install.sh --arm`; the built image # is then verified to be linux/arm64 (linux/amd64 for the default variant). +# - Runner OS (22.04 or 24.04) is only the CI host. ROS Humble always runs inside +# Jammy-based images (`osrf/ros:humble-desktop` / `ros:humble-ros-base-jammy`). +# - `ubuntu-24.04-arm` is GitHub's hosted ARM64 runner label; there is no 22.04-arm +# standard runner. The container OS stays Jammy regardless. # - The launch smoke test uses `./launch_lucy.sh --headless `, which runs a # single command inside the container (no control panel, no auto Gazebo/RViz). @@ -51,6 +55,27 @@ jobs: - name: Drop workspace .env if present run: rm -f .env .env.local || true + # Docker Hub metadata/pull flakes (i/o timeout) fail `docker build` before + # any Dockerfile layer runs. Pre-pull with retries so the build reuses cache. + - name: Pre-pull base images (retry on Hub flakes) + run: | + pull_with_retry() { + image="$1" + for attempt in 1 2 3 4 5; do + if docker pull "$image"; then + return 0 + fi + echo "docker pull ${image} failed (attempt ${attempt}/5), retrying..." + sleep $((attempt * 15)) + done + return 1 + } + if [ "${{ matrix.use_arm_install }}" = true ]; then + pull_with_retry ros:humble-ros-base-jammy + else + pull_with_retry osrf/ros:humble-desktop + fi + - name: Install (clone + Docker image + rosdep + colcon + yarn) run: | chmod +x install.sh launch_lucy.sh diff --git a/.gitignore b/.gitignore index a35523c..5dfe0d2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ __pycache__/ # Launcher remembered tick selection (persisted across container restarts) .lucy_launcher_state.json -config/repos.json +# Local repo override (forks/branches; takes precedence over config/repos.json) +config/repos.json.local diff --git a/Dockerfile.humble b/Dockerfile.humble index fd8e8eb..5131bce 100644 --- a/Dockerfile.humble +++ b/Dockerfile.humble @@ -1,4 +1,4 @@ -# Lucy runtime image — ROS 2 Humble (Ubuntu 22.04 Jammy) with Gazebo Fortress, +# Lucy runtime image — ROS 2 Humble (Ubuntu 22.04 Jammy) with Gazebo Harmonic, # ros2_control, RViz, rosbridge, GStreamer and Node 22 + Yarn for the control panel. # # Built and tagged as `lucy_ros2:humble` by docker/ensure_image.sh, which @@ -53,22 +53,24 @@ RUN printf '%s\n' \ 'Acquire::Retries "5";' \ > /etc/apt/apt.conf.d/99-lucy-mirror-resilience -# Gazebo Fortress (the `gz sim` binary used by ros_gz_sim launches). +# Gazebo Harmonic (the `gz sim` binary used by ros_gz_sim launches). +# Humble officially ships Fortress; Harmonic uses OSRF packages (ros-gzharmonic). +# https://gazebosim.org/docs/harmonic/ros_installation/ RUN apt-get update && apt-get install -y --no-install-recommends --fix-missing \ curl lsb-release gnupg \ && curl -sSL https://packages.osrfoundation.org/gazebo.gpg -o /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] https://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" \ > /etc/apt/sources.list.d/gazebo-stable.list \ - && apt-get update && apt-get install -y --no-install-recommends --fix-missing ignition-fortress \ - + && apt-get update && apt-get install -y --no-install-recommends --fix-missing gz-harmonic \ && rm -rf /var/lib/apt/lists/* # ROS sim/control stack + apt prerequisites for the rosdep run in install.sh # (lucy_bringup / camera_ros / GStreamer / pytest). +# ros-humble-ros-gzharmonic* debs are amd64-only on packages.osrfoundation.org; +# arm64 builds ros_gz from source in the layer below (gazebosim/ros_gz#614). RUN apt-get update && apt-get install -y --no-install-recommends \ - ros-humble-ros-gz-sim \ - ros-humble-ros-gz-bridge \ - ros-humble-gz-ros2-control \ + git wget \ + ros-humble-simulation-interfaces \ ros-humble-ros2-control \ ros-humble-ros2-control-cmake \ ros-humble-ros2-controllers \ @@ -83,9 +85,57 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gstreamer1.0-plugins-good \ libgstreamer-plugins-good1.0-0 \ tmux \ + && wget -q https://raw.githubusercontent.com/osrf/osrf-rosdep/master/gz/00-gazebo.list \ + -O /etc/ros/rosdep/sources.list.d/00-gazebo.list \ && rosdep update \ && rm -rf /var/lib/apt/lists/* +# amd64: OSRF hosts ros-humble-ros-gzharmonic debs (not built for arm64). +RUN if [ "$(dpkg --print-architecture)" = "amd64" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + ros-humble-ros-gzharmonic \ + ros-humble-ros-gzharmonic-bridge \ + && rm -rf /var/lib/apt/lists/*; \ + fi + +# Humble+Harmonic: gz_ros2_control has no apt package on any arch; ros_gz has no +# arm64 debs — build from source (humble branch, GZ_VERSION=harmonic). +# https://control.ros.org/humble/doc/gz_ros2_control/doc/index.html +RUN bash -c 'set -e \ + && ARCH=$(dpkg --print-architecture) \ + && mkdir -p /opt/gz_ros2_control_ws/src \ + && if [ "$ARCH" = "arm64" ]; then \ + git clone --depth 1 --branch humble https://github.com/gazebosim/ros_gz.git \ + /opt/gz_ros2_control_ws/src/ros_gz; \ + fi \ + && git clone --depth 1 --branch humble https://github.com/ros-controls/gz_ros2_control.git \ + /opt/gz_ros2_control_ws/src/gz_ros2_control \ + && source /opt/ros/humble/setup.bash \ + && export GZ_VERSION=harmonic \ + && cd /opt/gz_ros2_control_ws \ + && if [ "$ARCH" = "arm64" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + libgflags-dev \ + ros-humble-actuator-msgs \ + ros-humble-gps-msgs \ + ros-humble-vision-msgs \ + && rm -rf /var/lib/apt/lists/* \ + && rosdep install -r --from-paths \ + src/ros_gz/ros_gz_interfaces \ + src/ros_gz/ros_gz_bridge \ + src/ros_gz/ros_gz_sim \ + src/gz_ros2_control \ + --ignore-src --rosdistro humble -y \ + && colcon build --parallel-workers=1 \ + --packages-select ros_gz_interfaces ros_gz_bridge ros_gz_sim gz_ros2_control; \ + else \ + rosdep install -r --from-paths src --ignore-src --rosdistro humble -y \ + --skip-keys="ros_gz_bridge ros_gz_sim gz_ros2_control_demos" \ + && colcon build --packages-select gz_ros2_control; \ + fi \ + && rm -rf /opt/gz_ros2_control_ws/build /opt/gz_ros2_control_ws/log \ + /opt/gz_ros2_control_ws/src' + # Node 22 + Yarn — the lucy_control_panel Vite dev server runs inside this image. RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ @@ -116,4 +166,4 @@ RUN echo "alias exit=''" >> ~/.bashrc # Disable 'exit' to force launcher usage WORKDIR /workspace ENTRYPOINT ["/bin/bash"] -CMD ["-c", "source /opt/ros/humble/setup.bash && exec /bin/bash"] +CMD ["-c", "source /opt/ros/humble/setup.bash && source /opt/gz_ros2_control_ws/install/setup.bash && exec /bin/bash"] diff --git a/config/launcher_config.json b/config/launcher_config.json index da40d3c..8a013a8 100644 --- a/config/launcher_config.json +++ b/config/launcher_config.json @@ -12,6 +12,17 @@ "readiness_timeout": 60, "default_on": false }, + { + "id": "robot_inmoov", + "name": "Robot: InMoov", + "description": "(robot_package:=inmoov_urdf)", + "type": "modifier", + "dependencies": ["core"], + "conflicts": ["robot_thais"], + "command": "robot_package:=inmoov_urdf", + "requires_pkg": "inmoov_urdf", + "default_on": true + }, { "id": "gazebo", "name": "... with Simulator", @@ -25,6 +36,17 @@ "readiness_timeout": 120, "default_on": false }, + { + "id": "headless", + "name": "... headless", + "description": "(server-only, no GUI)", + "type": "modifier", + "dependencies": ["gazebo"], + "conflicts": [], + "command": "headless:=true", + "subitem": true, + "default_on": false + }, { "id": "rviz", "name": "... with Visualizer", diff --git a/config/repos.json b/config/repos.json index 77c3326..72a0047 100644 --- a/config/repos.json +++ b/config/repos.json @@ -1,20 +1,20 @@ { "repos": [ { - "name": "thais_urdf", - "branch": "dev", - "url_https": "https://github.com/Sentience-Robotics/thais_urdf.git", - "url_ssh": "git@github.com:Sentience-Robotics/thais_urdf.git" + "name": "inmoov_urdf", + "branch": "master", + "url_https": "https://github.com/Sentience-Robotics/inmoov_urdf.git", + "url_ssh": "git@github.com:Sentience-Robotics/inmoov_urdf.git" }, { "name": "lucy_ros_packages", - "branch": "dev", + "branch": "master", "url_https": "https://github.com/Sentience-Robotics/lucy_ros_packages.git", "url_ssh": "git@github.com:Sentience-Robotics/lucy_ros_packages.git" }, { "name": "lucy_control_panel", - "branch": "dev", + "branch": "master", "url_https": "https://github.com/Sentience-Robotics/lucy_control_panel.git", "url_ssh": "git@github.com:Sentience-Robotics/lucy_control_panel.git" } diff --git a/docs/developer_lucy_packages.md b/docs/developer_lucy_packages.md index 9fa360d..40407d5 100644 --- a/docs/developer_lucy_packages.md +++ b/docs/developer_lucy_packages.md @@ -44,6 +44,27 @@ Docker Desktop on Apple Silicon defaults to `linux/amd64` ROS images and runs th `config/repos.json` carries both `url_https` (default) and `url_ssh` for each repo. To clone over SSH, copy `.env.example` to `.env` and set `DEV=true` before running `install.sh`. SSH keys must be configured for the relevant host. +### Local repo overrides (`config/repos.json.local`) + +To point a repo at your own fork or a feature branch without editing the tracked `config/repos.json`, create **`config/repos.json.local`**. When present it is used instead of `repos.json` by both `install.sh` and the launcher (`windows/Lucy.py`), and it is gitignored so overrides are never committed. + +Use the same structure as `repos.json` — list only the repos you want to override (or all of them). Each entry needs `name` (the folder under `src/`), `branch`, and both `url_https` and `url_ssh` (Developer Mode selects SSH, otherwise HTTPS): + +```json +{ + "repos": [ + { + "name": "inmoov_urdf", + "branch": "my-feature-branch", + "url_https": "https://github.com/your-user/inmoov_urdf.git", + "url_ssh": "git@github.com:your-user/inmoov_urdf.git" + } + ] +} +``` + +Delete the file to fall back to the tracked `repos.json`. + ## `launch_lucy.sh` Builds the Docker image if needed, mounts the workspace at `/workspace`, sources the built ROS overlay, then: diff --git a/install.sh b/install.sh index a55d12a..164c82c 100755 --- a/install.sh +++ b/install.sh @@ -16,6 +16,7 @@ # combine with any other flag, e.g. --arm --build-only # # Optional .env (copy from .env.example): DEV=true selects `url_ssh` in repos.json (default: `url_https`). +# Optional config/repos.json.local (gitignored): overrides config/repos.json to point repos at forks/branches. set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -32,7 +33,14 @@ fi IMAGE_NAME="lucy_ros2:humble" DOCKERFILE_PATH="$SCRIPT_DIR/Dockerfile.humble" WORKSPACE="/workspace" +# config/repos.json.local (gitignored) overrides the tracked config/repos.json, +# so contributors can point repos at their own forks/branches without touching +# the committed file. Falls back to repos.json when no local override exists. CONFIG_FILE="${SCRIPT_DIR}/config/repos.json" +if [ -f "${SCRIPT_DIR}/config/repos.json.local" ]; then + CONFIG_FILE="${SCRIPT_DIR}/config/repos.json.local" + echo "install.sh: using local repo override config/repos.json.local" +fi # shellcheck disable=SC1091 source "$SCRIPT_DIR/docker/ensure_image.sh" @@ -146,6 +154,7 @@ docker_workspace_install() { local inner_cmd read -r -d '' inner_cmd <<'EOS' || true source /opt/ros/humble/setup.bash \ + && [ -f /opt/gz_ros2_control_ws/install/setup.bash ] && source /opt/gz_ros2_control_ws/install/setup.bash \ && cd /workspace \ && rosdep install --from-paths src --ignore-src -r -y --skip-keys="audio_common" \ && rm -rf build/camera_ros install/camera_ros \ diff --git a/launch_lucy.sh b/launch_lucy.sh index 09dc305..2584acf 100755 --- a/launch_lucy.sh +++ b/launch_lucy.sh @@ -194,7 +194,7 @@ DOCKER_PORT_ARGS=( # Container scripts # ---------------------------------------------------------------------------- -SETUP="source /opt/ros/humble/setup.bash" +SETUP="source /opt/ros/humble/setup.bash && [ -f /opt/gz_ros2_control_ws/install/setup.bash ] && source /opt/gz_ros2_control_ws/install/setup.bash" SOURCE_WORKSPACE="cd $WORKSPACE && source install/setup.bash" LAUNCH_GAZEBO_RVIZ_BRIDGE_CP="ros2 launch lucy_bringup lucy.launch.py gazebo:=true rviz:=true" LAUNCH_RVIZ_BRIDGE_CP="ros2 launch lucy_bringup lucy.launch.py rviz:=true" @@ -203,6 +203,7 @@ LAUNCH_RVIZ_BRIDGE_CP="ros2 launch lucy_bringup lucy.launch.py rviz:=true" read -r -d '' CONTAINER_PREAMBLE <<'EOS' || true set -e source /opt/ros/humble/setup.bash +[ -f /opt/gz_ros2_control_ws/install/setup.bash ] && source /opt/gz_ros2_control_ws/install/setup.bash cd /workspace if [[ ! -f install/setup.bash ]]; then echo "Workspace not built. Run Install/Update via Lucy.py" >&2 diff --git a/launcher.py b/launcher.py index 60a24bd..b36ef44 100644 --- a/launcher.py +++ b/launcher.py @@ -125,6 +125,13 @@ def __init__(self, data, running_modifiers): self.command = data.get('command', '') self.lifecycle_hooks = data.get('lifecycle_hooks', {}) self.selected = data.get('default_on', False) + # Robot-description package this entry selects (e.g. inmoov_urdf). When set, + # the entry is hidden unless that package is built, so only installed robots + # appear in the mutually-exclusive selector. + self.requires_pkg = data.get('requires_pkg') + # Render with a deeper indent so it reads as a sub-option of its dependency + # (e.g. headless under "... with Simulator"). + self.subitem = data.get('subitem', False) # Optional shell probe that exits 0 only once the package is truly up. # Without it, a package is considered "ready" the instant its window exists. self.readiness_check = data.get('readiness_check') @@ -145,9 +152,12 @@ def __init__(self, data, running_modifiers): self.pane_dead = False self.update_running_status(running_modifiers) + # Robot-package radios are mutually exclusive + if self.type == 'modifier' and self.requires_pkg: + self.selected = self.is_running # Reflect running state as ticked — but not while it is being stopped, so an # in-progress shutdown doesn't re-check the box the user just unticked. - if self.is_running and self.id not in _pkg_stop_times: + elif self.is_running and self.id not in _pkg_stop_times: self.selected = True def update_running_status(self, running_modifiers): @@ -190,11 +200,23 @@ def _env_enabled(var_name): return True return os.environ.get(var_name, "").strip().lower() in ("1", "true", "yes") +def _ros_pkg_installed(pkg_name): + """True when a ROS package is built in the workspace overlay (install/). + + Used to gate the robot-package selector entries so only robots that are + actually built show up — mirrors lucy.launch.py's runtime discovery.""" + if not pkg_name: + return True + return os.path.isdir(os.path.join("/workspace", "install", pkg_name)) + def _pkg_visible(pkg_config, dev_mode): """Whether a package appears in the launcher: hidden when it is `dev_only` and - Developer Mode is off, or when its `requires_env` gate isn't satisfied.""" + Developer Mode is off, when its `requires_env` gate isn't satisfied, or when a + `requires_pkg` robot package isn't built.""" if pkg_config.get('dev_only') and not dev_mode: return False + if not _ros_pkg_installed(pkg_config.get('requires_pkg')): + return False return _env_enabled(pkg_config.get('requires_env')) class LauncherState: @@ -220,25 +242,44 @@ def refresh_status(self): for pkg in self.packages: pkg.update_running_status(running_state['modifiers']) + def _enable(self, pkg): + """Tick a package, clearing anything it conflicts with first.""" + for conflict_id in pkg.conflicts: + conflict_pkg = self.get_by_id(conflict_id) + if conflict_pkg and conflict_pkg.selected: + conflict_pkg.selected = False + pkg.selected = True + + def _enable_with_deps(self, pkg): + """Tick a package and any of its (transitive) dependencies that are off, so a + sub-option pulls in its parent (e.g. headless ticks the simulator).""" + for dep_id in pkg.dependencies: + dep = self.get_by_id(dep_id) + if dep and not dep.selected: + self._enable_with_deps(dep) + self._enable(pkg) + + def _disable_with_dependents(self, pkg): + """Untick a package and any (transitive) dependents, so turning off a parent + also turns off its sub-options (e.g. unticking core drops the simulator and + headless together rather than leaving them orphaned).""" + for other_pkg in self.packages: + if pkg.id in other_pkg.dependencies and other_pkg.selected: + self._disable_with_dependents(other_pkg) + pkg.selected = False + def toggle(self, pkg_id): pkg = self.get_by_id(pkg_id) if not pkg: return None if pkg_id in _pkg_stop_times and not pkg.selected: return "Still stopping…" if not pkg.selected: - missing_deps = [dep for dep in pkg.dependencies if not self.get_by_id(dep).selected] - if missing_deps: - return f"Needs: {', '.join(missing_deps)}" - for conflict_id in pkg.conflicts: - conflict_pkg = self.get_by_id(conflict_id) - if conflict_pkg and conflict_pkg.selected: - conflict_pkg.selected = False - pkg.selected = True + # Ticking any option auto-enables its (transitive) dependencies instead + # of being blocked — e.g. the simulator/RViz/a robot pulls in core, and + # headless pulls in the simulator. + self._enable_with_deps(pkg) else: - for other_pkg in self.packages: - if pkg_id in other_pkg.dependencies and other_pkg.selected: - other_pkg.selected = False - pkg.selected = False + self._disable_with_dependents(pkg) return None def get_pkg_status(pkg): @@ -367,43 +408,47 @@ def draw_tui(stdscr, state, current_idx, error_msg, status_msg, unapplied=False) elif unapplied: stdscr.addstr(h - 2, 2, "Unapplied changes — press Enter to apply", curses.color_pair(1)) - cores_and_mods = [p for p in state.packages if p.type in ['core', 'modifier']] + # Robot-package selectors are modifiers (their command is appended to the core + # launch), but get their own section so the robot choice reads as a distinct + # group rather than another core toggle. + robots = [p for p in state.packages if p.type == 'modifier' and p.requires_pkg] + cores_and_mods = [p for p in state.packages if p.type in ['core', 'modifier'] and not p.requires_pkg] interfaces = [p for p in state.packages if p.type == 'interface'] tools = [p for p in state.packages if p.type == 'tool'] - display_list = cores_and_mods + interfaces + tools + display_list = cores_and_mods + robots + interfaces + tools + + def draw_section(title, color, items, offset, gap=1, indent_all=False): + nonlocal row + stdscr.addstr(row, 2, title, curses.A_BOLD | color) + row += gap + for i, p in enumerate(items): + list_idx = offset + i + prefix = "> " if current_idx == list_idx else " " + checkbox = "[x]" if p.selected else "[ ]" + can_enable = all(state.get_by_id(dep).selected for dep in p.dependencies) + attr = curses.A_NORMAL if can_enable else curses.A_DIM + if p.type == 'core': + attr |= curses.A_BOLD + if p.subitem: + indent = " " + elif indent_all or p.type == 'modifier': + indent = " " + else: + indent = "" + status = get_pkg_status(p) + _draw_pkg_row(stdscr, row + i, 4, prefix, indent, checkbox, p.name, attr, + status, _vnc_hint(p, state), _status_url(p)) + row += len(items) + 1 row = 2 - stdscr.addstr(row, 2, "Primary Launch Targets", curses.A_BOLD | curses.color_pair(1)) - row += 2 - for i, p in enumerate(cores_and_mods): - prefix = "> " if current_idx == i else " " - checkbox = "[x]" if p.selected else "[ ]" - can_enable = all(state.get_by_id(dep).selected for dep in p.dependencies) - attr = curses.A_NORMAL if can_enable else curses.A_DIM - if p.type == 'core': attr |= curses.A_BOLD - indent = " " if p.type == 'modifier' else "" - status = get_pkg_status(p) - _draw_pkg_row(stdscr, row + i, 4, prefix, indent, checkbox, p.name, attr, status, _vnc_hint(p, state), _status_url(p)) - - row += len(cores_and_mods) + 1 - stdscr.addstr(row, 2, "Interfaces", curses.A_BOLD | curses.color_pair(3)) - row += 1 - for i, p in enumerate(interfaces): - list_idx = i + len(cores_and_mods) - prefix = "> " if current_idx == list_idx else " " - checkbox = "[x]" if p.selected else "[ ]" - status = get_pkg_status(p) - _draw_pkg_row(stdscr, row + i, 4, prefix, "", checkbox, p.name, curses.A_NORMAL, status, _vnc_hint(p, state), _status_url(p)) - - row += len(interfaces) + 1 - stdscr.addstr(row, 2, "Tools", curses.A_BOLD | curses.color_pair(3)) - row += 1 - for i, p in enumerate(tools): - list_idx = i + len(cores_and_mods) + len(interfaces) - prefix = "> " if current_idx == list_idx else " " - checkbox = "[x]" if p.selected else "[ ]" - status = get_pkg_status(p) - _draw_pkg_row(stdscr, row + i, 4, prefix, "", checkbox, p.name, curses.A_NORMAL, status, _vnc_hint(p, state), _status_url(p)) + draw_section("Primary Launch Targets", curses.color_pair(1), cores_and_mods, 0, gap=2) + offset = len(cores_and_mods) + if robots: + draw_section("Robot", curses.color_pair(1), robots, offset, gap=1, indent_all=True) + offset += len(robots) + draw_section("Interfaces", curses.color_pair(3), interfaces, offset, gap=1) + offset += len(interfaces) + draw_section("Tools", curses.color_pair(3), tools, offset, gap=1) stdscr.refresh() return display_list @@ -547,8 +592,35 @@ def restore_selection(state): saved = load_selection() if saved is None: return + robots = [p for p in state.packages if p.requires_pkg] for pkg in state.packages: + if pkg.requires_pkg: + continue pkg.selected = (pkg.id in saved) or pkg.is_running + # Robot radios: at most one ticked (saved choice wins over stale is_running). + chosen = next((p for p in robots if p.id in saved), None) + if chosen is None: + running = [p for p in robots if p.is_running] + chosen = running[0] if len(running) == 1 else None + for pkg in robots: + pkg.selected = pkg is chosen + +def default_robot_selection(state): + """Auto-tick a robot-package modifier when none is selected yet (mirrors + lucy.launch.py: sole installed robot, or inmoov_urdf when several are built). + Gated on core being selected so it can be applied alongside core.""" + core = state.get_by_id('core') + robots = [p for p in state.packages if p.requires_pkg] + if not robots or not (core and core.selected): + return + if any(p.selected for p in robots): + return + if len(robots) == 1: + robots[0].selected = True + return + inmoov = state.get_by_id('robot_inmoov') + if inmoov: + inmoov.selected = True def main(stdscr): curses.curs_set(0) @@ -565,6 +637,7 @@ def main(stdscr): state = LauncherState(load_config()) restore_selection(state) + default_robot_selection(state) current_idx = 0 error_msg = None status_msg = None @@ -579,9 +652,11 @@ def main(stdscr): core_pkg.selected = True if lcp_pkg: lcp_pkg.selected = True + default_robot_selection(state) apply_changes(state) status_msg = "Starting default services for production mode..." state = LauncherState(load_config()) + restore_selection(state) while True: try: @@ -636,6 +711,7 @@ def main(stdscr): status_msg = "Configuration Applied!" status_msg_until = time.time() + 2.0 state = LauncherState(load_config()) + restore_selection(state) elif key in [ord('x'), ord('X')]: h, w = stdscr.getmaxyx() stdscr.addstr(h - 2, 2, "Stop all processes and exit Docker? (y/n)", curses.A_BOLD | curses.color_pair(2)) diff --git a/windows/Lucy.py b/windows/Lucy.py index 3ac6da0..0955d8e 100644 --- a/windows/Lucy.py +++ b/windows/Lucy.py @@ -29,7 +29,12 @@ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ENV_FILE = os.path.join(PROJECT_ROOT, ".env") -REPOS_FILE = os.path.join(PROJECT_ROOT, "config", "repos.json") +# config/repos.json.local (gitignored) overrides the tracked config/repos.json +# so contributors can point repos at their own forks/branches. Falls back to +# repos.json when no local override exists (mirrors install.sh). +_REPOS_FILE_DEFAULT = os.path.join(PROJECT_ROOT, "config", "repos.json") +_REPOS_FILE_LOCAL = os.path.join(PROJECT_ROOT, "config", "repos.json.local") +REPOS_FILE = _REPOS_FILE_LOCAL if os.path.exists(_REPOS_FILE_LOCAL) else _REPOS_FILE_DEFAULT DOCKERFILE = os.path.join(PROJECT_ROOT, "Dockerfile.humble") IMAGE_NAME = "lucy_ros2:humble" WORKSPACE_DIR_HOST = PROJECT_ROOT @@ -136,6 +141,7 @@ def build_workspace(): print("Building workspace inside the container...") inner_cmd = ( 'source /opt/ros/humble/setup.bash && ' + '[ -f /opt/gz_ros2_control_ws/install/setup.bash ] && source /opt/gz_ros2_control_ws/install/setup.bash; ' 'cd /workspace && ' 'rosdep install --from-paths src --ignore-src -r -y --skip-keys="audio_common" && ' 'colcon build --symlink-install && ' @@ -225,6 +231,7 @@ def launch_workspace(): container_script = ( "source /opt/ros/humble/setup.bash && " + "[ -f /opt/gz_ros2_control_ws/install/setup.bash ] && source /opt/gz_ros2_control_ws/install/setup.bash; " "cd /workspace && source install/setup.bash && " "tmux start-server && " "if ! tmux has-session -t lucy_ws 2>/dev/null; then "