Build a robot task out of blocks, watch it run in a digital twin, then run it on a real arm.
An End-User Development environment where a person with no programming background can compose a task for a Denso COBOTTA, including the steps the robot cannot do alone and has to wait for a human to do.
You describe a task in chat or drag it together out of blocks. The system turns it into a program the robot can run, shows you the arm executing it in a live 3D twin, and stops when the task reaches a step only a person can do.
A block that says "pause and show a message" suspends the interpreter. The arm holds the position it reached without dropping what it is carrying, and the program resumes when one of four channels says so.
| Channel | How you answer | Kind |
|---|---|---|
| Button | Click Confirm in the robot panel | direct |
| Voice | Say one of four commands (sì / no / fatto / procedi, Italian or English) |
direct |
| Gesture | Show one of six hand signs to your webcam | direct |
| Object | Put the expected object in front of the cell camera | indirect: nobody signals, the world changes |
Note
The same program runs in simulation and on the physical arm. Which one moves is decided by two independent keys, a server flag and a per-request flag. Neither alone moves anything.
Five processes on the local network. All of them have to be up for the full system.
┌──────────────────────────────┐
│ FRONTEND :3000 │ chat · Blockly editor · robot panel
└──────┬────────────────▲──────┘
task │ │ video + "I am on this block"
▼ │
┌──────────────────────────────┐
│ BACKEND :8000 │ REST · SQLite · LLM · inverse kinematics
└──────┬────────────────▲──────┘
joints │ │ "object seen" / "gesture seen"
▼ │
┌──────────────────────────────┐ ┌──────────────────┐
│ BRIDGE :5000 │───────▶│ VISION NODE │
│ HTTP → ROS 2 :5001 (ws) │◀───────│ YOLOE·MediaPipe │
└──────┬───────────────────────┘ └──────────────────┘
▼
┌──────────────────────────────┐ ····▶ ┌──────────────────┐
│ ROS 2 JAZZY + GAZEBO │ two │ DENSO COBOTTA │
│ gz_ros2_control · MJPEG:8080│ keys │ RC8 · b-CAP │
└──────────────────────────────┘ └──────────────────┘
Important
Two Python environments coexist, and mixing them breaks things.
| Environment | Holds | Used by |
|---|---|---|
Poetry (pyproject.toml) |
Django, ikpy, ultralytics, mediapipe |
backend, and vision_node |
ros2_ws/.venv (--system-site-packages) |
Flask, ROS 2 bindings | Flask bridge, all other ROS nodes |
This is also why bcapclient.py exists twice. Neither environment can see the other's packages,
so do not "deduplicate" it.
The Flask bridge exists for the same reason: without that boundary the Django backend would have to live inside the ROS environment and inherit its dependencies.
Tip
Code names and UI labels differ on purpose (action_block in code is "Skills" on screen).
Conversion table: docs/ui-naming-map.md.
Already set up? Three terminals, in this order.
# 1 — simulation stack (Gazebo, bridge, controllers, video)
cd ros2_ws/Cobotta && bash launch_sim.sh # add SKIP_BUILD=1 after the first run
# 2 — backend
poetry run python manage.py runserver
# 3 — frontend
npm startOpen http://localhost:3000 and log in as operator1 / Operator_1!.
# Did the stack actually come up? launch_sim.sh does not verify anything.
poetry run python testing/preflight.py1 · Prerequisites
- Ubuntu 24.04 (Noble), native or in WSL2 or a VM
- ROS 2 Jazzy (install guide)
- Gazebo Harmonic (install guide)
- Python 3.12 or 3.13, pinned
>=3.12,<3.14. Ubuntu 24.04 ships 3.12 - Poetry (install guide)
- Node.js 20.19+ or 22 LTS. Vite 8 requires
≥20.19/≥22.12; older 20.x fails
sudo apt update && sudo apt install -y \
ros-jazzy-ros-gz python3-colcon-common-extensions python3-rosdep \
python3-virtualenv curl psmisc build-essential cmake \
libboost-dev libboost-filesystem-dev libboost-thread-dev \
libopencv-dev libasio-dev
sudo rosdep init # "already initialized" is safe to ignore
rosdep updateros-jazzy-ros-gz provides the ros_gz_bridge that launch_sim.sh needs. The
build-essential/cmake/boost/asio packages compile the C++ streaming packages in ros2_ws/src/.
2 · Clone this repository and the streaming packages
git clone https://github.com/SickCiQuattro/DTblocklyGPT.git
cd DTblocklyGPT[!NOTE] This fork is where the collaborative-step, digital-twin and vision work lives. It is intended to be merged back into luigigargioni/DTblocklyGPT; until that happens, clone from here. Upstream does not contain any of it yet.
Two ROS packages are not bundled here and must be cloned into the workspace:
cd ros2_ws/src
git clone https://github.com/fkie/async_web_server_cpp.git
git clone https://github.com/RobotWebTools/web_video_server.git
cd ../..[!WARNING] Both are moving development branches, so a fresh clone may not match what this project was built against. The versions used were
async_web_server_cppatbe0ca7b(2026-05-06) andweb_video_serverat20c30ab(2026-07-02). Ifcolcon buildfails on a fresh clone, check those out before assuming the fault is here.
3 · Environment files
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.envbackend/.env. The chat assistant does not work without a key:
FLASK_BRIDGE_URL = "http://localhost:5000"
LLM_PROVIDER = "gemini" # gemini · openai · ollama
LLM_MODEL = "gemini-3.5-flash-lite"
GEMINI_API_KEY = "your_key_here"
OPENAI_API_KEY = ""[!CAUTION] Set
LLM_MODELexplicitly. Left unset, the code falls back togemini-2.5-flash, which is a different model from the one this project is configured and evaluated on, and nothing says so.
frontend/.env. All six values are required:
VITE_BACKEND_PROTOCOL = http://
VITE_BACKEND_HOST = localhost
VITE_BACKEND_PORT = :8000
VITE_FRONTEND_PROTOCOL = http://
VITE_FRONTEND_HOST = localhost
VITE_FRONTEND_PORT = :3000[!NOTE] The three
VITE_FRONTEND_*values are read by Django (CSRF_TRUSTED_ORIGINS), not by any frontend code, despite the prefix. Leave them empty andmanage.py runserverfails its system check.
4 · Dependencies
poetry install # backend: Django, ikpy, ultralytics, mediapipe
npm ci --legacy-peer-deps # frontend[!TIP] Use
npm ci, notnpm install.package-lock.jsonis committed andciinstalls exactly what it pins.blocklyis declared^13.0.0, and the editor's keyboard layer depends on shortcut names a Blockly release can rename, which is whyfeatures/blockly/editor/appShortcuts.tscarries a drift check. Runnpm installonly when you mean to take updates, then re-runnpm run lintand try the shortcuts.
The gesture model is not bundled with its wheel. Download it once:
mkdir -p backend/assets
curl -L "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task" \
-o backend/assets/hand_landmarker.taskyolov8n.pt downloads itself on first inference; the first run needs network access.
5 · Build the ROS 2 workspace
cd ros2_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
virtualenv .venv --system-site-packages
source .venv/bin/activate
pip install Flask flask-socketio flask-cors
colcon build # the WHOLE workspace — no --packages-select
source install/setup.bash
cd ..[!WARNING] Do not skip the full build.
launch_sim.shrebuilds onlycobotta_rest_api, so if this never ran,web_video_serveris missing and the camera panel stays empty at runtime.
6 · Database
db.sqlite3 is committed and already contains users, robots, locations and example tasks, so a
fresh clone needs nothing.
To rebuild the catalogue from scratch, the order is mandatory:
poetry run python manage.py seed_library # objects, locations, skills
poetry run python manage.py seed_partb_tasks # the four study tasks
poetry run python manage.py seed_study_users --count 12[!CAUTION]
seed_library --resetdeletes every task and object owned by the target user before reseeding, and it has no--dry-run. It is not a safe way to apply a rename to a database that holds work you want to keep.
Everything above and below is identical on all three. These are the differences.
Windows + WSL2
# PowerShell as Administrator
wsl --update
wsl --install -d Ubuntu-24.04
wsl --set-default Ubuntu-24.04Restart when prompted, then open Ubuntu from the Start menu and finish the user setup.
All processes run inside WSL, and your Windows browser reaches them on localhost, because WSL2
forwards ports to the host automatically. Open http://localhost:3000.
For VS Code, install the WSL extension and run code . from inside the WSL terminal. Every
integrated terminal then runs in Linux.
Gazebo is heavy and WSL2's default memory allocation is a fraction of your RAM. Create
C:\Users\<you>\.wslconfig:
[wsl2]
memory=8GB
processors=4Then wsl --shutdown and reopen.
[!IMPORTANT] Gesture recognition works. Object detection needs one extra step.
Gestures are read by the browser (
getUserMedia), which runs on Windows, so your laptop webcam works with no configuration.
vision_nodeis different: it openscamera_sourceinside Linux, and WSL2 exposes no USB video device, so the default0finds nothing. Either attach the device with usbipd-win, or point the node at a network camera instead:ros2 run cobotta_rest_api vision_node --ros-args -p camera_source:="http://192.168.0.90/stream"
[!WARNING] Driving the physical arm from WSL2 needs mirrored networking. WSL2 is NAT'd by default and cannot reach a robot on a physical LAN such as
192.168.0.1. On Windows 11 22H2+, addnetworkingMode=mirroredunder[wsl2]in.wslconfig. Otherwise use a native Linux machine for hardware runs.
Gazebo's 3D window needs WSLg (Windows 11). On Windows 10 keep it headless, which is the default anyway.
Virtual machine (including macOS hosts)
There is no ROS 2 Jazzy / Gazebo Harmonic build for macOS. Run Ubuntu 24.04 in a VM (UTM / Parallels / VMware). On Apple Silicon use an ARM64 image, and keep Gazebo headless because the 3D GUI is CPU-rendered and slow.
Working over SSH, forward every port:
ssh -L 3000:localhost:3000 -L 8000:localhost:8000 \
-L 5000:localhost:5000 -L 5001:localhost:5001 \
-L 8080:localhost:8080 you@your-vmVS Code's Remote-SSH extension forwards these for you. Check its Ports tab.
[!NOTE] A stock QEMU/UTM VM has no
/dev/video*at all, sovision_nodecannot open a local camera. Pass a USB device through from the hypervisor, or givecamera_sourcea network camera URL. Browser-side gesture recognition is unaffected, since it uses the host's webcam.
Native Linux
Nothing extra. This is the path the project is developed and evaluated on, and the only one where the physical arm, the cell camera and the 3D GUI all work without configuration.
# Terminal 1
cd ros2_ws/Cobotta && bash launch_sim.sh # SKIP_BUILD=1 to skip the rebuild
# Terminal 2
poetry run python manage.py runserver
# Terminal 3
npm startlaunch_sim.sh starts Gazebo (headless), ros_gz_bridge, the ros2_control spawners and the
flask_node / polling_socket_node / web_video_server nodes. It launches them and returns
without verifying anything, so check the result with testing/preflight.py.
To see the 3D window, remove -s from the gz sim line inside the script.
Launching nodes individually
Each node needs its own terminal with the environment sourced:
source /opt/ros/jazzy/setup.bash
cd ros2_ws && source .venv/bin/activate && source install/setup.bashros2 run cobotta_rest_api flask_node
ros2 run cobotta_rest_api polling_socket_node
ros2 run cobotta_rest_api cobotta_node --ros-args -p enable_hardware:=true # real arm onlyvision_node is the exception. It needs the Poetry environment, so launch the file rather than
the entry point:
poetry run python ros2_ws/src/cobotta_rest_api/cobotta_rest_api/vision_node.py \
--ros-args -p camera_source:=0poetry run ros2 run … fails with ModuleNotFoundError: ultralytics, because the entry-point
shebang points at the system python.
The only real console scripts are flask_node, cobotta_node, polling_socket_node,
vision_node. There is no gazebo_command_node or gazebo_state_node. The wiring between Gazebo
and ROS is the standard gz_ros2_control plugin.
Prerequisites and startup
One-time setup:
- Client PC on the robot LAN
192.168.0.0/24(robot.1, camera.90, PC.100) through a PoE switch. See docs/cobotta-connection.md - Executable Token set on the controller (
Any, orEthernet+ PC IP) from a Teach Pendant, or the motors will not turn on. See docs/cobotta-physical-testing.md §6b
# T1 — twin + real arm + cell camera
cd ros2_ws/Cobotta
ENABLE_VISION=1 BCAP_HOST=192.168.0.1 EXT_SPEED=20 \
YOLO_MODEL=yoloe-11s-seg.pt YOLO_CLASSES="test tube,medicine bottle,beaker,bowl" \
bash launch_physical.sh
# wait for "B-CAP connected (ExtSpeed=20)"
# T2 — backend, hardware armed
VISION_MODEL=yoloe DRIVE_HARDWARE=1 poetry run python manage.py runserver
# T3 — frontend
npm start[!NOTE]
VISION_MODEL(backend) must matchYOLO_MODEL/YOLO_CLASSES(ROS). The two configs cannot discover each other, and a mismatch means the node publishes class names the backend never looks for.
curl -s http://localhost:5000/api/actual-joints-real | python3 -m json.tool # {"available":true,…}
curl -s http://localhost:5000/api/vision/state | python3 -m json.tool # detections[!CAUTION] Keep the teach-pendant e-stop in hand.
/api/stophalts the simulation and sends a soft halt to the arm over a dedicated b-CAP channel, but it is best-effort and not safety-rated. The e-stop is the only certified stop. Start atEXT_SPEED=20.
Field guide: docs/cobotta-quickstart.md. Camera and detection: docs/cobotta-camera-object-detection.md.
Running a measured session
Study mode removes every shortcut that could resolve a step without the participant, and records one JSON line per event.
# frontend/.env
VITE_STUDY_MODE=1# backend — one participant per run, both IDs updated together
VISION_MODEL=yoloe \
DRIVE_HARDWARE=1 \
HUMAN_STEP_TIMEOUT_S=60 \
STRICT_CONDITIONS=1 \
STUDY_LOG_PATH=studio-utenti/dati/P03.jsonl \
STUDY_PARTICIPANT_ID=P03 \
poetry run python manage.py runserver| Variable | Effect |
|---|---|
VITE_STUDY_MODE=1 |
Forces live execution, hides the "simulate event" escape hatch, locks the wait-time field, and disables HMR module updates |
STRICT_CONDITIONS=1 |
A condition can only be satisfied by a real signal, so no confirmation can be fabricated |
HUMAN_STEP_TIMEOUT_S |
Seconds a human step waits. In study mode this is the only way to set it |
STUDY_LOG_PATH |
Enables logging. Unset, nothing is written at all |
[!WARNING] Verify the log file is growing before the first real participant.
log_eventnever raises, because a logging failure must not abort a run, so a misconfigured path fails silently.
[!CAUTION] For a measured session, serve a build rather than the dev server. Vite's client reloads the page whenever its WebSocket drops, and a machine running Gazebo under thermal throttling drops one routinely.
VITE_STUDY_MODE=1setsserver.hmr: false, which stops module updates but does not stop that reload: measured on 2026-09-10, thevite-hmrsocket still connects with the flag on. A build has no Vite client in it at all, so it cannot reload itself.VITE_STUDY_MODE=1 npx vite build # once, before the session npx vite preview --port 3000 # serves at http://localhost:3000/static/The
/cameraproxy works in preview exactly as in dev. Rebuild after any code change; preview serves what was built, not what is on disk.
Outside study mode the wait time is set from the robot panel (5 to 300 seconds) and remembered per browser.
# Full offline suite — no Gazebo, no ROS, no robot, no network
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 poetry run python -m pytest testing/ -q
# Live end-to-end (needs the simulation running)
poetry run python testing/test_ik_regression.py
# Stack health
poetry run python testing/preflight.py675 tests guard the properties where a defect moves a mechanical arm in space shared with a person: the two-key hardware consent, inverse-kinematics discipline, twin-versus-encoder agreement, grasp verification, and the human-step wait semantics.
Note
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 is required. Without it, pytest picks up ROS Jazzy's
launch_testing_ros plugin from the system site-packages, which mis-resolves the package and
then fails importing a dependency this environment does not have.
ROS-side tests need the sourced ROS environment:
cd ros2_ws && colcon build --packages-select cobotta_rest_api
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest src/cobotta_rest_api/test/ -v| Role | Username | Password |
|---|---|---|
| Operator | operator1 |
Operator_1! |
| Manager | manager1 |
passwordmanager1 |
| Django admin | admin |
adminpassword |
Note
In the shipped database operator1 is actually in the Manager group despite its name, so it
can reach the admin pages too. Use manager1 to test the Operator-only restriction.
| Service | Port |
|---|---|
| Frontend | 3000 |
| Backend | 8000 |
| Flask bridge | 5000 |
| Event stream (SocketIO) | 5001 |
| Camera stream | 8080 |
| Symptom | Cause | Fix |
|---|---|---|
package 'web_video_server' not found |
Streaming repos not cloned | Setup step 2, then a full colcon build |
package 'ros_gz_bridge' not found |
Missing apt package | sudo apt install ros-jazzy-ros-gz |
colcon build fails with C++ errors |
Missing system libs | Install the build-essential/cmake/boost/asio set from step 1 |
ModuleNotFoundError: ikpy |
Wrong environment | ikpy is a Poetry dependency. Run poetry install, and never pip install into .venv |
ModuleNotFoundError: rclpy in the Poetry shell |
ROS bindings are not in Poetry | Use ros2_ws/.venv for ROS nodes |
ModuleNotFoundError: ultralytics from vision_node |
Launched via ros2 run |
Launch the file with poetry run python … |
Gesture always NONE, no error |
hand_landmarker.task missing |
Setup step 4, download the model |
POST /api/vision/frame/ returns 500 |
Same as above | Same as above |
| Object detection finds nothing on WSL/VM | No /dev/video* in the guest |
usbipd-win, or a network camera_source |
| The app loads but Run does nothing | Flask bridge down | Check terminal 1; :5000 must answer |
runserver fails on CSRF_TRUSTED_ORIGINS (4_0.E001) |
VITE_FRONTEND_* empty |
Fill all three in frontend/.env; Django reads them |
| CORS/CSRF errors in the browser | Ports not forwarded | Forward 3000, 8000, 5000, 5001, 8080 |
| The page reloads by itself mid-task | Vite's client reacting to a dropped WebSocket | Serve a build (vite build then vite preview), not the dev server. hmr: false alone does not stop it |
'X' didn't come up with the gripper |
Weld lost a gz message |
Fixed by the retry budget; if it persists, restart the Gazebo stack |
| Document | Covers |
|---|---|
| docs/cobotta-quickstart.md | Field guide for a physical session |
| docs/cobotta-connection.md | Network, PoE switch, controller setup |
| docs/cobotta-physical-testing.md | Hardware test procedure and safety |
| docs/cobotta-camera-object-detection.md | Cell camera and detection tuning |
| docs/ui-naming-map.md | Code identifiers and their on-screen labels |
This repository extends the prototype described in:
Gargioni, Luigi and Fogli, Daniela. "Integrating ChatGPT with Blockly for End-User Development of Robot Tasks". Companion of the 2024 ACM/IEEE International Conference on Human-Robot Interaction, pages 478-482, 2024. ACM, New York. doi.org/10.1145/3610978.3640653
@inproceedings{gargioni2024integrating,
title={Integrating ChatGPT with Blockly for End-User Development of Robot Tasks},
author={Gargioni, Luigi and Fogli, Daniela},
booktitle={Companion of the 2024 ACM/IEEE International Conference on Human-Robot Interaction},
pages={478--482},
year={2024}
}