feat: enhance debug mode with async block highlighting and call depth tracking; improve control flow for step execution
parent
fc17d9e9e9
commit
5738255423
57
readme.md
57
readme.md
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
> **Project**: Blockly ROS2 Robot Controller (Kiwi Wheel AMR)
|
||||
> **ROS2 Distro**: Jazzy
|
||||
> **Last Updated**: 2026-03-10
|
||||
> **Current Focus**: `kiwi_controller` — Adaptive control for Kiwi Wheel drive
|
||||
> **Last Updated**: 2026-03-12
|
||||
> **Current Focus**:
|
||||
|
||||
Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
|
||||
|
||||
|
|
@ -36,46 +36,31 @@ jelaskan apa yang dimaksut untuk menyelesaikan task
|
|||
|
||||
# Potential Enhancements
|
||||
this list is short by priority
|
||||
- **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.
|
||||
- **low level node enhancement**: porting gpio_node to c++ and all low level node that run on raspberry pi using c++, if you use cross compile it using host PC, and send binnary to raspberry pi it gonna help me develop it faster.
|
||||
- **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
|
||||
- **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
|
||||
|
||||
# Feature Task
|
||||
|
||||
## 1 GPIO Node (digital out + digital in) : [x]
|
||||
Membuat ROS2 node (`gpio_node`) yang berjalan di Raspberry Pi untuk mengontrol pin GPIO secara langsung melalui `gpiod`. Node ini menerima perintah digital output via topic `/gpio/write` dan mempublikasikan state digital input via topic `/gpio/state`. Executor handler di-wire-up untuk publish/subscribe ke topic-topic tersebut pada mode real hardware.
|
||||
## 1 Bug Fix: Blockly Debug Mode — Step Into for Function Blocks : [x]
|
||||
Debug mode tidak bisa step into ke function blocks karena `highlightBlock()` bersifat synchronous — tidak bisa pause execution. Hanya `executeAction()` yang bisa pause, sehingga blocks tanpa `executeAction()` (function calls, variables, math) tidak bisa di-debug. Fix ini mengubah arsitektur debug engine:
|
||||
|
||||
1. **Async `highlightBlock()`** — menjadi universal pause point. Semua block generators menggunakan `await highlightBlock()` sehingga setiap block bisa di-breakpoint dan di-step.
|
||||
2. **Call depth tracking** — `enterFunction()/exitFunction()` di-inject ke generated code di procedure calls. Step Over menggunakan `callDepth` untuk skip function bodies.
|
||||
3. **Step modes** — `stepMode` state machine ('into'|'over'|'continue') menggantikan monkey-patching `highlightBlock` di setiap step function.
|
||||
4. **Auto-pause at first block** — debug mode langsung pause di block pertama (tidak perlu breakpoint untuk mulai stepping).
|
||||
5. **Run = Continue** — Run button saat paused berfungsi sebagai Continue (resume sampai breakpoint berikutnya).
|
||||
|
||||
### Definition Of Done
|
||||
- Package `gpio_node` dibuat dengan entry point `gpio_node = gpio_node.gpio_node:main`
|
||||
- Custom message `GpioWrite.msg` dan `GpioRead.msg` didefinisikan di `blockly_interfaces`
|
||||
- Handler `digital_out` mempublish `GpioWrite` ke `/gpio/write` pada mode real
|
||||
- Handler `digital_in` subscribe ke `/gpio/state` dan membaca cache pin state
|
||||
- Block `digitalIn.js` ditambahkan sebagai output block (return 0/1)
|
||||
- Task `pixi run gpio-node` dan `pixi run build-gpio` tersedia
|
||||
- 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
|
||||
- `debug-engine.js` di-rewrite: async `highlightBlock()` override di `runDebug()`, `callDepth` tracking, `stepMode` state machine
|
||||
- `enterFunction()`/`exitFunction()` global helpers tersedia untuk generated code
|
||||
- `async-procedures.js`: `procedures_callreturn` wrapped dengan async IIFE + `highlightBlock()` + depth tracking
|
||||
- `async-procedures.js`: `procedures_callnoreturn` menggunakan `await highlightBlock()` + `enterFunction()/exitFunction()` dengan try/finally
|
||||
- Block generators (`digitalOut.js`, `delay.js`, `mainProgram.js`) menggunakan `await highlightBlock()`
|
||||
- `ui-controls.js`: Run button enabled saat paused (Continue behavior), `onRunClick()` memanggil `continueExecution()`
|
||||
- Step Into pada function call block → pause di block pertama dalam function body
|
||||
- Step Over pada function call block → skip function body, pause di block berikutnya
|
||||
- Debug mode pause di block pertama tanpa perlu breakpoint
|
||||
- Non-debug mode (`runProgram()`) tidak terpengaruh — `await` pada synchronous `highlightBlock()` adalah no-op
|
||||
- `pixi run build-app` berhasil tanpa error
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ BlockRegistry.register({
|
|||
generator: function (block) {
|
||||
const ms = block.getFieldValue('DURATION_MS');
|
||||
return (
|
||||
"highlightBlock('" + block.id + "');\n" +
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
"await executeAction('delay', { duration_ms: '" + ms + "' });\n"
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ BlockRegistry.register({
|
|||
const GPIO = block.getFieldValue('GPIO');
|
||||
const STATE = Blockly.JavaScript.valueToCode(block, 'digitalOut', Blockly.JavaScript.ORDER_ATOMIC) || 'false';
|
||||
return (
|
||||
'highlightBlock(\'' + block.id + '\');\n' +
|
||||
'await highlightBlock(\'' + block.id + '\');\n' +
|
||||
'await executeAction(\'digital_out\', { gpio: \'' + GPIO + '\', state: String(' + STATE + ') });\n'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ BlockRegistry.register({
|
|||
|
||||
generator: function (block) {
|
||||
const body = javascript.javascriptGenerator.statementToCode(block, 'BODY');
|
||||
return "highlightBlock('" + block.id + "');\n" + body;
|
||||
return "await highlightBlock('" + block.id + "');\n" + body;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,21 +50,34 @@
|
|||
|
||||
// ─── Override: procedures_callreturn ────────────────────────────────────────
|
||||
// Original returns: [funcName(args), ORDER.FUNCTION_CALL]
|
||||
// Override returns: [(await funcName(args)), ORDER.AWAIT]
|
||||
// Override wraps in async IIFE with highlightBlock + call depth tracking.
|
||||
// The IIFE is needed because this is a value expression (returns a value),
|
||||
// but we need to await highlightBlock() and track call depth as statements.
|
||||
gen.forBlock['procedures_callreturn'] = function (block, generator) {
|
||||
const [code, _order] = origCallReturn.call(this, block, generator);
|
||||
return ['(await ' + code + ')', javascript.Order.AWAIT];
|
||||
const id = block.id;
|
||||
// enterFunction() BEFORE highlightBlock() so that Step Over from the
|
||||
// parent block (which embeds this value expression) sees callDepth > stepStartDepth
|
||||
// and skips this highlight entirely. Step Into still pauses because it ignores depth.
|
||||
return [
|
||||
"(await (async function() { enterFunction(); await highlightBlock('" + id + "'); " +
|
||||
'try { return await ' + code + '; } ' +
|
||||
'finally { exitFunction(); } ' +
|
||||
'})())',
|
||||
javascript.Order.AWAIT,
|
||||
];
|
||||
};
|
||||
|
||||
// ─── Override: procedures_callnoreturn ──────────────────────────────────────
|
||||
// Original delegates to callreturn[0] + ";\n"
|
||||
// Override adds highlightBlock() for debug support + await
|
||||
// Override adds await highlightBlock() for debug + call depth tracking
|
||||
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'
|
||||
"await highlightBlock('" + block.id + "');\n" +
|
||||
'enterFunction();\n' +
|
||||
'try { await ' + code + '; } finally { exitFunction(); }\n'
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Debug Engine — Step-by-step execution, breakpoints, and program control.
|
||||
*
|
||||
* Provides Run, Debug, Step Over, Step Into, and Stop functionality
|
||||
* Provides Run, Debug, Step Over, Step Into, Continue, and Stop functionality
|
||||
* for Blockly programs. In debug mode, execution pauses at breakpoints
|
||||
* and allows step-by-step control.
|
||||
* and allows step-by-step control via async highlightBlock().
|
||||
*
|
||||
* Depends on: executeAction (bridge.js), activeBreakpoints/debugModeActive
|
||||
* (breakpoints.js), updateButtonStates (ui-controls.js),
|
||||
|
|
@ -16,6 +16,9 @@ const debugState = {
|
|||
isPaused: false,
|
||||
currentBlockId: null,
|
||||
stopRequested: false,
|
||||
callDepth: 0, // function call nesting depth
|
||||
stepMode: null, // 'into' | 'over' | 'continue' | null
|
||||
stepStartDepth: 0, // callDepth when step-over was initiated
|
||||
};
|
||||
|
||||
let debugResolve = null; // resolve function for pause/resume
|
||||
|
|
@ -23,6 +26,10 @@ let debugResolve = null; // resolve function for pause/resume
|
|||
// ─── Highlight Helper ───────────────────────────────────────────────────────
|
||||
/**
|
||||
* Highlight the currently executing block in the workspace.
|
||||
* In non-debug mode this is synchronous (await on undefined is a no-op).
|
||||
* In debug mode, runDebug() overrides this with an async version that
|
||||
* can pause execution at breakpoints and step boundaries.
|
||||
*
|
||||
* @param {string|null} blockId - Block ID to highlight, or null to clear
|
||||
*/
|
||||
function highlightBlock(blockId) {
|
||||
|
|
@ -32,10 +39,27 @@ function highlightBlock(blockId) {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Call Depth Tracking ────────────────────────────────────────────────────
|
||||
/**
|
||||
* Called before entering a user-defined function body.
|
||||
* Generated code injects enterFunction()/exitFunction() around procedure calls.
|
||||
*/
|
||||
function enterFunction() {
|
||||
debugState.callDepth++;
|
||||
}
|
||||
|
||||
function exitFunction() {
|
||||
debugState.callDepth--;
|
||||
}
|
||||
|
||||
// ─── Run Program (Non-Debug) ────────────────────────────────────────────────
|
||||
/**
|
||||
* Run the full Blockly program using async eval().
|
||||
* No step-by-step control — just run everything as fast as possible.
|
||||
*
|
||||
* Note: eval() is intentional here — Blockly generates JavaScript code
|
||||
* strings that must be evaluated at runtime. This is the core execution
|
||||
* model for the visual programming environment.
|
||||
*/
|
||||
async function runProgram() {
|
||||
const code = generateCode(workspace);
|
||||
|
|
@ -53,9 +77,8 @@ async function runProgram() {
|
|||
consoleLog('=== Program started ===', 'info');
|
||||
|
||||
try {
|
||||
// Wrap in async function to support await
|
||||
const wrappedCode = `(async function() {\n${code}\n})()`;
|
||||
await eval(wrappedCode);
|
||||
const wrappedCode = '(async function() {\n' + code + '\n})()';
|
||||
await (0, globalThis.eval)(wrappedCode);
|
||||
if (!debugState.stopRequested) {
|
||||
consoleLog('=== Program completed ===', 'success');
|
||||
}
|
||||
|
|
@ -63,7 +86,7 @@ async function runProgram() {
|
|||
if (e.message === 'STOP_EXECUTION') {
|
||||
consoleLog('=== Program stopped by user ===', 'info');
|
||||
} else {
|
||||
consoleLog(`Error: ${e.message}`, 'error');
|
||||
consoleLog('Error: ' + e.message, 'error');
|
||||
}
|
||||
} finally {
|
||||
debugState.isRunning = false;
|
||||
|
|
@ -76,7 +99,13 @@ async function runProgram() {
|
|||
// ─── Debug Engine ───────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Run the program in debug mode.
|
||||
* Pauses at breakpoints and supports Step Over/Step Into.
|
||||
* Pauses at the first block, at breakpoints, and supports Step Over/Into.
|
||||
*
|
||||
* All pause logic lives in the async highlightBlock() override — this is
|
||||
* the single pause point for the entire debug engine. executeAction()
|
||||
* only checks stopRequested.
|
||||
*
|
||||
* Note: eval() is intentional — see runProgram() comment.
|
||||
*/
|
||||
async function runDebug() {
|
||||
const code = generateCode(workspace);
|
||||
|
|
@ -89,77 +118,106 @@ async function runDebug() {
|
|||
debugState.isRunning = true;
|
||||
debugState.isPaused = false;
|
||||
debugState.stopRequested = false;
|
||||
debugState.callDepth = 0;
|
||||
debugState.stepMode = null;
|
||||
debugState.stepStartDepth = 0;
|
||||
updateButtonStates();
|
||||
|
||||
consoleLog('=== Debug started ===', 'info');
|
||||
|
||||
try {
|
||||
const wrappedCode = `
|
||||
(async function() {
|
||||
${code}
|
||||
})()
|
||||
`;
|
||||
let firstHighlight = true;
|
||||
|
||||
// Override executeAction temporarily to add debug pause logic
|
||||
try {
|
||||
const wrappedCode = '(async function() {\n' + code + '\n})()';
|
||||
|
||||
// Override executeAction — only check stop, no pause logic
|
||||
const originalExecuteAction = window.executeAction;
|
||||
|
||||
window.executeAction = async function (command, params) {
|
||||
// Check if stop was requested
|
||||
if (debugState.stopRequested) {
|
||||
throw new Error('STOP_EXECUTION');
|
||||
}
|
||||
|
||||
// Check for breakpoint on current block
|
||||
if (activeBreakpoints.has(debugState.currentBlockId)) {
|
||||
consoleLog(
|
||||
`Breakpoint hit on block ${debugState.currentBlockId}`,
|
||||
'feedback'
|
||||
);
|
||||
debugState.isPaused = true;
|
||||
updateButtonStates();
|
||||
// Wait until resumed
|
||||
await new Promise((resolve) => {
|
||||
debugResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
// If running in debug auto-step mode, add a small delay for visual feedback
|
||||
if (!debugState.isPaused && debugModeActive) {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// Check stop again after pause
|
||||
if (debugState.stopRequested) {
|
||||
throw new Error('STOP_EXECUTION');
|
||||
}
|
||||
|
||||
return await originalExecuteAction(command, params);
|
||||
};
|
||||
|
||||
// Override highlightBlock to also pause on breakpoints
|
||||
// Override highlightBlock with async version — the universal pause point
|
||||
const originalHighlight = window.highlightBlock;
|
||||
window.highlightBlock = function (blockId) {
|
||||
|
||||
window.highlightBlock = async function (blockId) {
|
||||
originalHighlight(blockId);
|
||||
|
||||
if (debugState.stopRequested) {
|
||||
throw new Error('STOP_EXECUTION');
|
||||
}
|
||||
|
||||
var shouldPause = false;
|
||||
|
||||
if (firstHighlight) {
|
||||
// Always pause on the very first block in debug mode
|
||||
firstHighlight = false;
|
||||
shouldPause = true;
|
||||
} else if (debugState.stepMode === 'into') {
|
||||
shouldPause = true;
|
||||
} else if (debugState.stepMode === 'over') {
|
||||
// Only pause when back at same depth or shallower
|
||||
if (debugState.callDepth <= debugState.stepStartDepth) {
|
||||
shouldPause = true;
|
||||
}
|
||||
} else if (debugState.stepMode === 'continue' || debugState.stepMode === null) {
|
||||
// Continue or auto-run: only pause at breakpoints
|
||||
if (activeBreakpoints.has(blockId)) {
|
||||
shouldPause = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPause) {
|
||||
debugState.isPaused = true;
|
||||
debugState.stepMode = null;
|
||||
updateButtonStates();
|
||||
|
||||
if (activeBreakpoints.has(blockId)) {
|
||||
consoleLog('Breakpoint hit on block ' + blockId, 'feedback');
|
||||
}
|
||||
|
||||
await new Promise(function (resolve) {
|
||||
debugResolve = resolve;
|
||||
});
|
||||
|
||||
if (debugState.stopRequested) {
|
||||
throw new Error('STOP_EXECUTION');
|
||||
}
|
||||
} else {
|
||||
// Not pausing — add small delay for visual feedback, but only when
|
||||
// it makes sense (auto-run mode). Skip delay when:
|
||||
// - continue mode (running to next breakpoint at full speed)
|
||||
// - step over inside a function body (should skip instantly)
|
||||
var inDeeperCall = debugState.stepMode === 'over' &&
|
||||
debugState.callDepth > debugState.stepStartDepth;
|
||||
if (debugState.stepMode === null && !inDeeperCall) {
|
||||
await new Promise(function (r) { setTimeout(r, 300); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await eval(wrappedCode);
|
||||
await (0, globalThis.eval)(wrappedCode);
|
||||
|
||||
if (!debugState.stopRequested) {
|
||||
consoleLog('=== Debug completed ===', 'success');
|
||||
}
|
||||
|
||||
// Restore originals
|
||||
window.executeAction = originalExecuteAction;
|
||||
window.highlightBlock = originalHighlight;
|
||||
} catch (e) {
|
||||
if (e.message === 'STOP_EXECUTION') {
|
||||
consoleLog('=== Program stopped by user ===', 'info');
|
||||
} else {
|
||||
consoleLog(`Error: ${e.message}`, 'error');
|
||||
consoleLog('Error: ' + e.message, 'error');
|
||||
}
|
||||
} finally {
|
||||
// Always restore overrides — even on error or stop
|
||||
window.executeAction = originalExecuteAction;
|
||||
window.highlightBlock = originalHighlight;
|
||||
debugState.isRunning = false;
|
||||
debugState.isPaused = false;
|
||||
debugState.callDepth = 0;
|
||||
debugState.stepMode = null;
|
||||
debugResolve = null;
|
||||
workspace.highlightBlock(null);
|
||||
updateButtonStates();
|
||||
|
|
@ -169,24 +227,15 @@ async function runDebug() {
|
|||
// ─── Control Functions ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Step Over: resume from pause, execute current block, pause at next block.
|
||||
* Step Over: execute current block and pause at next block at same depth.
|
||||
* Skips function bodies — only pauses when callDepth <= current depth.
|
||||
*/
|
||||
function stepOver() {
|
||||
if (!debugState.isPaused) return;
|
||||
|
||||
debugState.isPaused = false;
|
||||
|
||||
const currentBlock = debugState.currentBlockId;
|
||||
const origHighlight = window.highlightBlock;
|
||||
window.highlightBlock = function (blockId) {
|
||||
origHighlight(blockId);
|
||||
if (blockId !== currentBlock) {
|
||||
// Moved to a new block — pause again
|
||||
debugState.isPaused = true;
|
||||
updateButtonStates();
|
||||
window.highlightBlock = origHighlight;
|
||||
}
|
||||
};
|
||||
debugState.stepMode = 'over';
|
||||
debugState.stepStartDepth = debugState.callDepth;
|
||||
|
||||
if (debugResolve) {
|
||||
debugResolve();
|
||||
|
|
@ -196,21 +245,30 @@ function stepOver() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Step Into: resume from pause, execute one interpreter step.
|
||||
* For our async model, this pauses on the very next highlightBlock call.
|
||||
* Step Into: resume and pause on the very next highlightBlock call.
|
||||
* Enters function bodies — pauses at first block inside the function.
|
||||
*/
|
||||
function stepInto() {
|
||||
if (!debugState.isPaused) return;
|
||||
|
||||
debugState.isPaused = false;
|
||||
debugState.stepMode = 'into';
|
||||
|
||||
const origHighlight = window.highlightBlock;
|
||||
window.highlightBlock = function (blockId) {
|
||||
origHighlight(blockId);
|
||||
debugState.isPaused = true;
|
||||
updateButtonStates();
|
||||
window.highlightBlock = origHighlight;
|
||||
};
|
||||
if (debugResolve) {
|
||||
debugResolve();
|
||||
debugResolve = null;
|
||||
}
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue: resume execution and only pause at the next breakpoint.
|
||||
*/
|
||||
function continueExecution() {
|
||||
if (!debugState.isPaused) return;
|
||||
|
||||
debugState.isPaused = false;
|
||||
debugState.stepMode = 'continue';
|
||||
|
||||
if (debugResolve) {
|
||||
debugResolve();
|
||||
|
|
@ -225,6 +283,7 @@ function stepInto() {
|
|||
function stopExecution() {
|
||||
debugState.stopRequested = true;
|
||||
debugState.isPaused = false;
|
||||
debugState.isRunning = false; // Immediately reflect stopped state in UI
|
||||
|
||||
// Cancel any in-flight executeAction() call (JS + Python side)
|
||||
cancelCurrentAction();
|
||||
|
|
@ -235,6 +294,8 @@ function stopExecution() {
|
|||
debugResolve = null;
|
||||
}
|
||||
|
||||
// Clear block highlight and update UI immediately
|
||||
workspace.highlightBlock(null);
|
||||
consoleLog('Stop requested...', 'info');
|
||||
updateButtonStates();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@
|
|||
* Runs in debug mode or normal mode depending on debug toggle.
|
||||
*/
|
||||
function onRunClick() {
|
||||
if (debugModeActive) {
|
||||
if (debugState.isPaused) {
|
||||
// Already paused in debug — "Run" acts as "Continue" (resume until next breakpoint)
|
||||
continueExecution();
|
||||
} else if (debugModeActive) {
|
||||
runDebug();
|
||||
} else {
|
||||
runProgram();
|
||||
|
|
@ -58,7 +61,8 @@ function updateButtonStates() {
|
|||
const btnStop = document.getElementById('btn-stop');
|
||||
|
||||
if (debugState.isRunning) {
|
||||
btnRun.disabled = true;
|
||||
// When paused, enable Run (acts as Continue), Step Over, and Step Into
|
||||
btnRun.disabled = !debugState.isPaused;
|
||||
btnStop.disabled = false;
|
||||
btnStepOver.disabled = !debugState.isPaused;
|
||||
btnStepInto.disabled = !debugState.isPaused;
|
||||
|
|
|
|||
Loading…
Reference in New Issue