Compare commits
3 Commits
6f454c9707
...
6b912191d6
| Author | SHA1 | Date |
|---|---|---|
|
|
6b912191d6 | |
|
|
cfc390e1d6 | |
|
|
c19f6656b8 |
|
|
@ -14,12 +14,13 @@ See [readme.md](readme.md) for project overview and status.
|
|||
| Troubleshooting & known issues | [docs/troubleshooting.md](docs/troubleshooting.md) |
|
||||
| Guide: adding a new ROS2 package | [docs/ros2-package-guide.md](docs/ros2-package-guide.md) |
|
||||
| `blockly_app` — file reference (incl. HMI core modules) | [src/blockly_app/README.md](src/blockly_app/README.md) |
|
||||
| `blockly_app` — creating blocks (ROS2 action + client-side + HMI) | [src/blockly_app/BLOCKS.md](src/blockly_app/BLOCKS.md) |
|
||||
| `blockly_app` — creating blocks (ROS2 action + client-side + HMI) | [src/blockly_app/docs/BLOCKS.md](src/blockly_app/docs/BLOCKS.md) |
|
||||
| `blockly_executor` — file reference, handlers & testing guide | [src/blockly_executor/README.md](src/blockly_executor/README.md) |
|
||||
| `blockly_interfaces` — ROS2 action & message interfaces | [src/blockly_interfaces/README.md](src/blockly_interfaces/README.md) |
|
||||
| `gpio_node` — Raspberry Pi GPIO node (C++, libgpiod) | [src/gpio_node/README.md](src/gpio_node/README.md) |
|
||||
| `pca9685_node` — PCA9685 16-channel PWM controller (C++, I2C) | [src/pca9685_node/README.md](src/pca9685_node/README.md) |
|
||||
| `as5600_node` — AS5600 12-bit magnetic encoder (C++, I2C) | [src/as5600_node/README.md](src/as5600_node/README.md) |
|
||||
| `amr_bringup` — ROS2 launch files (desktop bringup) | [src/amr_bringup/launch/](src/amr_bringup/launch/) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -33,6 +34,8 @@ pixi run build-interfaces # must build interfaces first
|
|||
pixi run build # build all packages
|
||||
pixi run setup-ui # download Blockly JS vendor files (first time, needs internet)
|
||||
|
||||
pixi run launch-blockly # one command — executor (dummy) + app
|
||||
# or separately:
|
||||
pixi run executor # Terminal 1 — start Action Server
|
||||
pixi run app # Terminal 2 — start desktop GUI
|
||||
```
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ executor = { cmd = "bash -c 'source install/setup.bash && ros2 run block
|
|||
executor-hw = { cmd = "bash -c 'source install/setup.bash && ros2 run blockly_executor executor_node --ros-args -p use_real_hardware:=true'", depends-on = ["build-executor"] }
|
||||
app = { cmd = "bash -c 'source install/setup.bash && python -m blockly_app.app'", depends-on = ["build-app"] }
|
||||
test = { cmd = "bash -c 'source install/setup.bash && PYTHONPATH=$PYTHONPATH:src/blockly_executor pytest src/blockly_executor/test/ -v'", depends-on = ["build-interfaces"] }
|
||||
build-bringup = { cmd = "colcon build --symlink-install --packages-select amr_bringup", depends-on = ["build-interfaces"] }
|
||||
launch-blockly = { cmd = "bash -c 'source install/setup.bash && ros2 launch amr_bringup blockly.launch.py'", depends-on = ["build-bringup", "build-executor", "build-app"] }
|
||||
launch-blockly-hw = { cmd = "bash -c 'source install/setup.bash && ros2 launch amr_bringup blockly_hw.launch.py'", depends-on = ["build-bringup", "build-executor", "build-app"] }
|
||||
setup-ui = "npm install blockly gridstack && mkdir -p src/blockly_app/blockly_app/ui/vendor && cp node_modules/blockly/blockly.min.js src/blockly_app/blockly_app/ui/vendor/ && cp node_modules/blockly/javascript_compressed.js src/blockly_app/blockly_app/ui/vendor/ && cp node_modules/blockly/blocks_compressed.js src/blockly_app/blockly_app/ui/vendor/ && cp node_modules/blockly/msg/en.js src/blockly_app/blockly_app/ui/vendor/ && cp node_modules/gridstack/dist/gridstack-all.js src/blockly_app/blockly_app/ui/vendor/ && cp node_modules/gridstack/dist/gridstack.min.css src/blockly_app/blockly_app/ui/vendor/"
|
||||
|
||||
[target.linux-aarch64.tasks]
|
||||
|
|
|
|||
105
readme.md
105
readme.md
|
|
@ -7,45 +7,69 @@
|
|||
|
||||
Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
|
||||
|
||||
# Aturan pengggunaan dokumen
|
||||
**Aturan pengggunaan dokumen**
|
||||
|
||||
bab pada dokumen merepresentasikan alur rencana pengembangan.
|
||||
## Potential Enhancements
|
||||
|
||||
bab ini digunakan untuk Feasibility Study
|
||||
|
||||
## Planned Feature
|
||||
|
||||
Backlog. Setelah kita pelajari untuk di kerjakan maka kita pindah ke backlog
|
||||
|
||||
## Feature Task
|
||||
|
||||
penjabaran Pekerjaan yang ready untuk dikerjakan. Task harus dijelaskan apa yang akan dikerjakan dan terdapat definition of done nya
|
||||
Berikut ini adalah template untuk pembuatan task :
|
||||
|
||||
```
|
||||
|
||||
## <nomor task> <judul task> : <state: [ ] >
|
||||
jelaskan permasalah di bab ini
|
||||
### Bug 1 [ ] : Keterangan bug untuk rekap permasalahan
|
||||
**Symtomp** : jelaskan masalahnya!
|
||||
**Root Couse** : masalah ini dikarenakan apa?
|
||||
**Fix** : bagaimana cara fix nya?
|
||||
### Definition Of Done
|
||||
[ ] DoD 1
|
||||
[ ] DoD 2
|
||||
[ ] Bug 1 ...
|
||||
|
||||
```
|
||||
> **Potential Enhancements**
|
||||
>
|
||||
> bab ini digunakan untuk Feasibility Study
|
||||
>
|
||||
> **Planned Feature**
|
||||
>
|
||||
> Backlog. Setelah kita pelajari untuk di kerjakan maka kita pindah ke backlog
|
||||
>
|
||||
> **Feature Task**
|
||||
>
|
||||
> penjabaran Pekerjaan yang ready untuk dikerjakan. Task harus dijelaskan apa yang akan dikerjakan dan terdapat definition of done nya
|
||||
> Berikut ini adalah template untuk pembuatan task :
|
||||
>
|
||||
> ```
|
||||
> ## <nomor task> <judul task> : <state: [ ] >
|
||||
> jelaskan permasalah di bab ini
|
||||
> ### Bug 1 [ ] : Keterangan bug untuk rekap permasalahan
|
||||
> **Symtomp** : jelaskan masalahnya!
|
||||
> **Root Couse** : masalah ini dikarenakan apa?
|
||||
> **Fix** : bagaimana cara fix nya?
|
||||
> ### Definition Of Done
|
||||
> [ ] DoD 1
|
||||
> [ ] DoD 2
|
||||
> [ ] Bug 1 ...
|
||||
>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
# Potential Enhancements
|
||||
this list is short by priority
|
||||
- **Feasibility Study to implement Controller**: mobile robot need controller to move flawlesly.
|
||||
- **Launch files**: ROS2 launch files to start all nodes with one command includ node in raspberry pi. composite blockly dan executor yang memiliki composit 2 jenis yaitu menggunakan executor dummy dan executor-hw.
|
||||
- **Simulation**: Integrate with Gazebo/Isaac Sim for testing Kiwi Wheel kinematics before deploying to hardware
|
||||
- **Block categories**: Future blocks grouped into Robot, Sensors, Navigation categories
|
||||
|
||||
this sub title list is short by priority
|
||||
|
||||
## **Feasibility Study to implement Vision Sensor**
|
||||
|
||||
coba kamu lakukan penelitian literatur untuk penerapan vision sensor dengan kemampuan berikut ini
|
||||
1. Melakukan pengenalan warna dan obyek beserta prosedur untuk training warna
|
||||
2. Melakukan penghitungan obyek yang tersusun secara urut dari kiri ke kanan.
|
||||
|
||||
berikan proposal mu untuk mencakup penerapannya juga di blockly app.
|
||||
|
||||
berikut ini adalah path yang aku sarankan untuk menampung hasil penelitianmu
|
||||
```
|
||||
src/amr_vision_node
|
||||
├── docs/
|
||||
│ └── feasibility.md
|
||||
```
|
||||
## **Feasibility Study to implement Controller**
|
||||
|
||||
do some research about to make my mobile robot move flawlesly. the scope is controller, kenematics, identification system. kamu bisa rekap hasil feasibility study mu di path berikut ini dan folder amt_controller_node sekaligus sebagai workspace node kusus controller robot tapi tetap rekap task di amr-ros-k4/readme.md
|
||||
```
|
||||
src/amr_controller_node
|
||||
├── docs/
|
||||
│ └── feasibility.md
|
||||
```
|
||||
|
||||
## **Simulation**
|
||||
|
||||
Integrate with Gazebo/Isaac Sim for testing Kiwi Wheel kinematics before deploying to hardware
|
||||
|
||||
# Feature Task
|
||||
|
||||
|
|
@ -313,20 +337,3 @@ Fields: `X (cm)`, `Y (cm)`, `Heading (rad)`, `Vel X (cm/s)`, `Vel Y (cm/s)`, `An
|
|||
- [x] Handler `odometry_read` berfungsi di dummy mode (test passes)
|
||||
- [ ] End-to-end: Blockly → executor (real) → cache `odometry_encoder/odom` → return JSON
|
||||
- [x] Integration test `test_block_odometry.py` passes di dummy mode
|
||||
|
||||
## 6 Enhancement: HMI Interactive Widgets — Button, Slider, Switch : [x]
|
||||
Menambahkan widget **control** (dua arah: user input ↔ code): Button, Slider, dan Switch — mengikuti konsep LabVIEW "Controls vs Indicators". Setiap control memiliki SET block (statement) dan GET block (value). Semua client-side JS, tidak ada perubahan Python/ROS2.
|
||||
|
||||
Detail implementasi: [docs/architecture.md](docs/architecture.md) §2.4, [BLOCKS.md](src/blockly_app/BLOCKS.md) §7.16, [troubleshooting.md](docs/troubleshooting.md).
|
||||
|
||||
### Definition Of Done
|
||||
- [x] 6 block file (SET + GET untuk button, slider, switch)
|
||||
- [x] `hmi-manager.js` — render, setter, getter, user interaction tracking, layout serialization
|
||||
- [x] `hmi-preview.js` — design-time preview + auto-increment widget names
|
||||
- [x] `manifest.js` — 6 entry baru
|
||||
- [x] CSS untuk button, slider, switch di `index.html`
|
||||
- [x] Bug fix: `highlightBlock` periodic macrotask yield (`debug-engine.js`)
|
||||
- [x] Bug fix: `pointerdown` instead of `click` for button/switch (`hmi-manager.js`)
|
||||
- [x] Bug fix: `_userState` / `_userHasInteracted` tracking for switch/slider (`hmi-manager.js`)
|
||||
- [x] Bug fix: auto-increment duplicate widget names (`hmi-preview.js`)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
"""Launch blockly_executor (dummy mode) + blockly_app in one command.
|
||||
|
||||
When blockly_app window is closed, the entire launch is shut down.
|
||||
"""
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch.actions import ExecuteProcess, RegisterEventHandler, Shutdown
|
||||
from launch.event_handlers import OnProcessExit
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
executor_node = Node(
|
||||
package='blockly_executor',
|
||||
executable='executor_node',
|
||||
name='blockly_executor_node',
|
||||
parameters=[{'hardware_mode': 'dummy'}],
|
||||
output='screen',
|
||||
emulate_tty=True,
|
||||
)
|
||||
|
||||
# blockly_app uses pywebview which requires main thread ownership,
|
||||
# so we launch it as a process rather than a Node.
|
||||
blockly_app = ExecuteProcess(
|
||||
cmd=['python', '-m', 'blockly_app.app'],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
executor_node,
|
||||
blockly_app,
|
||||
# When blockly_app is closed, shut down everything
|
||||
RegisterEventHandler(OnProcessExit(
|
||||
target_action=blockly_app,
|
||||
on_exit=[Shutdown(reason='blockly_app closed')],
|
||||
)),
|
||||
])
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
"""Launch blockly_executor (real hardware mode) + blockly_app in one command.
|
||||
|
||||
Pi hardware nodes (gpio_node, pca9685_node, as5600_node) must already be
|
||||
running on the Raspberry Pi on the same ROS2 network (same ROS_DOMAIN_ID).
|
||||
|
||||
When blockly_app window is closed, the entire launch is shut down.
|
||||
"""
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch.actions import ExecuteProcess, RegisterEventHandler, Shutdown
|
||||
from launch.event_handlers import OnProcessExit
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
executor_node = Node(
|
||||
package='blockly_executor',
|
||||
executable='executor_node',
|
||||
name='blockly_executor_node',
|
||||
parameters=[{'hardware_mode': 'real'}],
|
||||
output='screen',
|
||||
emulate_tty=True,
|
||||
)
|
||||
|
||||
blockly_app = ExecuteProcess(
|
||||
cmd=['python', '-m', 'blockly_app.app'],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
executor_node,
|
||||
blockly_app,
|
||||
# When blockly_app is closed, shut down everything
|
||||
RegisterEventHandler(OnProcessExit(
|
||||
target_action=blockly_app,
|
||||
on_exit=[Shutdown(reason='blockly_app closed')],
|
||||
)),
|
||||
])
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0"?>
|
||||
<package format="3">
|
||||
<name>amr_bringup</name>
|
||||
<version>0.1.0</version>
|
||||
<description>Launch files for AMR ROS2 Kiwi Wheel system bringup</description>
|
||||
<maintainer email="dev@example.com">developer</maintainer>
|
||||
<license>MIT</license>
|
||||
|
||||
<exec_depend>blockly_executor</exec_depend>
|
||||
<exec_depend>blockly_app</exec_depend>
|
||||
<exec_depend>ros2launch</exec_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[develop]
|
||||
script_dir=$base/lib/amr_bringup
|
||||
[install]
|
||||
install_scripts=$base/lib/amr_bringup
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import os
|
||||
from glob import glob
|
||||
from setuptools import setup
|
||||
|
||||
package_name = 'amr_bringup'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.1.0',
|
||||
packages=[package_name],
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
|
||||
('share/' + package_name, ['package.xml']),
|
||||
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
|
||||
(os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
|
||||
],
|
||||
install_requires=['setuptools'],
|
||||
entry_points={},
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -182,4 +182,4 @@ All block files registered in [`manifest.js`](blockly_app/ui/blockly/blocks/mani
|
|||
| `hmiSetSwitch.js` | HMI | Statement | Client-side (`HMI.setSwitch()`) |
|
||||
| `hmiGetSwitch.js` | HMI | Output | Client-side (`HMI.getSwitch()`) |
|
||||
|
||||
Each block file calls `BlockRegistry.register()` with all metadata, so the toolbox is automatically generated. See [BLOCKS.md](BLOCKS.md) for the full guide to creating blocks.
|
||||
Each block file calls `BlockRegistry.register()` with all metadata, so the toolbox is automatically generated. See [BLOCKS.md](docs/BLOCKS.md) for the full guide to creating blocks.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
# Quickstart — Adding a New Block
|
||||
|
||||
## Overview: Auto-Discovery on Both Sides
|
||||
|
||||
Both JS and Python use the same pattern: **decorator/register + auto-discovery**. Adding a new block:
|
||||
|
||||
```
|
||||
Step File Action
|
||||
──── ──── ──────
|
||||
1. JS src/blockly_app/blockly_app/ui/blockly/blocks/<name>.js Create — BlockRegistry.register({...})
|
||||
src/blockly_app/blockly_app/ui/blockly/blocks/manifest.js Edit — add filename to BLOCK_FILES array
|
||||
2. Py src/blockly_executor/blockly_executor/handlers/<name>.py Create — @handler("command") function
|
||||
```
|
||||
|
||||
**Files that do NOT need changes:** [`index.html`](../blockly_app/ui/index.html), [`conftest.py`](../../blockly_executor/test/conftest.py), [`executor_node.py`](../../blockly_executor/blockly_executor/executor_node.py), [`BlocklyAction.action`](../../blockly_interfaces/action/BlocklyAction.action), [`handlers/__init__.py`](../../blockly_executor/blockly_executor/handlers/__init__.py). Both toolbox and handler registry are auto-generated.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Create Block File (JS)
|
||||
|
||||
Create `src/blockly_app/blockly_app/ui/blockly/blocks/move_forward.js`:
|
||||
|
||||
```javascript
|
||||
BlockRegistry.register({
|
||||
name: 'move_forward',
|
||||
category: 'Robot',
|
||||
categoryColor: '#5b80a5',
|
||||
color: '#7B1FA2',
|
||||
tooltip: 'Move robot forward with given speed and duration',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Move forward speed')
|
||||
.appendField(new Blockly.FieldNumber(50, 0, 100, 1), 'SPEED')
|
||||
.appendField('for')
|
||||
.appendField(new Blockly.FieldNumber(1000, 0), 'DURATION_MS')
|
||||
.appendField('ms');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#7B1FA2');
|
||||
this.setTooltip('Move robot forward with given speed and duration');
|
||||
}
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const speed = block.getFieldValue('SPEED');
|
||||
const durationMs = block.getFieldValue('DURATION_MS');
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('move_forward', { speed: '" + speed + "', duration_ms: '" + durationMs + "' });\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Then add to [`manifest.js`](../blockly_app/ui/blockly/blocks/manifest.js):
|
||||
|
||||
```javascript
|
||||
const BLOCK_FILES = [
|
||||
'digitalOut.js',
|
||||
'digitalIn.js',
|
||||
'delay.js',
|
||||
'move_forward.js', // ← add here
|
||||
];
|
||||
```
|
||||
|
||||
**No `index.html` changes needed.** No toolbox XML editing. The block appears automatically in the "Robot" category.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Register Handler in Python
|
||||
|
||||
Create a new file in `handlers/` or add to an existing one. Use the `@handler` decorator — auto-discovery handles the rest.
|
||||
|
||||
Each handler contains **both dummy and real hardware logic**. Use `hardware.is_real()` to branch, and `hardware.log()` to record actions for testing.
|
||||
|
||||
```python
|
||||
# src/blockly_executor/blockly_executor/handlers/movement.py
|
||||
from . import handler
|
||||
from .hardware import Hardware
|
||||
|
||||
@handler("move_forward")
|
||||
def handle_move_forward(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
speed = int(params["speed"])
|
||||
duration_ms = int(params["duration_ms"])
|
||||
if not (0 <= speed <= 100):
|
||||
raise ValueError(f"speed must be 0-100, got: {speed}")
|
||||
hardware.log(f"move_forward(speed={speed}, duration_ms={duration_ms})")
|
||||
|
||||
if hardware.is_real():
|
||||
# Real hardware — lazy import, publish to ROS2 topic
|
||||
# TODO: hardware.node.create_publisher(...) etc.
|
||||
hardware.node.get_logger().info(f"Moving forward at {speed} for {duration_ms}ms")
|
||||
|
||||
return (True, f"Moved forward at speed {speed} for {duration_ms}ms")
|
||||
```
|
||||
|
||||
No imports to update, no registry list to edit. The file is auto-discovered on startup.
|
||||
|
||||
**Verify immediately** (no Blockly UI needed):
|
||||
|
||||
```bash
|
||||
# Terminal 1
|
||||
pixi run executor
|
||||
|
||||
# Terminal 2 — send goal manually
|
||||
pixi run bash -c 'source install/setup.bash && ros2 action send_goal /execute_blockly_action blockly_interfaces/action/BlocklyAction "{command: move_forward, param_keys: [speed, duration_ms], param_values: [50, 1000]}"'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Write Integration Test (Optional)
|
||||
|
||||
Create `src/blockly_executor/test/test_block_move_forward.py`:
|
||||
|
||||
```python
|
||||
"""Integration test for Blockly instruction: move_forward"""
|
||||
|
||||
|
||||
def test_block_move_forward_returns_success(exe_action):
|
||||
result = exe_action("move_forward", speed="50", duration_ms="200")
|
||||
assert result.result.success is True
|
||||
|
||||
|
||||
def test_block_move_forward_sends_executing_feedback(exe_action):
|
||||
result = exe_action("move_forward", speed="50", duration_ms="200")
|
||||
assert len(result.feedbacks) > 0
|
||||
assert result.feedbacks[0].status == "executing"
|
||||
|
||||
|
||||
def test_block_move_forward_missing_speed_returns_failure(exe_action):
|
||||
result = exe_action("move_forward", duration_ms="200")
|
||||
assert result.result.success is False
|
||||
assert "speed" in result.result.message.lower()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Name Consistency Reference Table
|
||||
|
||||
```
|
||||
handlers.py blocks/<name>.js blocks/<name>.js
|
||||
─────────── ──────────────── ─────────────────
|
||||
"move_forward" == name: 'move_forward' == 'move_forward' in executeAction
|
||||
params["speed"] == 'SPEED' in FieldNumber == getFieldValue('SPEED')
|
||||
params["duration_ms"] == 'DURATION_MS' == getFieldValue('DURATION_MS')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: JS Block → Python Handler → Hardware
|
||||
|
||||
```
|
||||
User drags block into workspace
|
||||
│
|
||||
▼
|
||||
[Run button pressed]
|
||||
│
|
||||
▼
|
||||
Blockly generates JS code from block.generator()
|
||||
─ Statement: string code ending with \n
|
||||
─ Output: [expression_string, Order]
|
||||
│
|
||||
▼
|
||||
debug-engine.js executes the generated JS
|
||||
│
|
||||
▼
|
||||
await executeAction('my_command', { key: 'value', … })
|
||||
→ bridge.js calls window.pywebview.api.execute_action(command, keys, values)
|
||||
│
|
||||
▼
|
||||
BlocklyAPI.execute_action() in app.py
|
||||
→ ROS2 Action Client sends Goal { command, param_keys, param_values }
|
||||
│
|
||||
▼
|
||||
ExecutorNode receives goal, calls HandlerRegistry.execute(command, params)
|
||||
│
|
||||
▼
|
||||
@handler("my_command") function(params, hardware)
|
||||
→ hardware.log(...) ← always logged for testing
|
||||
→ if hardware.is_real(): ... ← real hardware (ROS2 publish/GPIO)
|
||||
│
|
||||
▼
|
||||
Returns (True, "LED on pin 3 turned ON")
|
||||
→ goal_handle.succeed() → result.success=True, result.message=...
|
||||
│
|
||||
▼
|
||||
await executeAction() resolves → JS continues to next block
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Checklist — Adding a New Block
|
||||
|
||||
**Path A — ROS2 Action Block** (robot commands, sensor reads):
|
||||
|
||||
```
|
||||
1. Create src/blockly_app/…/ui/blockly/blocks/<name>.js
|
||||
└─ Call BlockRegistry.register({ name, category, color, definition, generator })
|
||||
└─ generator calls await executeAction('name', { key: 'value' })
|
||||
└─ name must exactly match the Python @handler string
|
||||
|
||||
2. Edit src/blockly_app/…/ui/blockly/blocks/manifest.js
|
||||
└─ Add '<name>.js' to the BLOCK_FILES array
|
||||
|
||||
3. Create src/blockly_executor/…/handlers/<name>.py (or add to existing file)
|
||||
└─ from . import handler
|
||||
└─ from .hardware import Hardware
|
||||
└─ @handler("name")
|
||||
└─ def handle_name(params, hardware: Hardware): ... → return (bool, str)
|
||||
└─ hardware.log(...) for testing + hardware.is_real() for real logic
|
||||
|
||||
4. Test pixi run executor (Terminal 1)
|
||||
pixi run app (Terminal 2) — drag block, click Run
|
||||
pixi run test -- src/blockly_executor/test/test_block_<name>.py -v
|
||||
```
|
||||
|
||||
**Path B — Client-side Block** (print, HMI, pure JS):
|
||||
|
||||
```
|
||||
1. Create src/blockly_app/…/ui/blockly/blocks/<name>.js
|
||||
└─ Call BlockRegistry.register({ name, category, color, definition, generator })
|
||||
└─ generator calls JS function directly (consoleLog(), HMI.set*(), etc.)
|
||||
└─ NO executeAction(), NO Python handler needed
|
||||
|
||||
2. Edit src/blockly_app/…/ui/blockly/blocks/manifest.js
|
||||
└─ Add '<name>.js' to the BLOCK_FILES array
|
||||
|
||||
3. Test pixi run app — drag block, click Run (no executor needed for client-side blocks)
|
||||
```
|
||||
|
||||
No changes needed to `index.html`, `executor_node.py`, `handlers/__init__.py`, or `BlocklyAction.action`.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,383 @@
|
|||
# Block API Reference
|
||||
|
||||
## Block Type Overview
|
||||
|
||||
There are three fundamental block shapes. Choose based on what the block **does**:
|
||||
|
||||
| Type | Shape | Returns | Use case | Examples |
|
||||
|------|-------|---------|----------|---------|
|
||||
| **Statement** | Top + bottom notches | Nothing (side effect) | Execute an action on the robot | `digital_out`, `delay` |
|
||||
| **Output** | Left plug, no notches | A value | Read a sensor, compute something | `read_distance`, `read_temperature` |
|
||||
| **Container** | Statement input socket | Nothing | Wrap a block stack (loop body, condition body) | Custom `repeat_until_clear` |
|
||||
|
||||
> Output blocks can be plugged into `appendValueInput()` sockets of other blocks (e.g., plug `read_distance` into `move_to(X=..., Y=...)`)
|
||||
|
||||
---
|
||||
|
||||
## `BlockRegistry.register()` — All Fields
|
||||
|
||||
```js
|
||||
BlockRegistry.register({
|
||||
// ── Required ────────────────────────────────────────────────────────────
|
||||
name: 'my_command', // Unique block ID. Must match Python @handler("my_command")
|
||||
category: 'Robot', // Toolbox category label. New names auto-create a category.
|
||||
color: '#4CAF50', // Block body hex color
|
||||
definition: { init: function() { … } }, // Blockly visual definition
|
||||
generator: function(block) { … }, // JS code generator
|
||||
|
||||
// ── Optional ────────────────────────────────────────────────────────────
|
||||
categoryColor: '#5b80a5', // Category sidebar color (default: '#5b80a5')
|
||||
tooltip: 'Description', // Hover text (also set inside init via setTooltip)
|
||||
outputType: 'Number', // Set ONLY for output blocks ('Number', 'String', 'Boolean')
|
||||
// When present, generator must return [code, Order] array
|
||||
});
|
||||
```
|
||||
|
||||
**`name` is the contract** — it must exactly match the `@handler("name")` string in Python. A mismatch causes `Unknown command: 'name'` at runtime.
|
||||
|
||||
---
|
||||
|
||||
## `definition.init` — Complete Reference
|
||||
|
||||
The `definition` field takes a plain object with one required key: `init`. This function runs once when Blockly creates the block. Inside `init`, `this` refers to the block instance.
|
||||
|
||||
**Call order inside `init`:**
|
||||
1. `appendXxxInput()` rows — build the visual layout top-to-bottom
|
||||
2. Connection methods — define how the block connects to others
|
||||
3. Style methods — set color and tooltip
|
||||
|
||||
---
|
||||
|
||||
### Input Rows — Visual Layout
|
||||
|
||||
Every input row is a horizontal strip. Rows stack top-to-bottom. Fields chain left-to-right within a row.
|
||||
|
||||
```
|
||||
appendDummyInput() appendValueInput('X') appendStatementInput('DO')
|
||||
┌───────────────────────────┐ ┌─────────────────────┬──┐ ┌──────────────────────────┐
|
||||
│ [label] [field] [label]│ │ [label] [field] │◄─┤socket │ do │
|
||||
└───────────────────────────┘ └─────────────────────┴──┘ │ ┌────────────────────┐ │
|
||||
no sockets, no plug right side has an input socket │ │ (block stack here) │ │
|
||||
that accepts output block plugs │ └────────────────────┘ │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `appendDummyInput()` — Label / field row (no sockets)
|
||||
|
||||
Use when the block only needs static inline fields. No output block can connect here.
|
||||
|
||||
```js
|
||||
init: function () {
|
||||
this.appendDummyInput() // creates one horizontal row
|
||||
.appendField('Speed') // text label (no key needed)
|
||||
.appendField(
|
||||
new Blockly.FieldNumber(50, 0, 100, 1), 'SPEED'
|
||||
) // interactive field — key 'SPEED'
|
||||
.appendField('%'); // another text label after the field
|
||||
|
||||
// Visual: ┌─ Speed [50] % ─┐
|
||||
}
|
||||
```
|
||||
|
||||
Multiple `appendDummyInput()` calls stack as separate rows:
|
||||
|
||||
```js
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField('Motor Config'); // header label row
|
||||
this.appendDummyInput()
|
||||
.appendField('Left ')
|
||||
.appendField(new Blockly.FieldNumber(50, -100, 100, 1), 'LEFT');
|
||||
this.appendDummyInput()
|
||||
.appendField('Right')
|
||||
.appendField(new Blockly.FieldNumber(50, -100, 100, 1), 'RIGHT');
|
||||
|
||||
// Visual:
|
||||
// ┌──────────────────────────┐
|
||||
// │ Motor Config │
|
||||
// │ Left [-100..100] │
|
||||
// │ Right [-100..100] │
|
||||
// └──────────────────────────┘
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `appendValueInput('KEY')` — Socket row (accepts output blocks)
|
||||
|
||||
Use when you want the user to plug in a sensor or value block. The socket appears on the **right side** of the row. The label (if any) appears on the left.
|
||||
|
||||
```js
|
||||
init: function () {
|
||||
this.appendValueInput('SPEED') // 'SPEED' is the key used in valueToCode()
|
||||
.setCheck('Number') // only accept Number-type output blocks
|
||||
// use setCheck(null) or omit to accept any type
|
||||
.appendField('Drive at'); // label to the left of the socket
|
||||
|
||||
// Visual: ┌─ Drive at ◄──(output block plugs here) ─┐
|
||||
}
|
||||
```
|
||||
|
||||
Multiple `appendValueInput` rows, collapsed inline with `setInputsInline(true)`:
|
||||
|
||||
```js
|
||||
init: function () {
|
||||
this.appendValueInput('X').setCheck('Number').appendField('X');
|
||||
this.appendValueInput('Y').setCheck('Number').appendField('Y');
|
||||
this.setInputsInline(true); // ← collapses rows side-by-side
|
||||
|
||||
// setInputsInline(false) (default): rows stacked vertically
|
||||
// setInputsInline(true): rows placed side-by-side on one line
|
||||
|
||||
// Visual (inline): ┌─ X ◄─ Y ◄─ ─┐
|
||||
}
|
||||
```
|
||||
|
||||
Reading the plugged-in block value in the generator:
|
||||
```js
|
||||
generator: function (block) {
|
||||
// valueToCode returns the generated expression for the plugged-in block
|
||||
// Falls back to the default ('0', '', etc.) if the socket is empty
|
||||
const x = javascript.javascriptGenerator.valueToCode(
|
||||
block, 'X', javascript.Order.ATOMIC // Order.ATOMIC: value is a safe atom (no parens needed)
|
||||
) || '0'; // || '0': fallback if socket is empty
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `appendStatementInput('KEY')` — Indented block stack slot
|
||||
|
||||
Use for container blocks (loops, conditionals) where the user places a stack of blocks inside.
|
||||
|
||||
```js
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('While obstacle');
|
||||
|
||||
this.appendStatementInput('BODY') // 'BODY' is the key for statementToCode()
|
||||
.appendField('do'); // optional label next to the slot opening
|
||||
|
||||
// Visual:
|
||||
// ┌────────────────────────────────┐
|
||||
// │ While obstacle │
|
||||
// │ do │
|
||||
// │ ┌──────────────────────┐ │
|
||||
// │ │ (blocks stack here) │ │
|
||||
// │ └──────────────────────┘ │
|
||||
// └────────────────────────────────┘
|
||||
}
|
||||
```
|
||||
|
||||
Reading the inner stack in the generator:
|
||||
```js
|
||||
generator: function (block) {
|
||||
const inner = javascript.javascriptGenerator.statementToCode(block, 'BODY');
|
||||
// statementToCode returns the generated code for ALL blocks stacked inside 'BODY'
|
||||
// Returns '' if the slot is empty
|
||||
return "while (true) {\n" + inner + "}\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Connection / Shape Methods
|
||||
|
||||
These define the block's shape. Call them **after** all `appendXxx` rows.
|
||||
|
||||
```js
|
||||
// ── Statement block (stackable in a sequence) ─────────────────────────────
|
||||
this.setPreviousStatement(true, null);
|
||||
// ↑ ↑── type filter: null = accept any block above
|
||||
// └── true = show top notch
|
||||
this.setNextStatement(true, null);
|
||||
// same params — shows bottom notch so another block can connect below
|
||||
|
||||
// ── Output block (value-returning, plugs into sockets) ────────────────────
|
||||
this.setOutput(true, 'Number');
|
||||
// ↑── output type: 'Number' | 'String' | 'Boolean' | null (any)
|
||||
// ⚠ setOutput is MUTUALLY EXCLUSIVE with setPreviousStatement / setNextStatement
|
||||
|
||||
// ── Standalone (no connections — rare) ───────────────────────────────────
|
||||
// Omit all three. Block floats freely, cannot connect to anything.
|
||||
```
|
||||
|
||||
Type filter (second argument) restricts which blocks can connect:
|
||||
|
||||
```js
|
||||
this.setPreviousStatement(true, 'RobotAction'); // only connects below RobotAction blocks
|
||||
this.setNextStatement(true, 'RobotAction');
|
||||
// Matching blocks must also declare:
|
||||
// this.setOutput(true, 'RobotAction') ← on the output side
|
||||
// Rarely needed in this project — use null for unrestricted.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Style Methods
|
||||
|
||||
```js
|
||||
this.setColour('#4CAF50'); // block body color — hex string OR hue int (0–360)
|
||||
this.setTooltip('Hover text'); // shown when user hovers over the block
|
||||
this.setHelpUrl('https://...'); // optional: opens URL when user clicks '?' on block
|
||||
```
|
||||
|
||||
> Always use **hex strings** (e.g. `'#4CAF50'`) to match the project's color scheme. The `color` field in `BlockRegistry.register()` and `setColour()` inside `init` should use the same value.
|
||||
|
||||
---
|
||||
|
||||
### Layout Control — `setInputsInline`
|
||||
|
||||
```js
|
||||
this.setInputsInline(true); // compact: all appendValueInput rows on ONE line
|
||||
this.setInputsInline(false); // (default) each appendValueInput row on its OWN line
|
||||
```
|
||||
|
||||
`setInputsInline` only affects `appendValueInput` rows — `appendDummyInput` rows are always inline.
|
||||
|
||||
---
|
||||
|
||||
### Complete Multi-Row Example
|
||||
|
||||
A block combining all input types:
|
||||
|
||||
```js
|
||||
definition: {
|
||||
init: function () {
|
||||
// ── Row 1: DummyInput — header label + FieldNumber ──────────────────
|
||||
this.appendDummyInput()
|
||||
.appendField('Drive until <')
|
||||
.appendField(new Blockly.FieldNumber(20, 1, 500, 1), 'THRESHOLD')
|
||||
.appendField('cm');
|
||||
|
||||
// ── Row 2+3: ValueInput — two sockets, collapsed inline ─────────────
|
||||
this.appendValueInput('SPEED_L')
|
||||
.setCheck('Number')
|
||||
.appendField('L');
|
||||
this.appendValueInput('SPEED_R')
|
||||
.setCheck('Number')
|
||||
.appendField('R');
|
||||
this.setInputsInline(true); // collapse rows 2+3 side-by-side
|
||||
|
||||
// ── Row 4: StatementInput — block stack slot ─────────────────────────
|
||||
this.appendStatementInput('ON_ARRIVAL')
|
||||
.appendField('then');
|
||||
|
||||
// ── Connection shape ─────────────────────────────────────────────────
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
|
||||
// ── Style ────────────────────────────────────────────────────────────
|
||||
this.setColour('#FF5722');
|
||||
this.setTooltip('Drive at given speeds until obstacle is closer than threshold');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const threshold = block.getFieldValue('THRESHOLD');
|
||||
const speedL = javascript.javascriptGenerator.valueToCode(block, 'SPEED_L', javascript.Order.ATOMIC) || '50';
|
||||
const speedR = javascript.javascriptGenerator.valueToCode(block, 'SPEED_R', javascript.Order.ATOMIC) || '50';
|
||||
const body = javascript.javascriptGenerator.statementToCode(block, 'ON_ARRIVAL');
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('drive_until', {" +
|
||||
" threshold: '" + threshold + "'," +
|
||||
" speed_l: String(" + speedL + ")," +
|
||||
" speed_r: String(" + speedR + ")" +
|
||||
" });\n" +
|
||||
body
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
Visual result:
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Drive until < [20] cm │
|
||||
│ L ◄─(speed) R ◄─(speed) │
|
||||
│ then │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ (on-arrival blocks stack here) │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `executeAction()` and `highlightBlock()` Reference
|
||||
|
||||
Both functions are provided by [bridge.js](../blockly_app/ui/blockly/core/bridge.js) and injected into the eval context by [debug-engine.js](../blockly_app/ui/blockly/core/debug-engine.js).
|
||||
|
||||
### `await executeAction(command, params)`
|
||||
|
||||
```js
|
||||
// Sends a ROS2 action goal and waits for the result.
|
||||
// Returns: { success: bool, message: string }
|
||||
const result = await executeAction('my_command', {
|
||||
key1: 'value1', // all values must be strings
|
||||
key2: String(someNumber),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(result.message);
|
||||
}
|
||||
```
|
||||
|
||||
- **Always `await`** — omitting `await` means the next block runs immediately before the hardware finishes.
|
||||
- **All param values must be strings** — the ROS2 action interface uses `string[]` arrays. Use `String(n)` or template literals for numbers.
|
||||
- Returns after the Python handler calls `goal_handle.succeed()` (which is always called, even for logical failures).
|
||||
|
||||
### `highlightBlock(blockId)`
|
||||
|
||||
```js
|
||||
highlightBlock(block.id);
|
||||
```
|
||||
|
||||
Visually highlights the currently executing block in the workspace. Call it **first** in every statement generator so the user can see which block is running. For output blocks it is optional (they have no visual position in the stack).
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Blockly Field Types
|
||||
|
||||
| Field | Constructor | Returns | Use Case |
|
||||
|-------|-------------|---------|----------|
|
||||
| **Number** | `new Blockly.FieldNumber(default, min, max, step)` | number string | Pin, duration, speed, PWM |
|
||||
| **Text** | `new Blockly.FieldTextInput('default')` | string | Topic name, label |
|
||||
| **Dropdown** | `new Blockly.FieldDropdown([['Label','value'], …])` | value string | Direction, sensor ID, mode |
|
||||
| **Checkbox** | `new Blockly.FieldCheckbox('TRUE')` | `'TRUE'` / `'FALSE'` | On/off toggle, enable flag |
|
||||
| **Colour** | `new Blockly.FieldColour('#ff0000')` | hex string | LED RGB color |
|
||||
| **Angle** | `new Blockly.FieldAngle(90)` | angle string | Rotation, steering |
|
||||
| **Image** | `new Blockly.FieldImage('url', w, h)` | _(no value)_ | Icon decoration on block |
|
||||
|
||||
All field values retrieved via `block.getFieldValue('FIELD_NAME')` are **strings**. Cast with `parseInt()`, `parseFloat()`, or `Number()` in JS, or `int()` / `float()` in Python, as needed.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Input and Connection Types
|
||||
|
||||
| Method | What it creates | When to use |
|
||||
|--------|----------------|-------------|
|
||||
| `setPreviousStatement(true, null)` | Top notch | Block can connect below another block |
|
||||
| `setNextStatement(true, null)` | Bottom notch | Block can connect above another block |
|
||||
| `setOutput(true, 'Type')` | Left plug | Block returns a value (output block) — mutually exclusive with previous/next |
|
||||
| `appendDummyInput()` | Horizontal row | Inline fields only, no sockets |
|
||||
| `appendValueInput('NAME')` | Input socket (right side) | Accept an output block plug |
|
||||
| `appendStatementInput('NAME')` | Statement socket (indented slot) | Accept a block stack (loop body, etc.) |
|
||||
| `setInputsInline(true)` | Collapse multi-input rows into one line | When `appendValueInput` rows should be side-by-side |
|
||||
| `.setCheck('Type')` | Type constraint on socket | Restrict which output blocks can plug in (`'Number'`, `'String'`, `'Boolean'`) |
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Item | Convention | Example |
|
||||
|------|-----------|---------|
|
||||
| Block `name` / `@handler` string | `snake_case` | `digital_out`, `read_distance`, `move_to` |
|
||||
| JS file | `<name>.js` or `camelCase.js` | `digitalOut.js`, `digitalIn.js` |
|
||||
| Python handler file | `<topic>.py` (group related handlers) | `gpio.py`, `timing.py`, `motors.py` |
|
||||
| Field key in `getFieldValue` | `UPPER_SNAKE` | `'PIN'`, `'DURATION_MS'`, `'SENSOR_ID'` |
|
||||
| param key in `executeAction` | `snake_case` | `{ pin: '3' }`, `{ duration_ms: '500' }` |
|
||||
| param key in Python `params["…"]` | `snake_case` | `params["duration_ms"]` |
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
# Block Templates
|
||||
|
||||
Copy-paste starting points for all block shapes. Choose the template that matches your block type, then customize.
|
||||
|
||||
---
|
||||
|
||||
## Template A — Statement Block (action command)
|
||||
|
||||
Copy-paste starting point for any new action block:
|
||||
|
||||
```js
|
||||
// src/blockly_app/blockly_app/ui/blockly/blocks/MY_BLOCK.js
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'MY_COMMAND', // ← change this (must match @handler)
|
||||
category: 'Robot', // ← choose or create a category
|
||||
categoryColor: '#5b80a5',
|
||||
color: '#9C27B0', // ← pick a hex color
|
||||
tooltip: 'Short description of what this block does',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Label text')
|
||||
.appendField(new Blockly.FieldNumber(0, 0, 100, 1), 'PARAM1');
|
||||
// add more .appendField(...) calls for more parameters
|
||||
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#9C27B0');
|
||||
this.setTooltip('Short description of what this block does');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const param1 = block.getFieldValue('PARAM1');
|
||||
// Statement generators return a string (not an array)
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('MY_COMMAND', { param1: '" + param1 + "' });\n"
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Corresponding Python handler:
|
||||
```python
|
||||
# src/blockly_executor/blockly_executor/handlers/MY_FILE.py
|
||||
from . import handler
|
||||
from .hardware import Hardware
|
||||
|
||||
@handler("MY_COMMAND")
|
||||
def handle_my_command(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
param1 = params["param1"] # always str — cast as needed (int, float, etc.)
|
||||
hardware.log(f"my_command(param1={param1})")
|
||||
|
||||
if hardware.is_real():
|
||||
# Real hardware logic — ROS2 publish, GPIO, etc.
|
||||
# Use lazy imports here for dependencies not available in dev
|
||||
hardware.node.get_logger().info(f"my_command: {param1}")
|
||||
|
||||
return (True, f"Done: {param1}")
|
||||
```
|
||||
|
||||
After creating both files, add the JS filename to [`manifest.js`](../blockly_app/ui/blockly/blocks/manifest.js):
|
||||
```js
|
||||
const BLOCK_FILES = [
|
||||
'digitalOut.js',
|
||||
'digitalIn.js',
|
||||
'delay.js',
|
||||
'MY_BLOCK.js', // ← add here
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template B — Output Block (sensor / computed value)
|
||||
|
||||
Output blocks expose a **left plug** and plug into input sockets of other blocks. They have **no** top/bottom notches and cannot stand alone in a stack.
|
||||
|
||||
```js
|
||||
// src/blockly_app/blockly_app/ui/blockly/blocks/read_distance.js
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'read_distance',
|
||||
category: 'Sensors',
|
||||
categoryColor: '#a5745b',
|
||||
color: '#E91E63',
|
||||
tooltip: 'Read distance from ultrasonic sensor in cm',
|
||||
outputType: 'Number', // ← required for output blocks; determines socket type-check
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Distance')
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
['Front', 'front'],
|
||||
['Left', 'left'],
|
||||
['Right', 'right'],
|
||||
]), 'SENSOR_ID');
|
||||
|
||||
this.setOutput(true, 'Number'); // ← left plug; NO setPreviousStatement / setNextStatement
|
||||
this.setColour('#E91E63');
|
||||
this.setTooltip('Read distance from ultrasonic sensor in cm');
|
||||
},
|
||||
},
|
||||
|
||||
// Output block generators MUST return [expression_string, Order] — NOT a plain string
|
||||
generator: function (block) {
|
||||
const sensorId = block.getFieldValue('SENSOR_ID');
|
||||
// executeAction returns { success, message } — use .message to extract the value
|
||||
const code = "(await executeAction('read_distance', { sensor_id: '" + sensorId + "' })).message";
|
||||
return [code, javascript.Order.AWAIT];
|
||||
// ↑ expression ↑ operator precedence (Order.AWAIT for async expressions)
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Python handler:
|
||||
```python
|
||||
@handler("read_distance")
|
||||
def handle_read_distance(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
sensor_id = params["sensor_id"]
|
||||
hardware.log(f"read_distance(sensor_id={sensor_id})")
|
||||
|
||||
if hardware.is_real():
|
||||
# TODO: subscribe to sensor topic, return actual reading
|
||||
distance = 0.0 # placeholder
|
||||
else:
|
||||
distance = 42.0 # dummy test value
|
||||
|
||||
return (True, str(distance)) # message becomes the expression value in JS
|
||||
```
|
||||
|
||||
> The JS expression `.message` retrieves the string from the ROS2 result. If the value needs to be numeric, wrap it: `parseFloat((await executeAction(…)).message)`.
|
||||
|
||||
---
|
||||
|
||||
## Template C — Block with Value Input Sockets (accepts other blocks)
|
||||
|
||||
Value input sockets let output blocks plug into a statement block as dynamic parameters.
|
||||
|
||||
```js
|
||||
// src/blockly_app/blockly_app/ui/blockly/blocks/move_to.js
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'move_to',
|
||||
category: 'Navigation',
|
||||
categoryColor: '#5ba55b',
|
||||
color: '#00BCD4',
|
||||
tooltip: 'Move robot to target X and Y coordinates',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
// appendValueInput creates a socket where output blocks plug in
|
||||
this.appendValueInput('X')
|
||||
.setCheck('Number') // only accept Number-type output blocks
|
||||
.appendField('Move to X');
|
||||
this.appendValueInput('Y')
|
||||
.setCheck('Number')
|
||||
.appendField('Y');
|
||||
this.setInputsInline(true); // place inputs side-by-side instead of stacked
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#00BCD4');
|
||||
this.setTooltip('Move robot to target X and Y coordinates');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
// valueToCode generates code for the plugged-in block; falls back to '0' if empty
|
||||
const x = javascript.javascriptGenerator.valueToCode(block, 'X', javascript.Order.ATOMIC) || '0';
|
||||
const y = javascript.javascriptGenerator.valueToCode(block, 'Y', javascript.Order.ATOMIC) || '0';
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('move_to', { x: String(" + x + "), y: String(" + y + ") });\n"
|
||||
// ↑ wrap in String() — params must be strings
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Python handler:
|
||||
```python
|
||||
@handler("move_to")
|
||||
def handle_move_to(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
x = float(params["x"])
|
||||
y = float(params["y"])
|
||||
hardware.log(f"move_to(x={x}, y={y})")
|
||||
|
||||
if hardware.is_real():
|
||||
# TODO: publish to cmd_vel or navigation topic
|
||||
hardware.node.get_logger().info(f"Moving to ({x}, {y})")
|
||||
|
||||
return (True, f"Moved to ({x}, {y})")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template D — Container Block (statement input socket)
|
||||
|
||||
Container blocks hold a **stack of other blocks** inside them, like a loop or conditional. They use `appendStatementInput()`.
|
||||
|
||||
```js
|
||||
BlockRegistry.register({
|
||||
name: 'repeat_n_times',
|
||||
category: 'Control',
|
||||
categoryColor: '#5ba55b',
|
||||
color: '#FF5722',
|
||||
tooltip: 'Repeat the inner blocks N times',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Repeat')
|
||||
.appendField(new Blockly.FieldNumber(3, 1, 100, 1), 'TIMES')
|
||||
.appendField('times');
|
||||
this.appendStatementInput('DO') // ← creates an indented slot for block stacks
|
||||
.appendField('do');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#FF5722');
|
||||
this.setTooltip('Repeat the inner blocks N times');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const times = block.getFieldValue('TIMES');
|
||||
// statementToCode generates code for all blocks stacked inside DO
|
||||
const inner = javascript.javascriptGenerator.statementToCode(block, 'DO');
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"for (let _i = 0; _i < " + times + "; _i++) {\n" +
|
||||
inner +
|
||||
"}\n"
|
||||
);
|
||||
// Note: no executeAction needed — this block is pure JS control flow
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> Container blocks that implement pure JS control flow (loops, if/else) do **not** need a Python handler. Only blocks that call `executeAction()` need one.
|
||||
|
||||
---
|
||||
|
||||
## Template E — Client-side Statement Block (no Python handler)
|
||||
|
||||
Client-side blocks call JavaScript functions directly — no `executeAction()`, no Python handler, no ROS2 round-trip. Used for **print** (console output) and **HMI widgets** (LED, Number, Text, Gauge).
|
||||
|
||||
**Print block** — calls `consoleLog()` directly:
|
||||
|
||||
```js
|
||||
// src/blockly_app/blockly_app/ui/blockly/blocks/print.js
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'print',
|
||||
category: 'Program',
|
||||
categoryColor: '#FF9800',
|
||||
color: '#FFCA28',
|
||||
tooltip: 'Print a value to the console for debugging',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendValueInput('TEXT')
|
||||
.appendField('Print');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#FFCA28');
|
||||
this.setTooltip('Print a value to the console for debugging');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
var value = Blockly.JavaScript.valueToCode(
|
||||
block, 'TEXT', Blockly.JavaScript.ORDER_ATOMIC
|
||||
) || "''";
|
||||
return (
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
"consoleLog(String( ' USER LOG >>> ' + " + value + "), 'print');\n"
|
||||
// ^^^^^^^^^^^^^^^^ direct JS function call — no executeAction!
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**No Python handler needed.** The block works entirely in the browser. Add the JS filename to `manifest.js` and it's done.
|
||||
|
||||
---
|
||||
|
||||
## Template F — HMI Widget Block (client-side, design-time preview)
|
||||
|
||||
HMI blocks create/update widgets in the HMI Panel. They call `HMI.set*()` functions directly (client-side) and support design-time preview — widgets appear when blocks are placed, not just at runtime.
|
||||
|
||||
**HMI LED block:**
|
||||
|
||||
```js
|
||||
// src/blockly_app/blockly_app/ui/blockly/blocks/hmiSetLed.js
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'hmiSetLed',
|
||||
category: 'HMI',
|
||||
categoryColor: '#00BCD4',
|
||||
color: '#00BCD4',
|
||||
tooltip: 'Set an LED indicator on/off in the HMI panel',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendValueInput('STATE')
|
||||
.appendField('HMI LED')
|
||||
.appendField(new Blockly.FieldTextInput('LED1'), 'NAME')
|
||||
.appendField('color')
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
['green', '#4caf50'],
|
||||
['red', '#f44336'],
|
||||
['yellow', '#ffeb3b'],
|
||||
['blue', '#2196f3'],
|
||||
['orange', '#ff9800'],
|
||||
['white', '#ffffff'],
|
||||
]), 'COLOR')
|
||||
.setCheck('Boolean')
|
||||
.appendField('state:');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#00BCD4');
|
||||
this.setTooltip('Set an LED indicator on/off in the HMI panel');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
var color = block.getFieldValue('COLOR');
|
||||
var state = Blockly.JavaScript.valueToCode(
|
||||
block, 'STATE', Blockly.JavaScript.ORDER_ATOMIC
|
||||
) || 'false';
|
||||
return (
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
"HMI.setLED('" + name + "', Boolean(" + state + "), '" + color + "');\n"
|
||||
// ^^^^^^^^^^ direct call to HMI manager — no executeAction!
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**HMI Number block** — numeric display with unit:
|
||||
|
||||
```js
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
var unit = block.getFieldValue('UNIT');
|
||||
var value = Blockly.JavaScript.valueToCode(
|
||||
block, 'VALUE', Blockly.JavaScript.ORDER_ATOMIC
|
||||
) || '0';
|
||||
return (
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
"HMI.setNumber('" + name + "', Number(" + value + "), '" + unit + "');\n"
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
**HMI Gauge block** — bar gauge with min/max:
|
||||
|
||||
```js
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
var min = block.getFieldValue('MIN');
|
||||
var max = block.getFieldValue('MAX');
|
||||
var value = Blockly.JavaScript.valueToCode(
|
||||
block, 'VALUE', Blockly.JavaScript.ORDER_ATOMIC
|
||||
) || '0';
|
||||
return (
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
"HMI.setGauge('" + name + "', Number(" + value + "), " + min + ", " + max + ");\n"
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
**Key differences from ROS2 action blocks:**
|
||||
- Generator calls `HMI.set*()` instead of `executeAction()`
|
||||
- No Python handler file needed
|
||||
- Widgets get design-time preview via `hmi-preview.js` (auto-detected by block type)
|
||||
- All HMI blocks use `FieldTextInput('Name')` for widget identifier (`NAME` field)
|
||||
- Category: `HMI`, color: `#00BCD4`
|
||||
|
||||
**Adding a new HMI widget type:**
|
||||
1. Add render function `_renderFoo(body, widget)` in `core/hmi-manager.js`
|
||||
2. Add public API method `setFoo(name, ...)` that stores state and calls `_scheduleRender(name)`
|
||||
3. Add `case 'foo':` to the `_render()` switch
|
||||
4. Create block file `blocks/hmiFoo.js` — generator calls `HMI.setFoo(...)`
|
||||
5. Add to `manifest.js`
|
||||
6. Add entry to `HMI_BLOCK_TYPES` in `core/hmi-preview.js` for design-time preview
|
||||
|
||||
### HMI Control Blocks (interactive — SET + GET pair)
|
||||
|
||||
Control widgets (Button, Slider, Switch) have both a **SET block** (statement, create/configure) and a **GET block** (value, read user interaction). User interaction state is tracked separately from programmatic state to prevent HMI loop overwrites.
|
||||
|
||||
**HMI Button GET block** (value, latch-until-read):
|
||||
|
||||
```js
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
return ['HMI.getButton(\'' + name + '\')', Blockly.JavaScript.ORDER_FUNCTION_CALL];
|
||||
// Returns Boolean — true once per click, then auto-resets to false
|
||||
},
|
||||
```
|
||||
|
||||
**HMI Slider GET block** (value, user-drag tracking):
|
||||
|
||||
```js
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
return ['HMI.getSlider(\'' + name + '\')', Blockly.JavaScript.ORDER_FUNCTION_CALL];
|
||||
// Returns Number — user-dragged value takes priority over programmatic setSlider()
|
||||
},
|
||||
```
|
||||
|
||||
**HMI Switch GET block** (value, user-toggle tracking):
|
||||
|
||||
```js
|
||||
generator: function (block) {
|
||||
var name = block.getFieldValue('NAME');
|
||||
return ['HMI.getSwitch(\'' + name + '\')', Blockly.JavaScript.ORDER_FUNCTION_CALL];
|
||||
// Returns Boolean — user-toggled state takes priority over programmatic setSwitch()
|
||||
},
|
||||
```
|
||||
|
||||
**User interaction tracking pattern:**
|
||||
- **Button**: `pointerdown` event → `widget._pressed = true`. `getButton()` reads and resets (latch).
|
||||
- **Slider**: `_userHasInteracted` flag persists after drag release. `setSlider()` skips `_userValue` overwrite once user has interacted.
|
||||
- **Switch**: `_userState` stores user toggle separately. `setSwitch()` skips re-render once `_userState` is set.
|
||||
- **Design-time**: `hmi-preview.js` auto-increments widget names when duplicate blocks are placed (`LED1` → `LED2`).
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
# Real Block Examples
|
||||
|
||||
Annotated source code of blocks already built in this project, and a master table of all current block categories.
|
||||
|
||||
---
|
||||
|
||||
## `digitalOut` — Statement block with FieldNumber + ValueInput
|
||||
|
||||
**JS** ([blocks/digitalOut.js](../blockly_app/ui/blockly/blocks/digitalOut.js)):
|
||||
```js
|
||||
BlockRegistry.register({
|
||||
name: 'digitalOut',
|
||||
category: 'Robot',
|
||||
categoryColor: '#5b80a5',
|
||||
color: '#4CAF50',
|
||||
tooltip: 'set a digital output pin to HIGH (turn on LED)',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendValueInput('digitalOut')
|
||||
.appendField(' gpio:')
|
||||
.appendField(new Blockly.FieldNumber(1, 0, 27, 1), 'GPIO')
|
||||
// ↑ ↑ ↑ ↑
|
||||
// default min max step
|
||||
.setCheck('Boolean') // accepts Boolean output blocks
|
||||
.appendField(' state:');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#4CAF50');
|
||||
this.setTooltip('set a digital output pin to HIGH (turn on LED)');
|
||||
}
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const GPIO = block.getFieldValue('GPIO');
|
||||
const STATE = Blockly.JavaScript.valueToCode(
|
||||
block, 'digitalOut', Blockly.JavaScript.ORDER_ATOMIC
|
||||
) || 'false';
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('digital_out', { gpio: '" + GPIO + "', state: '" + STATE + "' });\n"
|
||||
// ↑ command name (must match @handler)
|
||||
// ↑ param keys ↑ param values (always string)
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Python** ([handlers/gpio.py](../../blockly_executor/blockly_executor/handlers/gpio.py)):
|
||||
```python
|
||||
from . import handler
|
||||
from .hardware import Hardware
|
||||
|
||||
@handler("digital_out") # ← must match JS name
|
||||
def handle_digital_out(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
gpio = int(params["gpio"]) # params values are always strings — cast
|
||||
state_raw = str(params["state"]).lower()
|
||||
state = state_raw in ("true", "1", "high") # explicit string-to-bool parsing
|
||||
state_str = "HIGH" if state else "LOW"
|
||||
hardware.log(f"set_digital_out(gpio={gpio}, state={state})")
|
||||
|
||||
if hardware.is_real():
|
||||
from blockly_interfaces.msg import GpioWrite
|
||||
pub = _get_gpio_write_publisher(hardware) # lazy publisher creation
|
||||
msg = GpioWrite()
|
||||
msg.pin = gpio
|
||||
msg.state = state
|
||||
pub.publish(msg) # publish to /gpio/write → gpio_node
|
||||
|
||||
return (True, f"GPIO pin {gpio} set to {state_str}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `digitalIn` — Output block (returns a value)
|
||||
|
||||
**JS** ([blocks/digitalIn.js](../blockly_app/ui/blockly/blocks/digitalIn.js)):
|
||||
```js
|
||||
BlockRegistry.register({
|
||||
name: 'digitalIn',
|
||||
category: 'Robot',
|
||||
categoryColor: '#5b80a5',
|
||||
color: '#E91E63',
|
||||
tooltip: 'Read digital input pin state (returns 0 or 1)',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Digital Read gpio:')
|
||||
.appendField(new Blockly.FieldNumber(5, 0, 27, 1), 'GPIO');
|
||||
this.setOutput(true, 'Number'); // ← output block, returns Number
|
||||
this.setColour('#E91E63');
|
||||
this.setTooltip('Read digital input pin state (returns 0 or 1)');
|
||||
}
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const gpio = block.getFieldValue('GPIO');
|
||||
const code =
|
||||
"parseFloat((await executeAction('digital_in', { gpio: '" + gpio + "' })).message)";
|
||||
return [code, Blockly.JavaScript.ORDER_AWAIT];
|
||||
// ↑ returns [code, precedence] for output blocks
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Python** ([handlers/gpio.py](../../blockly_executor/blockly_executor/handlers/gpio.py)):
|
||||
```python
|
||||
@handler("digital_in")
|
||||
def handle_digital_in(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
gpio = int(params["gpio"])
|
||||
hardware.log(f"read_digital_in(gpio={gpio})")
|
||||
|
||||
if hardware.is_real():
|
||||
cache = _get_gpio_state_subscriber(hardware) # lazy subscriber + cache
|
||||
with hardware.node._gpio_state_lock:
|
||||
state = cache.get(gpio)
|
||||
return (True, "1" if state else "0")
|
||||
|
||||
return (True, "0") # dummy mode — always LOW
|
||||
```
|
||||
|
||||
> Multiple handlers can live in the same `.py` file — they are auto-discovered by `pkgutil.iter_modules`.
|
||||
|
||||
---
|
||||
|
||||
## `delay` — Statement block with multiple field decorators
|
||||
|
||||
**JS** ([blocks/delay.js](../blockly_app/ui/blockly/blocks/delay.js)):
|
||||
```js
|
||||
BlockRegistry.register({
|
||||
name: 'delay',
|
||||
category: 'Robot',
|
||||
categoryColor: '#5b80a5',
|
||||
color: '#2196F3',
|
||||
tooltip: 'Wait for the specified duration in milliseconds',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('Delay')
|
||||
.appendField(new Blockly.FieldNumber(500, 0, 60000, 100), 'DURATION_MS')
|
||||
.appendField('ms'); // ← plain text label appended after the field
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour('#2196F3');
|
||||
this.setTooltip('Wait for the specified duration in milliseconds');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const ms = block.getFieldValue('DURATION_MS');
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('delay', { duration_ms: '" + ms + "' });\n"
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Python** ([handlers/timing.py](../../blockly_executor/blockly_executor/handlers/timing.py)):
|
||||
```python
|
||||
import time
|
||||
from . import handler
|
||||
from .hardware import Hardware
|
||||
|
||||
@handler("delay")
|
||||
def handle_delay(params: dict[str, str], hardware: Hardware) -> tuple[bool, str]:
|
||||
duration_ms = int(params["duration_ms"])
|
||||
time.sleep(duration_ms / 1000.0)
|
||||
return (True, f"Delayed {duration_ms}ms")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Block Type Overview — All Categories
|
||||
|
||||
| Category | Blocks | Execution Model |
|
||||
|----------|--------|-----------------|
|
||||
| **Program** | `main_program`, `main_hmi_program`, `print` | Entry points / client-side |
|
||||
| **Robot** | `digitalOut`, `digitalIn`, `delay`, `pwmWrite`, `odometryRead`, `odometryGet` | ROS2 action (except `odometryGet`) |
|
||||
| **HMI** | `hmiSetLed`, `hmiSetNumber`, `hmiSetText`, `hmiSetGauge`, `hmiSetButton`, `hmiGetButton`, `hmiSetSlider`, `hmiGetSlider`, `hmiSetSwitch`, `hmiGetSwitch` | Client-side (`HMI.set*()`/`HMI.get*()`) |
|
||||
| **Logic** | `controls_if`, `logic_compare`, `logic_operation`, `logic_boolean` | Built-in Blockly |
|
||||
| **Loops** | `controls_repeat_ext`, `controls_whileUntil` | Built-in Blockly |
|
||||
| **Math** | `math_number`, `math_arithmetic` | Built-in Blockly |
|
||||
| **Text** | `text`, `text_join`, `text_length` | Built-in Blockly |
|
||||
| **Variables** | `variables_set`, `variables_get` | Built-in Blockly (with highlight override) |
|
||||
| **Functions** | `procedures_defnoreturn`, `procedures_callnoreturn`, etc. | Built-in Blockly (with async override) |
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Creating Custom Blocks — Documentation Index
|
||||
|
||||
This directory documents how to create, configure, and extend Blockly blocks
|
||||
for the AMR ROS2 K4 visual programming interface.
|
||||
|
||||
## Contents
|
||||
|
||||
| File | What it covers |
|
||||
|------|----------------|
|
||||
| [01-quickstart.md](01-quickstart.md) | End-to-end walkthrough: create JS block file, register Python handler, write integration test, verify with CLI. Includes the full data-flow diagram and a step-by-step checklist. |
|
||||
| [02-block-api-ref.md](02-block-api-ref.md) | Complete API reference: `BlockRegistry.register()` fields, `definition.init` input rows, connection/shape methods, style methods, field type table, naming conventions, `executeAction()` / `highlightBlock()` runtime API. |
|
||||
| [03-templates.md](03-templates.md) | Copy-paste starting points for all block shapes: Template A (statement/ROS2), B (output/sensor), C (value input sockets), D (container/loop), E (client-side statement), F (HMI widget). |
|
||||
| [04-examples.md](04-examples.md) | Annotated real-project examples (`digitalOut`, `digitalIn`, `delay`) and a master table of all current block categories and their execution models. |
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
- **Adding your first block?** → Start with [01-quickstart.md](01-quickstart.md)
|
||||
- **Need a copy-paste template?** → Go to [03-templates.md](03-templates.md)
|
||||
- **Looking up a specific API (field types, connection methods)?** → [02-block-api-ref.md](02-block-api-ref.md)
|
||||
- **Want to see how an existing block is built?** → [04-examples.md](04-examples.md)
|
||||
|
||||
## Two Execution Models
|
||||
|
||||
All blocks fall into one of two categories:
|
||||
|
||||
| Model | Execution | Python handler needed? |
|
||||
|-------|-----------|----------------------|
|
||||
| **ROS2 action block** | `await executeAction()` → network round-trip to executor | Yes — `@handler("name")` in `handlers/<file>.py` |
|
||||
| **Client-side block** | Direct JS call (`consoleLog()`, `HMI.set*()`) | No |
|
||||
|
|
@ -133,7 +133,10 @@ def main(args=None):
|
|||
pass
|
||||
finally:
|
||||
node.destroy_node()
|
||||
try:
|
||||
rclpy.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -79,4 +79,4 @@ from blockly_interfaces.msg import GpioWrite, GpioRead, PwmWrite, EncoderRead
|
|||
|
||||
## Usage
|
||||
|
||||
See [`src/blockly_executor/README.md`](../blockly_executor/README.md) for how the executor uses this interface, and [`src/blockly_app/BLOCKS.md`](../blockly_app/BLOCKS.md#79-data-flow-js-block--python-handler--hardware) for the full data flow from JS block to hardware.
|
||||
See [`src/blockly_executor/README.md`](../blockly_executor/README.md) for how the executor uses this interface, and [`src/blockly_app/docs/01-quickstart.md`](../blockly_app/docs/01-quickstart.md#data-flow-js-block--python-handler--hardware) for the full data flow from JS block to hardware.
|
||||
|
|
|
|||
|
|
@ -386,6 +386,15 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "U,h/dhHRu214`~i0M6u$",
|
||||
"fields": {
|
||||
"DURATION_MS": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -743,7 +752,7 @@
|
|||
"name": "Switch1",
|
||||
"type": "switch",
|
||||
"x": 2,
|
||||
"y": 6,
|
||||
"y": 7,
|
||||
"w": 2,
|
||||
"h": 1,
|
||||
"config": {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue