diff --git a/readme.md b/readme.md index 392138f..531787e 100644 --- a/readme.md +++ b/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 diff --git a/src/blockly_app/blockly_app/ui/blockly/blocks/digitalOut.js b/src/blockly_app/blockly_app/ui/blockly/blocks/digitalOut.js index 2352b1b..71ddf4b 100644 --- a/src/blockly_app/blockly_app/ui/blockly/blocks/digitalOut.js +++ b/src/blockly_app/blockly_app/ui/blockly/blocks/digitalOut.js @@ -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' ); } }); \ No newline at end of file diff --git a/src/blockly_app/blockly_app/ui/blockly/blocks/mainProgram.js b/src/blockly_app/blockly_app/ui/blockly/blocks/mainProgram.js new file mode 100644 index 0000000..9328c10 --- /dev/null +++ b/src/blockly_app/blockly_app/ui/blockly/blocks/mainProgram.js @@ -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; + }, +}); diff --git a/src/blockly_app/blockly_app/ui/blockly/blocks/manifest.js b/src/blockly_app/blockly_app/ui/blockly/blocks/manifest.js index 3c4e4c8..84d28b5 100644 --- a/src/blockly_app/blockly_app/ui/blockly/blocks/manifest.js +++ b/src/blockly_app/blockly_app/ui/blockly/blocks/manifest.js @@ -9,6 +9,7 @@ // eslint-disable-next-line no-unused-vars const BLOCK_FILES = [ + 'mainProgram.js', 'digitalOut.js', 'digitalIn.js', 'delay.js', diff --git a/src/blockly_app/blockly_app/ui/blockly/core/async-procedures.js b/src/blockly_app/blockly_app/ui/blockly/core/async-procedures.js new file mode 100644 index 0000000..4ec2756 --- /dev/null +++ b/src/blockly_app/blockly_app/ui/blockly/core/async-procedures.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; + }; +})(); diff --git a/src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js b/src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js index f98e851..ef1bd55 100644 --- a/src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js +++ b/src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js @@ -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'); diff --git a/src/blockly_app/blockly_app/ui/blockly/core/registry.js b/src/blockly_app/blockly_app/ui/blockly/core/registry.js index 651a605..a1b26ea 100644 --- a/src/blockly_app/blockly_app/ui/blockly/core/registry.js +++ b/src/blockly_app/blockly_app/ui/blockly/core/registry.js @@ -127,6 +127,12 @@ const BlockRegistry = (() => { name: 'Variables', colour: '#a55b80', custom: 'VARIABLE', + }, + { + kind: 'category', + name: 'Functions', + colour: '#995ba5', + custom: 'PROCEDURE', } ); diff --git a/src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js b/src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js index 57ab8f6..761f1d3 100644 --- a/src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js +++ b/src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js @@ -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)'; } diff --git a/src/blockly_app/blockly_app/ui/blockly/workspace-init.js b/src/blockly_app/blockly_app/ui/blockly/workspace-init.js index 953e6ce..d5f4617 100644 --- a/src/blockly_app/blockly_app/ui/blockly/workspace-init.js +++ b/src/blockly_app/blockly_app/ui/blockly/workspace-init.js @@ -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'); diff --git a/src/blockly_app/blockly_app/ui/index.html b/src/blockly_app/blockly_app/ui/index.html index 8a67d93..ca5b281 100644 --- a/src/blockly_app/blockly_app/ui/index.html +++ b/src/blockly_app/blockly_app/ui/index.html @@ -242,6 +242,7 @@ + diff --git a/workspace.json b/workspace.json index 5aee7bc..05d3c68 100644 --- a/workspace.json +++ b/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=" + } + ] } \ No newline at end of file