Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .env.example

This file was deleted.

34 changes: 33 additions & 1 deletion .github/workflows/install-and-launch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
# - The launch smoke test uses `./launch_lucy.sh --headless <command>`, which runs a
# single command inside the container (no control panel, no auto Gazebo/RViz).

name: Install and launch
name: Install, Launch & Release

on:
workflow_dispatch:
push:
branches: [master, dev]
tags:
- 'v*' # Run on version tags like v1.0, v2.3.4
pull_request:
branches: [master, dev]

Expand Down Expand Up @@ -70,3 +72,33 @@ jobs:
# Single command in the container -> bypasses the control-panel / Gazebo / RViz auto-launch.
- name: Launch smoke test (headless, no TTY)
run: ./launch_lucy.sh --headless ros2 doctor --report

build-and-release-windows-exe:
name: Build and Release Windows Executable
# Only run this job when a new tag is pushed
if: startsWith(github.ref, 'refs/tags/')
runs-on: windows-latest
needs: install-and-launch
permissions:
contents: write # Required to create a release and upload assets
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install PyInstaller
run: pip install pyinstaller

- name: Build the executable
run: pyinstaller --onefile --name Lucy windows/Lucy.py

- name: Create Release and Upload Asset
uses: softprops/action-gh-release@v2
with:
files: dist/Lucy.exe
# This creates a draft release. Remove `draft: true` to publish it automatically.
draft: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ __pycache__/
# IDE / OS
.idea/
.vscode/
.vs/
.DS_Store
*~

Expand Down
4 changes: 4 additions & 0 deletions Dockerfile.humble
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& npm install -g yarn \
&& rm -rf /var/lib/apt/lists/*

# Setup bashrc with alias for the TUI launcher
RUN echo "alias launcher='python3 /workspace/launcher.py'" >> ~/.bashrc
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"]
182 changes: 182 additions & 0 deletions Lucy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env python3

import curses
import os
import subprocess
import sys

MIN_TERM_HEIGHT = 15
MIN_TERM_WIDTH = 65

def get_dev_mode():
if not os.path.exists(".env"):
return False
with open(".env", "r") as f:
for line in f:
if line.strip().startswith("DEV="):
return line.strip().split("=")[1].lower() == "true"
return False

def set_dev_mode(is_enabled):
lines = []
dev_found = False
if os.path.exists(".env"):
with open(".env", "r") as f:
lines = f.readlines()

with open(".env", "w") as f:
for line in lines:
if line.strip().startswith("DEV="):
f.write(f"DEV={str(is_enabled).lower()}\n")
dev_found = True
else:
f.write(line)
if not dev_found:
f.write(f"DEV={str(is_enabled).lower()}\n")

def run_command(command, interactive=False):
"""Runs a command.

If interactive is True, runs natively in the terminal.
"""
print(f"--- Running: {' '.join(command)} ---")
try:
if interactive:
# Inherit standard IO to maintain terminal size and TTY functionality
return subprocess.run(command).returncode
else:
# Popen is fine for non-interactive scripts like install/build
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
return process.poll()

except FileNotFoundError:
print(f"Error: Command '{command[0]}' not found. Make sure it's in your PATH and executable.")
return -1
except Exception as e:
print(f"An error occurred: {e}")
return -1

def main_tui(stdscr):
"""The main curses TUI function. Returns the command to run."""
h, w = stdscr.getmaxyx()
if h < MIN_TERM_HEIGHT or w < MIN_TERM_WIDTH:
return "TerminalTooSmall"

curses.curs_set(0)
stdscr.nodelay(0)
stdscr.timeout(-1)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_CYAN, -1)

is_dev_mode = get_dev_mode()
current_idx = 0
options = ["Launch", "---", "Install/Update", "Rebuild", "Exit", "---", "Developer Mode"]

while True:
stdscr.clear()
h, w = stdscr.getmaxyx()
title = "Lucy Workspace Manager"
stdscr.addstr(0, max(0, (w - len(title)) // 2), title, curses.A_BOLD)

for i, option in enumerate(options):
if option == "---":
stdscr.addstr(2 + i, 4, "----------------------")
continue

prefix = "> " if current_idx == i else " "

if option == "Developer Mode":
checkbox = "[x]" if is_dev_mode else "[ ]"
stdscr.addstr(2 + i, 4, f"{prefix}{checkbox} {option}")
else:
stdscr.addstr(2 + i, 4, f"{prefix}{option}")

stdscr.addstr(h - 2, 2, "Enter/Space: Select/Toggle | Up/Down: Navigate", curses.A_DIM)
stdscr.refresh()

key = stdscr.getch()

if key == curses.KEY_UP:
current_idx = (current_idx - 1 + len(options)) % len(options)
if options[current_idx] == "---":
current_idx = (current_idx - 1 + len(options)) % len(options)
elif key == curses.KEY_DOWN:
current_idx = (current_idx + 1) % len(options)
if options[current_idx] == "---":
current_idx = (current_idx + 1) % len(options)
elif key in [ord(' '), ord('\n')]:
selected_option = options[current_idx]

if selected_option == "Developer Mode":
is_dev_mode = not is_dev_mode
set_dev_mode(is_dev_mode)
elif selected_option == "Install/Update":
return {"cmd": ["./install.sh"], "interactive": False, "name": "Install"}
elif selected_option == "Rebuild":
return {"cmd": ["./install.sh", "--build-only"], "interactive": False, "name": "Rebuild"}
elif selected_option == "Launch":
return {"cmd": ["./launch_lucy.sh"], "interactive": True, "name": "Launch"}
elif selected_option == "Exit":
return None

if __name__ == "__main__":
# This initial check is done before curses.wrapper to provide a clean error message
# without the screen flicker of initializing and de-initializing curses.
def check_initial_size():
stdscr = curses.initscr()
h, w = stdscr.getmaxyx()
curses.endwin()
return h >= MIN_TERM_HEIGHT and w >= MIN_TERM_WIDTH

if not check_initial_size():
print("Error: Terminal window is too small.", file=sys.stderr)
print(f"Please increase the terminal size to at least {MIN_TERM_WIDTH}x{MIN_TERM_HEIGHT} characters.", file=sys.stderr)
sys.exit(1)

while True:
task = None
try:
task = curses.wrapper(main_tui)
except KeyboardInterrupt:
print("\nExiting.")
sys.exit(0)
except curses.error as e:
print(f"A terminal error occurred: {e}", file=sys.stderr)
print("This might be due to resizing the window. Please restart.", file=sys.stderr)
sys.exit(1)

if isinstance(task, str) and task == "TerminalTooSmall":
# This case is handled by the pre-check, but as a fallback.
print("Error: Terminal window is too small.", file=sys.stderr)
print(f"Please increase the terminal size to at least {MIN_TERM_WIDTH}x{MIN_TERM_HEIGHT} characters.", file=sys.stderr)
sys.exit(1)

if not task:
# User selected Exit
break

rc = run_command(task["cmd"], interactive=task.get("interactive", False))

if task.get("interactive", False):
print(f"--- Session finished with exit code {rc} ---")
break

task_name = task.get("name")
if task_name in ["Install", "Rebuild"] and rc == 0:
print(f"\n--- Task '{task_name}' finished successfully. ---")
print("Press Enter to return to the menu.")
input()
else:
print(f"\n--- Task '{task_name}' finished with exit code {rc} ---")
print("Press Enter to exit.")
input()
break

sys.exit(0)
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,63 @@

Workspace bringup for the Lucy / InMoov humanoid. Everything (ROS 2 Humble, Gazebo, RViz, the web control panel) runs inside a single Docker container — you only need **Docker**, **Git** and **Python 3** on the host (plus **`xhost`** on Linux for GUI forwarding).

## Requirements

- Python3
- Docker

## Install

> For UNIX based system:
```bash
chmod +x install.sh launch_lucy.sh
./install.sh # Linux, Intel Mac, Windows WSL, x86_64 VMs
./install.sh --arm # Apple Silicon (M1 / M2 / M3) under Docker Desktop
```

`install.sh` clones the sub-repositories listed in `config/repos.json` into `src/`, builds the Docker image, and compiles the workspace inside the container.
The installation is handled by the `Lucy.py` script. Only use the `./install.sh` script as a fallback.

Comment thread
Mael-RABOT marked this conversation as resolved.
## Launch
## Launch & Manage

```bash
./launch_lucy.sh
```
We provide a Python-based Text User Interface (TUI) to easily manage the workspace. It handles installing, rebuilding, and launching the environment.

From the repository root, run the manager for your platform:

| OS | Command |
| :--- |:-------------------------------------------------------------------|
| **Linux / macOS** | `python3 Lucy.py` |
| **Windows** | `python windows/Lucy.py` (See [Windows README](windows/README.md)) |

> The windows installation require the installation of a 3rd party software, as a Windows X Server is needed.

### Developer mode

The manager includes a **Developer Mode** toggle. When ON:
- repositories are pulled using SSH instead of HTTP
- The core & control panel aren't launch automatically

This setting is stored in a .env file

### Using the Workspace

Selecting **Launch** from the manager starts the workspace, running everything inside a single **tmux** session within the Docker container.

You will immediately see the **Lucy Control Center** TUI:
- Use **Up/Down Arrows** to navigate.
- Press **Space** to toggle a package or tool on/off.
- Press **Enter** to apply your changes. (New tools open in their own background windows).
- Press **X** to stop all processes and exit the Docker container entirely.

### Managing Tmux Windows

Because all tools (like the console or the control panel) run in background windows, you need to know a few basic `tmux` commands to navigate between them:

Starts the **control panel** in the background and runs `lucy_bringup` with Gazebo and RViz inside the container (GUI / X11 forwarded automatically when available).
- **`Ctrl+B` then `W`**: Opens a menu of all running windows. Use the arrows to select one and press Enter to switch to it.
- **`Ctrl+B` then `N`**: Go to the next window.
- **`Ctrl+B` then `P`**: Go to the previous window.
- **`Ctrl+B` then `D`**: Detach from the session (keeps the container running in the background).

Open the control panel at **http://localhost:5000/**.

## More

- [`docs/developer_lucy_packages.md`](docs/developer_lucy_packages.md) — developer guide: per-repo docs, all `install.sh` / `launch_lucy.sh` flags, dev mode, ports, environment overrides, packages overview.
- [`docs/launcher_packages.md`](docs/launcher_packages.md) — launcher guide: how to add new packages to the launcher UI and understand the configuration fields.
Loading
Loading