feat: implement export/import functionality and UI enhancements for Blockly workspace

master
a2nr 2026-03-09 11:43:27 +07:00
parent 09e67ccbbc
commit c82e30b9b1
8 changed files with 380 additions and 15 deletions

View File

@ -234,7 +234,9 @@ amr-ros-k4/ # ROS2 Workspace root
│ ├── breakpoints.js # Debug breakpoint management │ ├── breakpoints.js # Debug breakpoint management
│ ├── bridge.js # executeAction — pywebview bridge │ ├── bridge.js # executeAction — pywebview bridge
│ ├── debug-engine.js # Run, debug, step, stop logic │ ├── debug-engine.js # Run, debug, step, stop logic
│ └── ui-controls.js # Button states and callbacks │ ├── ui-controls.js # Button states and callbacks
│ ├── ui-tabs.js # switchTab(), refreshCodePanel()
│ └── workspace-io.js # exportWorkspace(), importWorkspace()
├── blocks/ # One file per block (auto-discovered) ├── blocks/ # One file per block (auto-discovered)
│ ├── manifest.js # BLOCK_FILES array │ ├── manifest.js # BLOCK_FILES array
│ ├── led_on.js │ ├── led_on.js
@ -357,10 +359,13 @@ If the executor is not running, tests are **skipped** with an informative messag
| Component | Description | | Component | Description |
|---|---| |---|---|
| [`_wait_for_future(future, timeout_sec)`](src/blockly_app/blockly_app/app.py:26) | Polls `future.done()` without calling `rclpy.spin()`. Used because the node is already being spun by a background thread. | | [`_native_save_dialog()` / `_native_open_dialog()`](src/blockly_app/blockly_app/app.py:29) | 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. |
| [`BlocklyAPI`](src/blockly_app/blockly_app/app.py:46) | Python class exposed to JavaScript via pywebview. Its methods are callable as `window.pywebview.api.<method>()`. | | [`_wait_for_future(future, timeout_sec)`](src/blockly_app/blockly_app/app.py:63) | Polls `future.done()` without calling `rclpy.spin()`. Used because the node is already being spun by a background thread. |
| [`BlocklyAPI.execute_action()`](src/blockly_app/blockly_app/app.py:61) | Sends a ROS2 Action Goal and blocks until the result arrives. Returns `{success: bool, message: str}` to JavaScript. | | [`BlocklyAPI`](src/blockly_app/blockly_app/app.py:80) | Python class exposed to JavaScript via pywebview. Its methods are callable as `window.pywebview.api.<method>()`. |
| [`main()`](src/blockly_app/blockly_app/app.py:145) | Initializes rclpy, creates the Action Client, starts the background spin thread with `MultiThreadedExecutor`, creates the pywebview window, and handles cleanup on exit. | | [`BlocklyAPI.execute_action()`](src/blockly_app/blockly_app/app.py:95) | Sends a ROS2 Action Goal and blocks until the result arrives. Returns `{success: bool, message: str}` to JavaScript. |
| [`BlocklyAPI.save_workspace(json_string)`](src/blockly_app/blockly_app/app.py:145) | Opens native "Save As" dialog via tkinter, writes workspace JSON to chosen file. Returns `{success, path}` directly to JS. |
| [`BlocklyAPI.load_workspace()`](src/blockly_app/blockly_app/app.py:168) | Opens native "Open" dialog via tkinter, reads and validates JSON, returns `{success, data, path}` to JS. |
| [`main()`](src/blockly_app/blockly_app/app.py:190) | 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. **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.
@ -376,7 +381,7 @@ If the executor is not running, tests are **skipped** with an informative messag
**Script loading order** (fixed — never needs changing when adding blocks): **Script loading order** (fixed — never needs changing when adding blocks):
1. `vendor/blockly.min.js` + other Blockly libs 1. `vendor/blockly.min.js` + other Blockly libs
2. `blockly/core/registry.js``breakpoints.js``bridge.js``debug-engine.js``ui-controls.js` 2. `blockly/core/registry.js``breakpoints.js``bridge.js``debug-engine.js``ui-controls.js``ui-tabs.js``workspace-io.js`
3. `blockly/blocks/manifest.js` + `blockly/loader.js` 3. `blockly/blocks/manifest.js` + `blockly/loader.js`
4. `blockly/workspace-init.js` 4. `blockly/workspace-init.js`
5. Inline script: `loadAllBlocks().then(() => initWorkspace())` 5. Inline script: `loadAllBlocks().then(() => initWorkspace())`
@ -391,6 +396,28 @@ If the executor is not running, tests are **skipped** with an informative messag
| `BlockRegistry.getBlocks()` | Get all registered blocks | | `BlockRegistry.getBlocks()` | Get all registered blocks |
| `BlockRegistry.getToolboxJSON()` | Build Blockly toolbox JSON from registered blocks + built-in categories | | `BlockRegistry.getToolboxJSON()` | Build Blockly toolbox JSON from registered blocks + built-in categories |
#### [`blockly_app/ui/blockly/core/ui-tabs.js`](src/blockly_app/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`](src/blockly_app/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`](src/blockly_app/blockly_app/ui/blockly/core/bridge.js) — pywebview Bridge #### [`blockly_app/ui/blockly/core/bridge.js`](src/blockly_app/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). **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).
@ -1164,6 +1191,17 @@ pixi run executor
pixi run test pixi run test
``` ```
### Export/Import button has no effect (force close or nothing happens)
**Symptom:** Clicking Export or Import either force-closes the app or does nothing.
**Cause:** Qt file dialogs (`QFileDialog`, `pywebview.create_file_dialog`) must run on the Qt main thread. pywebview calls Python API methods from a background thread. Attempting to open a Qt dialog from there causes:
- `pywebview.create_file_dialog` → deadlock via `BlockingQueuedConnection` → force close
- `QFileDialog` via `QTimer.singleShot` → no effect, because non-QThread background threads have no Qt event loop
**Solution:** Use `tkinter.filedialog` — tkinter uses its own Tcl/Tk interpreter, completely separate from Qt. `filedialog.asksaveasfilename()` blocks the calling background thread until the user responds. Already available in the pixi environment (no extra dependency needed).
See [`_native_save_dialog()`](src/blockly_app/blockly_app/app.py:29) in `app.py`.
### pywebview shows "GTK cannot be loaded" ### pywebview shows "GTK cannot be loaded"
**Symptom:** Warning about `ModuleNotFoundError: No module named 'gi'` followed by "Using Qt 5.15". **Symptom:** Warning about `ModuleNotFoundError: No module named 'gi'` followed by "Using Qt 5.15".

View File

@ -2,7 +2,8 @@
> **Project**: Blockly ROS2 Robot Controller (Kiwi Wheel AMR) > **Project**: Blockly ROS2 Robot Controller (Kiwi Wheel AMR)
> **ROS2 Distro**: Jazzy > **ROS2 Distro**: Jazzy
> **Last Updated**: 2026-03-08 > **Last Updated**: 2026-03-09
> **Current Focus**: `kiwi_controller` — Adaptive control for Kiwi Wheel drive
Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md). Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
@ -11,13 +12,12 @@ Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
bab pada dokumen merepresentasikan alur rencana pengembangan. bab pada dokumen merepresentasikan alur rencana pengembangan.
Potential Enhancements : Feasibility Study Potential Enhancements : Feasibility Study
Planned Feature : Backlog Planned Feature : Backlog
Feature Task : Penjabaran Pekerjaan yang ready untuk dikerjakan Feature Task : Penjabaran Pekerjaan yang ready untuk dikerjakan. Task harus dijelaskan apa yang akan dikerjakan dan terdapat definition of done nya
--- ---
# Potential Enhancements # Potential Enhancements
this list is short by priority this list is short by priority
- **Enhance UI**: add tab to get generate version of block blocky, export/import from/to block blocky generated file, fixing visibelity text color in left colom view (curently it greyed with white text).
- **Launch files**: `blockly_bringup` package with ROS2 launch files to start all nodes with one command - **Launch files**: `blockly_bringup` package with ROS2 launch files to start all nodes with one command
- **Sensor integration**: Subscriber nodes for sensor data feeding back into Blockly visual feedback - **Sensor integration**: Subscriber nodes for sensor data feeding back into Blockly visual feedback
- **RealHardware implementation**: Fill in ROS2 publishers/service clients for actual Pi hardware nodes (topics TBD) - **RealHardware implementation**: Fill in ROS2 publishers/service clients for actual Pi hardware nodes (topics TBD)
@ -33,8 +33,26 @@ this list is short by priority
| `blockly_executor` | Action Server — command handler registry | ✅ Done | | `blockly_executor` | Action Server — command handler registry | ✅ Done |
| `blockly_interfaces` | Custom ROS2 action definitions | ✅ Done | | `blockly_interfaces` | Custom ROS2 action definitions | ✅ Done |
| `kiwi_controller` | Adaptive control for Kiwi Wheel drive | 📋 Planned | | `kiwi_controller` | Adaptive control for Kiwi Wheel drive | 📋 Planned |
| **Enhance UI** | Tab Code preview, Export/Import, dark toolbox | ✅ Done |
# Future Tasks # Feature Task
<!-- Add new phases / features here --> ## Enhance UI
| # | Task | Status |
|---|------|--------|
| 1 | Fix toolbox text color (dark theme via `Blockly.Theme.defineTheme`) | ✅ Done |
| 2 | Add "Code" tab — realtime generated JS preview | ✅ Done |
| 3 | Export/Import workspace (.json) via toolbar buttons | ✅ Done |
**Files changed:**
- `src/blockly_app/blockly_app/ui/blockly/workspace-init.js` — dark theme definition + code panel change listener
- `src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js` *(new)*`switchTab()`, `refreshCodePanel()`
- `src/blockly_app/blockly_app/ui/blockly/core/workspace-io.js` *(new)*`exportWorkspace()`, `importWorkspace()` via `await window.pywebview.api`
- `src/blockly_app/blockly_app/ui/index.html` — CSS, HTML tab bar, code panel, toolbar buttons
- `src/blockly_app/blockly_app/app.py``save_workspace()`, `load_workspace()` via zenity subprocess
**Note — Export/Import implementation:**
`pywebview.create_file_dialog` dan `QFileDialog` via `QTimer.singleShot` keduanya gagal dari background thread (deadlock / no event loop).
Solusi final: `tkinter.filedialog` — interpreter Tcl/Tk terpisah dari Qt, tidak ada konflik thread, sudah tersedia di pixi environment tanpa dependency tambahan.

View File

@ -6,12 +6,16 @@ in a background thread. The BlocklyAPI class exposes execute_action()
to JavaScript, bridging Blockly's runtime with the ROS2 Executor Node. to JavaScript, bridging Blockly's runtime with the ROS2 Executor Node.
""" """
import json
import os import os
import sys import sys
import time import time
import threading import threading
import logging import logging
import tkinter as _tk
from tkinter import filedialog as _filedialog
import webview import webview
import rclpy import rclpy
from rclpy.action import ActionClient from rclpy.action import ActionClient
@ -23,6 +27,43 @@ from blockly_interfaces.action import BlocklyAction
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _native_save_dialog(suggested: str = "workspace.json") -> str:
"""
Open a native "Save As" file dialog using tkinter.
tkinter uses its own Tcl/Tk interpreter no Qt thread conflict.
filedialog.asksaveasfilename() blocks the calling thread until the
user picks a file or cancels. Returns "" on cancel.
"""
root = _tk.Tk()
root.withdraw()
root.wm_attributes("-topmost", True)
path = _filedialog.asksaveasfilename(
title="Save Workspace",
defaultextension=".json",
initialfile=suggested,
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
)
root.destroy()
return path or ""
def _native_open_dialog() -> str:
"""
Open a native "Open" file dialog using tkinter.
Returns "" on cancel.
"""
root = _tk.Tk()
root.withdraw()
root.wm_attributes("-topmost", True)
path = _filedialog.askopenfilename(
title="Load Workspace",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
)
root.destroy()
return path or ""
def _wait_for_future(future, timeout_sec: float = 30.0): def _wait_for_future(future, timeout_sec: float = 30.0):
""" """
Wait for an rclpy Future to complete without calling spin. Wait for an rclpy Future to complete without calling spin.
@ -141,6 +182,50 @@ class BlocklyAPI:
"message": f"Error: {str(e)}", "message": f"Error: {str(e)}",
} }
def save_workspace(self, json_string: str) -> dict:
"""
Open a native OS "Save As" dialog via zenity/kdialog subprocess and
write workspace JSON to the chosen file.
Called from JS: await window.pywebview.api.save_workspace(jsonString)
Using subprocess avoids all Qt thread constraints zenity/kdialog run
in their own process, blocking the pywebview bridge thread until the
user confirms or cancels.
"""
try:
path = _native_save_dialog("workspace.json")
if not path:
return {"success": False, "error": "Cancelled"}
with open(path, "w", encoding="utf-8") as f:
f.write(json_string)
logger.info(f"Workspace saved to: {path}")
return {"success": True, "path": path}
except Exception as e:
logger.error(f"save_workspace error: {e}")
return {"success": False, "error": str(e)}
def load_workspace(self) -> dict:
"""
Open a native OS "Open" dialog via zenity/kdialog and read the file.
Called from JS: await window.pywebview.api.load_workspace()
"""
try:
path = _native_open_dialog()
if not path:
return {"success": False, "error": "Cancelled"}
with open(path, "r", encoding="utf-8") as f:
data = f.read()
json.loads(data) # validate before sending to JS
logger.info(f"Workspace loaded from: {path}")
return {"success": True, "data": data, "path": path}
except json.JSONDecodeError:
return {"success": False, "error": "File is not valid JSON"}
except Exception as e:
logger.error(f"load_workspace error: {e}")
return {"success": False, "error": str(e)}
def main(): def main():
"""Application entry point.""" """Application entry point."""

View File

@ -0,0 +1,45 @@
/**
* UI Tabs Tab switching between Blocks editor and Code preview.
*
* Depends on: workspace (global, index.html), refreshCodePanel (this file),
* Blockly (vendor), javascript (vendor)
*/
/**
* Switch between "blocks" and "code" tabs.
* Calls Blockly.svgResize when returning to blocks tab (canvas was hidden).
*
* @param {string} tab - "blocks" or "code"
*/
function switchTab(tab) {
const blocksArea = document.getElementById('blockly-area');
const codePanel = document.getElementById('code-panel');
const tabBlocks = document.getElementById('tab-blocks');
const tabCode = document.getElementById('tab-code');
if (tab === 'blocks') {
blocksArea.style.display = 'block';
codePanel.style.display = 'none';
tabBlocks.classList.add('active');
tabCode.classList.remove('active');
// Restore Blockly canvas dimensions — they're zeroed while hidden
if (typeof workspace !== 'undefined' && workspace) {
Blockly.svgResize(workspace);
}
} else {
blocksArea.style.display = 'none';
codePanel.style.display = 'block';
tabBlocks.classList.remove('active');
tabCode.classList.add('active');
refreshCodePanel();
}
}
/**
* Regenerate JS code from workspace and display in code panel.
*/
function refreshCodePanel() {
if (typeof workspace === 'undefined' || !workspace) return;
const code = javascript.javascriptGenerator.workspaceToCode(workspace);
document.getElementById('code-output').textContent = code || '// (no blocks)';
}

View File

@ -0,0 +1,57 @@
/**
* Workspace I/O Export and import workspace as JSON via native OS dialogs.
*
* Calls Python save_workspace() / load_workspace() via pywebview bridge.
* Both methods are synchronous on the Python side (blocking file dialog),
* and return their result directly no evaluate_js callback needed.
*
* Depends on: workspace (global, index.html), consoleLog (index.html),
* Blockly (vendor), refreshCodePanel (ui-tabs.js)
*/
/**
* Serialize workspace and open native OS "Save As" dialog.
*/
async function exportWorkspace() {
if (typeof workspace === 'undefined' || !workspace) {
consoleLog('No workspace to export', 'error');
return;
}
const state = Blockly.serialization.workspaces.save(workspace);
const json = JSON.stringify(state, null, 2);
try {
const res = await window.pywebview.api.save_workspace(json);
if (res.success) {
consoleLog('Workspace saved to: ' + res.path, 'success');
} else if (res.error !== 'Cancelled') {
consoleLog('Export failed: ' + res.error, 'error');
}
} catch (err) {
consoleLog('Export error: ' + err, 'error');
}
}
/**
* Open native OS file picker and load selected JSON into workspace.
*/
async function importWorkspace() {
try {
const res = await window.pywebview.api.load_workspace();
if (!res.success) {
if (res.error !== 'Cancelled') {
consoleLog('Import failed: ' + res.error, 'error');
}
return;
}
const state = JSON.parse(res.data);
workspace.clear();
Blockly.serialization.workspaces.load(state, workspace);
consoleLog('Workspace loaded from: ' + res.path, 'success');
const codePanel = document.getElementById('code-panel');
if (codePanel && codePanel.style.display !== 'none') {
refreshCodePanel();
}
} catch (err) {
consoleLog('Import error: ' + err, 'error');
}
}

View File

@ -20,6 +20,17 @@ function initWorkspace() {
// Build toolbox from registry // Build toolbox from registry
const toolbox = BlockRegistry.getToolboxJSON(); const toolbox = BlockRegistry.getToolboxJSON();
// Dark theme — fixes toolbox text visibility against dark UI background
const DarkTheme = Blockly.Theme.defineTheme('dark-ros2', {
base: Blockly.Themes.Classic,
componentStyles: {
toolboxBackgroundColour: '#252526',
toolboxForegroundColour: '#cccccc',
flyoutBackgroundColour: '#2d2d2d',
flyoutOpacity: 0.95,
},
});
// Inject Blockly workspace // Inject Blockly workspace
const ws = Blockly.inject('blockly-div', { const ws = Blockly.inject('blockly-div', {
toolbox: toolbox, toolbox: toolbox,
@ -38,7 +49,7 @@ function initWorkspace() {
scaleSpeed: 1.2, scaleSpeed: 1.2,
}, },
trashcan: true, trashcan: true,
theme: Blockly.Themes.Classic, theme: DarkTheme,
}); });
// Set up breakpoint click listener // Set up breakpoint click listener
@ -51,6 +62,14 @@ function initWorkspace() {
} }
}); });
// Refresh Code tab when blocks change
ws.addChangeListener(function () {
const codePanel = document.getElementById('code-panel');
if (codePanel && codePanel.style.display !== 'none') {
refreshCodePanel();
}
});
// Resize handler // Resize handler
function onResize() { function onResize() {
const area = document.getElementById('blockly-area'); const area = document.getElementById('blockly-area');

View File

@ -85,12 +85,77 @@
height: 16px; height: 16px;
} }
#workspace-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#tab-bar {
display: flex;
background: #2d2d2d;
border-bottom: 1px solid #404040;
flex-shrink: 0;
height: 32px;
}
.tab-btn {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: #888;
cursor: pointer;
font-size: 13px;
padding: 0 16px;
height: 32px;
}
.tab-btn.active {
color: #d4d4d4;
border-bottom-color: #4caf50;
}
.tab-btn:hover:not(.active) {
color: #bbb;
background: rgba(255,255,255,0.05);
}
#blockly-area { #blockly-area {
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
#code-panel {
flex: 1;
background: #1a1a1a;
overflow: auto;
padding: 12px;
}
#code-output {
margin: 0;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 13px;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-word;
}
#btn-export {
background: #607d8b;
color: white;
}
#btn-import {
background: #455a64;
color: white;
}
/* SVG flyout label text — not controlled by Blockly theme componentStyles */
.blocklyFlyoutLabelText { fill: #cccccc !important; }
#blockly-div { #blockly-div {
position: absolute; position: absolute;
top: 0; top: 0;
@ -129,16 +194,28 @@
<button id="btn-step-into" onclick="stepInto()" disabled>Step Into</button> <button id="btn-step-into" onclick="stepInto()" disabled>Step Into</button>
<div class="separator"></div> <div class="separator"></div>
<button id="btn-stop" onclick="stopExecution()" disabled>Stop</button> <button id="btn-stop" onclick="stopExecution()" disabled>Stop</button>
<div class="separator"></div>
<button id="btn-export" onclick="exportWorkspace()">Export</button>
<button id="btn-import" onclick="importWorkspace()">Import</button>
<div id="debug-toggle"> <div id="debug-toggle">
<input type="checkbox" id="chk-debug" onchange="onDebugToggle(this.checked)"> <input type="checkbox" id="chk-debug" onchange="onDebugToggle(this.checked)">
<label for="chk-debug">Debug Mode</label> <label for="chk-debug">Debug Mode</label>
</div> </div>
</div> </div>
<!-- Blockly Workspace --> <!-- Blockly Workspace + Code Tab -->
<div id="workspace-area">
<div id="tab-bar">
<button class="tab-btn active" id="tab-blocks" onclick="switchTab('blocks')">Blocks</button>
<button class="tab-btn" id="tab-code" onclick="switchTab('code')">Code</button>
</div>
<div id="blockly-area"> <div id="blockly-area">
<div id="blockly-div"></div> <div id="blockly-div"></div>
</div> </div>
<div id="code-panel" style="display:none">
<pre id="code-output"></pre>
</div>
</div>
<!-- Console Output --> <!-- Console Output -->
<div id="console-panel"></div> <div id="console-panel"></div>
@ -169,6 +246,8 @@
<script src="blockly/core/bridge.js"></script> <script src="blockly/core/bridge.js"></script>
<script src="blockly/core/debug-engine.js"></script> <script src="blockly/core/debug-engine.js"></script>
<script src="blockly/core/ui-controls.js"></script> <script src="blockly/core/ui-controls.js"></script>
<script src="blockly/core/ui-tabs.js"></script>
<script src="blockly/core/workspace-io.js"></script>
<!-- Block manifest and loader --> <!-- Block manifest and loader -->
<script src="blockly/blocks/manifest.js"></script> <script src="blockly/blocks/manifest.js"></script>

24
workspace.json Normal file
View File

@ -0,0 +1,24 @@
{
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "controls_if",
"id": "WLmpkq!^,rcoKGH+bQgN",
"x": 210,
"y": 70,
"inputs": {
"DO0": {
"block": {
"type": "led_on",
"id": "._%j-2DZ*BZJlyW:1#`Q",
"fields": {
"PIN": 1
}
}
}
}
}
]
}
}