diff --git a/index.html b/index.html index 6ad14ae..7537a63 100644 --- a/index.html +++ b/index.html @@ -135,6 +135,17 @@ + + + diff --git a/public/firmware/RPI_PICO-20251209-v1.27.0.uf2 b/public/firmware/RPI_PICO-20251209-v1.27.0.uf2 new file mode 100644 index 0000000..5841c91 Binary files /dev/null and b/public/firmware/RPI_PICO-20251209-v1.27.0.uf2 differ diff --git a/src/blocks/categories/neopixel.js b/src/blocks/categories/neopixel.js index 2370a26..105cc8d 100644 --- a/src/blocks/categories/neopixel.js +++ b/src/blocks/categories/neopixel.js @@ -6,13 +6,39 @@ export const neopixelCategory = { { kind: 'block', type: 'neopixel_init' }, { kind: 'block', - type: 'neopixel_set_color', + type: 'colour_rgb', 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: 'tuple_create_3', + inputs: { + A: { shadow: { type: 'math_number', fields: { NUM: 255 } } }, + B: { shadow: { type: 'math_number', fields: { NUM: 0 } } }, + C: { shadow: { type: 'math_number', fields: { NUM: 0 } } }, + }, + }, + { + kind: 'block', + type: 'neopixel_set_color', + inputs: { + INDEX: { shadow: { type: 'math_number', fields: { NUM: 0 } } }, + COLOR: { + shadow: { + type: 'colour_rgb', + 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' }, ], }; diff --git a/src/blocks/categories/pwm.js b/src/blocks/categories/pwm.js index 2719a3a..642889e 100644 --- a/src/blocks/categories/pwm.js +++ b/src/blocks/categories/pwm.js @@ -4,7 +4,15 @@ export const pwmCategory = { colour: '160', contents: [ { kind: 'block', type: 'pwm_init' }, - { kind: 'block', type: 'pwm_set_duty' }, - { kind: 'block', type: 'pwm_set_freq' }, + { + kind: 'block', + type: 'pwm_set_duty', + inputs: { DUTY: { shadow: { type: 'math_number', fields: { NUM: 512 } } } }, + }, + { + kind: 'block', + type: 'pwm_set_freq', + inputs: { FREQ: { shadow: { type: 'math_number', fields: { NUM: 1000 } } } }, + }, ], }; diff --git a/src/blocks/categories/random.js b/src/blocks/categories/random.js new file mode 100644 index 0000000..1553b6e --- /dev/null +++ b/src/blocks/categories/random.js @@ -0,0 +1,31 @@ +export const randomCategory = { + kind: 'category', + name: 'Random', + colour: '200', + contents: [ + { + kind: 'block', + type: 'random_int', + inputs: { + FROM: { shadow: { type: 'math_number', fields: { NUM: 1 } } }, + TO: { shadow: { type: 'math_number', fields: { NUM: 10 } } }, + }, + }, + { kind: 'block', type: 'random_float' }, + { + kind: 'block', + type: 'random_uniform', + inputs: { + LOW: { shadow: { type: 'math_number', fields: { NUM: 0 } } }, + HIGH: { shadow: { type: 'math_number', fields: { NUM: 1 } } }, + }, + }, + { + kind: 'block', + type: 'random_seed', + inputs: { + SEED: { shadow: { type: 'math_number', fields: { NUM: 0 } } }, + }, + }, + ], +}; diff --git a/src/blocks/categories/sensors.js b/src/blocks/categories/sensors.js new file mode 100644 index 0000000..9621bc6 --- /dev/null +++ b/src/blocks/categories/sensors.js @@ -0,0 +1,15 @@ +export const sensorsCategory = { + kind: 'category', + name: 'Sensors', + colour: '65', + contents: [ + { + kind: 'block', + type: 'sonar_distance', + inputs: { + TRIG: { shadow: { type: 'math_number', fields: { NUM: 2 } } }, + ECHO: { shadow: { type: 'math_number', fields: { NUM: 3 } } }, + }, + }, + ], +}; diff --git a/src/blocks/categories/superbit.js b/src/blocks/categories/superbit.js new file mode 100644 index 0000000..e9cc214 --- /dev/null +++ b/src/blocks/categories/superbit.js @@ -0,0 +1,13 @@ +export const superbitCategory = { + kind: 'category', + name: 'SuperBit', + colour: '45', + contents: [ + { + kind: 'block', + type: 'superbit_motor_run', + inputs: { SPEED: { shadow: { type: 'math_number', fields: { NUM: 255 } } } }, + }, + { kind: 'block', type: 'superbit_motor_stop_all' }, + ], +}; diff --git a/src/blocks/esp32_blocks.js b/src/blocks/esp32_blocks.js index 8761ea7..1775232 100644 --- a/src/blocks/esp32_blocks.js +++ b/src/blocks/esp32_blocks.js @@ -154,6 +154,60 @@ Blockly.Blocks['ticks_ms'] = { }, }; +// ─── Random ─────────────────────────────────────────────── + +Blockly.Blocks['random_int'] = { + init() { + this.appendValueInput('FROM') + .setCheck('Number') + .appendField('random integer from'); + this.appendValueInput('TO') + .setCheck('Number') + .appendField('to'); + this.setOutput(true, 'Number'); + this.setInputsInline(true); + this.setColour(200); + this.setTooltip('Random integer N where from ≤ N ≤ to (inclusive)'); + }, +}; + +Blockly.Blocks['random_float'] = { + init() { + this.appendDummyInput() + .appendField('random float (0.0 to 1.0)'); + this.setOutput(true, 'Number'); + this.setColour(200); + this.setTooltip('Random float in range [0.0, 1.0)'); + }, +}; + +Blockly.Blocks['random_uniform'] = { + init() { + this.appendValueInput('LOW') + .setCheck('Number') + .appendField('random float from'); + this.appendValueInput('HIGH') + .setCheck('Number') + .appendField('to'); + this.setOutput(true, 'Number'); + this.setInputsInline(true); + this.setColour(200); + this.setTooltip('Random float in range [low, high]'); + }, +}; + +Blockly.Blocks['random_seed'] = { + init() { + this.appendValueInput('SEED') + .setCheck('Number') + .appendField('random seed'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(200); + this.setTooltip('Set random seed for reproducible sequences (optional)'); + }, +}; + // ─── WiFi ───────────────────────────────────────────────── Blockly.Blocks['wifi_connect'] = { @@ -197,19 +251,43 @@ Blockly.Blocks['neopixel_init'] = { }, }; -Blockly.Blocks['neopixel_set_color'] = { +Blockly.Blocks['colour_rgb'] = { init() { - this.appendDummyInput() - .appendField('set NeoPixel #') - .appendField(new Blockly.FieldNumber(0, 0, 1023, 1), 'INDEX'); - this.appendValueInput('R').setCheck('Number').appendField('R'); + this.appendValueInput('R').setCheck('Number').appendField('colour R'); this.appendValueInput('G').setCheck('Number').appendField('G'); this.appendValueInput('B').setCheck('Number').appendField('B'); + this.setOutput(true, 'Colour'); + this.setInputsInline(true); + this.setColour(10); + this.setTooltip('RGB colour as array [R, G, B] (values 0–255)'); + }, +}; + +Blockly.Blocks['tuple_create_3'] = { + init() { + this.appendValueInput('A').appendField('tuple'); + this.appendValueInput('B').appendField(','); + this.appendValueInput('C').appendField(','); + this.setOutput(true, 'Colour'); + this.setInputsInline(true); + this.setColour(10); + this.setTooltip('Create a 3-element tuple (e.g. for NeoPixel colour). Use numbers or variables.'); + }, +}; + +Blockly.Blocks['neopixel_set_color'] = { + init() { + this.appendValueInput('INDEX') + .setCheck('Number') + .appendField('set NeoPixel #'); + this.appendValueInput('COLOR') + .setCheck(null) + .appendField('colour'); 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)'); + this.setTooltip('Set NeoPixel to a colour. Use the colour/tuple block or a variable (e.g. from a function parameter).'); }, }; @@ -391,3 +469,54 @@ Blockly.Blocks['microbit_display_all'] = { this.setTooltip('Click LEDs to set the 5×5 display (micro:bit only). On = 9, off = 0.'); }, }; + +// ─── SuperBit (Yahboom SuperBit V2, micro:bit only) ─────── + +Blockly.Blocks['superbit_motor_run'] = { + init() { + this.appendDummyInput() + .appendField('SuperBit motor') + .appendField(new Blockly.FieldDropdown([ + ['M1', 'M1'], + ['M2', 'M2'], + ['M3', 'M3'], + ['M4', 'M4'], + ]), 'MOTOR'); + this.appendValueInput('SPEED') + .setCheck('Number') + .appendField('speed (-255 to 255)'); + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(45); + this.setTooltip('Run a SuperBit V2 motor (PCA9685 over I2C). Negative = reverse.'); + }, +}; + +Blockly.Blocks['superbit_motor_stop_all'] = { + init() { + this.appendDummyInput() + .appendField('SuperBit motor stop all'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(45); + this.setTooltip('Stop all four SuperBit V2 motors.'); + }, +}; + +// ─── Sensors (HC-SR04 style sonar, all devices) ─────────── + +Blockly.Blocks['sonar_distance'] = { + init() { + this.appendValueInput('TRIG') + .setCheck('Number') + .appendField('sonar distance trigger'); + this.appendValueInput('ECHO') + .setCheck('Number') + .appendField('echo'); + this.setInputsInline(true); + this.setOutput(true, 'Number'); + this.setColour(65); + this.setTooltip('HC-SR04 style ultrasonic distance in cm. Trigger and echo pin numbers.'); + }, +}; diff --git a/src/blocks/esp32_generators.js b/src/blocks/esp32_generators.js index b39ddf3..e8bf546 100644 --- a/src/blocks/esp32_generators.js +++ b/src/blocks/esp32_generators.js @@ -91,6 +91,58 @@ pythonGenerator.forBlock['adc_read'] = function (block) { return [`adc_${pin}.read()`, Order.FUNCTION_CALL]; }; +// ─── Sensors (sonar / HC-SR04) ──────────────────────────── + +const SONAR_HELPER_MACHINE = `def _sonar_cm(trig, echo): + from machine import Pin, time_pulse_us + import time + t = Pin(int(trig), Pin.OUT) + e = Pin(int(echo), Pin.IN) + t.off() + time.sleep_us(2) + t.on() + time.sleep_us(10) + t.off() + d = time_pulse_us(e, 1, 30000) + return round(d / 58.0, 1) if d >= 0 else -1 +`; + +const SONAR_HELPER_MICROBIT = `def _sonar_cm(trig, echo): + import utime + import microbit + trig, echo = int(trig), int(echo) + t = getattr(microbit, 'pin' + str(trig), microbit.pin0) + e = getattr(microbit, 'pin' + str(echo), microbit.pin0) + t.write_digital(0) + utime.sleep_us(2) + t.write_digital(1) + utime.sleep_us(10) + t.write_digital(0) + start = utime.ticks_us() + while e.read_digital() == 0: + if utime.ticks_diff(utime.ticks_us(), start) > 20000: + return -1 + t0 = utime.ticks_us() + while e.read_digital() == 1: + if utime.ticks_diff(utime.ticks_us(), t0) > 20000: + return -1 + t1 = utime.ticks_us() + duration = utime.ticks_diff(t1, t0) + return round(duration / 58.0, 1) +`; + +pythonGenerator.forBlock['sonar_distance'] = function (block) { + const trig = pythonGenerator.valueToCode(block, 'TRIG', Order.NONE) || '2'; + const echo = pythonGenerator.valueToCode(block, 'ECHO', Order.NONE) || '3'; + if (DEVICE() === 'microbit') { + pythonGenerator.definitions_['sonar_ultrasonic'] = SONAR_HELPER_MICROBIT; + } else { + pythonGenerator.definitions_['import_time'] = 'import time'; + pythonGenerator.definitions_['sonar_ultrasonic'] = SONAR_HELPER_MACHINE; + } + return [`_sonar_cm(${trig}, ${echo})`, Order.FUNCTION_CALL]; +}; + // ─── Time ───────────────────────────────────────────────── pythonGenerator.forBlock['sleep_seconds'] = function (block) { @@ -122,6 +174,33 @@ pythonGenerator.forBlock['ticks_ms'] = function () { return ['time.ticks_ms()', Order.FUNCTION_CALL]; }; +// ─── Random ─────────────────────────────────────────────── + +pythonGenerator.forBlock['random_int'] = function (block) { + const from_ = pythonGenerator.valueToCode(block, 'FROM', Order.NONE) || '0'; + const to = pythonGenerator.valueToCode(block, 'TO', Order.NONE) || '10'; + pythonGenerator.definitions_['import_random'] = 'import random'; + return [`random.randint(${from_}, ${to})`, Order.FUNCTION_CALL]; +}; + +pythonGenerator.forBlock['random_float'] = function () { + pythonGenerator.definitions_['import_random'] = 'import random'; + return ['random.random()', Order.FUNCTION_CALL]; +}; + +pythonGenerator.forBlock['random_uniform'] = function (block) { + const low = pythonGenerator.valueToCode(block, 'LOW', Order.NONE) || '0'; + const high = pythonGenerator.valueToCode(block, 'HIGH', Order.NONE) || '1'; + pythonGenerator.definitions_['import_random'] = 'import random'; + return [`random.uniform(${low}, ${high})`, Order.FUNCTION_CALL]; +}; + +pythonGenerator.forBlock['random_seed'] = function (block) { + const seed = pythonGenerator.valueToCode(block, 'SEED', Order.NONE) || '0'; + pythonGenerator.definitions_['import_random'] = 'import random'; + return `random.seed(${seed})\n`; +}; + // ─── WiFi ───────────────────────────────────────────────── pythonGenerator.forBlock['wifi_connect'] = function (block) { @@ -152,28 +231,39 @@ pythonGenerator.forBlock['wifi_get_ip'] = function (block) { // ─── NeoPixel ───────────────────────────────────────────── 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 num = block.getFieldValue('NUM'); + if (DEVICE() === 'microbit') { + pythonGenerator.definitions_['import_microbit_neopixel'] = 'from microbit import *\nfrom neopixel import NeoPixel'; + return `np = NeoPixel(pin${pin}, ${num})\n`; + } 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) { - if (DEVICE() === 'microbit') return ''; - const index = block.getFieldValue('INDEX'); +pythonGenerator.forBlock['colour_rgb'] = function (block) { 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`; + return [`(${r}, ${g}, ${b})`, Order.ATOMIC]; +}; + +pythonGenerator.forBlock['tuple_create_3'] = function (block) { + const a = pythonGenerator.valueToCode(block, 'A', Order.NONE) || '0'; + const b = pythonGenerator.valueToCode(block, 'B', Order.NONE) || '0'; + const c = pythonGenerator.valueToCode(block, 'C', Order.NONE) || '0'; + return [`(${a}, ${b}, ${c})`, Order.ATOMIC]; +}; + +pythonGenerator.forBlock['neopixel_set_color'] = function (block) { + const index = pythonGenerator.valueToCode(block, 'INDEX', Order.NONE) || '0'; + const color = pythonGenerator.valueToCode(block, 'COLOR', Order.NONE) || '(0, 0, 0)'; + return `np[${index}] = ${color}\n`; }; pythonGenerator.forBlock['neopixel_show'] = function () { - if (DEVICE() === 'microbit') return ''; - return 'np.write()\n'; + return DEVICE() === 'microbit' ? 'np.show()\n' : 'np.write()\n'; }; // ─── I2C ────────────────────────────────────────────────── @@ -297,3 +387,68 @@ pythonGenerator.forBlock['microbit_display_all'] = function (block) { pythonGenerator.definitions_['import_microbit_display'] = 'from microbit import display, Image'; return `display.show(Image("${imageStr}"))\n`; }; + +// ─── SuperBit (Yahboom SuperBit V2, micro:bit only) ─────── + +const SUPERBIT_DEF = `from microbit import i2c, sleep +_superbit_initialized = False +def _superbit_init(): + global _superbit_initialized + if _superbit_initialized: + return + PCA9685 = 0x40 + i2c.write(PCA9685, bytearray([0x00, 0x00])) + prescale = 25000000 // 4096 // 50 - 1 + i2c.write(PCA9685, bytearray([0x00, 0x10])) + i2c.write(PCA9685, bytearray([0xFE, prescale])) + i2c.write(PCA9685, bytearray([0x00, 0x00])) + sleep(5) + i2c.write(PCA9685, bytearray([0x00, 0xa1])) + _superbit_initialized = True +def _superbit_set_pwm(ch, on, off): + reg = 0x06 + 4 * ch + i2c.write(0x40, bytearray([reg, on & 0xff, on >> 8, off & 0xff, off >> 8])) +def _superbit_motor_run(index, speed): + _superbit_init() + speed = max(-4095, min(4095, int(speed) * 16)) + a, b = index, index + 1 + if index > 10: + if speed >= 0: + _superbit_set_pwm(a, 0, speed) + _superbit_set_pwm(b, 0, 0) + else: + _superbit_set_pwm(a, 0, 0) + _superbit_set_pwm(b, 0, -speed) + else: + if speed >= 0: + _superbit_set_pwm(b, 0, speed) + _superbit_set_pwm(a, 0, 0) + else: + _superbit_set_pwm(b, 0, 0) + _superbit_set_pwm(a, 0, -speed) +def _superbit_motor_stop_all(): + _superbit_init() + for ch in range(8, 16): + _superbit_set_pwm(ch, 0, 0) +`; + +const SUPERBIT_MOTOR_INDEX = { M1: 8, M2: 10, M3: 12, M4: 14 }; + +pythonGenerator.forBlock['superbit_motor_run'] = function (block) { + if (DEVICE() !== 'microbit') { + return '# SuperBit blocks are for micro:bit with Yahboom SuperBit V2 hat\n'; + } + pythonGenerator.definitions_['superbit_lib'] = SUPERBIT_DEF; + const motor = block.getFieldValue('MOTOR'); + const speed = pythonGenerator.valueToCode(block, 'SPEED', Order.NONE) || '0'; + const index = SUPERBIT_MOTOR_INDEX[motor] ?? 8; + return `_superbit_motor_run(${index}, ${speed})\n`; +}; + +pythonGenerator.forBlock['superbit_motor_stop_all'] = function (block) { + if (DEVICE() !== 'microbit') { + return '# SuperBit blocks are for micro:bit with Yahboom SuperBit V2 hat\n'; + } + pythonGenerator.definitions_['superbit_lib'] = SUPERBIT_DEF; + return '_superbit_motor_stop_all()\n'; +}; diff --git a/src/devices/esp32s3.js b/src/devices/esp32s3.js index ddeaf90..a49d29c 100644 --- a/src/devices/esp32s3.js +++ b/src/devices/esp32s3.js @@ -1,12 +1,14 @@ import { pinIoCategory } from '../blocks/categories/pinIo.js'; import { pwmCategory } from '../blocks/categories/pwm.js'; import { adcCategory } from '../blocks/categories/adc.js'; +import { sensorsCategory } from '../blocks/categories/sensors.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'; +import { randomCategory } from '../blocks/categories/random.js'; export const esp32s3 = { id: 'esp32s3', @@ -21,11 +23,13 @@ export const esp32s3 = { pinIoCategory, pwmCategory, adcCategory, + sensorsCategory, timeCategory, wifiCategory, neopixelCategory, i2cCategory, soundCategory(), serialPrintCategory, + randomCategory, ], }; diff --git a/src/devices/microbit.js b/src/devices/microbit.js index 17695f9..9e9b8a8 100644 --- a/src/devices/microbit.js +++ b/src/devices/microbit.js @@ -1,10 +1,14 @@ import { pinIoCategory } from '../blocks/categories/pinIo.js'; import { pwmCategory } from '../blocks/categories/pwm.js'; import { adcCategory } from '../blocks/categories/adc.js'; +import { sensorsCategory } from '../blocks/categories/sensors.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'; +import { neopixelCategory } from '../blocks/categories/neopixel.js'; +import { randomCategory } from '../blocks/categories/random.js'; +import { superbitCategory } from '../blocks/categories/superbit.js'; export const microbit = { id: 'microbit', @@ -19,9 +23,13 @@ export const microbit = { pinIoCategory, pwmCategory, adcCategory, + sensorsCategory, timeCategory, microbitDisplayCategory, + superbitCategory, soundCategory({ hasSpeaker: true }), serialPrintCategory, + neopixelCategory, + randomCategory, ], }; diff --git a/src/devices/rp2040.js b/src/devices/rp2040.js index bb70857..1b6b66b 100644 --- a/src/devices/rp2040.js +++ b/src/devices/rp2040.js @@ -1,10 +1,13 @@ import { pinIoCategory } from '../blocks/categories/pinIo.js'; import { pwmCategory } from '../blocks/categories/pwm.js'; import { adcCategory } from '../blocks/categories/adc.js'; +import { sensorsCategory } from '../blocks/categories/sensors.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'; +import { neopixelCategory } from '../blocks/categories/neopixel.js'; +import { randomCategory } from '../blocks/categories/random.js'; export const rp2040 = { id: 'rp2040', @@ -19,9 +22,12 @@ export const rp2040 = { pinIoCategory, pwmCategory, adcCategory, + sensorsCategory, timeCategory, i2cCategory, soundCategory(), serialPrintCategory, + neopixelCategory, + randomCategory, ], }; diff --git a/src/main.js b/src/main.js index 1e6e8f3..02afda2 100644 --- a/src/main.js +++ b/src/main.js @@ -105,6 +105,28 @@ const btnSave = document.getElementById('btn-save'); const btnProjects = document.getElementById('btn-projects'); const statusEl = document.getElementById('connection-status'); const terminalInput = document.getElementById('terminal-input'); +const sendOverlayEl = document.getElementById('send-overlay'); +const sendModalTitle = document.getElementById('send-modal-title'); +const sendProgressFill = document.getElementById('send-progress-fill'); +const sendProgressText = document.getElementById('send-progress-text'); + +function showSendProgress(title = 'Sending code to device') { + sendModalTitle.textContent = title; + sendProgressFill.style.width = '0%'; + sendProgressText.textContent = '0%'; + sendOverlayEl.classList.remove('hidden'); +} + +function updateSendProgress(sent, total) { + const pct = total > 0 ? Math.round((sent / total) * 100) : 0; + sendProgressFill.style.width = pct + '%'; + sendProgressText.textContent = pct + '%'; +} + +function hideSendProgress() { + sendOverlayEl.classList.add('hidden'); + sendProgressFill.style.width = '0%'; +} // Sync device dropdown with stored device deviceSelect.value = getDeviceId(); @@ -298,10 +320,15 @@ btnRun.addEventListener('click', async () => { return; } appendToTerminal('\n>>> Running...\n'); + showSendProgress('Sending code to device'); try { - await executeCode(code); + await executeCode(code, { + onProgress: (sent, total) => updateSendProgress(sent, total), + }); } catch (err) { appendToTerminal(`\nRun error: ${err.message}\n`); + } finally { + hideSendProgress(); } }); @@ -321,10 +348,15 @@ btnSave.addEventListener('click', async () => { return; } appendToTerminal('\nSaving to device as main.py...\n'); + showSendProgress('Saving to device'); try { - await saveToDevice(code); + await saveToDevice(code, 'main.py', { + onProgress: (sent, total) => updateSendProgress(sent, total), + }); } catch (err) { appendToTerminal(`\nSave error: ${err.message}\n`); + } finally { + hideSendProgress(); } }); diff --git a/src/serial/connection.js b/src/serial/connection.js index ca7e279..1f3b388 100644 --- a/src/serial/connection.js +++ b/src/serial/connection.js @@ -47,6 +47,21 @@ export async function writeString(str) { if (writer) await writer.write(str); } +/** Send string in small chunks with delays to avoid overflowing device UART buffer (e.g. micro:bit). + * Optional onProgress(sentBytes, totalBytes) is called after each chunk. */ +export async function writeStringChunked(str, chunkSize = 32, delayMs = 25, onProgress = null) { + if (!writer) return; + const total = str.length; + for (let i = 0; i < str.length; i += chunkSize) { + const chunk = str.slice(i, i + chunkSize); + await writer.write(chunk); + if (onProgress) onProgress(Math.min(i + chunk.length, total), total); + if (i + chunkSize < str.length && delayMs > 0) { + await new Promise((r) => setTimeout(r, delayMs)); + } + } +} + const listeners = new Set(); export function onData(callback) { diff --git a/src/serial/repl.js b/src/serial/repl.js index 5fc644a..66d45f8 100644 --- a/src/serial/repl.js +++ b/src/serial/repl.js @@ -1,4 +1,4 @@ -import { writeString } from './connection.js'; +import { writeString, writeStringChunked } from './connection.js'; export async function enterRawRepl() { await writeString('\x03\x03'); @@ -7,9 +7,10 @@ export async function enterRawRepl() { await sleep(100); } -export async function executeCode(code) { +export async function executeCode(code, options = {}) { + const { onProgress } = options; await enterRawRepl(); - await writeString(code); + await writeStringChunked(code, 32, 25, onProgress || null); await writeString('\x04'); } @@ -17,14 +18,14 @@ export async function stopExecution() { await writeString('\x03\x03'); } -export async function saveToDevice(code, filename = 'main.py') { +export async function saveToDevice(code, filename = 'main.py', options = {}) { const script = [ `f = open('${filename}', 'w')`, `f.write(${JSON.stringify(code)})`, `f.close()`, `print('Saved to ${filename}')`, ].join('\n'); - await executeCode(script); + await executeCode(script, options); } export async function writeFileToDevice(content, filename) { diff --git a/src/style.css b/src/style.css index b7e5f0a..9d2ceee 100644 --- a/src/style.css +++ b/src/style.css @@ -600,3 +600,61 @@ html, body { color: var(--bg-toolbar); border-color: var(--accent); } + +/* --- Send code progress modal (Run / Save) --- */ +#send-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); +} + +#send-overlay.hidden { + display: none !important; +} + +#send-modal { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 28px 40px; + width: 360px; + max-width: 90vw; + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; +} + +#send-modal h3 { + margin: 0; + color: var(--accent); + font-size: 16px; + text-align: center; +} + +#send-modal #send-progress-bar { + width: 100%; + height: 10px; + background: var(--bg-secondary); + border-radius: 5px; + overflow: hidden; +} + +#send-modal #send-progress-fill { + height: 100%; + width: 0%; + background: var(--accent); + border-radius: 5px; + transition: width 0.15s ease; +} + +#send-modal #send-progress-text { + font-size: 14px; + color: var(--text-secondary); + font-weight: 600; +}