# rmscript - Kid-Friendly Robot Programming rmscript is a natural language-inspired programming language designed to make robot programming accessible and fun for children. It compiles to Python code that controls the Reachy Mini robot. ## Example Usage Create a file `hello.rmscript`: ```rmscript DESCRIPTION Wave hello to someone antenna up wait 1s antenna down look left look right look center ``` Test the script using the provided runner (after starting the reachy-mini-daemon): ``` python src/reachy_mini_conversation_app/rmscript/run_rmscript.py path/to/hello.rmscript ``` ## Language Syntax ### File Structure Every rmscript file has a simple structure: ```rmscript DESCRIPTION Wave hello to someone # Your commands here look left wait 1s ``` **Key features:** - **Tool name**: Automatically derived from the filename (e.g., `wave_hello.rmscript` → tool name is `wave_hello`) - **DESCRIPTION** (optional): One-line description used for LLM tool registration ### Basic Commands ```rmscript # Comments start with # # Movement commands look left turn right antenna up head forward 10 # Wait command wait 2s wait 0.5s # Camera command picture # Sound playback play mysound play othersound pause ``` ### Case Insensitivity rmscript is case-insensitive for keywords: ```rmscript LOOK left # Same as "look left" Look Left # Same as "look left" WAIT 1s # Same as "wait 1s" ``` ## Movement Commands ### Look (Head Orientation) Control the robot's head orientation (pitch and yaw): ```rmscript look left # Turn head left (30° default) look right 45 # Turn head right 45° look up # Tilt head up (30° default) look down 20 # Tilt head down 20° look center # Return to center position # Synonyms look straight # Same as center look neutral # Same as center ``` **Physical Limits:** - Pitch (up/down): ±40° - Yaw (left/right): ±180° absolute, ±65° relative to body ### Turn (Body Rotation) Rotate the robot's body (the head rotates together with the body): ```rmscript turn left # Rotate body and head left (30° default) turn right 90 # Rotate body and head right 90° turn center # Face forward ``` **Note:** The `turn` command rotates both the body yaw and the head yaw together, since the body carries the head. **Physical Limits:** - Body yaw: ±160° (safe range) ### Antenna Control the antenna positions using multiple syntaxes: **Clock Position (Numeric 0-12):** ```rmscript antenna both 0 # 0 o'clock = 0° (straight up) antenna both 3 # 3 o'clock = 90° (external/right) antenna both 6 # 6 o'clock = 180° (straight down) antenna both 9 # 9 o'clock = -90° (internal/left) antenna left 4.5 # Left antenna to 4.5 o'clock (135°) ``` **Clock Keywords:** ```rmscript antenna both high # 0° (high position) antenna both ext # 90° (external) antenna both low # 180° (low position) antenna both int # -90° (internal) ``` **Directional Keywords (Natural Language):** ```rmscript antenna both up # 0° (up) antenna both right # 90° (right/external) antenna both down # 180° (down) antenna both left # -90° (left/internal) # Individual antenna control antenna left up # Left antenna pointing up antenna right down # Right antenna pointing down # Even in potentially confusing cases, it works naturally: antenna left left # Left antenna pointing left (-90°) antenna right right # Right antenna pointing right (90°) ``` **Physical Limits:** - Antenna angle: ±180° (safe recommended: ±120°) ### Head Translation Move the head forward/back/left/right/up/down in space: ```rmscript head forward 10 # Move head forward 10mm head back 5 # Move head back 5mm head backward 5 # Same as "back" (synonym) head backwards 5 # Same as "back" (synonym) head left 8 # Move head left 8mm head right 8 # Move head right 8mm head up 5 # Move head up 5mm head down 3 # Move head down 3mm ``` **Physical Limits:** - X (forward/back): ±50mm typical - Y (left/right): ±50mm typical - Z (up/down): +20mm / -30mm typical ### Tilt (Head Roll) Tilt the head side-to-side: ```rmscript tilt left 15 # Tilt head left tilt right 15 # Tilt head right tilt center # Return to level ``` **Physical Limits:** - Roll: ±40° ### Wait Pause between movements: ```rmscript wait 1s # Wait 1 second wait 0.5s # Wait 0.5 seconds wait 2.5s # Wait 2.5 seconds ``` **Important:** The `s` suffix is **required** for consistency. `wait 1` will produce a compilation error. ### Picture Take a picture with the robot's camera and add it to the conversation: ```rmscript picture # Take a picture ``` The picture command captures a frame from the camera and returns it as a base64-encoded image. **How it works:** 1. **During execution**: The `picture` command is queued as a movement (instant duration: 0.01s) 2. **Capture timing**: Picture is captured at the right moment in the movement sequence 3. **File storage**: Pictures are automatically saved to `/tmp/reachy_picture_YYYYMMDD_HHMMSS_microseconds.jpg` 4. **Color handling**: Images are captured in RGB format with proper color conversion 5. **LLM integration**: When called by the LLM, pictures are automatically added to the conversation **Single picture (LLM-compatible):** ```rmscript DESCRIPTION Check behind by taking a picture turn left maximum wait 0.5s picture wait 0.5s turn center ``` This script: - Turns the robot to look behind (120° left) - Waits for movement to stabilize - Captures a picture at the right moment - Returns to center position - The LLM receives the picture and can describe what it sees **Important timing notes:** - Add `wait` commands before `picture` to let movements complete - The tool automatically waits for all movements + picture capture before returning - Pictures are captured when the movement queue reaches them (not immediately) **Multiple pictures:** ```rmscript DESCRIPTION Look around and take pictures look left wait 0.5s picture look right wait 0.5s picture look center ``` Each `picture` command captures a separate image. For single-picture scripts, the image is returned in Camera-compatible format (`b64_im`) for LLM integration. For multi-picture scripts, images are returned as an array. **Use with `run_rmscript.py`:** When testing scripts with `run_rmscript.py`, pictures are: - Saved to `/tmp` with timestamped filenames - Logged with full paths for easy access - Displayed in the execution summary **Use with LLM conversation:** When called as a tool by the LLM: - Single picture: Automatically fed to the LLM conversation (like Camera tool) - Multiple pictures: Available as base64 data (future enhancement for multi-image display) - Gradio UI: Picture thumbnails displayed in the conversation interface **Example conversation:** ``` User: "Can you check what's behind you?" → LLM calls: check_behind() → Robot: turns, captures picture, returns to center → LLM sees the picture and responds: "I can see a desk with a laptop and some books!" ``` ### Play Sound Play sound files (.wav, .mp3, .ogg, .flac) during script execution: ```rmscript play soundname # Play sound in background (async, continues immediately) play soundname pause # Wait for sound to finish before continuing (blocking) play soundname 5s # Play sound for exactly 5 seconds (blocking) ``` The script automatically searches for sound files in this order: 1. **Directory containing the .rmscript file** (highest priority - co-located sounds) 2. Current working directory 3. `sounds/` subdirectory **Asynchronous playback (default):** ```rmscript play intro # Starts playing, script continues immediately look left wait 1s ``` The sound plays in the background while movements execute. **Blocking playback (wait for sound to finish):** ```rmscript play mysound pause # Waits for sound to finish # OR use synonyms: play mysound fully play mysound wait play mysound block play mysound complete ``` The script pauses until the sound finishes playing, then continues. **Duration-limited playback:** ```rmscript play mysound 10s # Play sound for exactly 10 seconds (blocks for 10s) ``` Plays the sound for the specified duration. If the sound is shorter, it will finish early. If longer, it will be cut off. ### Loop Sound Loop a sound file repeatedly for a specified duration: ```rmscript loop soundname # Loop sound for 10 seconds (default) loop soundname 5s # Loop sound for 5 seconds loop soundname 30s # Loop sound for 30 seconds ``` The loop command automatically repeats the sound until the duration expires. This is useful for background music or ambient sounds. **Example use case:** ```rmscript DESCRIPTION Greet with sound and movement play hello pause # Play greeting sound fully wait 0.5s antenna both up # Wave antennas wait 1s antenna both down ``` **Sound files:** - File extensions: `.wav`, `.mp3`, `.ogg`, `.flac` - Example: `play hello` looks for `hello.wav`, `hello.mp3`, etc. - **Best practice**: Place sound files in the same directory as your .rmscript file - Alternative locations: working directory or `sounds/` subdirectory ## Advanced Features ### Keyword Reuse with `and` Chain multiple directions with the same action keyword: ```rmscript # Reuse "look" keyword look left and up 25 # → Equivalent to: look left + look up 25 # → Merged into single command # Different keywords - no reuse turn left and look right # → turn left + look right # → Two separate commands merged ``` This creates more natural, flowing descriptions while optimizing execution. ### Qualitative Strength Use descriptive words instead of numbers - works for both angles and distances: ```rmscript # 5 levels of strength - values are context-aware! # Examples: turn left tiny # 10° (body yaw can handle larger movements) look up tiny # 5° (head pitch limited by cone constraint) head forward tiny # 2mm turn left maximum # 120° (body yaw safe maximum) look up maximum # 38° (respects ±40° pitch limit) look left maximum # 60° (respects ±65° body-head differential) head forward maximum # 28mm (under 30mm limit) ``` **Available Qualitative Keywords:** - **VERY_SMALL**: minuscule, mini, verysmall, tiny - **SMALL**: little, slightly, small, alittle - **MEDIUM**: medium, normal, regular, standard, normally - **LARGE**: lot, big, large, very, alot, huge, strong, strongly - **VERY_LARGE**: verybig, enormous, verylarge, maximum **Context-Aware Values:** The same qualitative keyword maps to different values depending on the movement type, respecting physical limits: | Keyword | VERY_SMALL | SMALL | MEDIUM | LARGE | VERY_LARGE | |---------|------------|-------|--------|-------|------------| | **Body Yaw (turn)** | 10° | 30° | 60° | 90° | 120° | | **Head Pitch/Roll (look up/down, tilt)** | 5° | 10° | 20° | 30° | 38° | | **Head Yaw (look left/right)** | 5° | 15° | 30° | 45° | 60° | | **Head Translation (head forward/back/etc)** | 2mm | 5mm | 10mm | 20mm | 28mm | | **Antennas** | 10° | 30° | 60° | 90° | 110° | **Note:** These values are carefully chosen to stay within the robot's physical limits while maximizing safe range of motion. ### Duration Keywords Use descriptive speed words: ```rmscript look left superfast # 0.2 seconds look right fast # 0.5 seconds look up slow # 2.0 seconds look up slowly # 2.0 seconds (synonym for slow) look down superslow # 3.0 seconds ``` Combine with 'and': ```rmscript turn left and look right fast # Both movements complete in 0.5 seconds ``` ### Repeat Blocks Repeat a sequence of commands: ```rmscript repeat 3 look left wait 0.5s look right wait 0.5s # Nested repeat blocks work too repeat 2 antenna up repeat 3 look left look right antenna down ``` **Indentation:** Use spaces or tabs consistently (tabs = 4 spaces). ### Combining Movements Combine multiple movements into a single smooth motion: ```rmscript # All happen simultaneously antenna both up and look up 25 and turn left 30 ``` This merges into a single `goto_target()` call with all parameters. **Important:** The `and` keyword can only combine movement commands (turn, look, head, tilt, antenna). You **cannot** combine movements with control commands (picture, play, loop, wait) using `and`. Use separate lines instead: ```rmscript # ❌ ERROR - Cannot combine movement with picture look left and picture # ✓ CORRECT - Use separate lines look left wait 0.5s picture # ❌ ERROR - Cannot combine movement with play turn right and play mysound # ✓ CORRECT - Use separate lines turn right play mysound ``` ## Compiler Architecture ### Overview The compiler uses a 5-stage pipeline: ``` Source Code → Lexer → Parser → Semantic Analyzer → Optimizer → Code Generator ``` ### Stage 1: Lexer (Tokenization) Converts source text into tokens with indentation tracking: ```python from reachy_mini.rmscript.lexer import Lexer source = "look left\nwait 1s" lexer = Lexer(source) tokens = lexer.tokenize() # → [Token(KEYWORD_LOOK, 'look', L1:C1), Token(DIRECTION, 'left', L1:C6), ...] ``` Features: - Python-like INDENT/DEDENT tokens - Case-insensitive keyword matching - Comment stripping - Line/column tracking for error messages ### Stage 2: Parser (AST Construction) Builds an Abstract Syntax Tree from tokens: ```python from reachy_mini.rmscript.parser import Parser ast = Parser(tokens).parse() # → Program(tool_name='...', description='...', statements=[...]) ``` Features: - Keyword reuse detection for `and` chains - Direction validation per keyword - Repeat block nesting - Syntax error reporting ### Stage 3: Semantic Analyzer Applies defaults, validates ranges, and generates IR: ```python from reachy_mini.rmscript.semantic import SemanticAnalyzer ir, errors, warnings = SemanticAnalyzer().analyze(ast) # → List[Action | WaitAction], List[CompilationError], List[CompilationError] ``` Features: - Default value resolution (30° for angles, 1s for duration) - Qualitative → quantitative conversion - Duration keyword mapping - Physical limit validation - Action merging (multiple SingleActions → one Action) ### Stage 4: Optimizer Optimizes the IR: ```python from reachy_mini.rmscript.optimizer import Optimizer optimized_ir = Optimizer().optimize(ir) ``` Features: - Consecutive wait merging - No-op action removal ### Stage 5: Code Generator Creates executable Python functions: ```python from reachy_mini.rmscript.codegen import CodeGenerator executable = CodeGenerator().generate(tool_name, description, ir) # → Callable that executes the behavior ``` Features: - Direct execution: `executable(mini)` - Readable code generation: `to_python_code()` - Proper imports and function signatures ## API Reference ### rmscriptCompiler Main compiler class. ```python from reachy_mini.rmscript import rmscriptCompiler compiler = rmscriptCompiler(log_level="INFO") tool = compiler.compile(source_code) ``` **Parameters:** - `log_level` (str): Logging level - "DEBUG", "INFO", "WARNING", "ERROR" **Returns:** - `CompiledTool`: Compilation result object ### verify_rmscript Verification function that checks if rmscript code compiles without generating executable code. ```python from reachy_mini_conversation_app.rmscript import verify_rmscript is_valid, errors = verify_rmscript(source_code) if not is_valid: for error in errors: print(error) ``` **Parameters:** - `source_code` (str): rmscript source code to verify **Returns:** - `tuple[bool, list[str]]`: Tuple of (is_valid, error_messages) - `is_valid`: True if compilation succeeds, False otherwise - `error_messages`: List of error and warning messages (empty if valid) **Example:** ```python script = """ DESCRIPTION Test script look left and picture """ is_valid, errors = verify_rmscript(script) # is_valid = False # errors = ["❌ Line 3: Cannot combine movement with 'picture' using 'and'. Use separate lines instead."] ``` ### CompiledTool Result of compilation. ```python class CompiledTool: name: str # Tool name description: str # Tool description executable: Callable # Function to execute on robot success: bool # Compilation succeeded? errors: List[CompilationError] # Compilation errors warnings: List[CompilationError] # Compilation warnings source_code: str # Original source ir: List[Action | WaitAction] # Intermediate representation def execute(self, mini: ReachyMini) -> None: """Execute the compiled behavior on the robot.""" def to_python_code(self) -> str: """Generate readable Python code.""" ``` **Usage:** ```python if tool.success: # Execute on robot with ReachyMini() as mini: tool.execute(mini) # Or get Python code print(tool.to_python_code()) else: # Handle errors for error in tool.errors: print(f"{error.severity} Line {error.line}: {error.message}") ``` ### CompilationError Error or warning from compilation. ```python @dataclass class CompilationError: line: int # Line number (1-indexed) column: int # Column number (1-indexed) message: str # Error/warning message severity: str # "ERROR" or "WARNING" ``` ### Action (IR) Intermediate representation of a movement. ```python @dataclass class Action: head_pose: Optional[np.ndarray] # 4x4 transformation matrix antennas: Optional[np.ndarray] # [left, right] angles in radians body_yaw: Optional[float] # Body rotation in radians duration: float # Movement duration in seconds interpolation: str # "minjerk", "linear", "ease", "cartoon" ``` ### WaitAction (IR) Intermediate representation of a wait/pause. ```python @dataclass class WaitAction: duration: float # Wait duration in seconds ``` ## Examples ### Example 1: Simple Greeting ```rmscript DESCRIPTION Greet someone warmly # Wave with antennas antenna up wait 0.5s antenna down wait 0.5s # Nod look down 20 wait 0.3s look up 20 wait 0.3s look center ``` ### Example 2: Search Pattern ```rmscript DESCRIPTION Look around to search for something # Scan left to right look left 45 wait 1s look center wait 0.5s look right 45 wait 1s look center # Check up and down look up 30 wait 0.5s look down 30 wait 0.5s look center ``` ### Example 3: Dance Choreography ```rmscript DESCRIPTION Perform a fun dance routine # Opening pose antenna both up and look up 25 wait 1s # Main sequence repeat 2 # Move left turn left 30 and look right 20 wait 0.5s # Move right turn right 30 and look left 20 wait 0.5s # Ending pose turn center and look center and antenna down ``` ### Example 4: Using Qualitative Strength ```rmscript DESCRIPTION Wave shyly at someone # Small movements look down slightly antenna left up a little wait 1s antenna left down look center ``` ### Example 5: Complex Combination ```rmscript DESCRIPTION Show excitement when greeting # Quick movements repeat 3 antenna both up superfast antenna both down superfast # Energetic looking around look left and up fast wait 0.2s look right and up fast wait 0.2s look center slow ``` ## Error Handling ### Compilation Errors Errors prevent code generation: ```rmscript # Invalid keyword jump up ``` **Output:** `❌ Line 1: Unexpected token: 'jump'` ```rmscript # Invalid direction for keyword turn up ``` **Output:** `❌ Line 1: Invalid direction 'up' for keyword 'turn'` ```rmscript # Missing indentation repeat 3 look left ``` **Output:** `❌ Line 2: Expected indented block after 'repeat'` ### Compilation Warnings Warnings allow compilation but alert to potential issues: ```rmscript # Exceeds safe range turn left 200 ``` **Output:** `⚠️ Line 1: Body yaw 200.0° exceeds safe range (±160.0°), will be clamped` ```rmscript # Exceeds physical limit look up 50 ``` **Output:** `⚠️ Line 1: Head pitch 50.0° exceeds limit (±40.0°), will be clamped` ### Handling Errors in Code ```python tool = compiler.compile(source) if not tool.success: print("Compilation failed!") for error in tool.errors: print(f" ❌ Line {error.line}: {error.message}") exit(1) if tool.warnings: print("Compilation succeeded with warnings:") for warning in tool.warnings: print(f" ⚠️ Line {warning.line}: {warning.message}") # Safe to execute tool.execute(mini) ``` ## Tips and Best Practices ### 1. Use Comments ```rmscript # Good: explain what you're doing # Opening sequence: get attention antenna up wait 1s # Main behavior: scan the room look left 45 wait 1s look right 45 ``` ### 2. Use Descriptive Tool Names ```python # Good # Avoid ``` ### 3. Break Complex Behaviors into Steps ```rmscript # Good: readable steps antenna up wait 1s look left wait 0.5s look right wait 0.5s look center # Avoid: too compressed antenna up and look left and wait 1s and look right ``` ### 4. Use Repeat for Patterns ```rmscript # Good: use repeat repeat 3 antenna up antenna down # Avoid: repetitive antenna up antenna down antenna up antenna down antenna up antenna down ``` ### 5. Test with Small Values First ```rmscript # Start conservative look left 10 turn right 15 # Then increase if needed look left 45 turn right 90 ``` ## Physical Safety Limits rmscript validates all movements against these limits: | Movement | Limit | Warning Threshold | |----------|-------|------------------| | Body yaw (turn) | ±180° | ±160° | | Head yaw (look left/right) | ±180° absolute | ±65° relative to body | | Head pitch (look up/down) | ±40° | ±40° | | Head roll (tilt) | ±40° | ±40° | | Antenna | ±90° | ±65° | | Head X (forward/back) | ±50mm typical | - | | Head Y (left/right) | ±50mm typical | - | | Head Z (up/down) | +20mm / -30mm typical | - | Exceeding these limits generates **warnings** but still compiles. The robot's hardware will clamp values to safe ranges. ## Default Values When values aren't specified: | Parameter | Default | |-----------|---------| | Angle (look, turn, tilt) | 30° | | Distance (head translation) | 10mm | | Antenna angle | 45° | | Duration | 1.0s | **Qualitative Strength Values:** | Level | Keywords | Angle | Distance | |-------|----------|-------|----------| | VERY_SMALL | tiny, minuscule, mini, verysmall | 5° | 2mm | | SMALL | little, slightly, small, alittle | 15° | 5mm | | MEDIUM | medium, normal, regular, standard, normally | 30° | 10mm | | LARGE | strong, lot, big, large, very, alot, huge, strongly | 45° | 20mm | | VERY_LARGE | enormous, verybig, verylarge, maximum | 60° | 30mm | ## Troubleshooting ### Issue: "Inconsistent indentation" **Cause:** Mixing tabs and spaces, or inconsistent indentation levels. **Solution:** Use only spaces or only tabs (not mixed). Ensure nested blocks are indented consistently. ```rmscript # Bad: mixed tabs and spaces repeat 3 look left # 4 spaces → look right # 1 tab (shows as →) # Good: consistent spaces repeat 3 look left # 4 spaces look right # 4 spaces ``` ### Issue: "Expected indented block after 'repeat'" **Cause:** No indentation after `repeat`. **Solution:** Indent the repeat block body. ```rmscript # Bad repeat 3 look left # Good repeat 3 look left ``` ### Issue: Generated code has unexpected angles **Cause:** Sign convention confusion (left/right). **Solution:** Remember: `left` = positive yaw, `right` = negative yaw in the generated code. `up` = negative pitch, `down` = positive pitch. This matches the robot's actual coordinate system. ## Contributing To extend rmscript: 1. **Add new keywords**: Update `lexer.py`, `parser.py`, and `semantic.py` 2. **Add new features**: Modify the appropriate compiler stage 3. **Add tests**: Create integration tests in `tests/test_rmscript/` 4. **Update docs**: Keep this README and examples current ## License Same as Reachy Mini SDK.