12 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):
vendor/blockly.min.js+ other Blockly libsblockly/core/registry.js→breakpoints.js→bridge.js→debug-engine.js→ui-controls.js→ui-tabs.js→workspace-io.jsblockly/blocks/manifest.js+blockly/loader.jsblockly/workspace-init.js- 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/async-procedures.js — Async Code Generation
Purpose: Overrides Blockly's built-in procedure generators for async/await support, overrides variables_set for debug highlight, and provides generateCode(ws) which replaces workspaceToCode().
| Component | Description |
|---|---|
procedures_defreturn override |
Generates async function instead of function |
procedures_callreturn override |
Wraps in async IIFE with enterFunction() / exitFunction() for call depth tracking |
procedures_callnoreturn override |
Adds await highlightBlock() + call depth tracking |
variables_set override |
Adds await highlightBlock() before variable assignment for debug support |
generateCode(ws) |
Main-block-aware code generation. Returns plain string or { definitions, mainCode, hmiCode } for concurrent execution. |
blockly_app/ui/blockly/core/debug-engine.js — Debug Engine
Purpose: Implements Run, Debug, Step Over, Step Into, and Stop functionality. Supports single-program and concurrent (main+HMI) execution modes.
| Function | Description |
|---|---|
runProgram() |
Non-debug execution: detects single vs concurrent mode, dispatches accordingly |
_runSingle(code) |
Single program execution (no HMI block present) |
_runConcurrent(codeResult) |
Concurrent execution: main + HMI via Promise.all() with shared variable scope |
runDebug() |
Debug execution: detects single vs concurrent mode |
_runDebugSingle(code) |
Single program debug with breakpoints and stepping |
_runDebugConcurrent(codeResult) |
Concurrent debug: main has stepping, HMI runs uninterrupted |
stepOver() |
Resumes from pause, executes current block, pauses at next block at same call depth |
stepInto() |
Resumes from pause, pauses at very next highlightBlock call |
continueExecution() |
Resumes and runs until next breakpoint |
stopExecution() |
Sets stopRequested flag, cancels in-flight actions, resolves 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 = ['digitalOut.js', 'digitalIn.js', 'delay.js'];
blockly_app/ui/blockly/core/hmi-manager.js — HMI State Manager
Purpose: Global HMI object providing LabVIEW-style Front Panel with gridstack.js grid layout. Manages widget lifecycle, mode switching (design/runtime), and layout serialization.
| Method | Description |
|---|---|
HMI.init() |
Initialize GridStack instance. Called once after DOM is ready. |
HMI.addWidget(name, type, config, blockId) |
Add widget to grid. Updates if exists. |
HMI.removeWidget(name) |
Remove widget from grid and DOM. |
HMI.setMode(mode) |
Switch between 'design' (grid unlocked) and 'runtime' (grid locked). |
HMI.setLED(name, state, color) |
Set LED on/off with color. Called from generated code. |
HMI.setNumber(name, value, unit) |
Display numeric value with unit label. |
HMI.setText(name, text) |
Display text string. |
HMI.setGauge(name, value, min, max) |
Display gauge bar with range. |
HMI.setButton(name, label, color) |
Create/configure button widget. |
HMI.getButton(name) → Boolean |
Latch-until-read: return true once per click, then auto-reset. |
HMI.setSlider(name, value, min, max) |
Create/configure slider widget. _userHasInteracted protects user drag. |
HMI.getSlider(name) → Number |
Return user-dragged value (or programmatic value if no interaction). |
HMI.setSwitch(name, state) |
Create/configure toggle switch widget. _userState protects user toggle. |
HMI.getSwitch(name) → Boolean |
Return user-toggled state (or programmatic state if no interaction). |
HMI.getLayout() / HMI.loadLayout(items) |
Serialize/restore grid positions for save/load. |
HMI.clearAll() |
Remove all widgets and reset grid. |
blockly_app/ui/blockly/core/hmi-preview.js — Design-time Widget Preview
Purpose: Workspace change listener that creates/removes HMI widgets as blocks are placed/deleted. Widgets appear in the HMI panel at design time, not just at runtime.
| Function | Description |
|---|---|
initHMIPreview(ws) |
Sets up workspace change listener. Tracks block-to-widget mapping. |
_handleCreate(event) |
Creates widget when HMI block is placed. |
_handleDelete(event) |
Removes widget when HMI block is deleted. |
_handleChange(event) |
Updates widget when block field changes (NAME, COLOR, UNIT, MIN, MAX). |
_reconcile() |
Debounced (100ms) full sync — catches undo/redo edge cases. |
window._hmiPreviewScan() |
Scans existing blocks after workspace import to recreate previews. |
blockly_app/ui/blockly/core/resizable-panels.js — Resizable Split Panels
Purpose: Drag-to-resize dividers between Blockly workspace, HMI panel, and console. Updates #blockly-div dimensions and calls Blockly.svgResize() on each move.
| Divider | Direction | Range |
|---|---|---|
| Vertical (Blockly↔HMI) | Horizontal drag | HMI 200px–50% viewport |
| Horizontal (workspace↔console) | Vertical drag | Console 80px–40% viewport |
blockly_app/ui/blockly/blocks/ — Block Definitions
All block files registered in manifest.js:
| File | Category | Type | Execution Model |
|---|---|---|---|
mainProgram.js |
Program | Hat block | N/A (entry point) |
mainHmiProgram.js |
Program | Hat block | N/A (HMI entry point) |
print.js |
Program | Statement | Client-side (consoleLog()) |
digitalOut.js |
Robot | Statement | ROS2 action (digital_out) |
digitalIn.js |
Robot | Output | ROS2 action (digital_in) |
delay.js |
Robot | Statement | ROS2 action (delay) |
pwmWrite.js |
Robot | Statement | ROS2 action (pwm_write) |
odometryRead.js |
Robot | Output | ROS2 action (odometry_read) |
odometryGet.js |
Robot | Output | Client-side (JSON field extract) |
hmiSetLed.js |
HMI | Statement | Client-side (HMI.setLED()) |
hmiSetNumber.js |
HMI | Statement | Client-side (HMI.setNumber()) |
hmiSetText.js |
HMI | Statement | Client-side (HMI.setText()) |
hmiSetGauge.js |
HMI | Statement | Client-side (HMI.setGauge()) |
hmiSetButton.js |
HMI | Statement | Client-side (HMI.setButton()) |
hmiGetButton.js |
HMI | Output | Client-side (HMI.getButton()) |
hmiSetSlider.js |
HMI | Statement | Client-side (HMI.setSlider()) |
hmiGetSlider.js |
HMI | Output | Client-side (HMI.getSlider()) |
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 for the full guide to creating blocks.