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