From d7a9c3ef772beec49c826851bd2ad08c7b02a60d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 18:57:54 +0900 Subject: [PATCH 1/8] fix: improve 'moved' gesture event behavior in MicrobitMore - Implement cooldown (165ms) for 'moved' event to prevent continuous firing - Move 'moved' gesture to the top of the gestures menu - Set 'moved' as the default value for the gesture block Co-Authored-By: Gemini --- src/extensions/microbitMore/index.js | 39 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index 0f2451be06..e419572d74 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -1606,6 +1606,14 @@ class MbitMoreBlocks { */ get GESTURES_MENU () { return [ + { + text: formatMessage({ + id: 'mbitMore.gesturesMenu.moved', + default: 'moved', + description: 'label for moved gesture in gesture picker for microbit more extension' + }), + value: 'MOVED' + }, { text: formatMessage({ id: 'mbitMore.gesturesMenu.tiltUp', @@ -1693,14 +1701,6 @@ class MbitMoreBlocks { description: 'label for shaken gesture in gesture picker for microbit more extension' }), value: MbitMoreGestureName.SHAKE - }, - { - text: formatMessage({ - id: 'mbitMore.gesturesMenu.moved', - default: 'moved', - description: 'label for moved gesture in gesture picker for microbit more extension' - }), - value: 'MOVED' } ]; } @@ -2149,6 +2149,12 @@ class MbitMoreBlocks { */ this.prevGestureEvents = {}; + /** + * The last time the "moved" event was fired. + * @type {number} + */ + this.lastMovedEventTime = null; + /** * The previous timestamps of pin events. * @type {Object.>} @@ -2278,7 +2284,7 @@ class MbitMoreBlocks { GESTURE: { type: ArgumentType.STRING, menu: 'gestures', - defaultValue: MbitMoreGestureName.SHAKE + defaultValue: 'MOVED' } } }, @@ -2873,10 +2879,23 @@ class MbitMoreBlocks { } const gestureName = args.GESTURE; if (gestureName === 'MOVED') { - return Object.entries(this._peripheral.gestureEvents).some(([name, timestamp]) => { + // クールダウン期間中はfalseを返す + const cooldownTime = this.runtime.currentStepTime * 5; + if (this.lastMovedEventTime !== null && + (Date.now() - this.lastMovedEventTime) < cooldownTime) { + return false; + } + + const eventOccurred = Object.entries(this._peripheral.gestureEvents).some(([name, timestamp]) => { if (!this.prevGestureEvents[name]) return true; return timestamp !== this.prevGestureEvents[name]; }); + + if (eventOccurred) { + this.lastMovedEventTime = Date.now(); + } + + return eventOccurred; } const lastTimestamp = this._peripheral.getGestureEventTimestamp(gestureName); From c8e01032910a4c2aa2b6597e3c658ebe2f8afdee Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 19:54:46 +0900 Subject: [PATCH 2/8] fix: add high-resolution logging for 'moved' event cooldown verification --- src/extensions/microbitMore/index.js | 41 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index e419572d74..1119a31534 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2871,7 +2871,7 @@ class MbitMoreBlocks { * @return {boolean} - true if the event raised. */ whenGesture (args) { - if (!this.updateLastGestureEventTimer) { + if (this.updateLastGestureEventTimer === null) { this.updateLastGestureEventTimer = setTimeout(() => { this.updatePrevGestureEvents(); this.updateLastGestureEventTimer = null; @@ -2879,20 +2879,43 @@ class MbitMoreBlocks { } const gestureName = args.GESTURE; if (gestureName === 'MOVED') { - // クールダウン期間中はfalseを返す + const now = Date.now(); + const perfNow = performance.now(); const cooldownTime = this.runtime.currentStepTime * 5; - if (this.lastMovedEventTime !== null && - (Date.now() - this.lastMovedEventTime) < cooldownTime) { - return false; - } + const diff = this.lastMovedEventTime === null ? Infinity : (now - this.lastMovedEventTime); + const inCooldown = diff < cooldownTime; + let triggerEvent = null; const eventOccurred = Object.entries(this._peripheral.gestureEvents).some(([name, timestamp]) => { - if (!this.prevGestureEvents[name]) return true; - return timestamp !== this.prevGestureEvents[name]; + let isChanged = false; + if (this.prevGestureEvents[name]) { + isChanged = timestamp !== this.prevGestureEvents[name]; + } else { + isChanged = true; + } + if (isChanged) { + triggerEvent = {name, timestamp, prev: this.prevGestureEvents[name]}; + } + return isChanged; }); + if (eventOccurred || diff >= cooldownTime) { + const logMsg = `[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms (Date: ${now}), ` + + `Diff: ${diff}ms, Cooldown: ${cooldownTime}ms, InCooldown: ${inCooldown}`; + console.log(logMsg); + if (eventOccurred) { + console.log(`[DEBUG MOVED] Triggered by: ${triggerEvent.name}, ` + + `Timestamp: ${triggerEvent.timestamp}, Prev: ${triggerEvent.prev}`); + } + } + + if (inCooldown) { + return false; + } + if (eventOccurred) { - this.lastMovedEventTime = Date.now(); + this.lastMovedEventTime = now; + console.log(`[DEBUG MOVED] Event FIRED at ${performance.now().toFixed(3)}ms`); } return eventOccurred; From 462b3519c6120dd07e5a304302ea89d0387867c9 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 20:21:57 +0900 Subject: [PATCH 3/8] fix: finalize 'moved' gesture improvements and remove debug logs --- src/extensions/microbitMore/index.js | 34 ++++++---------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index 1119a31534..7b016cf4c7 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2880,42 +2880,22 @@ class MbitMoreBlocks { const gestureName = args.GESTURE; if (gestureName === 'MOVED') { const now = Date.now(); - const perfNow = performance.now(); const cooldownTime = this.runtime.currentStepTime * 5; - const diff = this.lastMovedEventTime === null ? Infinity : (now - this.lastMovedEventTime); - const inCooldown = diff < cooldownTime; + if (this.lastMovedEventTime !== null) { + if ((now - this.lastMovedEventTime) < cooldownTime) { + return false; + } + } - let triggerEvent = null; const eventOccurred = Object.entries(this._peripheral.gestureEvents).some(([name, timestamp]) => { - let isChanged = false; if (this.prevGestureEvents[name]) { - isChanged = timestamp !== this.prevGestureEvents[name]; - } else { - isChanged = true; + return timestamp !== this.prevGestureEvents[name]; } - if (isChanged) { - triggerEvent = {name, timestamp, prev: this.prevGestureEvents[name]}; - } - return isChanged; + return true; }); - if (eventOccurred || diff >= cooldownTime) { - const logMsg = `[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms (Date: ${now}), ` + - `Diff: ${diff}ms, Cooldown: ${cooldownTime}ms, InCooldown: ${inCooldown}`; - console.log(logMsg); - if (eventOccurred) { - console.log(`[DEBUG MOVED] Triggered by: ${triggerEvent.name}, ` + - `Timestamp: ${triggerEvent.timestamp}, Prev: ${triggerEvent.prev}`); - } - } - - if (inCooldown) { - return false; - } - if (eventOccurred) { this.lastMovedEventTime = now; - console.log(`[DEBUG MOVED] Event FIRED at ${performance.now().toFixed(3)}ms`); } return eventOccurred; From 08a93857a34fea6b789f7698662c4f09a1950646 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 20:58:51 +0900 Subject: [PATCH 4/8] fix: restore debug logs and fix updateLastGestureEventTimer initialization --- src/extensions/microbitMore/index.js | 38 ++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index 7b016cf4c7..b374262647 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2155,6 +2155,12 @@ class MbitMoreBlocks { */ this.lastMovedEventTime = null; + /** + * The timer of updateLastGestureEvent. + * @type {number} + */ + this.updateLastGestureEventTimer = null; + /** * The previous timestamps of pin events. * @type {Object.>} @@ -2858,6 +2864,7 @@ class MbitMoreBlocks { * Update the last occured time of all gesture events. */ updatePrevGestureEvents () { + console.log(`[DEBUG MOVED] updatePrevGestureEvents at ${performance.now().toFixed(3)}ms`); this.prevGestureEvents = {}; Object.entries(this._peripheral.gestureEvents).forEach(([gestureName, timestamp]) => { this.prevGestureEvents[gestureName] = timestamp; @@ -2880,22 +2887,37 @@ class MbitMoreBlocks { const gestureName = args.GESTURE; if (gestureName === 'MOVED') { const now = Date.now(); + const perfNow = performance.now(); const cooldownTime = this.runtime.currentStepTime * 5; - if (this.lastMovedEventTime !== null) { - if ((now - this.lastMovedEventTime) < cooldownTime) { - return false; - } - } + const diff = this.lastMovedEventTime === null ? Infinity : (now - this.lastMovedEventTime); + const inCooldown = diff < cooldownTime; - const eventOccurred = Object.entries(this._peripheral.gestureEvents).some(([name, timestamp]) => { + const changedGestures = []; + Object.entries(this._peripheral.gestureEvents).forEach(([name, timestamp]) => { if (this.prevGestureEvents[name]) { - return timestamp !== this.prevGestureEvents[name]; + if (timestamp !== this.prevGestureEvents[name]) { + changedGestures.push(`${name}(${this.prevGestureEvents[name]}->${timestamp})`); + } + } else { + changedGestures.push(`${name}(new->${timestamp})`); } - return true; }); + const eventOccurred = changedGestures.length > 0; + + if (eventOccurred || diff >= cooldownTime) { + console.log(`[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms, Diff: ${diff}ms, ` + + `InCooldown: ${inCooldown}, EventOccurred: ${eventOccurred}, ` + + `Changes: [${changedGestures.join(', ')}]`); + } + + if (inCooldown) { + return false; + } + if (eventOccurred) { this.lastMovedEventTime = now; + console.log(`[DEBUG MOVED] *** FIRED *** at ${perfNow.toFixed(3)}ms`); } return eventOccurred; From 9f511ddcb38f560c37c7953c0a046376110f151d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 21:47:58 +0900 Subject: [PATCH 5/8] fix: implement gap-based 'moved' event logic with timeout (with debug logs) - Fire event on movement, then suppress until a 5-frame gap occurs - Force fire after 30 frames if movement is continuous - Includes high-resolution debug logs for verification --- src/extensions/microbitMore/index.js | 66 +++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index b374262647..8a9c319b90 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2155,6 +2155,18 @@ class MbitMoreBlocks { */ this.lastMovedEventTime = null; + /** + * The last time any gesture event was detected. + * @type {number} + */ + this.lastGestureOccurredTime = null; + + /** + * Whether the extension is waiting for a gap in movement before firing again. + * @type {boolean} + */ + this.isWaitingForGap = false; + /** * The timer of updateLastGestureEvent. * @type {number} @@ -2888,9 +2900,9 @@ class MbitMoreBlocks { if (gestureName === 'MOVED') { const now = Date.now(); const perfNow = performance.now(); - const cooldownTime = this.runtime.currentStepTime * 5; - const diff = this.lastMovedEventTime === null ? Infinity : (now - this.lastMovedEventTime); - const inCooldown = diff < cooldownTime; + const stepTime = this.runtime.currentStepTime; + const gapThreshold = stepTime * 5; // 5フレームの空白 + const timeoutThreshold = stepTime * 30; // 30フレームで強制発火 const changedGestures = []; Object.entries(this._peripheral.gestureEvents).forEach(([name, timestamp]) => { @@ -2903,24 +2915,54 @@ class MbitMoreBlocks { } }); - const eventOccurred = changedGestures.length > 0; + const eventDetected = changedGestures.length > 0; + if (eventDetected) { + this.lastGestureOccurredTime = now; + } + + // 静止判定(隙間ができたか) + const timeSinceLastOccurred = this.lastGestureOccurredTime === null ? + Infinity : (now - this.lastGestureOccurredTime); + if (timeSinceLastOccurred >= gapThreshold) { + if (this.isWaitingForGap) { + console.log(`[DEBUG MOVED] Gap detected (${timeSinceLastOccurred.toFixed(1)}ms). Resetting.`); + } + this.isWaitingForGap = false; + } - if (eventOccurred || diff >= cooldownTime) { - console.log(`[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms, Diff: ${diff}ms, ` + - `InCooldown: ${inCooldown}, EventOccurred: ${eventOccurred}, ` + - `Changes: [${changedGestures.join(', ')}]`); + let shouldFire = false; + let fireReason = ''; + + if (eventDetected) { + if (this.isWaitingForGap) { + // 動き続けている場合の強制発火判定 + const timeSinceLastFired = this.lastMovedEventTime === null ? + Infinity : (now - this.lastMovedEventTime); + if (timeSinceLastFired >= timeoutThreshold) { + shouldFire = true; + fireReason = `Continuous movement timeout (${timeSinceLastFired.toFixed(1)}ms)`; + } + } else { + shouldFire = true; + fireReason = 'Movement started after gap'; + } } - if (inCooldown) { - return false; + if (eventDetected || shouldFire || this.isWaitingForGap) { + console.log(`[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms, Detected: ${eventDetected}, ` + + `Waiting: ${this.isWaitingForGap}, ShouldFire: ${shouldFire}, Reason: ${fireReason}`); + if (eventDetected) { + console.log(`[DEBUG MOVED] Changes: [${changedGestures.join(', ')}]`); + } } - if (eventOccurred) { + if (shouldFire) { this.lastMovedEventTime = now; + this.isWaitingForGap = true; console.log(`[DEBUG MOVED] *** FIRED *** at ${perfNow.toFixed(3)}ms`); } - return eventOccurred; + return shouldFire; } const lastTimestamp = this._peripheral.getGestureEventTimestamp(gestureName); From 50097a0c48a98645e5d41bc36f95430a754b5b53 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 21:50:58 +0900 Subject: [PATCH 6/8] fix: finalize 'moved' event logic by removing debug logs --- src/extensions/microbitMore/index.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index 8a9c319b90..cfca31a3ed 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2876,7 +2876,6 @@ class MbitMoreBlocks { * Update the last occured time of all gesture events. */ updatePrevGestureEvents () { - console.log(`[DEBUG MOVED] updatePrevGestureEvents at ${performance.now().toFixed(3)}ms`); this.prevGestureEvents = {}; Object.entries(this._peripheral.gestureEvents).forEach(([gestureName, timestamp]) => { this.prevGestureEvents[gestureName] = timestamp; @@ -2899,7 +2898,6 @@ class MbitMoreBlocks { const gestureName = args.GESTURE; if (gestureName === 'MOVED') { const now = Date.now(); - const perfNow = performance.now(); const stepTime = this.runtime.currentStepTime; const gapThreshold = stepTime * 5; // 5フレームの空白 const timeoutThreshold = stepTime * 30; // 30フレームで強制発火 @@ -2924,14 +2922,10 @@ class MbitMoreBlocks { const timeSinceLastOccurred = this.lastGestureOccurredTime === null ? Infinity : (now - this.lastGestureOccurredTime); if (timeSinceLastOccurred >= gapThreshold) { - if (this.isWaitingForGap) { - console.log(`[DEBUG MOVED] Gap detected (${timeSinceLastOccurred.toFixed(1)}ms). Resetting.`); - } this.isWaitingForGap = false; } let shouldFire = false; - let fireReason = ''; if (eventDetected) { if (this.isWaitingForGap) { @@ -2940,26 +2934,15 @@ class MbitMoreBlocks { Infinity : (now - this.lastMovedEventTime); if (timeSinceLastFired >= timeoutThreshold) { shouldFire = true; - fireReason = `Continuous movement timeout (${timeSinceLastFired.toFixed(1)}ms)`; } } else { shouldFire = true; - fireReason = 'Movement started after gap'; - } - } - - if (eventDetected || shouldFire || this.isWaitingForGap) { - console.log(`[DEBUG MOVED] Time: ${perfNow.toFixed(3)}ms, Detected: ${eventDetected}, ` + - `Waiting: ${this.isWaitingForGap}, ShouldFire: ${shouldFire}, Reason: ${fireReason}`); - if (eventDetected) { - console.log(`[DEBUG MOVED] Changes: [${changedGestures.join(', ')}]`); } } if (shouldFire) { this.lastMovedEventTime = now; this.isWaitingForGap = true; - console.log(`[DEBUG MOVED] *** FIRED *** at ${perfNow.toFixed(3)}ms`); } return shouldFire; From 238aa0c2f4c8bae2fa0fabb550f640094436b3b9 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 22:15:24 +0900 Subject: [PATCH 7/8] fix: increase 'moved' event gap threshold to 10 frames --- src/extensions/microbitMore/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index cfca31a3ed..b155e51bda 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2899,7 +2899,7 @@ class MbitMoreBlocks { if (gestureName === 'MOVED') { const now = Date.now(); const stepTime = this.runtime.currentStepTime; - const gapThreshold = stepTime * 5; // 5フレームの空白 + const gapThreshold = stepTime * 10; // 10フレームの空白 const timeoutThreshold = stepTime * 30; // 30フレームで強制発火 const changedGestures = []; From 1f531a7834f25414ca4b50e40873946680820e0c Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 15 Jan 2026 22:20:32 +0900 Subject: [PATCH 8/8] fix: revert 'moved' event gap threshold to 5 frames --- src/extensions/microbitMore/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/microbitMore/index.js b/src/extensions/microbitMore/index.js index b155e51bda..cfca31a3ed 100644 --- a/src/extensions/microbitMore/index.js +++ b/src/extensions/microbitMore/index.js @@ -2899,7 +2899,7 @@ class MbitMoreBlocks { if (gestureName === 'MOVED') { const now = Date.now(); const stepTime = this.runtime.currentStepTime; - const gapThreshold = stepTime * 10; // 10フレームの空白 + const gapThreshold = stepTime * 5; // 5フレームの空白 const timeoutThreshold = stepTime * 30; // 30フレームで強制発火 const changedGestures = [];