I was developing a custom Scratch extension:
kankaung-extension-bt.js
inside a modified scratch-gui.
Goal:
Scratch Blocks
↓
KanKaung Extension
↓
Web Bluetooth
↓
ESP32 BLE
The ESP32 BLE connection worked when testing directly in Chrome:
navigator.bluetooth.requestDevice()
but when running from Scratch blocks, it failed.
Problem 1: Web Bluetooth worked in console but not from Scratch block
Error
Permissions policy violation:
bluetooth is not allowed in this document
or:
SecurityError:
Failed to execute requestDevice()
Cause
Scratch extensions are not running in the main browser window.
The extension is loaded inside an iframe:
window === top
false
Meaning:
Browser Window
│
├── scratch-gui
│
└── Extension iframe
|
└── kankaung-extension-bt.js
The iframe has different permissions and cannot directly access Web Bluetooth.
Problem 2: Trying to access Scratch VM
I tried:
Scratch.vm
or:
Scratch.runtime
Result:
undefined
Cause
External extensions only receive the public Scratch API:
Scratch.BlockType
Scratch.ArgumentType
Scratch.extensions.register()
They do NOT receive:
vm
runtime
redux store
Wrong Approach
Trying:
window.dispatchEvent(
new CustomEvent("KANKAUNG_CONNECT")
)
and listening in:
scratch-gui
did not work.
Why?
Because events stay inside the iframe.
Extension iframe
window.dispatchEvent()
|
X
|
scratch-gui window
The two windows are separated.
Wrong Approach 2
Trying:
window.parent.dispatchEvent()
or:
window.top.dispatchEvent()
caused:
Blocked a frame with origin "null"
from accessing a cross-origin frame
Cause
Browser security prevents direct access between different origins.
Even localhost pages can become:
origin: null
when loaded through iframe sandboxing.
Final Solution: Use postMessage()
postMessage() is designed for iframe communication.
Architecture:
Scratch Extension iframe
|
|
| window.parent.postMessage()
|
↓
scratch-gui
containers/gui.jsx
|
|
| Web Bluetooth API
|
↓
ESP32 BLE
Extension → scratch-gui
Example:
window.parent.postMessage(
{
type:"KANKAUNG_SEND",
command:"LED_ON"
},
"*");
scratch-gui receives message
In:
src/containers/gui.jsx
listen:
window.addEventListener(
"message",
(event)=>{
if(event.data.type==="KANKAUNG_SEND")
{
kankaungBluetooth.send(
event.data.command
);
}
});
scratch-gui → Extension
Do NOT use:
dispatchEvent()
Use:
window.postMessage(
{
type:"KANKAUNG_STATUS",
status:true
},
"*");
Extension listens:
window.addEventListener(
"message",
(event)=>{
if(event.data.type==="KANKAUNG_STATUS")
{
this.connected =
event.data.status;
}
});
Final Communication Flow
User clicks Scratch block
↓
kankaung-extension-bt.js
↓
postMessage()
↓
src/containers/gui.jsx
↓
kankaung-bluetooth.js
↓
navigator.bluetooth
↓
ESP32 BLE
↓
response
↓
postMessage()
↓
Scratch Extension
Key Lessons To Remember
- Scratch extension ≠ scratch-gui Extension:
iframe
GUI:
main application
They are separated.
Never use:
Scratch.vm
Scratch.runtime
window.dispatchEvent()
window.parent.dispatchEvent()
for communication between extension and scratch-gui.Use:
window.parent.postMessage()
Extension → GUI
and:
window.postMessage()
GUI → Extension
- For custom hardware extensions in scratch-gui: Recommended architecture:
Extension
|
| postMessage
|
scratch-gui container
|
| Hardware manager
|
ESP32 / Arduino / Raspberry Pi
This approach is similar to how professional Scratch forks and hardware extensions communicate. You solved the hardest part: getting a custom Scratch extension to talk to your own hardware layer without Scratch Link.
Top comments (0)