micro:bit support added
parent
83c4988a2c
commit
ef0f3e12d1
10
index.html
10
index.html
|
|
@ -3,14 +3,20 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ESP32-S3 Blockly IDE</title>
|
<title>Blockly IDE</title>
|
||||||
<link rel="stylesheet" href="/src/style.css" />
|
<link rel="stylesheet" href="/src/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Top toolbar -->
|
<!-- Top toolbar -->
|
||||||
<header id="toolbar">
|
<header id="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<span class="app-title">ESP32-S3 Blockly</span>
|
<span class="app-title">Blockly IDE</span>
|
||||||
|
<label for="device-select" class="device-label">Device</label>
|
||||||
|
<select id="device-select" title="Target device (changes code generator and firmware)">
|
||||||
|
<option value="esp32s3">ESP32-S3</option>
|
||||||
|
<option value="microbit">micro:bit</option>
|
||||||
|
<option value="rp2040">RP2040 (Pico)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
|
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const adcCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'ADC',
|
||||||
|
colour: '30',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'adc_read' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
export const builtinCategories = [
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Logic',
|
||||||
|
categorystyle: 'logic_category',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'controls_if' },
|
||||||
|
{ kind: 'block', type: 'logic_compare' },
|
||||||
|
{ kind: 'block', type: 'logic_operation' },
|
||||||
|
{ kind: 'block', type: 'logic_negate' },
|
||||||
|
{ kind: 'block', type: 'logic_boolean' },
|
||||||
|
{ kind: 'block', type: 'logic_null' },
|
||||||
|
{ kind: 'block', type: 'logic_ternary' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Loops',
|
||||||
|
categorystyle: 'loop_category',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
kind: 'block', type: 'controls_repeat_ext',
|
||||||
|
inputs: { TIMES: { shadow: { type: 'math_number', fields: { NUM: 10 } } } },
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'controls_whileUntil' },
|
||||||
|
{
|
||||||
|
kind: 'block', type: 'controls_for',
|
||||||
|
inputs: {
|
||||||
|
FROM: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
||||||
|
TO: { shadow: { type: 'math_number', fields: { NUM: 10 } } },
|
||||||
|
BY: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'controls_forEach' },
|
||||||
|
{ kind: 'block', type: 'controls_flow_statements' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Math',
|
||||||
|
categorystyle: 'math_category',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'math_number', fields: { NUM: 0 } },
|
||||||
|
{ kind: 'block', type: 'math_arithmetic' },
|
||||||
|
{ kind: 'block', type: 'math_single' },
|
||||||
|
{ kind: 'block', type: 'math_trig' },
|
||||||
|
{ kind: 'block', type: 'math_constant' },
|
||||||
|
{ kind: 'block', type: 'math_number_property' },
|
||||||
|
{ kind: 'block', type: 'math_round' },
|
||||||
|
{ kind: 'block', type: 'math_modulo' },
|
||||||
|
{
|
||||||
|
kind: 'block', type: 'math_constrain',
|
||||||
|
inputs: {
|
||||||
|
LOW: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
||||||
|
HIGH: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'block', type: 'math_random_int',
|
||||||
|
inputs: {
|
||||||
|
FROM: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
||||||
|
TO: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'math_random_float' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Text',
|
||||||
|
categorystyle: 'text_category',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'text' },
|
||||||
|
{ kind: 'block', type: 'text_join' },
|
||||||
|
{ kind: 'block', type: 'text_append' },
|
||||||
|
{ kind: 'block', type: 'text_length' },
|
||||||
|
{ kind: 'block', type: 'text_isEmpty' },
|
||||||
|
{ kind: 'block', type: 'text_indexOf' },
|
||||||
|
{ kind: 'block', type: 'text_charAt' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Variables',
|
||||||
|
categorystyle: 'variable_category',
|
||||||
|
custom: 'VARIABLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Functions',
|
||||||
|
categorystyle: 'procedure_category',
|
||||||
|
custom: 'PROCEDURE',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const i2cCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'I2C',
|
||||||
|
colour: '200',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'i2c_init' },
|
||||||
|
{ kind: 'block', type: 'i2c_scan' },
|
||||||
|
{ kind: 'block', type: 'i2c_writeto' },
|
||||||
|
{ kind: 'block', type: 'i2c_readfrom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const microbitDisplayCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Display (micro:bit)',
|
||||||
|
colour: '10',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'microbit_display_all' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const neopixelCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'NeoPixel',
|
||||||
|
colour: '10',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'neopixel_init' },
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'neopixel_set_color',
|
||||||
|
inputs: {
|
||||||
|
R: { shadow: { type: 'math_number', fields: { NUM: 255 } } },
|
||||||
|
G: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
||||||
|
B: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'neopixel_show' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const pinIoCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Pin I/O',
|
||||||
|
colour: '230',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'pin_set_mode' },
|
||||||
|
{ kind: 'block', type: 'pin_digital_write' },
|
||||||
|
{ kind: 'block', type: 'pin_digital_read' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const pwmCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'PWM',
|
||||||
|
colour: '160',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'pwm_init' },
|
||||||
|
{ kind: 'block', type: 'pwm_set_duty' },
|
||||||
|
{ kind: 'block', type: 'pwm_set_freq' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const serialPrintCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Serial / Print',
|
||||||
|
colour: '60',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'print_text',
|
||||||
|
inputs: {
|
||||||
|
TEXT: { shadow: { type: 'text', fields: { TEXT: 'Hello!' } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
export function soundCategory({ hasSpeaker = false } = {}) {
|
||||||
|
const contents = [];
|
||||||
|
|
||||||
|
if (hasSpeaker) {
|
||||||
|
contents.push(
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'sound_play_tone_speaker',
|
||||||
|
inputs: {
|
||||||
|
FREQ: { shadow: { type: 'math_number', fields: { NUM: 440 } } },
|
||||||
|
DURATION: { shadow: { type: 'math_number', fields: { NUM: 500 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'sound_stop_speaker' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.push(
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'sound_play_tone',
|
||||||
|
inputs: {
|
||||||
|
FREQ: { shadow: { type: 'math_number', fields: { NUM: 440 } } },
|
||||||
|
DURATION: { shadow: { type: 'math_number', fields: { NUM: 500 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'sound_stop' },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Sound',
|
||||||
|
colour: '300',
|
||||||
|
contents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
export const timeCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Time',
|
||||||
|
colour: '120',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'sleep_seconds',
|
||||||
|
inputs: {
|
||||||
|
SECONDS: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'sleep_ms',
|
||||||
|
inputs: {
|
||||||
|
MS: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'ticks_ms' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const wifiCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'WiFi',
|
||||||
|
colour: '290',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'wifi_connect' },
|
||||||
|
{ kind: 'block', type: 'wifi_get_ip' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -292,3 +292,102 @@ Blockly.Blocks['print_text'] = {
|
||||||
this.setTooltip('Print to serial output');
|
this.setTooltip('Print to serial output');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Sound ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
Blockly.Blocks['sound_play_tone_speaker'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput().appendField('play tone on speaker');
|
||||||
|
this.appendValueInput('FREQ')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('freq');
|
||||||
|
this.appendDummyInput().appendField('Hz');
|
||||||
|
this.appendValueInput('DURATION')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('for');
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('ms')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['wait', 'TRUE'],
|
||||||
|
['continue', 'FALSE'],
|
||||||
|
]), 'WAIT');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Play a tone on the built-in speaker (micro:bit v2). "continue" runs code while tone plays.');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['sound_stop_speaker'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput().appendField('stop speaker');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Stop sound on the built-in speaker (micro:bit v2)');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['sound_play_tone'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('play tone on pin')
|
||||||
|
.appendField(new Blockly.FieldNumber(0, 0, 48, 1), 'PIN');
|
||||||
|
this.appendValueInput('FREQ')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('freq');
|
||||||
|
this.appendDummyInput().appendField('Hz');
|
||||||
|
this.appendValueInput('DURATION')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('for');
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('ms')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['wait', 'TRUE'],
|
||||||
|
['continue', 'FALSE'],
|
||||||
|
]), 'WAIT');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Play a tone via PWM on a specific pin. "continue" runs code while tone plays.');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['sound_stop'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('stop sound on pin')
|
||||||
|
.appendField(new Blockly.FieldNumber(0, 0, 48, 1), 'PIN');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Stop sound on a specific pin');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── micro:bit Display ────────────────────────────────────
|
||||||
|
|
||||||
|
// Helper to add one row of 5 checkboxes (Pxy = pixel at row x, col y)
|
||||||
|
function addMicrobitDisplayRow(block, row) {
|
||||||
|
const input = block.appendDummyInput(`ROW${row}`);
|
||||||
|
for (let col = 0; col < 5; col++) {
|
||||||
|
const name = `P${row}${col}`;
|
||||||
|
input.appendField(new Blockly.FieldCheckbox(false), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blockly.Blocks['microbit_display_all'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('micro:bit display 5×5');
|
||||||
|
for (let row = 0; row < 5; row++) {
|
||||||
|
addMicrobitDisplayRow(this, row);
|
||||||
|
}
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(10);
|
||||||
|
this.setTooltip('Click LEDs to set the 5×5 display (micro:bit only). On = 9, off = 0.');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
import { pythonGenerator, Order } from 'blockly/python';
|
import { pythonGenerator, Order } from 'blockly/python';
|
||||||
|
import { getDeviceId } from '../devices/registry.js';
|
||||||
|
|
||||||
|
const DEVICE = () => getDeviceId();
|
||||||
|
|
||||||
|
// micro:bit pin name: pin0..pin20 (clamp to valid range)
|
||||||
|
function mbPin(pin) {
|
||||||
|
const n = Math.max(0, Math.min(20, parseInt(pin, 10) || 0));
|
||||||
|
return `pin${n}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Pin I/O ──────────────────────────────────────────────
|
// ─── Pin I/O ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -12,6 +21,9 @@ function ensurePinVar(pin, mode) {
|
||||||
|
|
||||||
pythonGenerator.forBlock['pin_set_mode'] = function (block) {
|
pythonGenerator.forBlock['pin_set_mode'] = function (block) {
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return ''; // micro:bit has no explicit set_mode
|
||||||
|
}
|
||||||
const mode = block.getFieldValue('MODE');
|
const mode = block.getFieldValue('MODE');
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
const varName = `pin_${pin}`;
|
const varName = `pin_${pin}`;
|
||||||
|
|
@ -23,12 +35,18 @@ pythonGenerator.forBlock['pin_set_mode'] = function (block) {
|
||||||
pythonGenerator.forBlock['pin_digital_write'] = function (block) {
|
pythonGenerator.forBlock['pin_digital_write'] = function (block) {
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
const value = block.getFieldValue('VALUE');
|
const value = block.getFieldValue('VALUE');
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return `${mbPin(pin)}.write_digital(${value})\n`;
|
||||||
|
}
|
||||||
const varName = ensurePinVar(pin, 'OUT');
|
const varName = ensurePinVar(pin, 'OUT');
|
||||||
return `${varName}.value(${value})\n`;
|
return `${varName}.value(${value})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['pin_digital_read'] = function (block) {
|
pythonGenerator.forBlock['pin_digital_read'] = function (block) {
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return [`${mbPin(pin)}.read_digital()`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
const varName = ensurePinVar(pin, 'IN');
|
const varName = ensurePinVar(pin, 'IN');
|
||||||
return [`${varName}.value()`, Order.FUNCTION_CALL];
|
return [`${varName}.value()`, Order.FUNCTION_CALL];
|
||||||
};
|
};
|
||||||
|
|
@ -36,6 +54,7 @@ pythonGenerator.forBlock['pin_digital_read'] = function (block) {
|
||||||
// ─── PWM ──────────────────────────────────────────────────
|
// ─── PWM ──────────────────────────────────────────────────
|
||||||
|
|
||||||
pythonGenerator.forBlock['pwm_init'] = function (block) {
|
pythonGenerator.forBlock['pwm_init'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return ''; // no separate init on micro:bit
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
const freq = block.getFieldValue('FREQ');
|
const freq = block.getFieldValue('FREQ');
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
|
|
@ -45,11 +64,15 @@ pythonGenerator.forBlock['pwm_init'] = function (block) {
|
||||||
pythonGenerator.forBlock['pwm_set_duty'] = function (block) {
|
pythonGenerator.forBlock['pwm_set_duty'] = function (block) {
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
const duty = pythonGenerator.valueToCode(block, 'DUTY', Order.NONE) || '0';
|
const duty = pythonGenerator.valueToCode(block, 'DUTY', Order.NONE) || '0';
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return `${mbPin(pin)}.write_analog(${duty})\n`;
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
return `pwm_${pin}.duty(${duty})\n`;
|
return `pwm_${pin}.duty(${duty})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['pwm_set_freq'] = function (block) {
|
pythonGenerator.forBlock['pwm_set_freq'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return ''; // micro:bit doesn't support changing PWM freq per-pin
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '1000';
|
const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '1000';
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
|
|
@ -60,6 +83,9 @@ pythonGenerator.forBlock['pwm_set_freq'] = function (block) {
|
||||||
|
|
||||||
pythonGenerator.forBlock['adc_read'] = function (block) {
|
pythonGenerator.forBlock['adc_read'] = function (block) {
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return [`${mbPin(pin)}.read_analog()`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
pythonGenerator.definitions_[`adc_init_${pin}`] = `adc_${pin} = ADC(Pin(${pin}))`;
|
pythonGenerator.definitions_[`adc_init_${pin}`] = `adc_${pin} = ADC(Pin(${pin}))`;
|
||||||
return [`adc_${pin}.read()`, Order.FUNCTION_CALL];
|
return [`adc_${pin}.read()`, Order.FUNCTION_CALL];
|
||||||
|
|
@ -69,17 +95,29 @@ pythonGenerator.forBlock['adc_read'] = function (block) {
|
||||||
|
|
||||||
pythonGenerator.forBlock['sleep_seconds'] = function (block) {
|
pythonGenerator.forBlock['sleep_seconds'] = function (block) {
|
||||||
const seconds = pythonGenerator.valueToCode(block, 'SECONDS', Order.NONE) || '1';
|
const seconds = pythonGenerator.valueToCode(block, 'SECONDS', Order.NONE) || '1';
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_microbit_sleep'] = 'from microbit import sleep';
|
||||||
|
return `sleep(${seconds} * 1000)\n`;
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||||
return `time.sleep(${seconds})\n`;
|
return `time.sleep(${seconds})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['sleep_ms'] = function (block) {
|
pythonGenerator.forBlock['sleep_ms'] = function (block) {
|
||||||
const ms = pythonGenerator.valueToCode(block, 'MS', Order.NONE) || '100';
|
const ms = pythonGenerator.valueToCode(block, 'MS', Order.NONE) || '100';
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_microbit_sleep'] = 'from microbit import sleep';
|
||||||
|
return `sleep(${ms})\n`;
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||||
return `time.sleep_ms(${ms})\n`;
|
return `time.sleep_ms(${ms})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['ticks_ms'] = function () {
|
pythonGenerator.forBlock['ticks_ms'] = function () {
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_microbit_running_time'] = 'from microbit import running_time';
|
||||||
|
return ['running_time()', Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||||
return ['time.ticks_ms()', Order.FUNCTION_CALL];
|
return ['time.ticks_ms()', Order.FUNCTION_CALL];
|
||||||
};
|
};
|
||||||
|
|
@ -87,6 +125,9 @@ pythonGenerator.forBlock['ticks_ms'] = function () {
|
||||||
// ─── WiFi ─────────────────────────────────────────────────
|
// ─── WiFi ─────────────────────────────────────────────────
|
||||||
|
|
||||||
pythonGenerator.forBlock['wifi_connect'] = function (block) {
|
pythonGenerator.forBlock['wifi_connect'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit' || DEVICE() === 'rp2040') {
|
||||||
|
return '# WiFi not available on this device\n';
|
||||||
|
}
|
||||||
const ssid = block.getFieldValue('SSID');
|
const ssid = block.getFieldValue('SSID');
|
||||||
const password = block.getFieldValue('PASSWORD');
|
const password = block.getFieldValue('PASSWORD');
|
||||||
pythonGenerator.definitions_['import_network'] = 'import network';
|
pythonGenerator.definitions_['import_network'] = 'import network';
|
||||||
|
|
@ -100,7 +141,10 @@ pythonGenerator.forBlock['wifi_connect'] = function (block) {
|
||||||
].join('');
|
].join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['wifi_get_ip'] = function () {
|
pythonGenerator.forBlock['wifi_get_ip'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit' || DEVICE() === 'rp2040') {
|
||||||
|
return ["''", Order.ATOMIC];
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_network'] = 'import network';
|
pythonGenerator.definitions_['import_network'] = 'import network';
|
||||||
return ['network.WLAN(network.STA_IF).ifconfig()[0]', Order.FUNCTION_CALL];
|
return ['network.WLAN(network.STA_IF).ifconfig()[0]', Order.FUNCTION_CALL];
|
||||||
};
|
};
|
||||||
|
|
@ -108,6 +152,9 @@ pythonGenerator.forBlock['wifi_get_ip'] = function () {
|
||||||
// ─── NeoPixel ─────────────────────────────────────────────
|
// ─── NeoPixel ─────────────────────────────────────────────
|
||||||
|
|
||||||
pythonGenerator.forBlock['neopixel_init'] = function (block) {
|
pythonGenerator.forBlock['neopixel_init'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return '# NeoPixel on micro:bit: use neopixel library with pin\n';
|
||||||
|
}
|
||||||
const pin = block.getFieldValue('PIN');
|
const pin = block.getFieldValue('PIN');
|
||||||
const num = block.getFieldValue('NUM');
|
const num = block.getFieldValue('NUM');
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
|
|
@ -116,6 +163,7 @@ pythonGenerator.forBlock['neopixel_init'] = function (block) {
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['neopixel_set_color'] = function (block) {
|
pythonGenerator.forBlock['neopixel_set_color'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return '';
|
||||||
const index = block.getFieldValue('INDEX');
|
const index = block.getFieldValue('INDEX');
|
||||||
const r = pythonGenerator.valueToCode(block, 'R', Order.NONE) || '0';
|
const r = pythonGenerator.valueToCode(block, 'R', Order.NONE) || '0';
|
||||||
const g = pythonGenerator.valueToCode(block, 'G', Order.NONE) || '0';
|
const g = pythonGenerator.valueToCode(block, 'G', Order.NONE) || '0';
|
||||||
|
|
@ -124,12 +172,14 @@ pythonGenerator.forBlock['neopixel_set_color'] = function (block) {
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['neopixel_show'] = function () {
|
pythonGenerator.forBlock['neopixel_show'] = function () {
|
||||||
|
if (DEVICE() === 'microbit') return '';
|
||||||
return 'np.write()\n';
|
return 'np.write()\n';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── I2C ──────────────────────────────────────────────────
|
// ─── I2C ──────────────────────────────────────────────────
|
||||||
|
|
||||||
pythonGenerator.forBlock['i2c_init'] = function (block) {
|
pythonGenerator.forBlock['i2c_init'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return ''; // micro:bit has built-in i2c
|
||||||
const sda = block.getFieldValue('SDA');
|
const sda = block.getFieldValue('SDA');
|
||||||
const scl = block.getFieldValue('SCL');
|
const scl = block.getFieldValue('SCL');
|
||||||
const freq = block.getFieldValue('FREQ');
|
const freq = block.getFieldValue('FREQ');
|
||||||
|
|
@ -137,18 +187,23 @@ pythonGenerator.forBlock['i2c_init'] = function (block) {
|
||||||
return `i2c = I2C(0, sda=Pin(${sda}), scl=Pin(${scl}), freq=${freq})\n`;
|
return `i2c = I2C(0, sda=Pin(${sda}), scl=Pin(${scl}), freq=${freq})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['i2c_scan'] = function () {
|
pythonGenerator.forBlock['i2c_scan'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
return ['[]', Order.ATOMIC]; // stub
|
||||||
|
}
|
||||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
return ['i2c.scan()', Order.FUNCTION_CALL];
|
return ['i2c.scan()', Order.FUNCTION_CALL];
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['i2c_writeto'] = function (block) {
|
pythonGenerator.forBlock['i2c_writeto'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return '';
|
||||||
const addr = block.getFieldValue('ADDR');
|
const addr = block.getFieldValue('ADDR');
|
||||||
const data = pythonGenerator.valueToCode(block, 'DATA', Order.NONE) || 'b""';
|
const data = pythonGenerator.valueToCode(block, 'DATA', Order.NONE) || 'b""';
|
||||||
return `i2c.writeto(${addr}, ${data})\n`;
|
return `i2c.writeto(${addr}, ${data})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
pythonGenerator.forBlock['i2c_readfrom'] = function (block) {
|
pythonGenerator.forBlock['i2c_readfrom'] = function (block) {
|
||||||
|
if (DEVICE() === 'microbit') return ['b""', Order.ATOMIC];
|
||||||
const addr = block.getFieldValue('ADDR');
|
const addr = block.getFieldValue('ADDR');
|
||||||
const nbytes = block.getFieldValue('NBYTES');
|
const nbytes = block.getFieldValue('NBYTES');
|
||||||
return [`i2c.readfrom(${addr}, ${nbytes})`, Order.FUNCTION_CALL];
|
return [`i2c.readfrom(${addr}, ${nbytes})`, Order.FUNCTION_CALL];
|
||||||
|
|
@ -160,3 +215,85 @@ pythonGenerator.forBlock['print_text'] = function (block) {
|
||||||
const text = pythonGenerator.valueToCode(block, 'TEXT', Order.NONE) || "''";
|
const text = pythonGenerator.valueToCode(block, 'TEXT', Order.NONE) || "''";
|
||||||
return `print(${text})\n`;
|
return `print(${text})\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Sound ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
pythonGenerator.forBlock['sound_play_tone_speaker'] = function (block) {
|
||||||
|
const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
|
||||||
|
const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
|
||||||
|
const wait = block.getFieldValue('WAIT') === 'TRUE';
|
||||||
|
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_music'] = 'import music';
|
||||||
|
return `music.pitch(${freq}, ${duration}, wait=${wait ? 'True' : 'False'})\n`;
|
||||||
|
}
|
||||||
|
return '# Built-in speaker only available on micro:bit\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
pythonGenerator.forBlock['sound_stop_speaker'] = function () {
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_music'] = 'import music';
|
||||||
|
return 'music.stop()\n';
|
||||||
|
}
|
||||||
|
return '# Built-in speaker only available on micro:bit\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
pythonGenerator.forBlock['sound_play_tone'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
|
||||||
|
const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
|
||||||
|
const wait = block.getFieldValue('WAIT') === 'TRUE';
|
||||||
|
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_music'] = 'import music';
|
||||||
|
pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
|
||||||
|
return `music.pitch(${freq}, ${duration}, pin=${mbPin(pin)}, wait=${wait ? 'True' : 'False'})\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
|
const varName = `buzzer_${pin}`;
|
||||||
|
pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;
|
||||||
|
|
||||||
|
if (wait) {
|
||||||
|
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||||
|
return `${varName}.freq(${freq})\n${varName}.duty(512)\ntime.sleep_ms(${duration})\n${varName}.duty(0)\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pythonGenerator.definitions_['import_timer'] = 'from machine import Timer';
|
||||||
|
return `${varName}.freq(${freq})\n${varName}.duty(512)\nTimer(0).init(period=${duration}, mode=Timer.ONE_SHOT, callback=lambda t: ${varName}.duty(0))\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
pythonGenerator.forBlock['sound_stop'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
|
||||||
|
if (DEVICE() === 'microbit') {
|
||||||
|
pythonGenerator.definitions_['import_music'] = 'import music';
|
||||||
|
return 'music.stop()\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||||
|
const varName = `buzzer_${pin}`;
|
||||||
|
pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;
|
||||||
|
return `${varName}.duty(0)\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── micro:bit Display ────────────────────────────────────
|
||||||
|
|
||||||
|
pythonGenerator.forBlock['microbit_display_all'] = function (block) {
|
||||||
|
if (DEVICE() !== 'microbit') {
|
||||||
|
return '# micro:bit display block (select micro:bit device)\n';
|
||||||
|
}
|
||||||
|
// Build 5x5 string from checkboxes P00..P44 (each row = 5 chars, rows joined by :)
|
||||||
|
const rows = [];
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
let row = '';
|
||||||
|
for (let c = 0; c < 5; c++) {
|
||||||
|
const val = block.getFieldValue(`P${r}${c}`);
|
||||||
|
row += val === 'TRUE' ? '9' : '0';
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
const imageStr = rows.join(':');
|
||||||
|
pythonGenerator.definitions_['import_microbit_display'] = 'from microbit import display, Image';
|
||||||
|
return `display.show(Image("${imageStr}"))\n`;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
export const toolbox = {
|
|
||||||
kind: 'categoryToolbox',
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Pin I/O',
|
|
||||||
colour: '230',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'pin_set_mode' },
|
|
||||||
{ kind: 'block', type: 'pin_digital_write' },
|
|
||||||
{ kind: 'block', type: 'pin_digital_read' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'PWM',
|
|
||||||
colour: '160',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'pwm_init' },
|
|
||||||
{ kind: 'block', type: 'pwm_set_duty' },
|
|
||||||
{ kind: 'block', type: 'pwm_set_freq' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'ADC',
|
|
||||||
colour: '30',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'adc_read' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Time',
|
|
||||||
colour: '120',
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
kind: 'block',
|
|
||||||
type: 'sleep_seconds',
|
|
||||||
inputs: {
|
|
||||||
SECONDS: {
|
|
||||||
shadow: { type: 'math_number', fields: { NUM: 1 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'block',
|
|
||||||
type: 'sleep_ms',
|
|
||||||
inputs: {
|
|
||||||
MS: {
|
|
||||||
shadow: { type: 'math_number', fields: { NUM: 100 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'ticks_ms' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'WiFi',
|
|
||||||
colour: '290',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'wifi_connect' },
|
|
||||||
{ kind: 'block', type: 'wifi_get_ip' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'NeoPixel',
|
|
||||||
colour: '10',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'neopixel_init' },
|
|
||||||
{
|
|
||||||
kind: 'block',
|
|
||||||
type: 'neopixel_set_color',
|
|
||||||
inputs: {
|
|
||||||
R: { shadow: { type: 'math_number', fields: { NUM: 255 } } },
|
|
||||||
G: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
|
||||||
B: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'neopixel_show' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'I2C',
|
|
||||||
colour: '200',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'i2c_init' },
|
|
||||||
{ kind: 'block', type: 'i2c_scan' },
|
|
||||||
{ kind: 'block', type: 'i2c_writeto' },
|
|
||||||
{ kind: 'block', type: 'i2c_readfrom' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Serial / Print',
|
|
||||||
colour: '60',
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
kind: 'block',
|
|
||||||
type: 'print_text',
|
|
||||||
inputs: {
|
|
||||||
TEXT: {
|
|
||||||
shadow: { type: 'text', fields: { TEXT: 'Hello ESP32!' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ kind: 'sep' },
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Logic',
|
|
||||||
categorystyle: 'logic_category',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'controls_if' },
|
|
||||||
{ kind: 'block', type: 'logic_compare' },
|
|
||||||
{ kind: 'block', type: 'logic_operation' },
|
|
||||||
{ kind: 'block', type: 'logic_negate' },
|
|
||||||
{ kind: 'block', type: 'logic_boolean' },
|
|
||||||
{ kind: 'block', type: 'logic_null' },
|
|
||||||
{ kind: 'block', type: 'logic_ternary' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Loops',
|
|
||||||
categorystyle: 'loop_category',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'controls_repeat_ext',
|
|
||||||
inputs: {
|
|
||||||
TIMES: { shadow: { type: 'math_number', fields: { NUM: 10 } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'controls_whileUntil' },
|
|
||||||
{ kind: 'block', type: 'controls_for',
|
|
||||||
inputs: {
|
|
||||||
FROM: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
|
||||||
TO: { shadow: { type: 'math_number', fields: { NUM: 10 } } },
|
|
||||||
BY: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'controls_forEach' },
|
|
||||||
{ kind: 'block', type: 'controls_flow_statements' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Math',
|
|
||||||
categorystyle: 'math_category',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'math_number', fields: { NUM: 0 } },
|
|
||||||
{ kind: 'block', type: 'math_arithmetic' },
|
|
||||||
{ kind: 'block', type: 'math_single' },
|
|
||||||
{ kind: 'block', type: 'math_trig' },
|
|
||||||
{ kind: 'block', type: 'math_constant' },
|
|
||||||
{ kind: 'block', type: 'math_number_property' },
|
|
||||||
{ kind: 'block', type: 'math_round' },
|
|
||||||
{ kind: 'block', type: 'math_modulo' },
|
|
||||||
{ kind: 'block', type: 'math_constrain',
|
|
||||||
inputs: {
|
|
||||||
LOW: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
|
||||||
HIGH: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'math_random_int',
|
|
||||||
inputs: {
|
|
||||||
FROM: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
|
|
||||||
TO: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ kind: 'block', type: 'math_random_float' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Text',
|
|
||||||
categorystyle: 'text_category',
|
|
||||||
contents: [
|
|
||||||
{ kind: 'block', type: 'text' },
|
|
||||||
{ kind: 'block', type: 'text_join' },
|
|
||||||
{ kind: 'block', type: 'text_append' },
|
|
||||||
{ kind: 'block', type: 'text_length' },
|
|
||||||
{ kind: 'block', type: 'text_isEmpty' },
|
|
||||||
{ kind: 'block', type: 'text_indexOf' },
|
|
||||||
{ kind: 'block', type: 'text_charAt' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Variables',
|
|
||||||
categorystyle: 'variable_category',
|
|
||||||
custom: 'VARIABLE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'category',
|
|
||||||
name: 'Functions',
|
|
||||||
categorystyle: 'procedure_category',
|
|
||||||
custom: 'PROCEDURE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { pinIoCategory } from '../blocks/categories/pinIo.js';
|
||||||
|
import { pwmCategory } from '../blocks/categories/pwm.js';
|
||||||
|
import { adcCategory } from '../blocks/categories/adc.js';
|
||||||
|
import { timeCategory } from '../blocks/categories/time.js';
|
||||||
|
import { wifiCategory } from '../blocks/categories/wifi.js';
|
||||||
|
import { neopixelCategory } from '../blocks/categories/neopixel.js';
|
||||||
|
import { i2cCategory } from '../blocks/categories/i2c.js';
|
||||||
|
import { serialPrintCategory } from '../blocks/categories/serialPrint.js';
|
||||||
|
import { soundCategory } from '../blocks/categories/sound.js';
|
||||||
|
|
||||||
|
export const esp32s3 = {
|
||||||
|
id: 'esp32s3',
|
||||||
|
label: 'ESP32-S3',
|
||||||
|
firmware: {
|
||||||
|
label: 'MicroPython (ESP32-S3)',
|
||||||
|
url: '/firmware/ESP32_GENERIC_S3-20251209-v1.27.0.bin',
|
||||||
|
canFlashInBrowser: true,
|
||||||
|
instructions: null,
|
||||||
|
},
|
||||||
|
categories: [
|
||||||
|
pinIoCategory,
|
||||||
|
pwmCategory,
|
||||||
|
adcCategory,
|
||||||
|
timeCategory,
|
||||||
|
wifiCategory,
|
||||||
|
neopixelCategory,
|
||||||
|
i2cCategory,
|
||||||
|
soundCategory(),
|
||||||
|
serialPrintCategory,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { pinIoCategory } from '../blocks/categories/pinIo.js';
|
||||||
|
import { pwmCategory } from '../blocks/categories/pwm.js';
|
||||||
|
import { adcCategory } from '../blocks/categories/adc.js';
|
||||||
|
import { timeCategory } from '../blocks/categories/time.js';
|
||||||
|
import { serialPrintCategory } from '../blocks/categories/serialPrint.js';
|
||||||
|
import { microbitDisplayCategory } from '../blocks/categories/microbitDisplay.js';
|
||||||
|
import { soundCategory } from '../blocks/categories/sound.js';
|
||||||
|
|
||||||
|
export const microbit = {
|
||||||
|
id: 'microbit',
|
||||||
|
label: 'micro:bit',
|
||||||
|
firmware: {
|
||||||
|
label: 'MicroPython (micro:bit v2)',
|
||||||
|
url: 'https://micropython.org/resources/firmware/MICROBIT_V2.hex',
|
||||||
|
canFlashInBrowser: false,
|
||||||
|
instructions: 'Hold RESET, then drag the .hex file onto the MICROBIT drive.',
|
||||||
|
},
|
||||||
|
categories: [
|
||||||
|
pinIoCategory,
|
||||||
|
pwmCategory,
|
||||||
|
adcCategory,
|
||||||
|
timeCategory,
|
||||||
|
microbitDisplayCategory,
|
||||||
|
soundCategory({ hasSpeaker: true }),
|
||||||
|
serialPrintCategory,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { esp32s3 } from './esp32s3.js';
|
||||||
|
import { microbit } from './microbit.js';
|
||||||
|
import { rp2040 } from './rp2040.js';
|
||||||
|
import { builtinCategories } from '../blocks/categories/builtins.js';
|
||||||
|
|
||||||
|
const devices = {};
|
||||||
|
|
||||||
|
function register(profile) {
|
||||||
|
devices[profile.id] = profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(esp32s3);
|
||||||
|
register(microbit);
|
||||||
|
register(rp2040);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new device at runtime (e.g. a sub-device like "ESP32 robot").
|
||||||
|
* Call refreshToolbox() in main.js after registering to pick up changes.
|
||||||
|
*/
|
||||||
|
export function registerDevice(profile) {
|
||||||
|
register(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceIds() {
|
||||||
|
return Object.keys(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceProfile(id) {
|
||||||
|
return devices[id] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllDevices() {
|
||||||
|
return { ...devices };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- active device state ---
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'esp32block_device';
|
||||||
|
|
||||||
|
let currentId = localStorage.getItem(STORAGE_KEY) || 'esp32s3';
|
||||||
|
if (!devices[currentId]) currentId = 'esp32s3';
|
||||||
|
|
||||||
|
export function getDeviceId() {
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDevice() {
|
||||||
|
return devices[currentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDeviceId(id) {
|
||||||
|
if (!devices[id]) return;
|
||||||
|
currentId = id;
|
||||||
|
localStorage.setItem(STORAGE_KEY, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canFlashInBrowser() {
|
||||||
|
return getDevice().firmware?.canFlashInBrowser === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- toolbox builder ---
|
||||||
|
|
||||||
|
export function buildToolbox(deviceId) {
|
||||||
|
const profile = devices[deviceId || currentId];
|
||||||
|
if (!profile) return { kind: 'categoryToolbox', contents: [] };
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'categoryToolbox',
|
||||||
|
contents: [
|
||||||
|
...profile.categories,
|
||||||
|
{ kind: 'sep' },
|
||||||
|
...builtinCategories,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { pinIoCategory } from '../blocks/categories/pinIo.js';
|
||||||
|
import { pwmCategory } from '../blocks/categories/pwm.js';
|
||||||
|
import { adcCategory } from '../blocks/categories/adc.js';
|
||||||
|
import { timeCategory } from '../blocks/categories/time.js';
|
||||||
|
import { i2cCategory } from '../blocks/categories/i2c.js';
|
||||||
|
import { serialPrintCategory } from '../blocks/categories/serialPrint.js';
|
||||||
|
import { soundCategory } from '../blocks/categories/sound.js';
|
||||||
|
|
||||||
|
export const rp2040 = {
|
||||||
|
id: 'rp2040',
|
||||||
|
label: 'RP2040 (Pico)',
|
||||||
|
firmware: {
|
||||||
|
label: 'MicroPython (Raspberry Pi Pico)',
|
||||||
|
url: 'https://micropython.org/resources/firmware/RPI_PICO.uf2',
|
||||||
|
canFlashInBrowser: false,
|
||||||
|
instructions: 'Hold BOOTSEL while plugging in, then drag the .uf2 file onto the RPI-RP2 drive.',
|
||||||
|
},
|
||||||
|
categories: [
|
||||||
|
pinIoCategory,
|
||||||
|
pwmCategory,
|
||||||
|
adcCategory,
|
||||||
|
timeCategory,
|
||||||
|
i2cCategory,
|
||||||
|
soundCategory(),
|
||||||
|
serialPrintCategory,
|
||||||
|
],
|
||||||
|
};
|
||||||
41
src/main.js
41
src/main.js
|
|
@ -2,7 +2,13 @@ import * as Blockly from 'blockly';
|
||||||
import { pythonGenerator } from 'blockly/python';
|
import { pythonGenerator } from 'blockly/python';
|
||||||
import './blocks/esp32_blocks.js';
|
import './blocks/esp32_blocks.js';
|
||||||
import './blocks/esp32_generators.js';
|
import './blocks/esp32_generators.js';
|
||||||
import { toolbox } from './blocks/toolbox.js';
|
import {
|
||||||
|
getDeviceId,
|
||||||
|
setDeviceId,
|
||||||
|
getDevice,
|
||||||
|
canFlashInBrowser,
|
||||||
|
buildToolbox,
|
||||||
|
} from './devices/registry.js';
|
||||||
import { connect, disconnect, isConnected, onData, writeString } from './serial/connection.js';
|
import { connect, disconnect, isConnected, onData, writeString } from './serial/connection.js';
|
||||||
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
||||||
import { flashFirmware } from './serial/flasher.js';
|
import { flashFirmware } from './serial/flasher.js';
|
||||||
|
|
@ -13,7 +19,7 @@ import './style.css';
|
||||||
// ─── Blockly Workspace ───────────────────────────────────
|
// ─── Blockly Workspace ───────────────────────────────────
|
||||||
|
|
||||||
const workspace = Blockly.inject('blockly-div', {
|
const workspace = Blockly.inject('blockly-div', {
|
||||||
toolbox,
|
toolbox: buildToolbox(getDeviceId()),
|
||||||
theme: Blockly.Themes.Dark,
|
theme: Blockly.Themes.Dark,
|
||||||
grid: { spacing: 25, length: 3, colour: '#333', snap: true },
|
grid: { spacing: 25, length: 3, colour: '#333', snap: true },
|
||||||
zoom: { controls: true, wheel: true, startScale: 0.9, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 },
|
zoom: { controls: true, wheel: true, startScale: 0.9, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 },
|
||||||
|
|
@ -81,6 +87,7 @@ initResizablePanels();
|
||||||
|
|
||||||
// ─── UI State Helpers ────────────────────────────────────
|
// ─── UI State Helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
const deviceSelect = document.getElementById('device-select');
|
||||||
const btnConnect = document.getElementById('btn-connect');
|
const btnConnect = document.getElementById('btn-connect');
|
||||||
const btnFlash = document.getElementById('btn-flash');
|
const btnFlash = document.getElementById('btn-flash');
|
||||||
const btnRun = document.getElementById('btn-run');
|
const btnRun = document.getElementById('btn-run');
|
||||||
|
|
@ -91,6 +98,21 @@ const btnLoadWorkspace = document.getElementById('btn-load-workspace');
|
||||||
const statusEl = document.getElementById('connection-status');
|
const statusEl = document.getElementById('connection-status');
|
||||||
const terminalInput = document.getElementById('terminal-input');
|
const terminalInput = document.getElementById('terminal-input');
|
||||||
|
|
||||||
|
// Sync device dropdown with stored device
|
||||||
|
deviceSelect.value = getDeviceId();
|
||||||
|
deviceSelect.addEventListener('change', () => {
|
||||||
|
setDeviceId(deviceSelect.value);
|
||||||
|
workspace.updateToolbox(buildToolbox(getDeviceId()));
|
||||||
|
updateCodePreview();
|
||||||
|
// Update Flash button tooltip/label based on device
|
||||||
|
btnFlash.title = canFlashInBrowser()
|
||||||
|
? 'Flash MicroPython firmware'
|
||||||
|
: 'Download firmware (drag to device)';
|
||||||
|
});
|
||||||
|
btnFlash.title = canFlashInBrowser()
|
||||||
|
? 'Flash MicroPython firmware'
|
||||||
|
: 'Download firmware (drag to device)';
|
||||||
|
|
||||||
function setConnectedUI(connected) {
|
function setConnectedUI(connected) {
|
||||||
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
||||||
btnRun.disabled = !connected;
|
btnRun.disabled = !connected;
|
||||||
|
|
@ -239,8 +261,11 @@ btnFlash.addEventListener('click', async () => {
|
||||||
setConnectedUI(false);
|
setConnectedUI(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
showFlashOverlay();
|
const device = getDevice();
|
||||||
|
const fw = device.firmware;
|
||||||
|
|
||||||
|
if (canFlashInBrowser()) {
|
||||||
|
showFlashOverlay();
|
||||||
try {
|
try {
|
||||||
await flashFirmware(
|
await flashFirmware(
|
||||||
(msg) => appendFlashLog(msg),
|
(msg) => appendFlashLog(msg),
|
||||||
|
|
@ -253,6 +278,16 @@ btnFlash.addEventListener('click', async () => {
|
||||||
} finally {
|
} finally {
|
||||||
flashCloseBtn.classList.remove('hidden');
|
flashCloseBtn.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Download firmware: open URL and show instructions
|
||||||
|
window.open(fw.url, '_blank');
|
||||||
|
appendToTerminal(`\n--- Firmware: ${fw.label} ---\n`);
|
||||||
|
appendToTerminal(`Download opened in new tab: ${fw.url}\n`);
|
||||||
|
if (fw.instructions) {
|
||||||
|
appendToTerminal(`${fw.instructions}\n`);
|
||||||
|
}
|
||||||
|
appendToTerminal('After flashing, connect here to run code.\n');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
btnRun.addEventListener('click', async () => {
|
btnRun.addEventListener('click', async () => {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ html, body {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.app-title {
|
.app-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
@ -51,6 +57,30 @@ html, body {
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#device-select {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#device-select:hover,
|
||||||
|
#device-select:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue