basic blockly code editor, python generated, and upload system working
commit
2c00738b07
|
|
@ -0,0 +1,22 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ESP32-S3 Blockly IDE</title>
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top toolbar -->
|
||||
<header id="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="app-title">ESP32-S3 Blockly</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
|
||||
<span class="icon">▶</span> Connect
|
||||
</button>
|
||||
<button id="btn-flash" title="Flash MicroPython firmware" disabled>
|
||||
<span class="icon">⚡</span> Flash FW
|
||||
</button>
|
||||
<button id="btn-run" title="Upload and run code" disabled>
|
||||
<span class="icon">▷</span> Run
|
||||
</button>
|
||||
<button id="btn-stop" title="Stop running code" disabled>
|
||||
<span class="icon">■</span> Stop
|
||||
</button>
|
||||
<button id="btn-save" title="Save code to device as main.py" disabled>
|
||||
<span class="icon">💾</span> Save
|
||||
</button>
|
||||
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main workspace area -->
|
||||
<main id="workspace-container">
|
||||
<!-- Blockly editor fills the top area -->
|
||||
<div id="blockly-area">
|
||||
<div id="blockly-div"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom panels: code preview + serial terminal -->
|
||||
<div id="bottom-panels">
|
||||
<div id="resize-handle-h" class="resize-handle horizontal"></div>
|
||||
<div id="code-panel">
|
||||
<div class="panel-header">Generated Code</div>
|
||||
<pre id="code-preview"><code id="code-output"></code></pre>
|
||||
</div>
|
||||
<div id="terminal-panel">
|
||||
<div class="panel-header">Serial Terminal</div>
|
||||
<div id="terminal-output"></div>
|
||||
<div id="terminal-input-row">
|
||||
<input type="text" id="terminal-input" placeholder="Type command…" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "esp32block",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"blockly": "^11.2.1",
|
||||
"esptool-js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
import * as Blockly from 'blockly';
|
||||
|
||||
// ─── Pin I/O ──────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['pin_set_mode'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('set pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN')
|
||||
.appendField('as')
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
['OUTPUT', 'OUT'],
|
||||
['INPUT', 'IN'],
|
||||
['INPUT_PULLUP', 'PULL_UP'],
|
||||
]), 'MODE');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Configure a GPIO pin mode');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['pin_digital_write'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('digital write pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN')
|
||||
.appendField('to')
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
['HIGH', '1'],
|
||||
['LOW', '0'],
|
||||
]), 'VALUE');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Write HIGH or LOW to a digital pin');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['pin_digital_read'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('digital read pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN');
|
||||
this.setOutput(true, 'Number');
|
||||
this.setColour(230);
|
||||
this.setTooltip('Read digital value from a pin (0 or 1)');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── PWM ──────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['pwm_init'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('init PWM on pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN')
|
||||
.appendField('freq')
|
||||
.appendField(new Blockly.FieldNumber(1000, 1, 40000000, 1), 'FREQ')
|
||||
.appendField('Hz');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(160);
|
||||
this.setTooltip('Initialize PWM on a pin with a frequency');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['pwm_set_duty'] = {
|
||||
init() {
|
||||
this.appendValueInput('DUTY')
|
||||
.setCheck('Number')
|
||||
.appendField('set PWM duty on pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN')
|
||||
.appendField('to');
|
||||
this.appendDummyInput()
|
||||
.appendField('(0-1023)');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(160);
|
||||
this.setTooltip('Set PWM duty cycle (0-1023)');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['pwm_set_freq'] = {
|
||||
init() {
|
||||
this.appendValueInput('FREQ')
|
||||
.setCheck('Number')
|
||||
.appendField('set PWM freq on pin')
|
||||
.appendField(new Blockly.FieldNumber(2, 0, 48, 1), 'PIN')
|
||||
.appendField('to');
|
||||
this.appendDummyInput()
|
||||
.appendField('Hz');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(160);
|
||||
this.setTooltip('Set PWM frequency in Hz');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── ADC ──────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['adc_read'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('read ADC on pin')
|
||||
.appendField(new Blockly.FieldNumber(1, 0, 20, 1), 'PIN');
|
||||
this.setOutput(true, 'Number');
|
||||
this.setColour(30);
|
||||
this.setTooltip('Read analog value from ADC pin (0-4095)');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Time ─────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['sleep_seconds'] = {
|
||||
init() {
|
||||
this.appendValueInput('SECONDS')
|
||||
.setCheck('Number')
|
||||
.appendField('sleep');
|
||||
this.appendDummyInput()
|
||||
.appendField('seconds');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(120);
|
||||
this.setTooltip('Pause execution for N seconds');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['sleep_ms'] = {
|
||||
init() {
|
||||
this.appendValueInput('MS')
|
||||
.setCheck('Number')
|
||||
.appendField('sleep');
|
||||
this.appendDummyInput()
|
||||
.appendField('milliseconds');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(120);
|
||||
this.setTooltip('Pause execution for N milliseconds');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['ticks_ms'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('ticks (ms)');
|
||||
this.setOutput(true, 'Number');
|
||||
this.setColour(120);
|
||||
this.setTooltip('Get millisecond tick counter');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── WiFi ─────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['wifi_connect'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('connect WiFi SSID')
|
||||
.appendField(new Blockly.FieldTextInput('MyNetwork'), 'SSID')
|
||||
.appendField('password')
|
||||
.appendField(new Blockly.FieldTextInput('password'), 'PASSWORD');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(290);
|
||||
this.setTooltip('Connect to a WiFi network');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['wifi_get_ip'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('WiFi IP address');
|
||||
this.setOutput(true, 'String');
|
||||
this.setColour(290);
|
||||
this.setTooltip('Get the current IP address');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── NeoPixel ─────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['neopixel_init'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('init NeoPixel on pin')
|
||||
.appendField(new Blockly.FieldNumber(48, 0, 48, 1), 'PIN')
|
||||
.appendField('with')
|
||||
.appendField(new Blockly.FieldNumber(1, 1, 1024, 1), 'NUM')
|
||||
.appendField('LEDs');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(10);
|
||||
this.setTooltip('Initialize a NeoPixel strip');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['neopixel_set_color'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('set NeoPixel #')
|
||||
.appendField(new Blockly.FieldNumber(0, 0, 1023, 1), 'INDEX');
|
||||
this.appendValueInput('R').setCheck('Number').appendField('R');
|
||||
this.appendValueInput('G').setCheck('Number').appendField('G');
|
||||
this.appendValueInput('B').setCheck('Number').appendField('B');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(10);
|
||||
this.setTooltip('Set NeoPixel color (R, G, B values 0-255)');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['neopixel_show'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('NeoPixel show');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(10);
|
||||
this.setTooltip('Push color data to NeoPixel strip');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── I2C ──────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['i2c_init'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('init I2C SDA pin')
|
||||
.appendField(new Blockly.FieldNumber(8, 0, 48, 1), 'SDA')
|
||||
.appendField('SCL pin')
|
||||
.appendField(new Blockly.FieldNumber(9, 0, 48, 1), 'SCL')
|
||||
.appendField('freq')
|
||||
.appendField(new Blockly.FieldNumber(400000, 1, 1000000, 1), 'FREQ');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(200);
|
||||
this.setTooltip('Initialize I2C bus');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['i2c_scan'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('I2C scan devices');
|
||||
this.setOutput(true, 'Array');
|
||||
this.setColour(200);
|
||||
this.setTooltip('Scan for I2C devices, returns list of addresses');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['i2c_writeto'] = {
|
||||
init() {
|
||||
this.appendValueInput('DATA')
|
||||
.setCheck(null)
|
||||
.appendField('I2C write to address')
|
||||
.appendField(new Blockly.FieldNumber(0, 0, 127, 1), 'ADDR')
|
||||
.appendField('data');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(200);
|
||||
this.setTooltip('Write data bytes to an I2C device');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['i2c_readfrom'] = {
|
||||
init() {
|
||||
this.appendDummyInput()
|
||||
.appendField('I2C read from address')
|
||||
.appendField(new Blockly.FieldNumber(0, 0, 127, 1), 'ADDR')
|
||||
.appendField('bytes')
|
||||
.appendField(new Blockly.FieldNumber(1, 1, 256, 1), 'NBYTES');
|
||||
this.setOutput(true, null);
|
||||
this.setColour(200);
|
||||
this.setTooltip('Read N bytes from an I2C device');
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Print ────────────────────────────────────────────────
|
||||
|
||||
Blockly.Blocks['print_text'] = {
|
||||
init() {
|
||||
this.appendValueInput('TEXT')
|
||||
.setCheck(null)
|
||||
.appendField('print');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(60);
|
||||
this.setTooltip('Print to serial output');
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import { pythonGenerator, Order } from 'blockly/python';
|
||||
|
||||
// ─── Pin I/O ──────────────────────────────────────────────
|
||||
|
||||
function ensurePinVar(pin, mode) {
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
const varName = `pin_${pin}`;
|
||||
const modeStr = mode === 'IN' ? 'Pin.IN' : 'Pin.OUT';
|
||||
pythonGenerator.definitions_[`pin_init_${pin}`] = `${varName} = Pin(${pin}, ${modeStr})`;
|
||||
return varName;
|
||||
}
|
||||
|
||||
pythonGenerator.forBlock['pin_set_mode'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const mode = block.getFieldValue('MODE');
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
const varName = `pin_${pin}`;
|
||||
const pyMode = `Pin.${mode}`;
|
||||
pythonGenerator.definitions_[`pin_init_${pin}`] = `${varName} = Pin(${pin}, ${pyMode})`;
|
||||
return '';
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['pin_digital_write'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const value = block.getFieldValue('VALUE');
|
||||
const varName = ensurePinVar(pin, 'OUT');
|
||||
return `${varName}.value(${value})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['pin_digital_read'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const varName = ensurePinVar(pin, 'IN');
|
||||
return [`${varName}.value()`, Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// ─── PWM ──────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['pwm_init'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const freq = block.getFieldValue('FREQ');
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
return `pwm_${pin} = PWM(Pin(${pin}), freq=${freq})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['pwm_set_duty'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const duty = pythonGenerator.valueToCode(block, 'DUTY', Order.NONE) || '0';
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
return `pwm_${pin}.duty(${duty})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['pwm_set_freq'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '1000';
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
return `pwm_${pin}.freq(${freq})\n`;
|
||||
};
|
||||
|
||||
// ─── ADC ──────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['adc_read'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
pythonGenerator.definitions_[`adc_init_${pin}`] = `adc_${pin} = ADC(Pin(${pin}))`;
|
||||
return [`adc_${pin}.read()`, Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// ─── Time ─────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['sleep_seconds'] = function (block) {
|
||||
const seconds = pythonGenerator.valueToCode(block, 'SECONDS', Order.NONE) || '1';
|
||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||
return `time.sleep(${seconds})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['sleep_ms'] = function (block) {
|
||||
const ms = pythonGenerator.valueToCode(block, 'MS', Order.NONE) || '100';
|
||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||
return `time.sleep_ms(${ms})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['ticks_ms'] = function () {
|
||||
pythonGenerator.definitions_['import_time'] = 'import time';
|
||||
return ['time.ticks_ms()', Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// ─── WiFi ─────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['wifi_connect'] = function (block) {
|
||||
const ssid = block.getFieldValue('SSID');
|
||||
const password = block.getFieldValue('PASSWORD');
|
||||
pythonGenerator.definitions_['import_network'] = 'import network';
|
||||
return [
|
||||
`wlan = network.WLAN(network.STA_IF)\n`,
|
||||
`wlan.active(True)\n`,
|
||||
`wlan.connect('${ssid}', '${password}')\n`,
|
||||
`while not wlan.isconnected():\n`,
|
||||
` time.sleep(0.1)\n`,
|
||||
`print('Connected:', wlan.ifconfig()[0])\n`,
|
||||
].join('');
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['wifi_get_ip'] = function () {
|
||||
pythonGenerator.definitions_['import_network'] = 'import network';
|
||||
return ['network.WLAN(network.STA_IF).ifconfig()[0]', Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// ─── NeoPixel ─────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['neopixel_init'] = function (block) {
|
||||
const pin = block.getFieldValue('PIN');
|
||||
const num = block.getFieldValue('NUM');
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
pythonGenerator.definitions_['import_neopixel'] = 'import neopixel';
|
||||
return `np = neopixel.NeoPixel(Pin(${pin}), ${num})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['neopixel_set_color'] = function (block) {
|
||||
const index = block.getFieldValue('INDEX');
|
||||
const r = pythonGenerator.valueToCode(block, 'R', Order.NONE) || '0';
|
||||
const g = pythonGenerator.valueToCode(block, 'G', Order.NONE) || '0';
|
||||
const b = pythonGenerator.valueToCode(block, 'B', Order.NONE) || '0';
|
||||
return `np[${index}] = (${r}, ${g}, ${b})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['neopixel_show'] = function () {
|
||||
return 'np.write()\n';
|
||||
};
|
||||
|
||||
// ─── I2C ──────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['i2c_init'] = function (block) {
|
||||
const sda = block.getFieldValue('SDA');
|
||||
const scl = block.getFieldValue('SCL');
|
||||
const freq = block.getFieldValue('FREQ');
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
return `i2c = I2C(0, sda=Pin(${sda}), scl=Pin(${scl}), freq=${freq})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['i2c_scan'] = function () {
|
||||
pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
|
||||
return ['i2c.scan()', Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['i2c_writeto'] = function (block) {
|
||||
const addr = block.getFieldValue('ADDR');
|
||||
const data = pythonGenerator.valueToCode(block, 'DATA', Order.NONE) || 'b""';
|
||||
return `i2c.writeto(${addr}, ${data})\n`;
|
||||
};
|
||||
|
||||
pythonGenerator.forBlock['i2c_readfrom'] = function (block) {
|
||||
const addr = block.getFieldValue('ADDR');
|
||||
const nbytes = block.getFieldValue('NBYTES');
|
||||
return [`i2c.readfrom(${addr}, ${nbytes})`, Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
// ─── Print ────────────────────────────────────────────────
|
||||
|
||||
pythonGenerator.forBlock['print_text'] = function (block) {
|
||||
const text = pythonGenerator.valueToCode(block, 'TEXT', Order.NONE) || "''";
|
||||
return `print(${text})\n`;
|
||||
};
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
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,182 @@
|
|||
import * as Blockly from 'blockly';
|
||||
import { pythonGenerator } from 'blockly/python';
|
||||
import './blocks/esp32_blocks.js';
|
||||
import './blocks/esp32_generators.js';
|
||||
import { toolbox } from './blocks/toolbox.js';
|
||||
import { connect, disconnect, isConnected, onData, writeString } from './serial/connection.js';
|
||||
import { executeCode, stopExecution, saveToDevice } from './serial/repl.js';
|
||||
import { flashFirmware } from './serial/flasher.js';
|
||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||
import { initResizablePanels } from './ui/panels.js';
|
||||
import './style.css';
|
||||
|
||||
// ─── Blockly Workspace ───────────────────────────────────
|
||||
|
||||
const workspace = Blockly.inject('blockly-div', {
|
||||
toolbox,
|
||||
theme: Blockly.Themes.Dark,
|
||||
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 },
|
||||
trashcan: true,
|
||||
renderer: 'zelos',
|
||||
});
|
||||
|
||||
// ─── Live Code Preview ───────────────────────────────────
|
||||
|
||||
const codeOutput = document.getElementById('code-output');
|
||||
|
||||
function updateCodePreview() {
|
||||
const code = pythonGenerator.workspaceToCode(workspace);
|
||||
codeOutput.textContent = code || '# Drag blocks to generate MicroPython code';
|
||||
}
|
||||
|
||||
workspace.addChangeListener((event) => {
|
||||
if (event.isUiEvent) return;
|
||||
updateCodePreview();
|
||||
});
|
||||
|
||||
updateCodePreview();
|
||||
|
||||
// ─── Workspace Persistence (localStorage) ────────────────
|
||||
|
||||
const STORAGE_KEY = 'esp32block_workspace';
|
||||
|
||||
function saveWorkspace() {
|
||||
const state = Blockly.serialization.workspaces.save(workspace);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
function loadWorkspace() {
|
||||
const json = localStorage.getItem(STORAGE_KEY);
|
||||
if (json) {
|
||||
try {
|
||||
const state = JSON.parse(json);
|
||||
Blockly.serialization.workspaces.load(state, workspace);
|
||||
} catch (_) {
|
||||
/* corrupted state, ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace.addChangeListener((event) => {
|
||||
if (event.isUiEvent) return;
|
||||
saveWorkspace();
|
||||
});
|
||||
|
||||
loadWorkspace();
|
||||
|
||||
// ─── Resize Handling ─────────────────────────────────────
|
||||
|
||||
function onResize() {
|
||||
const blocklyArea = document.getElementById('blockly-area');
|
||||
const blocklyDiv = document.getElementById('blockly-div');
|
||||
blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
|
||||
blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
|
||||
Blockly.svgResize(workspace);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
onResize();
|
||||
initResizablePanels();
|
||||
|
||||
// ─── UI State Helpers ────────────────────────────────────
|
||||
|
||||
const btnConnect = document.getElementById('btn-connect');
|
||||
const btnFlash = document.getElementById('btn-flash');
|
||||
const btnRun = document.getElementById('btn-run');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const statusEl = document.getElementById('connection-status');
|
||||
const terminalInput = document.getElementById('terminal-input');
|
||||
|
||||
function setConnectedUI(connected) {
|
||||
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
||||
btnFlash.disabled = !connected;
|
||||
btnRun.disabled = !connected;
|
||||
btnStop.disabled = !connected;
|
||||
btnSave.disabled = !connected;
|
||||
terminalInput.disabled = !connected;
|
||||
statusEl.textContent = connected ? 'Connected' : 'Disconnected';
|
||||
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
||||
}
|
||||
|
||||
// ─── Serial Event Listeners ──────────────────────────────
|
||||
|
||||
onData((text) => appendToTerminal(text));
|
||||
|
||||
// ─── Toolbar Buttons ─────────────────────────────────────
|
||||
|
||||
btnConnect.addEventListener('click', async () => {
|
||||
try {
|
||||
if (isConnected()) {
|
||||
await disconnect();
|
||||
setConnectedUI(false);
|
||||
appendToTerminal('\n--- Disconnected ---\n');
|
||||
} else {
|
||||
await connect();
|
||||
setConnectedUI(true);
|
||||
appendToTerminal('--- Connected ---\n');
|
||||
}
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nConnection error: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
btnFlash.addEventListener('click', async () => {
|
||||
try {
|
||||
clearTerminal();
|
||||
appendToTerminal('Starting firmware flash...\n');
|
||||
await disconnect();
|
||||
const port = await navigator.serial.requestPort();
|
||||
await flashFirmware(port, (msg) => appendToTerminal(msg + '\n'));
|
||||
appendToTerminal('Flash complete! Reconnect to use the device.\n');
|
||||
setConnectedUI(false);
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nFlash error: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
btnRun.addEventListener('click', async () => {
|
||||
const code = pythonGenerator.workspaceToCode(workspace);
|
||||
if (!code.trim()) {
|
||||
appendToTerminal('\nNo code to run. Add some blocks!\n');
|
||||
return;
|
||||
}
|
||||
appendToTerminal('\n>>> Running...\n');
|
||||
try {
|
||||
await executeCode(code);
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nRun error: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
btnStop.addEventListener('click', async () => {
|
||||
try {
|
||||
await stopExecution();
|
||||
appendToTerminal('\n--- Stopped ---\n');
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nStop error: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
btnSave.addEventListener('click', async () => {
|
||||
const code = pythonGenerator.workspaceToCode(workspace);
|
||||
if (!code.trim()) {
|
||||
appendToTerminal('\nNo code to save.\n');
|
||||
return;
|
||||
}
|
||||
appendToTerminal('\nSaving to device as main.py...\n');
|
||||
try {
|
||||
await saveToDevice(code);
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nSave error: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
terminalInput.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const text = terminalInput.value;
|
||||
terminalInput.value = '';
|
||||
await writeString(text + '\r\n');
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
let port = null;
|
||||
let reader = null;
|
||||
let writer = null;
|
||||
|
||||
export function isConnected() {
|
||||
return port !== null;
|
||||
}
|
||||
|
||||
export function getWriter() {
|
||||
return writer;
|
||||
}
|
||||
|
||||
export async function connect(baudRate = 115200) {
|
||||
if (port) return port;
|
||||
port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate });
|
||||
|
||||
const textDecoder = new TextDecoderStream();
|
||||
const textEncoder = new TextEncoderStream();
|
||||
|
||||
port.readable.pipeTo(textDecoder.writable);
|
||||
textEncoder.readable.pipeTo(port.writable);
|
||||
|
||||
reader = textDecoder.readable.getReader();
|
||||
writer = textEncoder.writable.getWriter();
|
||||
|
||||
readLoop();
|
||||
return port;
|
||||
}
|
||||
|
||||
export async function disconnect() {
|
||||
if (reader) {
|
||||
try { await reader.cancel(); } catch (_) { /* already closed */ }
|
||||
reader = null;
|
||||
}
|
||||
if (writer) {
|
||||
try { await writer.close(); } catch (_) { /* already closed */ }
|
||||
writer = null;
|
||||
}
|
||||
if (port) {
|
||||
try { await port.close(); } catch (_) { /* already closed */ }
|
||||
port = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeString(str) {
|
||||
if (writer) await writer.write(str);
|
||||
}
|
||||
|
||||
const listeners = new Set();
|
||||
|
||||
export function onData(callback) {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
async function readLoop() {
|
||||
try {
|
||||
while (reader) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
for (const cb of listeners) cb(value);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
/* port disconnected */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// esptool-js firmware flasher wrapper
|
||||
// Actual flashing will use the esptool-js ESPLoader when the user triggers it.
|
||||
|
||||
export async function flashFirmware(port, onProgress) {
|
||||
const { ESPLoader, Transport } = await import('esptool-js');
|
||||
|
||||
const transport = new Transport(port);
|
||||
const loader = new ESPLoader({
|
||||
transport,
|
||||
baudrate: 115200,
|
||||
terminal: {
|
||||
clean() {},
|
||||
writeLine(data) { onProgress?.(data); },
|
||||
write(data) { onProgress?.(data); },
|
||||
},
|
||||
});
|
||||
|
||||
await loader.main();
|
||||
await loader.eraseFlash();
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.bin';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
input.onchange = async () => {
|
||||
try {
|
||||
const file = input.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
const data = await file.arrayBuffer();
|
||||
const uint8 = new Uint8Array(data);
|
||||
|
||||
onProgress?.('Writing firmware...');
|
||||
await loader.writeFlash({
|
||||
fileArray: [{ data: uint8, address: 0x0 }],
|
||||
flashSize: 'keep',
|
||||
eraseAll: false,
|
||||
compress: true,
|
||||
});
|
||||
|
||||
onProgress?.('Firmware flashed successfully!');
|
||||
await transport.disconnect();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { writeString } from './connection.js';
|
||||
|
||||
export async function enterRawRepl() {
|
||||
await writeString('\x03\x03');
|
||||
await sleep(100);
|
||||
await writeString('\x01');
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
export async function executeCode(code) {
|
||||
await enterRawRepl();
|
||||
await writeString(code);
|
||||
await writeString('\x04');
|
||||
}
|
||||
|
||||
export async function stopExecution() {
|
||||
await writeString('\x03\x03');
|
||||
}
|
||||
|
||||
export async function saveToDevice(code, filename = 'main.py') {
|
||||
const script = [
|
||||
`f = open('${filename}', 'w')`,
|
||||
`f.write(${JSON.stringify(code)})`,
|
||||
`f.close()`,
|
||||
`print('Saved to ${filename}')`,
|
||||
].join('\n');
|
||||
await executeCode(script);
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
/* --- Reset & Base --- */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #1e1e2e;
|
||||
--bg-secondary: #181825;
|
||||
--bg-surface: #252538;
|
||||
--bg-toolbar: #11111b;
|
||||
--text-primary: #cdd6f4;
|
||||
--text-secondary: #a6adc8;
|
||||
--text-muted: #6c7086;
|
||||
--accent: #89b4fa;
|
||||
--accent-hover: #74c7ec;
|
||||
--green: #a6e3a1;
|
||||
--red: #f38ba8;
|
||||
--yellow: #f9e2af;
|
||||
--border: #313244;
|
||||
--radius: 6px;
|
||||
--toolbar-height: 48px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Toolbar --- */
|
||||
#toolbar {
|
||||
height: var(--toolbar-height);
|
||||
background: var(--bg-toolbar);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-actions button {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.toolbar-actions button:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
color: var(--bg-toolbar);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.toolbar-actions button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toolbar-actions button .icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#connection-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: rgba(243, 139, 168, 0.15);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: rgba(166, 227, 161, 0.15);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* --- Main Layout --- */
|
||||
#workspace-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--toolbar-height));
|
||||
}
|
||||
|
||||
#blockly-area {
|
||||
flex: 1 1 60%;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#blockly-div {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* --- Bottom Panels --- */
|
||||
#bottom-panels {
|
||||
flex: 0 0 35%;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.resize-handle.horizontal {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#code-panel, #terminal-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#code-panel {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* --- Code Preview --- */
|
||||
#code-preview {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--bg-secondary);
|
||||
padding: 10px 14px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* --- Terminal --- */
|
||||
#terminal-panel {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
#terminal-output {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 14px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--green);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#terminal-input-row {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#terminal-input {
|
||||
flex: 1;
|
||||
background: var(--bg-toolbar);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#terminal-input:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#terminal-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- Blockly Overrides (dark theme) --- */
|
||||
.blocklyMainBackground {
|
||||
fill: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
.blocklyToolboxDiv {
|
||||
background: var(--bg-secondary) !important;
|
||||
border-right: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: var(--bg-surface) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow:hover {
|
||||
background: var(--bg-surface) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeSelected {
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
export function initResizablePanels() {
|
||||
const handle = document.getElementById('resize-handle-h');
|
||||
const container = document.getElementById('workspace-container');
|
||||
const blocklyArea = document.getElementById('blockly-area');
|
||||
const bottomPanels = document.getElementById('bottom-panels');
|
||||
|
||||
if (!handle || !container || !blocklyArea || !bottomPanels) return;
|
||||
|
||||
let dragging = false;
|
||||
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
dragging = true;
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const offset = e.clientY - containerRect.top;
|
||||
const totalHeight = containerRect.height;
|
||||
const topPercent = Math.max(20, Math.min(80, (offset / totalHeight) * 100));
|
||||
|
||||
blocklyArea.style.flex = `0 0 ${topPercent}%`;
|
||||
bottomPanels.style.flex = `0 0 ${100 - topPercent}%`;
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
dragging = false;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
const outputEl = document.getElementById('terminal-output');
|
||||
|
||||
export function appendToTerminal(text) {
|
||||
if (!outputEl) return;
|
||||
outputEl.textContent += text;
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
}
|
||||
|
||||
export function clearTerminal() {
|
||||
if (outputEl) outputEl.textContent = '';
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue