amr-ros-k4/src/blockly_app/README.md

6.5 KiB

blockly_app — File Reference

6.1 Application Layer — blockly_app

blockly_app/app.py — Application Entry Point

Purpose: Combines pywebview (desktop UI) with a ROS2 Action Client. This is the bridge between the JavaScript Blockly runtime and the ROS2 ecosystem.

Key components:

Component Description
_native_save_dialog() / _native_open_dialog() Opens native OS file dialogs via tkinter.filedialog. Uses Tcl/Tk interpreter separate from Qt — no thread conflict. Available in pixi environment without extra dependencies.
_wait_for_future(future, timeout_sec) Polls future.done() without calling rclpy.spin(). Used because the node is already being spun by a background thread.
BlocklyAPI Python class exposed to JavaScript via pywebview. Its methods are callable as window.pywebview.api.<method>().
BlocklyAPI.execute_action() Sends a ROS2 Action Goal and blocks until the result arrives. Returns {success: bool, message: str} to JavaScript.
BlocklyAPI.save_workspace(json_string) Opens native "Save As" dialog via tkinter, writes workspace JSON to chosen file. Returns {success, path} directly to JS.
BlocklyAPI.load_workspace() Opens native "Open" dialog via tkinter, reads and validates JSON, returns {success, data, path} to JS.
main() Initializes rclpy, creates the Action Client, starts the background spin thread with MultiThreadedExecutor, creates the pywebview window, and handles cleanup on exit.

Threading design: The main() function starts MultiThreadedExecutor.spin() in a daemon thread. When JavaScript calls execute_action(), the method uses _wait_for_future() to poll for completion — it never calls rclpy.spin_until_future_complete(), which would conflict with the background spin.

blockly_app/ui/index.html — Main UI

Purpose: The single HTML page that hosts the Blockly workspace, toolbar buttons, and console panel.

Structure:

  • Toolbar: Run, Step Over, Step Into, Stop buttons, and Debug Mode toggle
  • Blockly Workspace: The drag-and-drop canvas
  • Console Panel: Scrollable log output showing execution progress
  • Script loading: Loads Blockly vendor files, then core infrastructure, then blocks via auto-loader

Script loading order (fixed — never needs changing when adding blocks):

  1. vendor/blockly.min.js + other Blockly libs
  2. blockly/core/registry.jsbreakpoints.jsbridge.jsdebug-engine.jsui-controls.jsui-tabs.jsworkspace-io.js
  3. blockly/blocks/manifest.js + blockly/loader.js
  4. blockly/workspace-init.js
  5. Inline script: loadAllBlocks().then(() => initWorkspace())

blockly_app/ui/blockly/core/registry.js — Block Registry

Purpose: Central registration system for custom blocks. Each block file calls BlockRegistry.register() to self-register its visual definition, code generator, and toolbox metadata.

Method Description
BlockRegistry.register(config) Register a block with name, category, color, definition, and generator
BlockRegistry.getBlocks() Get all registered blocks
BlockRegistry.getToolboxJSON() Build Blockly toolbox JSON from registered blocks + built-in categories

blockly_app/ui/blockly/core/ui-tabs.js — Tab Switching

Purpose: Manages switching between the Blocks editor tab and the Code preview tab.

Function Description
switchTab(tab) Shows/hides #blockly-area and #code-panel. Calls Blockly.svgResize() when returning to Blocks tab (canvas dimensions are zeroed while hidden).
refreshCodePanel() Regenerates JS code from workspace via javascript.javascriptGenerator.workspaceToCode() and displays in #code-output.

The Code tab updates automatically on every workspace change (via a change listener in workspace-init.js).

blockly_app/ui/blockly/core/workspace-io.js — Workspace Export/Import

Purpose: Exports workspace to JSON (Save As dialog) and imports from JSON (Open dialog) via the Python bridge.

Function Description
exportWorkspace() Serializes workspace with Blockly.serialization.workspaces.save(), calls window.pywebview.api.save_workspace(json), logs result path.
importWorkspace() Calls window.pywebview.api.load_workspace(), clears workspace, loads returned JSON with Blockly.serialization.workspaces.load().

Both functions use async/await — they return after the file dialog closes and the file has been read/written.

blockly_app/ui/blockly/core/bridge.js — pywebview Bridge

Purpose: Provides executeAction(command, params) which calls Python via the pywebview JS-to-Python bridge. Falls back to a mock when running outside pywebview (browser dev).

blockly_app/ui/blockly/core/debug-engine.js — Debug Engine

Purpose: Implements Run, Debug, Step Over, Step Into, and Stop functionality.

Function Description
runProgram() Non-debug execution: wraps generated code in async function and eval()s it
runDebug() Debug execution: wraps executeAction to check breakpoints and add delays
stepOver() Resumes from pause, executes current block, pauses at next block
stepInto() Resumes from pause, pauses at very next highlightBlock call
stopExecution() Sets stopRequested flag, resolves any pending pause Promise
highlightBlock(blockId) Highlights the currently executing block in the workspace

blockly_app/ui/blockly/blocks/manifest.js — Block Manifest

Purpose: Lists all block files to auto-load. This is the only file you edit when adding a new block (besides creating the block file itself).

const BLOCK_FILES = ['led_on.js', 'led_off.js', 'delay.js'];

blockly_app/ui/blockly/blocks/led_on.js — Example Block

Purpose: Self-contained block definition. Contains both the visual appearance AND the code generator.

Each block file calls BlockRegistry.register() with all metadata, so the toolbox is automatically generated.