feat: add main program block and async procedure support in Blockly; enhance code generation and execution flow
parent
0bdd3040e0
commit
fc17d9e9e9
23
readme.md
23
readme.md
|
|
@ -36,11 +36,9 @@ jelaskan apa yang dimaksut untuk menyelesaikan task
|
|||
|
||||
# Potential Enhancements
|
||||
this list is short by priority
|
||||
- **Implement function and main function block in blockly**: we need that.
|
||||
- **ROS Feature in generated block blocky**: currently, block blocky only generate action client, and there is sub/pub and other ROS feature need to implement to get/set value to node.
|
||||
- **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
|
||||
- **RealHardware implementation**: Fill in remaining ROS2 publishers/service clients for Pi hardware nodes (GPIO done via `gpio_node`, motor/servo TBD)
|
||||
- **ROS2 lifecycle nodes**: Migrate executor and controller to lifecycle nodes for managed state transitions
|
||||
- **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
|
||||
|
|
@ -60,3 +58,24 @@ Membuat ROS2 node (`gpio_node`) yang berjalan di Raspberry Pi untuk mengontrol p
|
|||
- Dependency `gpiod` ditambahkan untuk `linux-aarch64`
|
||||
- Integration test `test_block_gpio.py` mencakup digital_out dan digital_in
|
||||
- `pixi run build` berhasil tanpa error
|
||||
|
||||
## 2 Function & Main Program Block in Blockly : [x]
|
||||
Saat ini Blockly workspace hanya mendukung linear execution — semua top-level blocks dijalankan berurutan tanpa entry point yang jelas. Belum ada cara untuk mendefinisikan reusable functions. Enhancement ini menambahkan:
|
||||
1. **Function blocks** — menggunakan built-in Blockly procedure blocks (`procedures_defnoreturn`, `procedures_defreturn`, `procedures_callnoreturn`, `procedures_callreturn`) dengan override code generator untuk menghasilkan `async function` + `await` calls agar kompatibel dengan `executeAction()`.
|
||||
2. **Main Program block** — custom hat block sebagai entry point program. Ketika ada Main Program block, hanya function definitions + main block body yang di-execute (orphan blocks diabaikan). Tanpa Main Program block, behavior tetap backward compatible.
|
||||
|
||||
Perubahan ini murni di sisi JS/frontend. Tidak ada perubahan di Python side (executor, handlers, action interface).
|
||||
|
||||
### Definition Of Done
|
||||
- File `async-procedures.js` dibuat di `blockly/core/` — override procedure generators ke async/await + fungsi `generateCode(ws)`
|
||||
- File `mainProgram.js` dibuat di `blockly/blocks/` — Main Program hat block (no top/bottom connector, statement input BODY)
|
||||
- Category "Functions" muncul di toolbox (menggunakan `custom: 'PROCEDURE'`)
|
||||
- Category "Program" muncul di toolbox dengan Main Program block
|
||||
- Hanya satu Main Program block diperbolehkan per workspace (enforced via change listener)
|
||||
- `debug-engine.js`, `ui-tabs.js` menggunakan `generateCode()` sebagai pengganti `workspaceToCode()`
|
||||
- Tanpa Main block: blocks lama tetap berjalan normal (backward compatible)
|
||||
- Dengan Main block: hanya function definitions + main block body yang di-execute
|
||||
- Code tab menampilkan generated code yang sesuai (async functions + main body)
|
||||
- Debug mode (breakpoints, step over/into) bekerja di dalam function calls
|
||||
- Export/Import workspace dengan main block & functions bisa di-save dan di-load
|
||||
- `pixi run build-app` berhasil tanpa error
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ BlockRegistry.register({
|
|||
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'
|
||||
'await executeAction(\'digital_out\', { gpio: \'' + GPIO + '\', state: String(' + STATE + ') });\n'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Block: main_program — Main program entry point (hat block).
|
||||
*
|
||||
* Type: Hat block (no top/bottom connectors, statement input for body)
|
||||
* Category: Program
|
||||
*
|
||||
* When present, only function definitions + this block's body are executed.
|
||||
* Orphan blocks outside the main block are ignored.
|
||||
* Only one main_program block is allowed per workspace.
|
||||
*/
|
||||
|
||||
BlockRegistry.register({
|
||||
name: 'main_program',
|
||||
category: 'Program',
|
||||
categoryColor: '#FF9800',
|
||||
color: '#FF9800',
|
||||
tooltip: 'Main program entry point. Only one allowed per workspace.',
|
||||
|
||||
definition: {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('\u{1F3C1} Main Program');
|
||||
this.appendStatementInput('BODY');
|
||||
this.setPreviousStatement(false);
|
||||
this.setNextStatement(false);
|
||||
this.setColour('#FF9800');
|
||||
this.setTooltip('Main program entry point. Only one allowed per workspace.');
|
||||
},
|
||||
},
|
||||
|
||||
generator: function (block) {
|
||||
const body = javascript.javascriptGenerator.statementToCode(block, 'BODY');
|
||||
return "highlightBlock('" + block.id + "');\n" + body;
|
||||
},
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const BLOCK_FILES = [
|
||||
'mainProgram.js',
|
||||
'digitalOut.js',
|
||||
'digitalIn.js',
|
||||
'delay.js',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* Async Procedures — Override Blockly's built-in procedure generators
|
||||
* for async/await support, and provide main-block-aware code generation.
|
||||
*
|
||||
* Since our custom blocks use `await executeAction()`, any user-defined
|
||||
* function that contains those blocks must be declared as `async function`
|
||||
* and called with `await`.
|
||||
*
|
||||
* Also provides `generateCode(ws)` which replaces `workspaceToCode()` —
|
||||
* when a main_program block exists, only function definitions + main body
|
||||
* are included (orphan blocks are ignored).
|
||||
*
|
||||
* Depends on: Blockly (vendor), javascript (vendor), BlockRegistry (registry.js)
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const gen = javascript.javascriptGenerator;
|
||||
|
||||
// ─── Save original generators ──────────────────────────────────────────────
|
||||
const origDefReturn = gen.forBlock['procedures_defreturn'];
|
||||
const origCallReturn = gen.forBlock['procedures_callreturn'];
|
||||
// Note: procedures_defnoreturn === procedures_defreturn in Blockly vendor
|
||||
// Note: procedures_callnoreturn delegates to callreturn dynamically
|
||||
|
||||
// ─── Override: procedures_defreturn & procedures_defnoreturn ────────────────
|
||||
// Makes generated function definitions async.
|
||||
// Original generates: function funcName(args) { ... }
|
||||
// Override produces: async function funcName(args) { ... }
|
||||
function asyncDefReturn(block, generator) {
|
||||
// Let original populate definitions_
|
||||
const result = origDefReturn.call(this, block, generator);
|
||||
|
||||
// Patch the definition: function → async function
|
||||
// scrub_() may prepend comments (e.g. "// Describe this function...\n"),
|
||||
// so we match by exact function name instead of start-of-string
|
||||
const funcName = generator.getProcedureName(block.getFieldValue('NAME'));
|
||||
const key = '%' + funcName;
|
||||
if (generator.definitions_[key]) {
|
||||
generator.definitions_[key] = generator.definitions_[key]
|
||||
.replace('function ' + funcName, 'async function ' + funcName);
|
||||
}
|
||||
|
||||
return result; // always null for def blocks
|
||||
}
|
||||
|
||||
gen.forBlock['procedures_defreturn'] = asyncDefReturn;
|
||||
gen.forBlock['procedures_defnoreturn'] = asyncDefReturn;
|
||||
|
||||
// ─── Override: procedures_callreturn ────────────────────────────────────────
|
||||
// Original returns: [funcName(args), ORDER.FUNCTION_CALL]
|
||||
// Override returns: [(await funcName(args)), ORDER.AWAIT]
|
||||
gen.forBlock['procedures_callreturn'] = function (block, generator) {
|
||||
const [code, _order] = origCallReturn.call(this, block, generator);
|
||||
return ['(await ' + code + ')', javascript.Order.AWAIT];
|
||||
};
|
||||
|
||||
// ─── Override: procedures_callnoreturn ──────────────────────────────────────
|
||||
// Original delegates to callreturn[0] + ";\n"
|
||||
// Override adds highlightBlock() for debug support + await
|
||||
gen.forBlock['procedures_callnoreturn'] = function (block, generator) {
|
||||
// Use original callreturn (pre-override) to get clean funcName(args)
|
||||
const [code, _order] = origCallReturn.call(this, block, generator);
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
'await ' + code + ';\n'
|
||||
);
|
||||
};
|
||||
|
||||
// ─── generateCode(ws) ─────────────────────────────────────────────────────
|
||||
/**
|
||||
* Generate executable code from the workspace.
|
||||
*
|
||||
* If a main_program block exists:
|
||||
* - Only function definitions + main block body are included
|
||||
* - Orphan top-level blocks are ignored
|
||||
* If no main_program block:
|
||||
* - Falls back to standard workspaceToCode() behavior (backward compatible)
|
||||
*
|
||||
* @param {Blockly.Workspace} ws
|
||||
* @returns {string} Generated JavaScript code
|
||||
*/
|
||||
window.generateCode = function (ws) {
|
||||
const mainBlocks = ws.getBlocksByType('main_program');
|
||||
|
||||
if (mainBlocks.length === 0) {
|
||||
// No main block — backward compatible
|
||||
return gen.workspaceToCode(ws);
|
||||
}
|
||||
|
||||
// Main block exists — selective code generation
|
||||
gen.init(ws);
|
||||
|
||||
// 1. Process all procedure definition blocks to populate definitions_
|
||||
const topBlocks = ws.getTopBlocks(true);
|
||||
for (const block of topBlocks) {
|
||||
if (
|
||||
block.type === 'procedures_defnoreturn' ||
|
||||
block.type === 'procedures_defreturn'
|
||||
) {
|
||||
gen.blockToCode(block);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process the main block body
|
||||
let mainCode = gen.blockToCode(mainBlocks[0]);
|
||||
if (Array.isArray(mainCode)) {
|
||||
mainCode = mainCode[0];
|
||||
}
|
||||
|
||||
// 3. Assemble: definitions (async functions) + main body code
|
||||
let code = gen.finish(mainCode);
|
||||
|
||||
// Clean up whitespace
|
||||
code = code.replace(/^\s+\n/, '');
|
||||
code = code.replace(/\n\s+$/, '\n');
|
||||
code = code.replace(/[ \t]+\n/g, '\n');
|
||||
|
||||
return code;
|
||||
};
|
||||
})();
|
||||
|
|
@ -38,7 +38,7 @@ function highlightBlock(blockId) {
|
|||
* No step-by-step control — just run everything as fast as possible.
|
||||
*/
|
||||
async function runProgram() {
|
||||
const code = javascript.javascriptGenerator.workspaceToCode(workspace);
|
||||
const code = generateCode(workspace);
|
||||
|
||||
if (!code.trim()) {
|
||||
consoleLog('No blocks to execute', 'error');
|
||||
|
|
@ -79,7 +79,7 @@ async function runProgram() {
|
|||
* Pauses at breakpoints and supports Step Over/Step Into.
|
||||
*/
|
||||
async function runDebug() {
|
||||
const code = javascript.javascriptGenerator.workspaceToCode(workspace);
|
||||
const code = generateCode(workspace);
|
||||
|
||||
if (!code.trim()) {
|
||||
consoleLog('No blocks to execute', 'error');
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ const BlockRegistry = (() => {
|
|||
name: 'Variables',
|
||||
colour: '#a55b80',
|
||||
custom: 'VARIABLE',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Functions',
|
||||
colour: '#995ba5',
|
||||
custom: 'PROCEDURE',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,6 @@ function switchTab(tab) {
|
|||
*/
|
||||
function refreshCodePanel() {
|
||||
if (typeof workspace === 'undefined' || !workspace) return;
|
||||
const code = javascript.javascriptGenerator.workspaceToCode(workspace);
|
||||
const code = generateCode(workspace);
|
||||
document.getElementById('code-output').textContent = code || '// (no blocks)';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,18 @@ function initWorkspace() {
|
|||
}
|
||||
});
|
||||
|
||||
// Enforce single main_program block per workspace
|
||||
ws.addChangeListener(function (event) {
|
||||
if (event.type === Blockly.Events.BLOCK_CREATE) {
|
||||
const mainBlocks = ws.getBlocksByType('main_program');
|
||||
if (mainBlocks.length > 1) {
|
||||
const newest = mainBlocks[mainBlocks.length - 1];
|
||||
newest.dispose();
|
||||
consoleLog('Only one Main Program block is allowed', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh Code tab when blocks change
|
||||
ws.addChangeListener(function () {
|
||||
const codePanel = document.getElementById('code-panel');
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@
|
|||
|
||||
<!-- Core infrastructure (load order matters) -->
|
||||
<script src="blockly/core/registry.js"></script>
|
||||
<script src="blockly/core/async-procedures.js"></script>
|
||||
<script src="blockly/core/breakpoints.js"></script>
|
||||
<script src="blockly/core/bridge.js"></script>
|
||||
<script src="blockly/core/debug-engine.js"></script>
|
||||
|
|
|
|||
171
workspace.json
171
workspace.json
|
|
@ -3,70 +3,66 @@
|
|||
"languageVersion": 0,
|
||||
"blocks": [
|
||||
{
|
||||
"type": "controls_repeat_ext",
|
||||
"id": "j+usYB.X4%o``Le;Q,LA",
|
||||
"x": 190,
|
||||
"y": 150,
|
||||
"type": "main_program",
|
||||
"id": "COLVqmFP{j*XNMc.9rz+",
|
||||
"x": 210,
|
||||
"y": 190,
|
||||
"inputs": {
|
||||
"TIMES": {
|
||||
"shadow": {
|
||||
"type": "math_number",
|
||||
"id": "cI`]|Us0+)Ol~XY0L`wE",
|
||||
"fields": {
|
||||
"NUM": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"DO": {
|
||||
"BODY": {
|
||||
"block": {
|
||||
"type": "digitalOut",
|
||||
"id": "3}G7M%~([nNy9YHlug!|",
|
||||
"type": "controls_whileUntil",
|
||||
"id": "UZH|b`4F=4X`zYjNW9zL",
|
||||
"fields": {
|
||||
"GPIO": 17
|
||||
"MODE": "WHILE"
|
||||
},
|
||||
"inputs": {
|
||||
"digitalOut": {
|
||||
"BOOL": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "Sw__`~LvxpKyo}./q]1/",
|
||||
"id": "nsD(~e:hYj[+9m-)yu-4",
|
||||
"fields": {
|
||||
"BOOL": "TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "?o-l1IBd(^YR,u[;k$-|",
|
||||
"fields": {
|
||||
"DURATION_MS": 500
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "digitalOut",
|
||||
"id": "t}@.X|Ac7F?J;C4v`5ic",
|
||||
"fields": {
|
||||
"GPIO": 17
|
||||
},
|
||||
"inputs": {
|
||||
"digitalOut": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "]I]_@v6=ErAYdiolo|+n",
|
||||
"fields": {
|
||||
"BOOL": "FALSE"
|
||||
},
|
||||
"DO": {
|
||||
"block": {
|
||||
"type": "digitalOut",
|
||||
"id": "Cz1MB5`}~cPRiZhh$/P9",
|
||||
"fields": {
|
||||
"GPIO": 17
|
||||
},
|
||||
"inputs": {
|
||||
"digitalOut": {
|
||||
"block": {
|
||||
"type": "procedures_callreturn",
|
||||
"id": "U|7!ynVp5Z.nwD_`4=Z,",
|
||||
"extraState": {
|
||||
"name": "foo",
|
||||
"params": [
|
||||
"logic"
|
||||
]
|
||||
},
|
||||
"inputs": {
|
||||
"ARG0": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "HXRaHRaPE)[G3WGmOi-T",
|
||||
"fields": {
|
||||
"BOOL": "TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "45BA([9g7/_tItSnUwB-",
|
||||
"fields": {
|
||||
"DURATION_MS": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "IXp?_lac7+V*GG!lW{]0",
|
||||
"fields": {
|
||||
"DURATION_MS": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +72,80 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "procedures_defreturn",
|
||||
"id": "4W(2:w1NGV^I;j6@^_I|",
|
||||
"x": 630,
|
||||
"y": 210,
|
||||
"extraState": {
|
||||
"params": [
|
||||
{
|
||||
"name": "logic",
|
||||
"id": "-HsGyh[-?q^.O;|%cRw="
|
||||
}
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"comment": {
|
||||
"text": "Describe this function...",
|
||||
"pinned": false,
|
||||
"height": 80,
|
||||
"width": 160
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"NAME": "foo"
|
||||
},
|
||||
"inputs": {
|
||||
"STACK": {
|
||||
"block": {
|
||||
"type": "digitalOut",
|
||||
"id": "@.#O-pmBQ/iD*yw?nVpw",
|
||||
"fields": {
|
||||
"GPIO": 17
|
||||
},
|
||||
"inputs": {
|
||||
"digitalOut": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "H%L0RpA8^Wt+Y~*sY0wH",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "-HsGyh[-?q^.O;|%cRw="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "GGjd9rTd!=`+-xt[nH-l",
|
||||
"fields": {
|
||||
"DURATION_MS": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"RETURN": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "xwUu7IW=*1qG5ae#*LzZ",
|
||||
"fields": {
|
||||
"BOOL": "FALSE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "logic",
|
||||
"id": "-HsGyh[-?q^.O;|%cRw="
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue