Skip to content
Open
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
89 changes: 89 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Pybricks Robotics Development Guide

## Project Overview
This is a **Pybricks MicroPython robotics project** for LEGO SPIKE Prime Hub. Code runs on the hub's MicroPython environment, not local Python - expect import errors in VS Code (this is normal).

## Critical Architecture Knowledge

### Dual-Environment Setup
- **Local Environment**: VS Code with `pybricksdev` tooling for development/debugging
- **Target Environment**: MicroPython on LEGO SPIKE Prime Hub where code actually executes
- **Key Insight**: Local Python is only for tooling; robot scripts use hub's MicroPython runtime

### Hardware Configuration
See `architecture/robot_wiring.md` for current port mappings and sensor configurations. Always check this file before writing hardware-specific code.

## Essential Development Workflows

### Environment Setup (Required First Step)
```bash
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
```

## Agent Mode Automation Rules

### Robot Name Discovery (REQUIRED PROCESS)
When running code via terminal commands, the agent MUST:
1. **ALWAYS check `.vscode/launch.json` FIRST** to extract the robot name
2. **Parse the `-n` argument** from the "args" array in the Pybricks configuration
3. **Use that exact name** in terminal commands automatically
4. **Never ask user for robot name** if it's already in launch.json
5. **If not found** try the same command again up to 2 more times

### Example Workflow:
```bash
# Agent should automatically extract robot name from launch.json and use:
pybricksdev run ble script_name.py -n "ExtractedName"
# NOT: pybricksdev run ble script_name.py (missing name)
```

### Mandatory Pre-Run Checklist
Before running ANY code, agent must verify:
- [ ] Virtual environment activated (`source venv/bin/activate`)
- [ ] Hub powered and in pairing mode (blinking blue)
- [ ] Robot name extracted from `.vscode/launch.json` if using terminal
- [ ] Hardware ports match `architecture/robot_wiring.md`

### Debug Configuration
Update `.vscode/launch.json` with your hub's actual name in the `-n` argument. The debugger uses the virtual environment's Python to run `pybricksdev` module.

## Project-Specific Patterns

### Script Structure Convention (REQUIRED PATTERN)
Every robot script MUST include:
```python
#!/usr/bin/env pybricks-micropython
from pybricks.hubs import PrimeHub
from pybricks.tools import wait, Matrix

hub = PrimeHub() # ALWAYS initialize first

try:
# Main logic here
# Define patterns as constants
# Main loop logic
pass
except KeyboardInterrupt:
print("Program interrupted")
finally:
hub.display.off() # ALWAYS cleanup
```

### MANDATORY ACTIONS (Never Skip)
1. **ALWAYS read `architecture/robot_wiring.md` FIRST** before writing hardware code
2. **ALWAYS suggest F5 debug BEFORE terminal commands**
3. **ALWAYS check launch.json for robot name** before BLE terminal commands
4. **ALWAYS mention retry attempts** (BLE discovery often fails first try)
5. **ALWAYS use absolute paths** for file operations

## Dependencies & Integration
- **Core**: `pybricks` (hub runtime) + `pybricksdev` (development tools)
- **No external services**: Self-contained robotics project
- **Hardware dependency**: Requires physical LEGO SPIKE Prime Hub

## Common Gotchas
- Import errors in VS Code are **expected** - code runs on hub's MicroPython, not locally
- Hub must be in pairing mode for Bluetooth connections
- NEVER use USB connection or try to connect to any hub while debugging via Bluetooth always suggest to retry a few times if the robot is not discovered the first time
72 changes: 67 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ pip install -r requirements.txt

This repository includes a simple MicroPython script for Pybricks hubs:

### `robot_blink.py` - Robot Face Animation
A script that prints "Hello World" and displays a blinking robot face using Matrix patterns on the LED display. The robot has sensors, eyes, and features that create a friendly animated face with periodic blinking.
### `robot_blink.py` - Matrix Animation & GIF Playback
- Prints "Hello World" in the console when the hub program starts.
- Plays animated GIFs on the 5x5 matrix by scaling each frame down and mapping pixel intensity to LED brightness.
- **New:** Ships with an embedded animation so the hub can play a GIF even when no external files are transferred.
- Falls back to the original blinking robot face if no GIF is provided or an error occurs while loading the GIF.

### `.vscode/launch.json` - VS Code Debug Configuration
A sample configuration for debugging Pybricks scripts directly from VS Code using the `pybricksdev` tool.
Expand All @@ -26,9 +29,68 @@ A sample configuration for debugging Pybricks scripts directly from VS Code usin

### Expected Behavior
- Console prints "Hello World!" and animation status messages
- LED matrix displays a robot face with sensors, eyes, and features
- Eyes blink periodically (closed eyes are dimmer)
- Pattern loops continuously with "Blink!" messages until stopped
- LED matrix plays the requested GIF at 5 FPS (one frame every 200 ms)
- LED brightness represents grayscale intensity from the GIF
- If no GIF is found, the LED matrix displays the classic blinking robot face with "Blink!" messages until stopped

### Playing a GIF on the Matrix

1. Place your animated GIF in the project directory (for example `robot_animation.gif`) or reference an existing GIF elsewhere.
2. Launch the script, passing the GIF path as a positional argument or with the `--gif` option:

```bash
# Default GIF in the project folder via Bluetooth ("John" is your hub name)
pybricksdev run ble robot_blink.py -n "John"

# USB connection
pybricksdev run usb robot_blink.py
```

### Choosing Which GIF Plays

`pybricksdev run` does not pass additional command-line arguments to your script. The renderer will therefore look for a GIF in this order:

1. The path specified by the `PYBRICKS_GIF` environment variable.
2. A file named `robot_animation.gif` in the project directory.
3. A file named `giphy-downsized.gif` in the project directory.
4. The first `*.gif` file found in the project directory.
5. **If none of the above exist but the script contains embedded frames, those frames will be used automatically.**

To select a specific file without renaming it, set the environment variable before running the command (PowerShell example):

```pwsh
$env:PYBRICKS_GIF = "C:\\path\\to\\my_animation.gif"
pybricksdev run ble robot_blink.py -n "John"
```

> ℹ️ The environment variable shortcut only applies when the host Python runtime provides the `os` module (e.g., running from your computer). On the hub itself the script simply continues to the default file search.

When the variable is unset, the script will automatically pick the first available GIF or fall back to the blinking robot animation if none are found.

### How GIF decoding works

- When the script runs on your computer (where Pillow is available), it uses Pillow to scale the GIF frames down to the 5×5 matrix.
- When Pillow is **not** available (for example on the hub), the script falls back to a built-in pure MicroPython GIF decoder that supports global/local palettes, transparency, and interlaced frames.
- Debug printouts (prefixed with “Debug”) describe which path is taken and why; they show up in the VS Code debug console or the terminal running `pybricksdev`.

3. To skip GIF playback and use the blink animation explicitly, add `--blink`.
4. To force the internally embedded animation regardless of available files, add `--embedded`.

### Embedding a GIF directly into the script

For Bluetooth transfers, only the Python source reaches the hub. To bake an animation into `robot_blink.py`:

1. Place your source GIF (default: `giphy-downsized.gif`) in the project root.
2. Run `python scripts/generate_embedded_frames.py` from an activated virtual environment.
- The script uses the same pure-Python GIF decoder found in `robot_blink.py`, so it has **no Pillow dependency**.
- It emits two helper files:
- `embedded_frames.json` – raw brightness matrices for inspection.
- `embedded_frames_snippet.py` – a ready-to-paste `EMBEDDED_FRAMES_DATA = [...]` literal.
3. Copy the literal from `embedded_frames_snippet.py` into `robot_blink.py` (replacing the existing `EMBEDDED_FRAMES_DATA`).
4. Optionally adjust `EMBEDDED_SOURCE` and `EMBEDDED_FPS` to describe your animation.
5. Deploy the updated `robot_blink.py`—the hub can now render the baked animation with no external assets.

The renderer converts each frame to grayscale, scales it down to 5×5 pixels, and maps brightness to the hub's 0-100 LED intensity range so you can perceive shades on the matrix.

### Compatible Hubs

Expand Down
13 changes: 13 additions & 0 deletions architecture/robot_wiring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Hub: Lego Spike Prime

Port A: no connection

Port B: Color Sensor

Port C: no connection

Port D: small motor controlling arms

Port E: no connection

Port F: small motor controlling hip movement
1 change: 1 addition & 0 deletions embedded_frames.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[48, 48, 48, 48, 48], [48, 95, 28, 48, 48], [48, 40, 97, 48, 48], [48, 48, 48, 48, 48], [48, 49, 48, 72, 2]], [[48, 48, 48, 48, 48], [48, 48, 48, 44, 48], [48, 48, 98, 48, 48], [48, 49, 48, 48, 48], [49, 48, 91, 44, 44]], [[48, 48, 48, 48, 48], [48, 48, 48, 96, 48], [48, 48, 96, 48, 48], [48, 0, 48, 48, 48], [48, 48, 95, 41, 49]], [[48, 48, 48, 20, 48], [48, 48, 48, 97, 48], [48, 48, 96, 96, 48], [48, 4, 48, 48, 48], [48, 49, 98, 7, 55]], [[48, 48, 48, 0, 48], [48, 48, 96, 96, 48], [48, 48, 96, 96, 48], [48, 48, 48, 48, 48], [48, 48, 95, 7, 54]], [[48, 48, 48, 0, 48], [48, 48, 98, 96, 48], [48, 48, 96, 0, 48], [48, 48, 48, 48, 48], [47, 49, 99, 2, 54]], [[48, 48, 24, 91, 48], [48, 48, 48, 96, 48], [48, 48, 96, 42, 48], [48, 48, 48, 49, 48], [49, 48, 99, 27, 15]], [[48, 91, 48, 48, 48], [48, 96, 48, 48, 48], [48, 79, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 95, 20, 7]], [[48, 48, 48, 48, 48], [48, 96, 48, 48, 48], [48, 98, 96, 48, 48], [48, 48, 48, 48, 48], [52, 48, 36, 55, 4]], [[48, 96, 48, 48, 48], [48, 69, 48, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [96, 50, 12, 23, 7]], [[48, 7, 48, 48, 48], [48, 97, 96, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 47], [51, 51, 99, 16, 56]], [[48, 95, 48, 48, 48], [48, 95, 49, 48, 48], [48, 1, 94, 48, 48], [48, 48, 48, 48, 48], [48, 49, 99, 23, 49]], [[48, 48, 48, 48, 48], [48, 48, 48, 44, 48], [48, 48, 97, 48, 48], [48, 48, 48, 48, 48], [48, 49, 99, 36, 35]], [[48, 48, 48, 48, 48], [48, 48, 48, 94, 48], [48, 48, 95, 2, 48], [48, 26, 49, 48, 48], [48, 48, 94, 24, 11]], [[48, 48, 48, 96, 48], [48, 48, 48, 96, 48], [48, 48, 96, 96, 48], [48, 48, 48, 48, 48], [49, 50, 99, 53, 11]], [[48, 48, 48, 4, 48], [48, 48, 96, 96, 48], [48, 48, 96, 96, 48], [48, 48, 48, 48, 48], [55, 48, 99, 56, 4]], [[48, 48, 48, 99, 48], [48, 48, 48, 96, 48], [48, 48, 96, 16, 48], [48, 48, 48, 48, 48], [52, 48, 99, 62, 4]], [[48, 48, 3, 97, 48], [48, 48, 48, 96, 48], [48, 48, 96, 0, 48], [48, 41, 48, 48, 48], [48, 48, 97, 75, 71]], [[48, 97, 48, 48, 48], [48, 96, 48, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [50, 48, 97, 66, 43]], [[48, 48, 48, 48, 48], [48, 96, 48, 48, 48], [48, 96, 97, 48, 48], [48, 48, 48, 48, 48], [0, 0, 0, 0, 0]], [[48, 97, 48, 48, 48], [48, 96, 98, 48, 48], [48, 96, 95, 48, 48], [48, 48, 47, 49, 48], [47, 48, 98, 66, 20]], [[48, 0, 48, 48, 48], [48, 96, 11, 48, 48], [48, 95, 96, 48, 48], [48, 48, 48, 48, 48], [48, 48, 96, 41, 75]], [[48, 96, 48, 48, 48], [48, 96, 49, 48, 48], [48, 50, 96, 48, 48], [48, 50, 48, 48, 48], [48, 48, 97, 69, 59]], [[48, 48, 48, 0, 48], [48, 48, 48, 99, 48], [48, 48, 96, 48, 48], [48, 48, 48, 48, 48], [48, 48, 97, 16, 11]], [[48, 48, 48, 0, 48], [48, 48, 48, 95, 48], [48, 48, 97, 95, 48], [48, 0, 48, 48, 48], [49, 48, 99, 20, 22]], [[48, 48, 48, 4, 48], [48, 48, 7, 96, 48], [48, 48, 98, 0, 48], [48, 48, 48, 48, 48], [54, 48, 95, 39, 35]], [[48, 48, 48, 0, 48], [48, 48, 96, 96, 48], [48, 48, 96, 13, 48], [48, 48, 48, 48, 48], [52, 48, 97, 75, 32]], [[48, 48, 48, 97, 48], [48, 48, 47, 94, 48], [48, 48, 96, 16, 48], [48, 48, 48, 48, 48], [48, 48, 99, 22, 54]], [[48, 48, 48, 32, 48], [48, 48, 96, 96, 48], [48, 48, 70, 48, 48], [48, 48, 48, 48, 48], [48, 49, 96, 55, 27]], [[48, 48, 48, 4, 48], [48, 96, 96, 0, 48], [48, 48, 70, 48, 48], [48, 48, 48, 48, 48], [48, 48, 98, 55, 7]], [[48, 48, 1, 48, 48], [48, 48, 32, 49, 48], [48, 48, 96, 48, 48], [48, 48, 48, 48, 48], [48, 48, 88, 49, 2]], [[48, 0, 48, 48, 48], [48, 48, 96, 48, 48], [48, 48, 0, 48, 48], [48, 48, 48, 48, 48], [49, 48, 15, 49, 7]], [[48, 29, 0, 48, 48], [48, 48, 96, 96, 48], [48, 48, 73, 48, 48], [48, 48, 48, 48, 48], [49, 48, 91, 52, 49]], [[48, 48, 97, 48, 48], [48, 48, 97, 48, 48], [48, 48, 96, 48, 48], [48, 48, 48, 48, 48], [50, 48, 10, 56, 4]], [[48, 48, 99, 48, 48], [48, 48, 48, 48, 48], [48, 48, 99, 48, 48], [48, 48, 4, 48, 48], [48, 48, 88, 51, 49]], [[48, 48, 0, 48, 48], [48, 48, 48, 48, 48], [48, 48, 95, 48, 48], [48, 48, 48, 48, 48], [48, 48, 96, 32, 27]], [[48, 48, 48, 49, 48], [48, 48, 48, 96, 48], [48, 48, 95, 49, 48], [48, 27, 48, 48, 48], [48, 48, 96, 66, 49]], [[48, 48, 48, 7, 48], [48, 48, 0, 96, 48], [48, 48, 96, 96, 48], [48, 48, 48, 48, 48], [51, 48, 71, 27, 26]], [[48, 48, 47, 99, 48], [48, 48, 48, 99, 48], [48, 48, 99, 95, 48], [48, 48, 48, 48, 48], [48, 49, 97, 17, 20]], [[48, 95, 48, 48, 48], [48, 96, 48, 48, 48], [48, 97, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 99, 36, 4]], [[48, 96, 48, 48, 48], [48, 96, 48, 48, 48], [48, 95, 95, 48, 48], [48, 48, 48, 49, 48], [49, 48, 99, 59, 4]], [[48, 94, 48, 48, 48], [48, 75, 2, 48, 48], [48, 97, 96, 48, 48], [48, 48, 15, 48, 48], [51, 48, 96, 75, 73]], [[48, 7, 48, 48, 48], [48, 97, 96, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [31, 49, 97, 73, 11]], [[48, 97, 48, 48, 48], [48, 96, 48, 48, 48], [48, 1, 96, 48, 48], [48, 48, 48, 48, 48], [50, 49, 99, 43, 43]], [[0, 48, 48, 95, 0], [48, 48, 48, 96, 48], [48, 48, 94, 49, 48], [48, 48, 48, 0, 0], [0, 0, 0, 0, 0]], [[48, 48, 48, 97, 48], [48, 48, 48, 96, 48], [48, 48, 95, 48, 48], [48, 4, 48, 48, 48], [49, 48, 99, 62, 27]], [[48, 48, 48, 96, 48], [48, 48, 49, 96, 48], [48, 48, 96, 96, 48], [48, 48, 48, 48, 48], [42, 48, 99, 66, 4]], [[48, 48, 48, 7, 48], [48, 48, 97, 99, 48], [48, 48, 95, 94, 48], [48, 48, 48, 48, 48], [17, 48, 95, 56, 2]], [[48, 48, 48, 96, 48], [48, 48, 49, 96, 48], [48, 48, 96, 11, 48], [48, 48, 48, 48, 48], [50, 51, 99, 64, 7]], [[48, 97, 48, 48, 48], [48, 96, 48, 48, 48], [48, 99, 97, 48, 48], [48, 48, 48, 48, 48], [49, 49, 97, 24, 20]], [[48, 97, 48, 48, 48], [48, 96, 48, 48, 48], [48, 97, 97, 48, 48], [48, 48, 48, 48, 48], [47, 48, 97, 36, 27]], [[48, 97, 48, 48, 48], [48, 96, 48, 48, 48], [48, 95, 97, 48, 48], [48, 48, 48, 48, 48], [49, 48, 97, 75, 44]], [[48, 96, 48, 48, 48], [48, 84, 95, 48, 48], [48, 96, 98, 48, 48], [48, 48, 48, 48, 48], [48, 48, 97, 21, 49]], [[48, 0, 48, 48, 48], [48, 96, 99, 48, 48], [48, 97, 95, 48, 48], [48, 48, 48, 48, 48], [49, 49, 97, 69, 2]], [[48, 48, 48, 50, 48], [48, 48, 48, 48, 48], [48, 48, 96, 48, 48], [48, 48, 48, 48, 48], [49, 47, 96, 43, 2]], [[48, 48, 48, 49, 48], [48, 48, 48, 95, 48], [48, 48, 95, 48, 48], [48, 32, 48, 48, 48], [48, 48, 94, 39, 4]], [[48, 48, 48, 7, 48], [48, 48, 48, 95, 48], [48, 48, 97, 98, 48], [48, 1, 48, 48, 48], [49, 48, 99, 22, 49]], [[48, 48, 48, 5, 48], [48, 48, 49, 94, 48], [48, 48, 94, 96, 48], [48, 48, 48, 48, 48], [49, 48, 55, 32, 48]], [[48, 48, 48, 0, 48], [48, 48, 96, 96, 48], [48, 48, 95, 27, 48], [48, 48, 48, 48, 48], [55, 48, 97, 43, 2]], [[48, 48, 0, 97, 48], [48, 48, 48, 96, 48], [48, 48, 95, 1, 48], [48, 48, 48, 48, 48], [47, 48, 95, 73, 4]], [[48, 94, 48, 48, 48], [48, 96, 48, 50, 48], [48, 97, 97, 48, 48], [48, 48, 48, 48, 48], [49, 48, 94, 49, 51]], [[48, 48, 48, 48, 48], [48, 97, 48, 48, 48], [48, 97, 96, 48, 48], [48, 48, 48, 48, 48], [51, 48, 99, 17, 46]], [[48, 96, 48, 48, 48], [48, 96, 48, 48, 48], [48, 94, 96, 48, 48], [48, 48, 48, 48, 48], [51, 48, 96, 43, 51]], [[48, 4, 48, 48, 48], [48, 96, 96, 48, 48], [48, 95, 96, 48, 48], [48, 48, 48, 48, 48], [55, 48, 95, 72, 66]], [[48, 8, 48, 48, 48], [48, 96, 48, 48, 48], [48, 95, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 97, 75, 73]], [[48, 53, 48, 48, 48], [48, 97, 49, 48, 48], [48, 99, 96, 48, 48], [48, 48, 48, 48, 48], [47, 48, 97, 62, 7]], [[48, 97, 48, 48, 48], [48, 97, 48, 48, 48], [48, 27, 95, 48, 48], [48, 48, 48, 48, 48], [48, 48, 96, 49, 4]], [[48, 99, 48, 48, 48], [48, 96, 48, 48, 48], [48, 0, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 96, 73, 75]], [[48, 99, 48, 48, 48], [48, 96, 48, 48, 48], [48, 98, 96, 48, 48], [48, 48, 48, 48, 48], [48, 49, 95, 71, 62]], [[0, 96, 0, 0, 0], [48, 98, 48, 48, 48], [48, 95, 0, 48, 48], [48, 48, 48, 48, 48], [0, 0, 0, 0, 44]], [[48, 98, 48, 48, 48], [48, 95, 48, 48, 48], [48, 97, 96, 48, 48], [48, 48, 50, 48, 48], [55, 48, 98, 73, 27]], [[48, 2, 48, 48, 51], [48, 96, 48, 48, 48], [48, 97, 96, 48, 48], [48, 48, 48, 48, 48], [35, 48, 94, 66, 4]], [[48, 96, 48, 48, 48], [48, 97, 49, 48, 48], [48, 79, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 98, 62, 7]], [[48, 95, 49, 48, 48], [48, 97, 32, 48, 48], [48, 32, 96, 48, 48], [48, 48, 48, 48, 48], [49, 48, 98, 59, 7]], [[48, 95, 48, 48, 48], [48, 97, 48, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [51, 48, 95, 41, 27]], [[48, 0, 48, 48, 48], [48, 96, 48, 48, 48], [48, 99, 97, 48, 48], [48, 48, 48, 48, 48], [48, 48, 98, 32, 44]], [[48, 0, 48, 48, 48], [48, 95, 48, 48, 48], [48, 98, 97, 48, 48], [48, 48, 48, 48, 48], [48, 48, 99, 20, 49]], [[48, 96, 48, 48, 48], [48, 96, 48, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [55, 49, 99, 2, 54]], [[48, 7, 48, 48, 48], [48, 96, 97, 48, 48], [48, 96, 96, 48, 48], [48, 48, 48, 48, 48], [54, 49, 99, 2, 53]], [[48, 95, 48, 48, 48], [48, 96, 2, 48, 48], [48, 47, 96, 48, 48], [48, 48, 48, 48, 48], [49, 49, 98, 4, 51]]]
Loading