arduino stk500 uploader and serial monitor implemented, need server side compiling
parent
952ea2b721
commit
18ea585320
26
index.html
26
index.html
|
|
@ -16,6 +16,7 @@
|
||||||
<option value="esp32s3">ESP32-S3</option>
|
<option value="esp32s3">ESP32-S3</option>
|
||||||
<option value="microbit">micro:bit</option>
|
<option value="microbit">micro:bit</option>
|
||||||
<option value="rp2040">RP2040 (Pico)</option>
|
<option value="rp2040">RP2040 (Pico)</option>
|
||||||
|
<option value="arduino_uno">Arduino Uno/Nano</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
|
|
@ -197,6 +198,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload .hex overlay (Arduino STK500) -->
|
||||||
|
<div id="hex-upload-overlay" class="hidden">
|
||||||
|
<div id="hex-upload-modal">
|
||||||
|
<div class="board-select-header">
|
||||||
|
<h3>Upload .hex to Arduino</h3>
|
||||||
|
<button id="hex-upload-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="board-select-description">Select your board type, choose a compiled <code>.hex</code> file, then click Upload. The browser will prompt you to pick a serial port.</p>
|
||||||
|
<div class="hex-upload-fields">
|
||||||
|
<label class="hex-field-label" for="hex-board-select">Board</label>
|
||||||
|
<select id="hex-board-select">
|
||||||
|
<option value="uno">Arduino Uno (115200 baud)</option>
|
||||||
|
<option value="nano">Arduino Nano — old bootloader (57600)</option>
|
||||||
|
<option value="nano_new">Arduino Nano — new bootloader (115200)</option>
|
||||||
|
</select>
|
||||||
|
<label class="hex-field-label" for="hex-file-input">.hex file</label>
|
||||||
|
<input type="file" id="hex-file-input" accept=".hex" />
|
||||||
|
</div>
|
||||||
|
<div class="board-select-actions">
|
||||||
|
<button id="hex-upload-btn">Upload</button>
|
||||||
|
</div>
|
||||||
|
<div id="hex-upload-status" class="hex-upload-status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"server": "node server/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blockly": "^11.2.1",
|
"blockly": "^11.2.1",
|
||||||
"esptool-js": "^0.5.0"
|
"esptool-js": "^0.5.0",
|
||||||
|
"express": "^5.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.1.0"
|
"vite": "^6.1.0"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CLI = process.env.ARDUINO_CLI || 'arduino-cli';
|
||||||
|
|
||||||
|
function run(args, timeout = 120_000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile(CLI, args, { timeout }, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
const msg = stderr?.trim() || stdout?.trim() || err.message;
|
||||||
|
reject(new Error(msg));
|
||||||
|
} else {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSketchDir(code) {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'arduino-sketch-'));
|
||||||
|
const sketchDir = join(dir, 'sketch');
|
||||||
|
const { mkdir } = await import('node:fs/promises');
|
||||||
|
await mkdir(sketchDir, { recursive: true });
|
||||||
|
await writeFile(join(sketchDir, 'sketch.ino'), code, 'utf-8');
|
||||||
|
return { dir, sketchDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupDir(dir) {
|
||||||
|
try {
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Routes ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
app.get('/api/arduino/status', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await run(['version', '--format', 'json']);
|
||||||
|
const info = JSON.parse(stdout);
|
||||||
|
res.json({ ok: true, version: info.VersionString || info.version || stdout.trim() });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(503).json({ ok: false, error: `arduino-cli not available: ${err.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/arduino/boards', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await run(['board', 'list', '--format', 'json']);
|
||||||
|
const raw = JSON.parse(stdout);
|
||||||
|
|
||||||
|
// arduino-cli >=0.35 returns { detected_ports: [...] }
|
||||||
|
// older versions return a flat array
|
||||||
|
const ports = Array.isArray(raw) ? raw : (raw.detected_ports || []);
|
||||||
|
|
||||||
|
const boards = ports
|
||||||
|
.filter(p => p.port)
|
||||||
|
.map(entry => {
|
||||||
|
const port = entry.port?.address || entry.port?.label || entry.address || '';
|
||||||
|
const matchingBoards = entry.matching_boards || entry.boards || [];
|
||||||
|
const first = matchingBoards[0] || {};
|
||||||
|
return {
|
||||||
|
port,
|
||||||
|
name: first.name || entry.port?.protocol_label || 'Unknown',
|
||||||
|
fqbn: first.fqbn || '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(b => b.port);
|
||||||
|
|
||||||
|
res.json(boards);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/arduino/compile', async (req, res) => {
|
||||||
|
const { code } = req.body;
|
||||||
|
if (!code) return res.status(400).json({ error: 'No code provided' });
|
||||||
|
|
||||||
|
let dir;
|
||||||
|
try {
|
||||||
|
({ dir } = await writeSketchDir(code));
|
||||||
|
const sketchDir = join(dir, 'sketch');
|
||||||
|
const { stdout, stderr } = await run([
|
||||||
|
'compile',
|
||||||
|
'--fqbn', req.body.fqbn || 'arduino:avr:uno',
|
||||||
|
sketchDir,
|
||||||
|
]);
|
||||||
|
res.json({ ok: true, output: stdout + stderr });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ ok: false, error: err.message });
|
||||||
|
} finally {
|
||||||
|
if (dir) cleanupDir(dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/arduino/upload', async (req, res) => {
|
||||||
|
const { code, port, fqbn } = req.body;
|
||||||
|
if (!code) return res.status(400).json({ error: 'No code provided' });
|
||||||
|
if (!port) return res.status(400).json({ error: 'No port specified' });
|
||||||
|
|
||||||
|
let dir;
|
||||||
|
try {
|
||||||
|
({ dir } = await writeSketchDir(code));
|
||||||
|
const sketchDir = join(dir, 'sketch');
|
||||||
|
const { stdout, stderr } = await run([
|
||||||
|
'compile',
|
||||||
|
'--upload',
|
||||||
|
'--fqbn', fqbn || 'arduino:avr:uno',
|
||||||
|
'--port', port,
|
||||||
|
sketchDir,
|
||||||
|
]);
|
||||||
|
res.json({ ok: true, output: stdout + stderr });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ ok: false, error: err.message });
|
||||||
|
} finally {
|
||||||
|
if (dir) cleanupDir(dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Start ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Arduino CLI server listening on http://localhost:${PORT}`);
|
||||||
|
console.log(`Using CLI: ${CLI}`);
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import * as Blockly from 'blockly';
|
import * as Blockly from 'blockly';
|
||||||
import { pythonGenerator } from 'blockly/python';
|
import { pythonGenerator } from 'blockly/python';
|
||||||
|
import { arduinoGenerator } from '../generators/arduino.js';
|
||||||
import {
|
import {
|
||||||
registerAddonCategories,
|
registerAddonCategories,
|
||||||
clearAddonCategories,
|
clearAddonCategories,
|
||||||
|
|
@ -44,6 +45,7 @@ function makeAddonApi() {
|
||||||
return {
|
return {
|
||||||
Blockly,
|
Blockly,
|
||||||
pythonGenerator,
|
pythonGenerator,
|
||||||
|
arduinoGenerator,
|
||||||
getDeviceId,
|
getDeviceId,
|
||||||
registerCategories(categories) {
|
registerCategories(categories) {
|
||||||
registerAddonCategories(categories);
|
registerAddonCategories(categories);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
const BASE = '/api/arduino';
|
||||||
|
|
||||||
|
export async function checkArduinoCli() {
|
||||||
|
const res = await fetch(`${BASE}/status`);
|
||||||
|
if (!res.ok) throw new Error('Arduino CLI server not reachable');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listBoards() {
|
||||||
|
const res = await fetch(`${BASE}/boards`);
|
||||||
|
if (!res.ok) throw new Error('Failed to list boards');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileAndUpload(code, port, fqbn) {
|
||||||
|
const res = await fetch(`${BASE}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ code, port, fqbn }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.error || 'Compile/upload failed');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileOnly(code) {
|
||||||
|
const res = await fetch(`${BASE}/compile`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.error || 'Compilation failed');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
// STK500v1 programmer for Arduino Uno/Nano via WebSerial.
|
||||||
|
// Implements the subset of the protocol needed to upload Intel HEX firmware
|
||||||
|
// through the Optiboot bootloader (115200 baud) or older bootloaders (57600).
|
||||||
|
|
||||||
|
// ─── STK500 constants ────────────────────────────────────
|
||||||
|
|
||||||
|
const Cmnd_STK_GET_SYNC = 0x30;
|
||||||
|
const Cmnd_STK_SET_DEVICE = 0x42;
|
||||||
|
const Cmnd_STK_ENTER_PROGMODE = 0x50;
|
||||||
|
const Cmnd_STK_LEAVE_PROGMODE = 0x51;
|
||||||
|
const Cmnd_STK_LOAD_ADDRESS = 0x55;
|
||||||
|
const Cmnd_STK_PROG_PAGE = 0x64;
|
||||||
|
const Cmnd_STK_READ_PAGE = 0x74;
|
||||||
|
const Cmnd_STK_READ_SIGN = 0x75;
|
||||||
|
const Sync_CRC_EOP = 0x20;
|
||||||
|
const Resp_STK_INSYNC = 0x14;
|
||||||
|
const Resp_STK_OK = 0x10;
|
||||||
|
|
||||||
|
// ─── Board profiles ─────────────────────────────────────
|
||||||
|
|
||||||
|
export const BOARDS = {
|
||||||
|
uno: {
|
||||||
|
name: 'Arduino Uno',
|
||||||
|
baudRate: 115200,
|
||||||
|
signature: new Uint8Array([0x1e, 0x95, 0x0f]),
|
||||||
|
pageSize: 128,
|
||||||
|
timeout: 400,
|
||||||
|
},
|
||||||
|
nano: {
|
||||||
|
name: 'Arduino Nano',
|
||||||
|
baudRate: 57600,
|
||||||
|
signature: new Uint8Array([0x1e, 0x95, 0x0f]),
|
||||||
|
pageSize: 128,
|
||||||
|
timeout: 400,
|
||||||
|
},
|
||||||
|
nano_new: {
|
||||||
|
name: 'Arduino Nano (new bootloader)',
|
||||||
|
baudRate: 115200,
|
||||||
|
signature: new Uint8Array([0x1e, 0x95, 0x0f]),
|
||||||
|
pageSize: 128,
|
||||||
|
timeout: 400,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Intel HEX parser ───────────────────────────────────
|
||||||
|
|
||||||
|
export function parseHex(hexString) {
|
||||||
|
const lines = hexString.split(/\r?\n/).filter(l => l.startsWith(':'));
|
||||||
|
let baseAddress = 0;
|
||||||
|
let maxAddress = 0;
|
||||||
|
const segments = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const byteCount = parseInt(line.substring(1, 3), 16);
|
||||||
|
const address = parseInt(line.substring(3, 7), 16);
|
||||||
|
const recordType = parseInt(line.substring(7, 9), 16);
|
||||||
|
|
||||||
|
if (recordType === 0x00) {
|
||||||
|
const data = new Uint8Array(byteCount);
|
||||||
|
for (let i = 0; i < byteCount; i++) {
|
||||||
|
data[i] = parseInt(line.substring(9 + i * 2, 11 + i * 2), 16);
|
||||||
|
}
|
||||||
|
const absAddress = baseAddress + address;
|
||||||
|
segments.push({ address: absAddress, data });
|
||||||
|
const end = absAddress + byteCount;
|
||||||
|
if (end > maxAddress) maxAddress = end;
|
||||||
|
} else if (recordType === 0x02) {
|
||||||
|
baseAddress = parseInt(line.substring(9, 13), 16) << 4;
|
||||||
|
} else if (recordType === 0x04) {
|
||||||
|
baseAddress = parseInt(line.substring(9, 13), 16) << 16;
|
||||||
|
} else if (recordType === 0x01) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new Uint8Array(maxAddress);
|
||||||
|
buffer.fill(0xff);
|
||||||
|
for (const seg of segments) {
|
||||||
|
buffer.set(seg.data, seg.address);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Low-level serial helpers ───────────────────────────
|
||||||
|
|
||||||
|
async function serialWrite(writer, data) {
|
||||||
|
if (data instanceof Array) data = new Uint8Array(data);
|
||||||
|
await writer.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffered reader: WebSerial delivers variable-size chunks, so we need to
|
||||||
|
// accumulate bytes and serve exact-length reads from the buffer.
|
||||||
|
function createBufferedReader(rawReader) {
|
||||||
|
let buffer = [];
|
||||||
|
let pending = null; // { needed, resolve, reject, timer }
|
||||||
|
|
||||||
|
const pump = async () => {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await rawReader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (value) {
|
||||||
|
for (let i = 0; i < value.length; i++) buffer.push(value[i]);
|
||||||
|
checkPending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// port closed / cancelled
|
||||||
|
if (pending) pending.reject(new Error('Serial read cancelled'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkPending() {
|
||||||
|
if (pending && buffer.length >= pending.needed) {
|
||||||
|
clearTimeout(pending.timer);
|
||||||
|
const bytes = new Uint8Array(buffer.splice(0, pending.needed));
|
||||||
|
const p = pending;
|
||||||
|
pending = null;
|
||||||
|
p.resolve(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pump();
|
||||||
|
|
||||||
|
return {
|
||||||
|
read(length, timeout) {
|
||||||
|
// If we already have enough buffered, return immediately
|
||||||
|
if (buffer.length >= length) {
|
||||||
|
return Promise.resolve(new Uint8Array(buffer.splice(0, length)));
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
pending = null;
|
||||||
|
reject(new Error(`Read timeout (expected ${length} bytes, got ${buffer.length})`));
|
||||||
|
}, timeout);
|
||||||
|
pending = { needed: length, resolve, reject, timer };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
drain() {
|
||||||
|
buffer.length = 0;
|
||||||
|
},
|
||||||
|
get rawReader() { return rawReader; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── STK500 protocol steps ──────────────────────────────
|
||||||
|
|
||||||
|
async function sync(writer, reader, timeout) {
|
||||||
|
const cmd = new Uint8Array([Cmnd_STK_GET_SYNC, Sync_CRC_EOP]);
|
||||||
|
for (let attempt = 0; attempt < 10; attempt++) {
|
||||||
|
reader.drain();
|
||||||
|
try {
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] === Resp_STK_INSYNC && resp[1] === Resp_STK_OK) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// retry
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 30));
|
||||||
|
}
|
||||||
|
throw new Error('Failed to sync with bootloader after 10 attempts');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSignature(writer, reader, timeout) {
|
||||||
|
const cmd = new Uint8Array([Cmnd_STK_READ_SIGN, Sync_CRC_EOP]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(5, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[4] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to read device signature');
|
||||||
|
}
|
||||||
|
return resp.slice(1, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDeviceParameters(writer, reader, pageSize, timeout) {
|
||||||
|
const cmd = new Uint8Array([
|
||||||
|
Cmnd_STK_SET_DEVICE,
|
||||||
|
0x86, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03,
|
||||||
|
0xff, 0xff, 0xff, 0xff,
|
||||||
|
(pageSize >> 8) & 0xff, pageSize & 0xff,
|
||||||
|
0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x00,
|
||||||
|
Sync_CRC_EOP,
|
||||||
|
]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to set device parameters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterProgMode(writer, reader, timeout) {
|
||||||
|
const cmd = new Uint8Array([Cmnd_STK_ENTER_PROGMODE, Sync_CRC_EOP]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to enter programming mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveProgMode(writer, reader, timeout) {
|
||||||
|
const cmd = new Uint8Array([Cmnd_STK_LEAVE_PROGMODE, Sync_CRC_EOP]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to leave programming mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAddress(writer, reader, addr, timeout) {
|
||||||
|
const cmd = new Uint8Array([
|
||||||
|
Cmnd_STK_LOAD_ADDRESS,
|
||||||
|
addr & 0xff,
|
||||||
|
(addr >> 8) & 0xff,
|
||||||
|
Sync_CRC_EOP,
|
||||||
|
]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to load address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function programPage(writer, reader, data, timeout) {
|
||||||
|
const sizeHigh = (data.length >> 8) & 0xff;
|
||||||
|
const sizeLow = data.length & 0xff;
|
||||||
|
const cmd = new Uint8Array([
|
||||||
|
Cmnd_STK_PROG_PAGE,
|
||||||
|
sizeHigh, sizeLow,
|
||||||
|
0x46, // 'F' for flash memory
|
||||||
|
...data,
|
||||||
|
Sync_CRC_EOP,
|
||||||
|
]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to program page');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPage(writer, reader, size, timeout) {
|
||||||
|
const cmd = new Uint8Array([
|
||||||
|
Cmnd_STK_READ_PAGE,
|
||||||
|
(size >> 8) & 0xff,
|
||||||
|
size & 0xff,
|
||||||
|
0x46, // 'F' for flash
|
||||||
|
Sync_CRC_EOP,
|
||||||
|
]);
|
||||||
|
await serialWrite(writer, cmd);
|
||||||
|
const resp = await reader.read(size + 2, timeout);
|
||||||
|
if (resp[0] !== Resp_STK_INSYNC || resp[resp.length - 1] !== Resp_STK_OK) {
|
||||||
|
throw new Error('Failed to read page');
|
||||||
|
}
|
||||||
|
return resp.slice(1, 1 + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DTR reset ──────────────────────────────────────────
|
||||||
|
// Arduino boards reset when DTR transitions HIGH → LOW. The RC circuit
|
||||||
|
// (100nF cap + 10k pull-up) converts the falling edge into a brief LOW
|
||||||
|
// pulse on the RESET pin. We hold DTR HIGH first to guarantee a clean
|
||||||
|
// falling edge, then wait for the bootloader to start (~100-250ms).
|
||||||
|
|
||||||
|
async function resetBoard(port) {
|
||||||
|
await port.setSignals({ dataTerminalReady: false, requestToSend: false });
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
await port.setSignals({ dataTerminalReady: true, requestToSend: true });
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
await port.setSignals({ dataTerminalReady: false, requestToSend: false });
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main upload function ───────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload an Intel HEX file to an Arduino via WebSerial.
|
||||||
|
*
|
||||||
|
* @param {string} hexString - The Intel HEX file contents as a string
|
||||||
|
* @param {object} [options] - Options
|
||||||
|
* @param {string} [options.board='uno'] - Board key from BOARDS
|
||||||
|
* @param {function} [options.onProgress] - Callback: (status: string, percent: number) => void
|
||||||
|
* @param {SerialPort} [options.port] - An already-opened SerialPort. If not provided, will prompt user.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function uploadHex(hexString, options = {}) {
|
||||||
|
const boardKey = options.board || 'uno';
|
||||||
|
const board = typeof boardKey === 'object' ? boardKey : BOARDS[boardKey];
|
||||||
|
if (!board) throw new Error(`Unknown board: ${boardKey}`);
|
||||||
|
const onProgress = options.onProgress || (() => {});
|
||||||
|
|
||||||
|
const hexData = parseHex(hexString);
|
||||||
|
if (hexData.length === 0) throw new Error('Hex file is empty');
|
||||||
|
|
||||||
|
onProgress('Connecting...', 0);
|
||||||
|
|
||||||
|
// Open a raw binary serial port (separate from the REPL text connection)
|
||||||
|
let port = options.port || null;
|
||||||
|
let weOpened = false;
|
||||||
|
|
||||||
|
if (!port) {
|
||||||
|
port = await navigator.serial.requestPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close if already open, then open fresh with correct baud rate
|
||||||
|
try { await port.close(); } catch { /* not open */ }
|
||||||
|
await port.open({ baudRate: board.baudRate });
|
||||||
|
weOpened = true;
|
||||||
|
|
||||||
|
let writer, rawReader, reader;
|
||||||
|
try {
|
||||||
|
writer = port.writable.getWriter();
|
||||||
|
rawReader = port.readable.getReader();
|
||||||
|
reader = createBufferedReader(rawReader);
|
||||||
|
|
||||||
|
onProgress('Resetting board...', 5);
|
||||||
|
await resetBoard(port);
|
||||||
|
|
||||||
|
// Start syncing immediately — don't waste time draining
|
||||||
|
reader.drain();
|
||||||
|
|
||||||
|
onProgress('Syncing with bootloader...', 10);
|
||||||
|
await sync(writer, reader, board.timeout);
|
||||||
|
|
||||||
|
onProgress('Reading signature...', 20);
|
||||||
|
const sig = await readSignature(writer, reader, board.timeout);
|
||||||
|
if (board.signature[0] !== sig[0] || board.signature[1] !== sig[1] || board.signature[2] !== sig[2]) {
|
||||||
|
const got = Array.from(sig).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ');
|
||||||
|
const expected = Array.from(board.signature).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ');
|
||||||
|
throw new Error(`Signature mismatch: got ${got}, expected ${expected}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress('Setting device parameters...', 25);
|
||||||
|
await setDeviceParameters(writer, reader, board.pageSize, board.timeout);
|
||||||
|
|
||||||
|
onProgress('Entering programming mode...', 30);
|
||||||
|
await enterProgMode(writer, reader, board.timeout);
|
||||||
|
|
||||||
|
// Program pages
|
||||||
|
const totalPages = Math.ceil(hexData.length / board.pageSize);
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const addr = (page * board.pageSize) >> 1; // word address
|
||||||
|
const chunk = hexData.subarray(
|
||||||
|
page * board.pageSize,
|
||||||
|
Math.min((page + 1) * board.pageSize, hexData.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
await loadAddress(writer, reader, addr, board.timeout);
|
||||||
|
await programPage(writer, reader, chunk, board.timeout);
|
||||||
|
|
||||||
|
const pct = 35 + Math.round((page / totalPages) * 40);
|
||||||
|
onProgress(`Writing page ${page + 1}/${totalPages}...`, pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify pages
|
||||||
|
onProgress('Verifying...', 75);
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const addr = (page * board.pageSize) >> 1;
|
||||||
|
const chunk = hexData.subarray(
|
||||||
|
page * board.pageSize,
|
||||||
|
Math.min((page + 1) * board.pageSize, hexData.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
await loadAddress(writer, reader, addr, board.timeout);
|
||||||
|
const readBack = await readPage(writer, reader, chunk.length, board.timeout);
|
||||||
|
|
||||||
|
for (let i = 0; i < chunk.length; i++) {
|
||||||
|
if (chunk[i] !== readBack[i]) {
|
||||||
|
throw new Error(`Verification failed at byte ${page * board.pageSize + i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pct = 75 + Math.round((page / totalPages) * 20);
|
||||||
|
onProgress(`Verifying page ${page + 1}/${totalPages}...`, pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress('Finishing...', 97);
|
||||||
|
await leaveProgMode(writer, reader, board.timeout);
|
||||||
|
|
||||||
|
onProgress('Upload complete!', 100);
|
||||||
|
} finally {
|
||||||
|
try { rawReader.releaseLock(); } catch { /* */ }
|
||||||
|
try { writer.releaseLock(); } catch { /* */ }
|
||||||
|
if (weOpened) {
|
||||||
|
try { await port.close(); } catch { /* */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
import { arduinoGenerator, Order } from '../generators/arduino.js';
|
||||||
|
|
||||||
|
// ─── Pin I/O ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pin_set_mode'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const mode = block.getFieldValue('MODE');
|
||||||
|
const modeMap = { OUT: 'OUTPUT', IN: 'INPUT', IN_PULLUP: 'INPUT_PULLUP' };
|
||||||
|
arduinoGenerator.addSetupCode(`pinMode(${pin}, ${modeMap[mode] || 'OUTPUT'});`);
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pin_digital_write'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const value = block.getFieldValue('VALUE');
|
||||||
|
return `digitalWrite(${pin}, ${value === '1' ? 'HIGH' : 'LOW'});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pin_digital_read'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
return [`digitalRead(${pin})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── PWM ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pwm_init'] = function () {
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pwm_set_duty'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const duty = arduinoGenerator.valueToCode(block, 'DUTY', Order.NONE) || '0';
|
||||||
|
return `analogWrite(${pin}, ${duty});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['pwm_set_freq'] = function () {
|
||||||
|
return '// PWM frequency change not supported on AVR\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── ADC ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['adc_read'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
return [`analogRead(${pin})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sensors (sonar / HC-SR04) ────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sonar_distance'] = function (block) {
|
||||||
|
const trig = arduinoGenerator.valueToCode(block, 'TRIG', Order.NONE) || '2';
|
||||||
|
const echo = arduinoGenerator.valueToCode(block, 'ECHO', Order.NONE) || '3';
|
||||||
|
|
||||||
|
arduinoGenerator.addDeclaration('sonar_helper',
|
||||||
|
'float _sonar_cm(int trigPin, int echoPin) {\n' +
|
||||||
|
' pinMode(trigPin, OUTPUT);\n' +
|
||||||
|
' pinMode(echoPin, INPUT);\n' +
|
||||||
|
' digitalWrite(trigPin, LOW);\n' +
|
||||||
|
' delayMicroseconds(2);\n' +
|
||||||
|
' digitalWrite(trigPin, HIGH);\n' +
|
||||||
|
' delayMicroseconds(10);\n' +
|
||||||
|
' digitalWrite(trigPin, LOW);\n' +
|
||||||
|
' long d = pulseIn(echoPin, HIGH, 30000);\n' +
|
||||||
|
' return d > 0 ? d / 58.0 : -1;\n' +
|
||||||
|
'}'
|
||||||
|
);
|
||||||
|
|
||||||
|
return [`_sonar_cm(${trig}, ${echo})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Time ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sleep_seconds'] = function (block) {
|
||||||
|
const seconds = arduinoGenerator.valueToCode(block, 'SECONDS', Order.MULTIPLICATIVE) || '1';
|
||||||
|
return `delay(${seconds} * 1000);\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sleep_ms'] = function (block) {
|
||||||
|
const ms = arduinoGenerator.valueToCode(block, 'MS', Order.NONE) || '100';
|
||||||
|
return `delay(${ms});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['ticks_ms'] = function () {
|
||||||
|
return ['millis()', Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Random ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['random_int'] = function (block) {
|
||||||
|
const from_ = arduinoGenerator.valueToCode(block, 'FROM', Order.NONE) || '0';
|
||||||
|
const to = arduinoGenerator.valueToCode(block, 'TO', Order.ADDITIVE) || '10';
|
||||||
|
return [`random(${from_}, ${to} + 1)`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['random_float'] = function () {
|
||||||
|
return ['(random(10001) / 10000.0)', Order.MULTIPLICATIVE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['random_uniform'] = function (block) {
|
||||||
|
const low = arduinoGenerator.valueToCode(block, 'LOW', Order.NONE) || '0';
|
||||||
|
const high = arduinoGenerator.valueToCode(block, 'HIGH', Order.NONE) || '1';
|
||||||
|
return [`(${low} + (random(10001) / 10000.0) * (${high} - ${low}))`, Order.ADDITIVE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['random_seed'] = function (block) {
|
||||||
|
const seed = arduinoGenerator.valueToCode(block, 'SEED', Order.NONE) || '0';
|
||||||
|
return `randomSeed(${seed});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Print ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['print_text'] = function (block) {
|
||||||
|
arduinoGenerator.addSetupCode('Serial.begin(9600);');
|
||||||
|
const text = arduinoGenerator.valueToCode(block, 'TEXT', Order.NONE) || '""';
|
||||||
|
return `Serial.println(${text});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sound (shared blocks) ────────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sound_play_tone'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const freq = arduinoGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
|
||||||
|
const duration = arduinoGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
|
||||||
|
return `tone(${pin}, ${freq}, ${duration});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sound_stop'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
return `noTone(${pin});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sound_play_tone_speaker'] = function () {
|
||||||
|
return '// Built-in speaker not available on Arduino\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['sound_stop_speaker'] = function () {
|
||||||
|
return '// Built-in speaker not available on Arduino\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Arduino-specific blocks ──────────────────────────────
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_builtin_led'] = function (block) {
|
||||||
|
const state = block.getFieldValue('STATE');
|
||||||
|
arduinoGenerator.addSetupCode('pinMode(LED_BUILTIN, OUTPUT);');
|
||||||
|
return `digitalWrite(LED_BUILTIN, ${state === '1' ? 'HIGH' : 'LOW'});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_analog_write'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const value = arduinoGenerator.valueToCode(block, 'VALUE', Order.NONE) || '0';
|
||||||
|
return `analogWrite(${pin}, ${value});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_analog_read'] = function (block) {
|
||||||
|
const ch = block.getFieldValue('CHANNEL');
|
||||||
|
return [`analogRead(A${ch})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_servo_attach'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
arduinoGenerator.addInclude('include_servo', '#include <Servo.h>');
|
||||||
|
arduinoGenerator.addDeclaration(`servo_decl_${pin}`, `Servo servo_${pin};`);
|
||||||
|
arduinoGenerator.addSetupCode(`servo_${pin}.attach(${pin});`);
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_servo_write'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const angle = arduinoGenerator.valueToCode(block, 'ANGLE', Order.NONE) || '90';
|
||||||
|
arduinoGenerator.addInclude('include_servo', '#include <Servo.h>');
|
||||||
|
arduinoGenerator.addDeclaration(`servo_decl_${pin}`, `Servo servo_${pin};`);
|
||||||
|
arduinoGenerator.addSetupCode(`servo_${pin}.attach(${pin});`);
|
||||||
|
return `servo_${pin}.write(${angle});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_tone'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
const freq = arduinoGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
|
||||||
|
const duration = arduinoGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
|
||||||
|
return `tone(${pin}, ${freq}, ${duration});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_no_tone'] = function (block) {
|
||||||
|
const pin = block.getFieldValue('PIN');
|
||||||
|
return `noTone(${pin});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['arduino_map'] = function (block) {
|
||||||
|
const value = arduinoGenerator.valueToCode(block, 'VALUE', Order.NONE) || '0';
|
||||||
|
const fromLow = arduinoGenerator.valueToCode(block, 'FROM_LOW', Order.NONE) || '0';
|
||||||
|
const fromHigh = arduinoGenerator.valueToCode(block, 'FROM_HIGH', Order.NONE) || '1023';
|
||||||
|
const toLow = arduinoGenerator.valueToCode(block, 'TO_LOW', Order.NONE) || '0';
|
||||||
|
const toHigh = arduinoGenerator.valueToCode(block, 'TO_HIGH', Order.NONE) || '255';
|
||||||
|
return [`map(${value}, ${fromLow}, ${fromHigh}, ${toLow}, ${toHigh})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Blocks not applicable to Arduino ─────────────────────
|
||||||
|
|
||||||
|
const unsupportedBlocks = [
|
||||||
|
'wifi_connect', 'wifi_get_ip',
|
||||||
|
'neopixel_init', 'neopixel_set_color', 'neopixel_show',
|
||||||
|
'colour_rgb', 'tuple_create_3',
|
||||||
|
'i2c_init', 'i2c_scan', 'i2c_writeto', 'i2c_readfrom',
|
||||||
|
'hid_key_press', 'hid_key_down', 'hid_key_up', 'hid_keyboard_type',
|
||||||
|
'hid_mouse_move', 'hid_mouse_click', 'hid_mouse_scroll',
|
||||||
|
'hid_gamepad_button', 'hid_gamepad_axis',
|
||||||
|
'microbit_display_all',
|
||||||
|
'superbit_motor_run', 'superbit_motor_stop_all',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const blockType of unsupportedBlocks) {
|
||||||
|
arduinoGenerator.forBlock[blockType] = function () {
|
||||||
|
return '// Block not supported on Arduino\n';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -672,3 +672,134 @@ Blockly.Blocks['sonar_distance'] = {
|
||||||
this.setTooltip('HC-SR04 style ultrasonic distance in cm. Trigger and echo pin numbers.');
|
this.setTooltip('HC-SR04 style ultrasonic distance in cm. Trigger and echo pin numbers.');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Arduino-specific blocks ──────────────────────────────
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_builtin_led'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('set built-in LED')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['ON', '1'],
|
||||||
|
['OFF', '0'],
|
||||||
|
]), 'STATE');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(230);
|
||||||
|
this.setTooltip('Turn the built-in LED (pin 13) on or off');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_analog_write'] = {
|
||||||
|
init() {
|
||||||
|
this.appendValueInput('VALUE')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('analog write pin')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['3', '3'], ['5', '5'], ['6', '6'],
|
||||||
|
['9', '9'], ['10', '10'], ['11', '11'],
|
||||||
|
]), 'PIN')
|
||||||
|
.appendField('value');
|
||||||
|
this.appendDummyInput().appendField('(0-255)');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(160);
|
||||||
|
this.setTooltip('Write a PWM value (0-255) to a PWM-capable pin');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_analog_read'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('analog read')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['A0', '0'], ['A1', '1'], ['A2', '2'],
|
||||||
|
['A3', '3'], ['A4', '4'], ['A5', '5'],
|
||||||
|
]), 'CHANNEL');
|
||||||
|
this.setOutput(true, 'Number');
|
||||||
|
this.setColour(30);
|
||||||
|
this.setTooltip('Read analog value from an ADC channel (0-1023)');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_servo_attach'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('attach servo on pin')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['9', '9'], ['10', '10'], ['11', '11'],
|
||||||
|
['3', '3'], ['5', '5'], ['6', '6'],
|
||||||
|
]), 'PIN');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(260);
|
||||||
|
this.setTooltip('Attach a servo motor to a PWM pin');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_servo_write'] = {
|
||||||
|
init() {
|
||||||
|
this.appendValueInput('ANGLE')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('set servo on pin')
|
||||||
|
.appendField(new Blockly.FieldDropdown([
|
||||||
|
['9', '9'], ['10', '10'], ['11', '11'],
|
||||||
|
['3', '3'], ['5', '5'], ['6', '6'],
|
||||||
|
]), 'PIN')
|
||||||
|
.appendField('to');
|
||||||
|
this.appendDummyInput().appendField('degrees');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(260);
|
||||||
|
this.setTooltip('Set servo angle (0-180 degrees)');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_tone'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('play tone on pin')
|
||||||
|
.appendField(new Blockly.FieldNumber(8, 0, 13, 1), 'PIN');
|
||||||
|
this.appendValueInput('FREQ')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('freq');
|
||||||
|
this.appendDummyInput().appendField('Hz');
|
||||||
|
this.appendValueInput('DURATION')
|
||||||
|
.setCheck('Number')
|
||||||
|
.appendField('for');
|
||||||
|
this.appendDummyInput().appendField('ms');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Play a tone on a digital pin using PWM');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_no_tone'] = {
|
||||||
|
init() {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('stop tone on pin')
|
||||||
|
.appendField(new Blockly.FieldNumber(8, 0, 13, 1), 'PIN');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setColour(300);
|
||||||
|
this.setTooltip('Stop any tone playing on a digital pin');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['arduino_map'] = {
|
||||||
|
init() {
|
||||||
|
this.appendValueInput('VALUE').setCheck('Number').appendField('map');
|
||||||
|
this.appendValueInput('FROM_LOW').setCheck('Number').appendField('from');
|
||||||
|
this.appendValueInput('FROM_HIGH').setCheck('Number').appendField('-');
|
||||||
|
this.appendValueInput('TO_LOW').setCheck('Number').appendField('to');
|
||||||
|
this.appendValueInput('TO_HIGH').setCheck('Number').appendField('-');
|
||||||
|
this.setInputsInline(true);
|
||||||
|
this.setOutput(true, 'Number');
|
||||||
|
this.setColour(200);
|
||||||
|
this.setTooltip('Re-map a number from one range to another');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { pinIoCategory } from '../blocks/categories/pinIo.js';
|
||||||
|
import { sensorsCategory } from '../blocks/categories/sensors.js';
|
||||||
|
import { timeCategory } from '../blocks/categories/time.js';
|
||||||
|
import { serialPrintCategory } from '../blocks/categories/serialPrint.js';
|
||||||
|
import { randomCategory } from '../blocks/categories/random.js';
|
||||||
|
|
||||||
|
const analogCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Analog',
|
||||||
|
colour: '30',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'arduino_analog_read' },
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'arduino_analog_write',
|
||||||
|
inputs: {
|
||||||
|
VALUE: { shadow: { type: 'math_number', fields: { NUM: 128 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'arduino_map',
|
||||||
|
inputs: {
|
||||||
|
VALUE: { shadow: { type: 'math_number', fields: { NUM: 512 } } },
|
||||||
|
FROM_LOW: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
||||||
|
FROM_HIGH: { shadow: { type: 'math_number', fields: { NUM: 1023 } } },
|
||||||
|
TO_LOW: { shadow: { type: 'math_number', fields: { NUM: 0 } } },
|
||||||
|
TO_HIGH: { shadow: { type: 'math_number', fields: { NUM: 255 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const servoCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Servo',
|
||||||
|
colour: '260',
|
||||||
|
contents: [
|
||||||
|
{ kind: 'block', type: 'arduino_servo_attach' },
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'arduino_servo_write',
|
||||||
|
inputs: {
|
||||||
|
ANGLE: { shadow: { type: 'math_number', fields: { NUM: 90 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const arduinoSoundCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Sound',
|
||||||
|
colour: '300',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
kind: 'block',
|
||||||
|
type: 'arduino_tone',
|
||||||
|
inputs: {
|
||||||
|
FREQ: { shadow: { type: 'math_number', fields: { NUM: 440 } } },
|
||||||
|
DURATION: { shadow: { type: 'math_number', fields: { NUM: 500 } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ kind: 'block', type: 'arduino_no_tone' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const arduinoPinIoCategory = {
|
||||||
|
kind: 'category',
|
||||||
|
name: 'Pin I/O',
|
||||||
|
colour: '230',
|
||||||
|
contents: [
|
||||||
|
...pinIoCategory.contents,
|
||||||
|
{ kind: 'block', type: 'arduino_builtin_led' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const arduinoUno = {
|
||||||
|
id: 'arduino_uno',
|
||||||
|
label: 'Arduino Uno/Nano',
|
||||||
|
language: 'arduino',
|
||||||
|
firmware: null,
|
||||||
|
categories: [
|
||||||
|
arduinoPinIoCategory,
|
||||||
|
analogCategory,
|
||||||
|
servoCategory,
|
||||||
|
arduinoSoundCategory,
|
||||||
|
sensorsCategory,
|
||||||
|
timeCategory,
|
||||||
|
serialPrintCategory,
|
||||||
|
randomCategory,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { esp32s3 } from './esp32s3.js';
|
import { esp32s3 } from './esp32s3.js';
|
||||||
import { microbit } from './microbit.js';
|
import { microbit } from './microbit.js';
|
||||||
import { rp2040 } from './rp2040.js';
|
import { rp2040 } from './rp2040.js';
|
||||||
|
import { arduinoUno } from './arduino_uno.js';
|
||||||
import { builtinCategories } from '../blocks/categories/builtins.js';
|
import { builtinCategories } from '../blocks/categories/builtins.js';
|
||||||
|
|
||||||
const devices = {};
|
const devices = {};
|
||||||
|
|
@ -12,6 +13,7 @@ function register(profile) {
|
||||||
register(esp32s3);
|
register(esp32s3);
|
||||||
register(microbit);
|
register(microbit);
|
||||||
register(rp2040);
|
register(rp2040);
|
||||||
|
register(arduinoUno);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new device at runtime (e.g. a sub-device like "ESP32 robot").
|
* Register a new device at runtime (e.g. a sub-device like "ESP32 robot").
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import * as Blockly from 'blockly';
|
||||||
|
|
||||||
|
// C++ operator precedence (lower number = tighter binding)
|
||||||
|
export const Order = {
|
||||||
|
ATOMIC: 0,
|
||||||
|
MEMBER: 1,
|
||||||
|
FUNCTION_CALL: 2,
|
||||||
|
POSTFIX: 3,
|
||||||
|
UNARY_PREFIX: 4,
|
||||||
|
MULTIPLICATIVE: 5,
|
||||||
|
ADDITIVE: 6,
|
||||||
|
SHIFT: 7,
|
||||||
|
RELATIONAL: 8,
|
||||||
|
EQUALITY: 9,
|
||||||
|
BITWISE_AND: 10,
|
||||||
|
BITWISE_XOR: 11,
|
||||||
|
BITWISE_OR: 12,
|
||||||
|
LOGICAL_AND: 13,
|
||||||
|
LOGICAL_OR: 14,
|
||||||
|
CONDITIONAL: 15,
|
||||||
|
ASSIGNMENT: 16,
|
||||||
|
COMMA: 17,
|
||||||
|
NONE: 99,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArduinoGenerator extends Blockly.CodeGenerator {
|
||||||
|
constructor() {
|
||||||
|
super('Arduino');
|
||||||
|
this.INDENT = ' ';
|
||||||
|
|
||||||
|
this.addReservedWords(
|
||||||
|
'setup,loop,void,int,long,float,double,char,bool,byte,unsigned,' +
|
||||||
|
'String,HIGH,LOW,INPUT,OUTPUT,INPUT_PULLUP,LED_BUILTIN,true,false,' +
|
||||||
|
'Serial,Wire,SPI,Servo,pinMode,digitalWrite,digitalRead,' +
|
||||||
|
'analogWrite,analogRead,delay,delayMicroseconds,millis,micros,' +
|
||||||
|
'tone,noTone,pulseIn,map,constrain,min,max,abs,random,randomSeed,' +
|
||||||
|
'sizeof,return,if,else,for,while,do,switch,case,break,continue,' +
|
||||||
|
'define,include,class,struct,enum,typedef,static,const,volatile'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ORDER_OVERRIDES = [
|
||||||
|
[Order.FUNCTION_CALL, Order.MEMBER],
|
||||||
|
[Order.MEMBER, Order.FUNCTION_CALL],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
init(workspace) {
|
||||||
|
super.init(workspace);
|
||||||
|
this.definitions_ = Object.create(null);
|
||||||
|
this.setupCode_ = [];
|
||||||
|
this.functionDefinitions_ = Object.create(null);
|
||||||
|
if (!this.nameDB_) {
|
||||||
|
this.nameDB_ = new Blockly.Names(this.RESERVED_WORDS_);
|
||||||
|
} else {
|
||||||
|
this.nameDB_.reset();
|
||||||
|
}
|
||||||
|
this.nameDB_.setVariableMap(workspace.getVariableMap());
|
||||||
|
this.nameDB_.populateVariables(workspace);
|
||||||
|
this.nameDB_.populateProcedures(workspace);
|
||||||
|
this.isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(code) {
|
||||||
|
const includes = [];
|
||||||
|
const globals = [];
|
||||||
|
for (const [key, val] of Object.entries(this.definitions_)) {
|
||||||
|
if (val.startsWith('#include')) {
|
||||||
|
includes.push(val);
|
||||||
|
} else {
|
||||||
|
globals.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const funcDefs = Object.values(this.functionDefinitions_);
|
||||||
|
|
||||||
|
const setupBody = this.setupCode_.map(s => this.INDENT + s).join('\n');
|
||||||
|
const indentedCode = code ? this.prefixLines(code, this.INDENT) : '';
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (includes.length) parts.push(includes.join('\n'));
|
||||||
|
if (globals.length) parts.push(globals.join('\n'));
|
||||||
|
if (funcDefs.length) parts.push(funcDefs.join('\n\n'));
|
||||||
|
|
||||||
|
const setupContent = [setupBody, indentedCode].filter(Boolean).join('\n');
|
||||||
|
parts.push(`void setup() {\n${setupContent}\n}`);
|
||||||
|
parts.push('void loop() {\n}');
|
||||||
|
|
||||||
|
super.finish('');
|
||||||
|
this.isInitialized = false;
|
||||||
|
return parts.join('\n\n') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
scrubNakedValue(line) {
|
||||||
|
return line + ';\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
quote_(str) {
|
||||||
|
str = str.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
return '"' + str + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
scrub_(block, code, opt_thisOnly) {
|
||||||
|
let commentCode = '';
|
||||||
|
if (!block.outputConnection || !block.outputConnection.targetBlock()) {
|
||||||
|
let comment = block.getCommentText();
|
||||||
|
if (comment) {
|
||||||
|
comment = Blockly.utils.string.wrap(comment, this.COMMENT_WRAP - 3);
|
||||||
|
commentCode += this.prefixLines(comment + '\n', '// ');
|
||||||
|
}
|
||||||
|
for (let i = 0; i < block.inputList.length; i++) {
|
||||||
|
const input = block.inputList[i];
|
||||||
|
if (input.connection && input.connection.type === 1) {
|
||||||
|
const childBlock = input.connection.targetBlock();
|
||||||
|
if (childBlock) {
|
||||||
|
comment = this.allNestedComments(childBlock);
|
||||||
|
if (comment) {
|
||||||
|
commentCode += this.prefixLines(comment, '// ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||||
|
const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock);
|
||||||
|
return commentCode + code + nextCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariableName(nameOrId) {
|
||||||
|
return this.nameDB_.getName(nameOrId, Blockly.Names.NameType.VARIABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSetupCode(code) {
|
||||||
|
if (this.setupCode_.indexOf(code) === -1) {
|
||||||
|
this.setupCode_.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addInclude(key, code) {
|
||||||
|
this.definitions_[key] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDeclaration(key, code) {
|
||||||
|
this.definitions_[key] = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const arduinoGenerator = new ArduinoGenerator();
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
import * as Blockly from 'blockly';
|
||||||
|
import { arduinoGenerator, Order } from './arduino.js';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Logic
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_if'] = function (block) {
|
||||||
|
let n = 0;
|
||||||
|
let code = '';
|
||||||
|
if (arduinoGenerator.STATEMENT_PREFIX) {
|
||||||
|
code += arduinoGenerator.injectId(arduinoGenerator.STATEMENT_PREFIX, block);
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
const conditionCode = arduinoGenerator.valueToCode(block, 'IF' + n, Order.NONE) || 'false';
|
||||||
|
let branchCode = arduinoGenerator.statementToCode(block, 'DO' + n);
|
||||||
|
if (arduinoGenerator.STATEMENT_SUFFIX) {
|
||||||
|
branchCode = arduinoGenerator.prefixLines(
|
||||||
|
arduinoGenerator.injectId(arduinoGenerator.STATEMENT_SUFFIX, block), arduinoGenerator.INDENT
|
||||||
|
) + branchCode;
|
||||||
|
}
|
||||||
|
code += (n > 0 ? ' else ' : '') + 'if (' + conditionCode + ') {\n' + branchCode + '}';
|
||||||
|
n++;
|
||||||
|
} while (block.getInput('IF' + n));
|
||||||
|
if (block.getInput('ELSE') || block.getInput('ELSE')) {
|
||||||
|
let branchCode = arduinoGenerator.statementToCode(block, 'ELSE');
|
||||||
|
if (arduinoGenerator.STATEMENT_SUFFIX) {
|
||||||
|
branchCode = arduinoGenerator.prefixLines(
|
||||||
|
arduinoGenerator.injectId(arduinoGenerator.STATEMENT_SUFFIX, block), arduinoGenerator.INDENT
|
||||||
|
) + branchCode;
|
||||||
|
}
|
||||||
|
code += ' else {\n' + branchCode + '}';
|
||||||
|
}
|
||||||
|
return code + '\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_compare'] = function (block) {
|
||||||
|
const OPERATORS = {
|
||||||
|
EQ: '==', NEQ: '!=', LT: '<', LTE: '<=', GT: '>', GTE: '>=',
|
||||||
|
};
|
||||||
|
const op = OPERATORS[block.getFieldValue('OP')];
|
||||||
|
const order = (op === '==' || op === '!=') ? Order.EQUALITY : Order.RELATIONAL;
|
||||||
|
const a = arduinoGenerator.valueToCode(block, 'A', order) || '0';
|
||||||
|
const b = arduinoGenerator.valueToCode(block, 'B', order) || '0';
|
||||||
|
return [a + ' ' + op + ' ' + b, order];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_operation'] = function (block) {
|
||||||
|
const op = block.getFieldValue('OP') === 'AND' ? '&&' : '||';
|
||||||
|
const order = op === '&&' ? Order.LOGICAL_AND : Order.LOGICAL_OR;
|
||||||
|
let a = arduinoGenerator.valueToCode(block, 'A', order);
|
||||||
|
let b = arduinoGenerator.valueToCode(block, 'B', order);
|
||||||
|
if (!a && !b) {
|
||||||
|
a = 'false';
|
||||||
|
b = 'false';
|
||||||
|
} else {
|
||||||
|
if (!a) a = 'true';
|
||||||
|
if (!b) b = 'true';
|
||||||
|
}
|
||||||
|
return [a + ' ' + op + ' ' + b, order];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_negate'] = function (block) {
|
||||||
|
const order = Order.UNARY_PREFIX;
|
||||||
|
const arg = arduinoGenerator.valueToCode(block, 'BOOL', order) || 'true';
|
||||||
|
return ['!' + arg, order];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_boolean'] = function (block) {
|
||||||
|
return [block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_null'] = function () {
|
||||||
|
return ['NULL', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['logic_ternary'] = function (block) {
|
||||||
|
const cond = arduinoGenerator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false';
|
||||||
|
const thenVal = arduinoGenerator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'NULL';
|
||||||
|
const elseVal = arduinoGenerator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'NULL';
|
||||||
|
return ['(' + cond + ' ? ' + thenVal + ' : ' + elseVal + ')', Order.CONDITIONAL];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Loops
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_repeat_ext'] = function (block) {
|
||||||
|
let repeats = arduinoGenerator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0';
|
||||||
|
let branch = arduinoGenerator.statementToCode(block, 'DO');
|
||||||
|
branch = arduinoGenerator.addLoopTrap(branch, block);
|
||||||
|
let loopVar = arduinoGenerator.nameDB_.getDistinctName('count', Blockly.Names.NameType.VARIABLE);
|
||||||
|
return `for (int ${loopVar} = 0; ${loopVar} < ${repeats}; ${loopVar}++) {\n${branch}}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_whileUntil'] = function (block) {
|
||||||
|
const until = block.getFieldValue('MODE') === 'UNTIL';
|
||||||
|
let cond = arduinoGenerator.valueToCode(block, 'BOOL', until ? Order.UNARY_PREFIX : Order.NONE) || 'false';
|
||||||
|
let branch = arduinoGenerator.statementToCode(block, 'DO');
|
||||||
|
branch = arduinoGenerator.addLoopTrap(branch, block);
|
||||||
|
if (until) {
|
||||||
|
cond = '!' + cond;
|
||||||
|
}
|
||||||
|
return `while (${cond}) {\n${branch}}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_for'] = function (block) {
|
||||||
|
const variable0 = arduinoGenerator.getVariableName(block.getFieldValue('VAR'));
|
||||||
|
const from_ = arduinoGenerator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0';
|
||||||
|
const to = arduinoGenerator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0';
|
||||||
|
const by = arduinoGenerator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1';
|
||||||
|
return `for (int ${variable0} = ${from_}; ${variable0} <= ${to}; ${variable0} += ${by}) {\n` +
|
||||||
|
arduinoGenerator.statementToCode(block, 'DO') + '}\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_forEach'] = function (block) {
|
||||||
|
// C++ for-each is limited; generate index-based loop comment
|
||||||
|
const variable0 = arduinoGenerator.getVariableName(block.getFieldValue('VAR'));
|
||||||
|
const list = arduinoGenerator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '""';
|
||||||
|
let branch = arduinoGenerator.statementToCode(block, 'DO');
|
||||||
|
return `// for-each not directly supported; use indexed loop\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['controls_flow_statements'] = function (block) {
|
||||||
|
switch (block.getFieldValue('FLOW')) {
|
||||||
|
case 'BREAK': return 'break;\n';
|
||||||
|
case 'CONTINUE': return 'continue;\n';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Math
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_number'] = function (block) {
|
||||||
|
const num = Number(block.getFieldValue('NUM'));
|
||||||
|
const order = num >= 0 ? Order.ATOMIC : Order.UNARY_PREFIX;
|
||||||
|
return [String(num), order];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_arithmetic'] = function (block) {
|
||||||
|
const OPERATORS = {
|
||||||
|
ADD: [' + ', Order.ADDITIVE],
|
||||||
|
MINUS: [' - ', Order.ADDITIVE],
|
||||||
|
MULTIPLY: [' * ', Order.MULTIPLICATIVE],
|
||||||
|
DIVIDE: [' / ', Order.MULTIPLICATIVE],
|
||||||
|
POWER: [null, Order.FUNCTION_CALL],
|
||||||
|
};
|
||||||
|
const tuple = OPERATORS[block.getFieldValue('OP')];
|
||||||
|
const op = tuple[0];
|
||||||
|
const order = tuple[1];
|
||||||
|
const a = arduinoGenerator.valueToCode(block, 'A', order) || '0';
|
||||||
|
const b = arduinoGenerator.valueToCode(block, 'B', order) || '0';
|
||||||
|
if (!op) {
|
||||||
|
return [`pow(${a}, ${b})`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
return [a + op + b, order];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_single'] = function (block) {
|
||||||
|
const op = block.getFieldValue('OP');
|
||||||
|
let arg = arduinoGenerator.valueToCode(block, 'NUM', Order.NONE) || '0';
|
||||||
|
arduinoGenerator.addInclude('include_math', '#include <math.h>');
|
||||||
|
switch (op) {
|
||||||
|
case 'ROOT': return [`sqrt(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'ABS': return [`abs(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'NEG': return [`-${arg}`, Order.UNARY_PREFIX];
|
||||||
|
case 'LN': return [`log(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'LOG10': return [`log10(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'EXP': return [`exp(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'POW10': return [`pow(10, ${arg})`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
return [arg, Order.NONE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_trig'] = function (block) {
|
||||||
|
const op = block.getFieldValue('OP');
|
||||||
|
const arg = arduinoGenerator.valueToCode(block, 'NUM', Order.NONE) || '0';
|
||||||
|
arduinoGenerator.addInclude('include_math', '#include <math.h>');
|
||||||
|
const RAD = `(${arg} * M_PI / 180.0)`;
|
||||||
|
switch (op) {
|
||||||
|
case 'SIN': return [`sin(${RAD})`, Order.FUNCTION_CALL];
|
||||||
|
case 'COS': return [`cos(${RAD})`, Order.FUNCTION_CALL];
|
||||||
|
case 'TAN': return [`tan(${RAD})`, Order.FUNCTION_CALL];
|
||||||
|
case 'ASIN': return [`(asin(${arg}) * 180.0 / M_PI)`, Order.MULTIPLICATIVE];
|
||||||
|
case 'ACOS': return [`(acos(${arg}) * 180.0 / M_PI)`, Order.MULTIPLICATIVE];
|
||||||
|
case 'ATAN': return [`(atan(${arg}) * 180.0 / M_PI)`, Order.MULTIPLICATIVE];
|
||||||
|
}
|
||||||
|
return ['0', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_constant'] = function (block) {
|
||||||
|
arduinoGenerator.addInclude('include_math', '#include <math.h>');
|
||||||
|
const CONSTANTS = {
|
||||||
|
PI: ['M_PI', Order.ATOMIC],
|
||||||
|
E: ['M_E', Order.ATOMIC],
|
||||||
|
GOLDEN_RATIO: ['1.61803398875', Order.ATOMIC],
|
||||||
|
SQRT2: ['M_SQRT2', Order.ATOMIC],
|
||||||
|
SQRT1_2: ['M_SQRT1_2', Order.ATOMIC],
|
||||||
|
INFINITY: ['INFINITY', Order.ATOMIC],
|
||||||
|
};
|
||||||
|
return CONSTANTS[block.getFieldValue('CONSTANT')] || ['0', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_number_property'] = function (block) {
|
||||||
|
const property = block.getFieldValue('PROPERTY');
|
||||||
|
const num = arduinoGenerator.valueToCode(block, 'NUMBER_TO_CHECK', Order.MULTIPLICATIVE) || '0';
|
||||||
|
switch (property) {
|
||||||
|
case 'EVEN': return [`((int)(${num}) % 2 == 0)`, Order.EQUALITY];
|
||||||
|
case 'ODD': return [`((int)(${num}) % 2 == 1)`, Order.EQUALITY];
|
||||||
|
case 'PRIME':
|
||||||
|
arduinoGenerator.addDeclaration('_is_prime',
|
||||||
|
'bool _is_prime(int n) {\n' +
|
||||||
|
' if (n < 2) return false;\n' +
|
||||||
|
' for (int i = 2; i * i <= n; i++) { if (n % i == 0) return false; }\n' +
|
||||||
|
' return true;\n' +
|
||||||
|
'}'
|
||||||
|
);
|
||||||
|
return [`_is_prime((int)(${num}))`, Order.FUNCTION_CALL];
|
||||||
|
case 'WHOLE': return [`(${num} == (int)(${num}))`, Order.EQUALITY];
|
||||||
|
case 'POSITIVE': return [`(${num} > 0)`, Order.RELATIONAL];
|
||||||
|
case 'NEGATIVE': return [`(${num} < 0)`, Order.RELATIONAL];
|
||||||
|
case 'DIVISIBLE_BY':
|
||||||
|
const divisor = arduinoGenerator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '1';
|
||||||
|
return [`((int)(${num}) % (int)(${divisor}) == 0)`, Order.EQUALITY];
|
||||||
|
}
|
||||||
|
return ['false', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_round'] = function (block) {
|
||||||
|
const op = block.getFieldValue('OP');
|
||||||
|
const arg = arduinoGenerator.valueToCode(block, 'NUM', Order.NONE) || '0';
|
||||||
|
arduinoGenerator.addInclude('include_math', '#include <math.h>');
|
||||||
|
switch (op) {
|
||||||
|
case 'ROUND': return [`round(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'ROUNDUP': return [`ceil(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
case 'ROUNDDOWN': return [`floor(${arg})`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
return [arg, Order.NONE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_modulo'] = function (block) {
|
||||||
|
const a = arduinoGenerator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0';
|
||||||
|
const b = arduinoGenerator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0';
|
||||||
|
return [`(int)(${a}) % (int)(${b})`, Order.MULTIPLICATIVE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_constrain'] = function (block) {
|
||||||
|
const val = arduinoGenerator.valueToCode(block, 'VALUE', Order.NONE) || '0';
|
||||||
|
const low = arduinoGenerator.valueToCode(block, 'LOW', Order.NONE) || '0';
|
||||||
|
const high = arduinoGenerator.valueToCode(block, 'HIGH', Order.NONE) || '100';
|
||||||
|
return [`constrain(${val}, ${low}, ${high})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_random_int'] = function (block) {
|
||||||
|
const from_ = arduinoGenerator.valueToCode(block, 'FROM', Order.NONE) || '0';
|
||||||
|
const to = arduinoGenerator.valueToCode(block, 'TO', Order.ADDITIVE) || '100';
|
||||||
|
return [`random(${from_}, ${to} + 1)`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['math_random_float'] = function () {
|
||||||
|
return ['(random(10001) / 10000.0)', Order.MULTIPLICATIVE];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Text
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text'] = function (block) {
|
||||||
|
return [arduinoGenerator.quote_(block.getFieldValue('TEXT')), Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_join'] = function (block) {
|
||||||
|
const count = block.itemCount_;
|
||||||
|
if (count === 0) {
|
||||||
|
return ['String("")', Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
if (count === 1) {
|
||||||
|
const element = arduinoGenerator.valueToCode(block, 'ADD0', Order.NONE) || '""';
|
||||||
|
return [`String(${element})`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
const elements = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
elements.push(arduinoGenerator.valueToCode(block, 'ADD' + i, Order.NONE) || '""');
|
||||||
|
}
|
||||||
|
// Build concatenation using String()
|
||||||
|
let code = `String(${elements[0]})`;
|
||||||
|
for (let i = 1; i < elements.length; i++) {
|
||||||
|
code += ` + String(${elements[i]})`;
|
||||||
|
}
|
||||||
|
return [code, Order.ADDITIVE];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_append'] = function (block) {
|
||||||
|
const varName = arduinoGenerator.getVariableName(block.getFieldValue('VAR'));
|
||||||
|
const value = arduinoGenerator.valueToCode(block, 'TEXT', Order.NONE) || '""';
|
||||||
|
return `${varName} += String(${value});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_length'] = function (block) {
|
||||||
|
const text = arduinoGenerator.valueToCode(block, 'VALUE', Order.MEMBER) || '""';
|
||||||
|
return [`String(${text}).length()`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_isEmpty'] = function (block) {
|
||||||
|
const text = arduinoGenerator.valueToCode(block, 'VALUE', Order.MEMBER) || '""';
|
||||||
|
return [`(String(${text}).length() == 0)`, Order.EQUALITY];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_indexOf'] = function (block) {
|
||||||
|
const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf';
|
||||||
|
const substr = arduinoGenerator.valueToCode(block, 'FIND', Order.NONE) || '""';
|
||||||
|
const text = arduinoGenerator.valueToCode(block, 'VALUE', Order.MEMBER) || '""';
|
||||||
|
return [`String(${text}).${operator}(String(${substr}))`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['text_charAt'] = function (block) {
|
||||||
|
const where = block.getFieldValue('WHERE') || 'FROM_START';
|
||||||
|
const text = arduinoGenerator.valueToCode(block, 'VALUE', Order.MEMBER) || '""';
|
||||||
|
let at;
|
||||||
|
switch (where) {
|
||||||
|
case 'FROM_START':
|
||||||
|
at = arduinoGenerator.valueToCode(block, 'AT', Order.NONE) || '1';
|
||||||
|
return [`String(String(${text}).charAt(${at} - 1))`, Order.FUNCTION_CALL];
|
||||||
|
case 'FROM_END':
|
||||||
|
at = arduinoGenerator.valueToCode(block, 'AT', Order.NONE) || '1';
|
||||||
|
return [`String(String(${text}).charAt(String(${text}).length() - ${at}))`, Order.FUNCTION_CALL];
|
||||||
|
case 'FIRST':
|
||||||
|
return [`String(String(${text}).charAt(0))`, Order.FUNCTION_CALL];
|
||||||
|
case 'LAST':
|
||||||
|
return [`String(String(${text}).charAt(String(${text}).length() - 1))`, Order.FUNCTION_CALL];
|
||||||
|
case 'RANDOM':
|
||||||
|
return [`String(String(${text}).charAt(random(String(${text}).length())))`, Order.FUNCTION_CALL];
|
||||||
|
}
|
||||||
|
return ['""', Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Variables
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['variables_get'] = function (block) {
|
||||||
|
const varName = arduinoGenerator.getVariableName(block.getFieldValue('VAR'));
|
||||||
|
return [varName, Order.ATOMIC];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['variables_set'] = function (block) {
|
||||||
|
const varName = arduinoGenerator.getVariableName(block.getFieldValue('VAR'));
|
||||||
|
const value = arduinoGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0';
|
||||||
|
return `${varName} = ${value};\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Functions / Procedures
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['procedures_defreturn'] = function (block) {
|
||||||
|
const funcName = arduinoGenerator.getProcedureName(block.getFieldValue('NAME'));
|
||||||
|
let xfix1 = '';
|
||||||
|
if (arduinoGenerator.STATEMENT_PREFIX) {
|
||||||
|
xfix1 += arduinoGenerator.injectId(arduinoGenerator.STATEMENT_PREFIX, block);
|
||||||
|
}
|
||||||
|
if (arduinoGenerator.STATEMENT_SUFFIX) {
|
||||||
|
xfix1 += arduinoGenerator.injectId(arduinoGenerator.STATEMENT_SUFFIX, block);
|
||||||
|
}
|
||||||
|
if (xfix1) xfix1 = arduinoGenerator.prefixLines(xfix1, arduinoGenerator.INDENT);
|
||||||
|
|
||||||
|
let branch = arduinoGenerator.statementToCode(block, 'STACK');
|
||||||
|
let returnValue = arduinoGenerator.valueToCode(block, 'RETURN', Order.NONE) || '';
|
||||||
|
let xfix2 = '';
|
||||||
|
if (branch && returnValue) {
|
||||||
|
xfix2 = xfix1;
|
||||||
|
}
|
||||||
|
if (returnValue) {
|
||||||
|
returnValue = arduinoGenerator.INDENT + 'return ' + returnValue + ';\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [];
|
||||||
|
const variables = block.getVars();
|
||||||
|
for (let i = 0; i < variables.length; i++) {
|
||||||
|
args.push('float ' + arduinoGenerator.getVariableName(variables[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnType = returnValue ? 'float' : 'void';
|
||||||
|
let code = `${returnType} ${funcName}(${args.join(', ')}) {\n${xfix1}${branch}${xfix2}${returnValue}}`;
|
||||||
|
arduinoGenerator.functionDefinitions_[funcName] = code;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['procedures_defnoreturn'] =
|
||||||
|
arduinoGenerator.forBlock['procedures_defreturn'];
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['procedures_callreturn'] = function (block) {
|
||||||
|
const funcName = arduinoGenerator.getProcedureName(block.getFieldValue('NAME'));
|
||||||
|
const args = [];
|
||||||
|
const variables = block.getVars();
|
||||||
|
for (let i = 0; i < variables.length; i++) {
|
||||||
|
args.push(arduinoGenerator.valueToCode(block, 'ARG' + i, Order.NONE) || '0');
|
||||||
|
}
|
||||||
|
return [`${funcName}(${args.join(', ')})`, Order.FUNCTION_CALL];
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['procedures_callnoreturn'] = function (block) {
|
||||||
|
const funcName = arduinoGenerator.getProcedureName(block.getFieldValue('NAME'));
|
||||||
|
const args = [];
|
||||||
|
const variables = block.getVars();
|
||||||
|
for (let i = 0; i < variables.length; i++) {
|
||||||
|
args.push(arduinoGenerator.valueToCode(block, 'ARG' + i, Order.NONE) || '0');
|
||||||
|
}
|
||||||
|
return `${funcName}(${args.join(', ')});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
arduinoGenerator.forBlock['procedures_ifreturn'] = function (block) {
|
||||||
|
const cond = arduinoGenerator.valueToCode(block, 'CONDITION', Order.NONE) || 'false';
|
||||||
|
let code = 'if (' + cond + ') {\n';
|
||||||
|
if (arduinoGenerator.STATEMENT_SUFFIX) {
|
||||||
|
code += arduinoGenerator.prefixLines(
|
||||||
|
arduinoGenerator.injectId(arduinoGenerator.STATEMENT_SUFFIX, block), arduinoGenerator.INDENT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (block.hasReturnValue_) {
|
||||||
|
const value = arduinoGenerator.valueToCode(block, 'VALUE', Order.NONE) || '0';
|
||||||
|
code += arduinoGenerator.INDENT + 'return ' + value + ';\n';
|
||||||
|
} else {
|
||||||
|
code += arduinoGenerator.INDENT + 'return;\n';
|
||||||
|
}
|
||||||
|
code += '}\n';
|
||||||
|
return code;
|
||||||
|
};
|
||||||
216
src/main.js
216
src/main.js
|
|
@ -1,7 +1,10 @@
|
||||||
import * as Blockly from 'blockly';
|
import * as Blockly from 'blockly';
|
||||||
import { pythonGenerator } from 'blockly/python';
|
import { pythonGenerator } from 'blockly/python';
|
||||||
|
import { arduinoGenerator } from './generators/arduino.js';
|
||||||
|
import './generators/arduino_builtins.js';
|
||||||
import './blocks/esp32_blocks.js';
|
import './blocks/esp32_blocks.js';
|
||||||
import './blocks/esp32_generators.js';
|
import './blocks/esp32_generators.js';
|
||||||
|
import './blocks/arduino_generators.js';
|
||||||
import {
|
import {
|
||||||
getDeviceId,
|
getDeviceId,
|
||||||
setDeviceId,
|
setDeviceId,
|
||||||
|
|
@ -18,13 +21,14 @@ import {
|
||||||
removeAddon,
|
removeAddon,
|
||||||
getInstalledAddons,
|
getInstalledAddons,
|
||||||
} from './addons/loader.js';
|
} from './addons/loader.js';
|
||||||
import { connect, disconnect, isConnected, onData, writeString } from './serial/connection.js';
|
import { connect, disconnect, isConnected, getPort, onData, writeString } from './serial/connection.js';
|
||||||
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
|
||||||
import { flashFirmware } from './serial/flasher.js';
|
import { flashFirmware } from './serial/flasher.js';
|
||||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||||
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
|
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
|
||||||
import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js';
|
import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js';
|
||||||
import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
|
import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
|
||||||
|
import { uploadHex, BOARDS } from './arduino/stk500.js';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
// ─── Blockly Workspace ───────────────────────────────────
|
// ─── Blockly Workspace ───────────────────────────────────
|
||||||
|
|
@ -67,13 +71,31 @@ setDeviceListRefreshCallback(rebuildDeviceSelect);
|
||||||
initToolboxCustomizer(refreshToolbox);
|
initToolboxCustomizer(refreshToolbox);
|
||||||
document.getElementById('btn-customize').addEventListener('click', toggleCustomizeMode);
|
document.getElementById('btn-customize').addEventListener('click', toggleCustomizeMode);
|
||||||
|
|
||||||
|
// ─── Generator Selection ─────────────────────────────────
|
||||||
|
|
||||||
|
function isArduinoDevice() {
|
||||||
|
const device = getDevice();
|
||||||
|
return device && device.language === 'arduino';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveGenerator() {
|
||||||
|
return isArduinoDevice() ? arduinoGenerator : pythonGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGeneratedCode() {
|
||||||
|
return getActiveGenerator().workspaceToCode(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Live Code Preview ───────────────────────────────────
|
// ─── Live Code Preview ───────────────────────────────────
|
||||||
|
|
||||||
const codeOutput = document.getElementById('code-output');
|
const codeOutput = document.getElementById('code-output');
|
||||||
|
|
||||||
function updateCodePreview() {
|
function updateCodePreview() {
|
||||||
const code = pythonGenerator.workspaceToCode(workspace);
|
const code = getGeneratedCode();
|
||||||
codeOutput.textContent = code || '# Drag blocks to generate MicroPython code';
|
const placeholder = isArduinoDevice()
|
||||||
|
? '// Drag blocks to generate Arduino code'
|
||||||
|
: '# Drag blocks to generate MicroPython code';
|
||||||
|
codeOutput.textContent = code || placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace.addChangeListener((event) => {
|
workspace.addChangeListener((event) => {
|
||||||
|
|
@ -167,6 +189,52 @@ function hideSendProgress() {
|
||||||
sendProgressFill.style.width = '0%';
|
sendProgressFill.style.width = '0%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Arduino-specific UI state ───────────────────────────
|
||||||
|
|
||||||
|
function updateDeviceUI() {
|
||||||
|
const arduino = isArduinoDevice();
|
||||||
|
|
||||||
|
if (arduino) {
|
||||||
|
btnRun.querySelector('.icon').innerHTML = '▷';
|
||||||
|
btnRun.childNodes[btnRun.childNodes.length - 1].textContent = ' Upload .hex';
|
||||||
|
btnRun.title = 'Upload a compiled .hex file to Arduino via WebSerial';
|
||||||
|
btnRun.disabled = false;
|
||||||
|
|
||||||
|
btnSave.querySelector('.icon').innerHTML = '💾';
|
||||||
|
btnSave.childNodes[btnSave.childNodes.length - 1].textContent = ' Download .ino';
|
||||||
|
btnSave.title = 'Download generated code as .ino file';
|
||||||
|
btnSave.disabled = false;
|
||||||
|
|
||||||
|
btnConnect.querySelector('.icon').innerHTML = '▶';
|
||||||
|
btnConnect.childNodes[btnConnect.childNodes.length - 1].textContent = ' Serial Monitor';
|
||||||
|
btnConnect.title = 'Open serial monitor via WebSerial';
|
||||||
|
|
||||||
|
btnFlash.classList.add('hidden');
|
||||||
|
btnStop.disabled = false;
|
||||||
|
} else {
|
||||||
|
btnRun.querySelector('.icon').innerHTML = '▷';
|
||||||
|
btnRun.childNodes[btnRun.childNodes.length - 1].textContent = ' Run';
|
||||||
|
btnRun.title = 'Upload and run code';
|
||||||
|
btnRun.disabled = !isConnected();
|
||||||
|
|
||||||
|
btnSave.querySelector('.icon').innerHTML = '💾';
|
||||||
|
btnSave.childNodes[btnSave.childNodes.length - 1].textContent = ' Save';
|
||||||
|
btnSave.title = 'Save code to device as main.py';
|
||||||
|
btnSave.disabled = !isConnected();
|
||||||
|
|
||||||
|
btnConnect.querySelector('.icon').innerHTML = '▶';
|
||||||
|
btnConnect.childNodes[btnConnect.childNodes.length - 1].textContent = isConnected() ? ' Disconnect' : ' Connect';
|
||||||
|
btnConnect.title = 'Connect to device via Web Serial';
|
||||||
|
|
||||||
|
btnFlash.classList.remove('hidden');
|
||||||
|
btnFlash.title = canFlashInBrowser()
|
||||||
|
? 'Flash MicroPython firmware'
|
||||||
|
: 'Download firmware (drag to device)';
|
||||||
|
|
||||||
|
btnStop.disabled = !isConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sync device dropdown with stored device
|
// Sync device dropdown with stored device
|
||||||
deviceSelect.value = getDeviceId();
|
deviceSelect.value = getDeviceId();
|
||||||
deviceSelect.addEventListener('change', () => {
|
deviceSelect.addEventListener('change', () => {
|
||||||
|
|
@ -174,26 +242,33 @@ deviceSelect.addEventListener('change', () => {
|
||||||
refreshToolbox();
|
refreshToolbox();
|
||||||
refreshCustomizer();
|
refreshCustomizer();
|
||||||
updateCodePreview();
|
updateCodePreview();
|
||||||
btnFlash.title = canFlashInBrowser()
|
updateDeviceUI();
|
||||||
? 'Flash MicroPython firmware'
|
|
||||||
: 'Download firmware (drag to device)';
|
|
||||||
});
|
});
|
||||||
btnFlash.title = canFlashInBrowser()
|
updateDeviceUI();
|
||||||
? 'Flash MicroPython firmware'
|
|
||||||
: 'Download firmware (drag to device)';
|
|
||||||
|
|
||||||
function refreshToolbox() {
|
function refreshToolbox() {
|
||||||
workspace.updateToolbox(buildToolbox(getDeviceId()));
|
workspace.updateToolbox(buildToolbox(getDeviceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConnectedUI(connected) {
|
function setConnectedUI(connected) {
|
||||||
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
if (isArduinoDevice()) {
|
||||||
btnRun.disabled = !connected;
|
btnConnect.innerHTML = connected
|
||||||
btnStop.disabled = !connected;
|
? '<span class="icon">⏻</span> Disconnect Monitor'
|
||||||
btnSave.disabled = !connected;
|
: '<span class="icon">▶</span> Serial Monitor';
|
||||||
terminalInput.disabled = !connected;
|
terminalInput.disabled = !connected;
|
||||||
statusEl.textContent = connected ? 'Connected' : 'Disconnected';
|
statusEl.textContent = connected ? 'Monitoring' : 'Disconnected';
|
||||||
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
||||||
|
} else {
|
||||||
|
btnConnect.innerHTML = connected
|
||||||
|
? '<span class="icon">⏻</span> Disconnect'
|
||||||
|
: '<span class="icon">▶</span> Connect';
|
||||||
|
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 Capture (reusable promise-based) ─────────────
|
// ─── Serial Capture (reusable promise-based) ─────────────
|
||||||
|
|
@ -322,6 +397,8 @@ flashCloseBtn.addEventListener('click', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
btnFlash.addEventListener('click', async () => {
|
btnFlash.addEventListener('click', async () => {
|
||||||
|
if (isArduinoDevice()) return;
|
||||||
|
|
||||||
if (isConnected()) {
|
if (isConnected()) {
|
||||||
await disconnect();
|
await disconnect();
|
||||||
setConnectedUI(false);
|
setConnectedUI(false);
|
||||||
|
|
@ -329,6 +406,7 @@ btnFlash.addEventListener('click', async () => {
|
||||||
|
|
||||||
const device = getDevice();
|
const device = getDevice();
|
||||||
const fw = device.firmware;
|
const fw = device.firmware;
|
||||||
|
if (!fw) return;
|
||||||
|
|
||||||
if (canFlashInBrowser()) {
|
if (canFlashInBrowser()) {
|
||||||
showFlashOverlay();
|
showFlashOverlay();
|
||||||
|
|
@ -357,7 +435,13 @@ btnFlash.addEventListener('click', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
btnRun.addEventListener('click', async () => {
|
btnRun.addEventListener('click', async () => {
|
||||||
const code = pythonGenerator.workspaceToCode(workspace);
|
if (isArduinoDevice()) {
|
||||||
|
document.getElementById('hex-upload-overlay').classList.remove('hidden');
|
||||||
|
document.getElementById('hex-upload-status').textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = getGeneratedCode();
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
appendToTerminal('\nNo code to run. Add some blocks!\n');
|
appendToTerminal('\nNo code to run. Add some blocks!\n');
|
||||||
return;
|
return;
|
||||||
|
|
@ -385,7 +469,26 @@ btnStop.addEventListener('click', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
btnSave.addEventListener('click', async () => {
|
btnSave.addEventListener('click', async () => {
|
||||||
const code = pythonGenerator.workspaceToCode(workspace);
|
if (isArduinoDevice()) {
|
||||||
|
const code = getGeneratedCode();
|
||||||
|
if (!code.trim()) {
|
||||||
|
appendToTerminal('\nNo code to download.\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = new Blob([code], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'sketch.ino';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
appendToTerminal('\n--- Downloaded sketch.ino ---\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = getGeneratedCode();
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
appendToTerminal('\nNo code to save.\n');
|
appendToTerminal('\nNo code to save.\n');
|
||||||
return;
|
return;
|
||||||
|
|
@ -430,6 +533,83 @@ terminalInput.addEventListener('keydown', async (e) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Hex Upload Modal (Arduino STK500) ───────────────────
|
||||||
|
|
||||||
|
const hexOverlay = document.getElementById('hex-upload-overlay');
|
||||||
|
const hexCloseBtn = document.getElementById('hex-upload-close');
|
||||||
|
const hexUploadBtn = document.getElementById('hex-upload-btn');
|
||||||
|
const hexFileInput = document.getElementById('hex-file-input');
|
||||||
|
const hexBoardSelect = document.getElementById('hex-board-select');
|
||||||
|
const hexStatus = document.getElementById('hex-upload-status');
|
||||||
|
|
||||||
|
if (hexCloseBtn) {
|
||||||
|
hexCloseBtn.addEventListener('click', () => {
|
||||||
|
hexOverlay.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hexOverlay) {
|
||||||
|
hexOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === hexOverlay) hexOverlay.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hexUploadBtn) {
|
||||||
|
hexUploadBtn.addEventListener('click', async () => {
|
||||||
|
const file = hexFileInput.files[0];
|
||||||
|
if (!file) {
|
||||||
|
hexStatus.textContent = 'Select a .hex file first.';
|
||||||
|
hexStatus.className = 'hex-upload-status status-err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardKey = hexBoardSelect.value;
|
||||||
|
hexOverlay.classList.add('hidden');
|
||||||
|
|
||||||
|
// Grab the existing serial port before tearing down the text-mode connection
|
||||||
|
const existingPort = getPort();
|
||||||
|
const wasConnected = isConnected();
|
||||||
|
|
||||||
|
if (wasConnected) {
|
||||||
|
await disconnect();
|
||||||
|
setConnectedUI(false);
|
||||||
|
appendToTerminal('\n--- Disconnected for upload ---\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexString = await file.text();
|
||||||
|
appendToTerminal(`\n--- Uploading ${file.name} (${boardKey})... ---\n`);
|
||||||
|
showSendProgress('Uploading .hex');
|
||||||
|
try {
|
||||||
|
await uploadHex(hexString, {
|
||||||
|
board: boardKey,
|
||||||
|
port: existingPort || undefined,
|
||||||
|
onProgress: (status, pct) => {
|
||||||
|
sendProgressFill.style.width = Math.round(pct) + '%';
|
||||||
|
sendProgressText.textContent = status;
|
||||||
|
appendToTerminal(` ${status} (${Math.round(pct)}%)\n`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appendToTerminal('--- Upload complete! ---\n');
|
||||||
|
} catch (err) {
|
||||||
|
appendToTerminal(`\nUpload error: ${err.message}\n`);
|
||||||
|
} finally {
|
||||||
|
hideSendProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume serial monitor on the same port (small delay for the board to reboot)
|
||||||
|
if (existingPort) {
|
||||||
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
|
try {
|
||||||
|
await connect(115200, existingPort);
|
||||||
|
setConnectedUI(true);
|
||||||
|
appendToTerminal('--- Serial monitor resumed ---\n');
|
||||||
|
} catch (err) {
|
||||||
|
appendToTerminal(`--- Could not resume monitor: ${err.message} ---\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Addons Manager UI ──────────────────────────────────
|
// ─── Addons Manager UI ──────────────────────────────────
|
||||||
|
|
||||||
const btnAddons = document.getElementById('btn-addons');
|
const btnAddons = document.getElementById('btn-addons');
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,18 @@ export function isConnected() {
|
||||||
return port !== null;
|
return port !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
export function getWriter() {
|
export function getWriter() {
|
||||||
return writer;
|
return writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function connect(baudRate = 115200) {
|
export async function connect(baudRate = 115200, existingPort = null) {
|
||||||
if (port) return port;
|
if (port) return port;
|
||||||
port = await navigator.serial.requestPort();
|
port = existingPort || await navigator.serial.requestPort();
|
||||||
|
try { await port.close(); } catch { /* already closed or never opened */ }
|
||||||
await port.open({ baudRate });
|
await port.open({ baudRate });
|
||||||
|
|
||||||
const textDecoder = new TextDecoderStream();
|
const textDecoder = new TextDecoderStream();
|
||||||
|
|
|
||||||
156
src/style.css
156
src/style.css
|
|
@ -1017,3 +1017,159 @@ html, body {
|
||||||
color: var(--bg-toolbar);
|
color: var(--bg-toolbar);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Hex Upload Overlay (Arduino) --- */
|
||||||
|
#hex-upload-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hex-upload-modal {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
width: 460px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-header button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.board-select-header button:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list {
|
||||||
|
list-style: none;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-empty {
|
||||||
|
padding: 12px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-item:hover {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-item.selected {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-actions button {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-select-actions button:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Hex Upload Modal fields --- */
|
||||||
|
.hex-upload-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-upload-fields select,
|
||||||
|
.hex-upload-fields input[type="file"] {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-upload-fields select:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-upload-status {
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 18px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.hex-upload-status.status-err { color: var(--red); }
|
||||||
|
.hex-upload-status.status-ok { color: var(--green); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue