improved microbit support, added sonar and yahboom i2c motors

main
Jake 2026-02-24 17:52:12 +08:00
parent 3a040552bb
commit 432f28ba1e
16 changed files with 537 additions and 25 deletions

View File

@ -135,6 +135,17 @@
</div>
</div>
<!-- Send code progress modal (Run / Save) -->
<div id="send-overlay" class="hidden">
<div id="send-modal">
<h3 id="send-modal-title">Sending code to device</h3>
<div id="send-progress-bar">
<div id="send-progress-fill"></div>
</div>
<span id="send-progress-text">0%</span>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

Binary file not shown.

View File

@ -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' },
],
};

View File

@ -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 } } } },
},
],
};

View File

@ -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 } } },
},
},
],
};

View File

@ -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 } } },
},
},
],
};

View File

@ -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' },
],
};

View File

@ -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 0255)');
},
};
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.');
},
};

View File

@ -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';
};

View File

@ -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,
],
};

View File

@ -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,
],
};

View File

@ -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,
],
};

View File

@ -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();
}
});

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}