implemented device addons

main
Jake 2026-02-25 09:49:34 +08:00
parent fd5a39db77
commit f2196f0c2a
3 changed files with 381 additions and 1 deletions

350
public/atmega328p-addon.js Normal file
View File

@ -0,0 +1,350 @@
// ATmega328P Arduino addon
//
// Demonstrates how an addon can register an entirely new microcontroller.
// Adds the ATmega328P (Arduino Uno / Nano) as a selectable device with:
// - Curated Pin I/O, PWM, ADC, Time, and Serial categories (existing blocks)
// - Arduino-specific blocks: built-in LED, analog write, servo
//
// Pin constraints: digital 0-13, analog A0-A5 (14-19), PWM on 3/5/6/9/10/11
// Generates MicroPython (machine module) since the IDE's code pipeline is
// MicroPython-based. Users can flash MicroPython firmware onto compatible
// ATmega328P boards to use this.
// ─── Arduino-specific block definitions ───────────────────
api.Blockly.Blocks['arduino_builtin_led'] = {
init() {
this.appendDummyInput()
.appendField('set built-in LED')
.appendField(new api.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');
},
};
api.Blockly.Blocks['arduino_analog_write'] = {
init() {
this.appendValueInput('VALUE')
.setCheck('Number')
.appendField('analog write pin')
.appendField(new api.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');
},
};
api.Blockly.Blocks['arduino_analog_read'] = {
init() {
this.appendDummyInput()
.appendField('analog read')
.appendField(new api.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)');
},
};
api.Blockly.Blocks['arduino_servo_attach'] = {
init() {
this.appendDummyInput()
.appendField('attach servo on pin')
.appendField(new api.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');
},
};
api.Blockly.Blocks['arduino_servo_write'] = {
init() {
this.appendValueInput('ANGLE')
.setCheck('Number')
.appendField('set servo on pin')
.appendField(new api.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)');
},
};
api.Blockly.Blocks['arduino_tone'] = {
init() {
this.appendDummyInput()
.appendField('play tone on pin')
.appendField(new api.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');
},
};
api.Blockly.Blocks['arduino_no_tone'] = {
init() {
this.appendDummyInput()
.appendField('stop tone on pin')
.appendField(new api.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');
},
};
api.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');
},
};
// ─── Code generators ──────────────────────────────────────
var gen = api.pythonGenerator;
gen.forBlock['arduino_builtin_led'] = function (block) {
var state = block.getFieldValue('STATE');
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['led_pin13'] = 'led = Pin(13, Pin.OUT)';
return 'led.value(' + state + ')\n';
};
gen.forBlock['arduino_analog_write'] = function (block) {
var pin = block.getFieldValue('PIN');
var value = gen.valueToCode(block, 'VALUE', gen.ORDER_NONE) || '0';
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['pwm_aw_' + pin] = 'pwm_' + pin + ' = PWM(Pin(' + pin + '), freq=490)';
// ATmega328P analogWrite is 0-255, MicroPython PWM duty is 0-1023
return 'pwm_' + pin + '.duty(int(' + value + ' * 4))\n';
};
gen.forBlock['arduino_analog_read'] = function (block) {
var ch = block.getFieldValue('CHANNEL');
var pin = parseInt(ch, 10) + 14;
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['adc_a' + ch] = 'adc_a' + ch + ' = ADC(Pin(' + pin + '))';
return ['adc_a' + ch + '.read()', gen.ORDER_FUNCTION_CALL];
};
var SERVO_HELPER = [
'def _servo_write(pwm, angle):',
' angle = max(0, min(180, int(angle)))',
' duty = int(26 + (angle / 180) * 102)',
' pwm.duty(duty)',
].join('\n');
gen.forBlock['arduino_servo_attach'] = function (block) {
var pin = block.getFieldValue('PIN');
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['servo_' + pin] = 'servo_' + pin + ' = PWM(Pin(' + pin + '), freq=50)';
gen.definitions_['servo_helper'] = SERVO_HELPER;
return '';
};
gen.forBlock['arduino_servo_write'] = function (block) {
var pin = block.getFieldValue('PIN');
var angle = gen.valueToCode(block, 'ANGLE', gen.ORDER_NONE) || '90';
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['servo_' + pin] = 'servo_' + pin + ' = PWM(Pin(' + pin + '), freq=50)';
gen.definitions_['servo_helper'] = SERVO_HELPER;
return '_servo_write(servo_' + pin + ', ' + angle + ')\n';
};
gen.forBlock['arduino_tone'] = function (block) {
var pin = block.getFieldValue('PIN');
var freq = gen.valueToCode(block, 'FREQ', gen.ORDER_NONE) || '440';
var duration = gen.valueToCode(block, 'DURATION', gen.ORDER_NONE) || '500';
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
gen.definitions_['import_time'] = 'import time';
var v = 'buzzer_' + pin;
gen.definitions_['buzzer_' + pin] = v + ' = PWM(Pin(' + pin + '))';
return v + '.freq(' + freq + ')\n' +
v + '.duty(512)\n' +
'time.sleep_ms(' + duration + ')\n' +
v + '.duty(0)\n';
};
gen.forBlock['arduino_no_tone'] = function (block) {
var pin = block.getFieldValue('PIN');
gen.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC';
var v = 'buzzer_' + pin;
gen.definitions_['buzzer_' + pin] = v + ' = PWM(Pin(' + pin + '))';
return v + '.duty(0)\n';
};
gen.forBlock['arduino_map'] = function (block) {
var value = gen.valueToCode(block, 'VALUE', gen.ORDER_NONE) || '0';
var fromLow = gen.valueToCode(block, 'FROM_LOW', gen.ORDER_NONE) || '0';
var fromHigh = gen.valueToCode(block, 'FROM_HIGH', gen.ORDER_NONE) || '1023';
var toLow = gen.valueToCode(block, 'TO_LOW', gen.ORDER_NONE) || '0';
var toHigh = gen.valueToCode(block, 'TO_HIGH', gen.ORDER_NONE) || '255';
gen.definitions_['_arduino_map'] =
'def _map(x, in_min, in_max, out_min, out_max):\n' +
' return int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)';
return [
'_map(' + value + ', ' + fromLow + ', ' + fromHigh + ', ' + toLow + ', ' + toHigh + ')',
gen.ORDER_FUNCTION_CALL,
];
};
// ─── Register device ──────────────────────────────────────
api.registerDevice({
id: 'atmega328p',
label: 'ATmega328P (Arduino)',
firmware: {
label: 'MicroPython (ATmega328P)',
url: 'https://micropython.org/download/',
canFlashInBrowser: false,
instructions: 'Flash MicroPython firmware via your preferred tool, then connect here.',
},
categories: [
{
kind: 'category',
name: 'Pin I/O',
colour: '230',
contents: [
{ kind: 'block', type: 'pin_set_mode' },
{ kind: 'block', type: 'pin_digital_write' },
{ kind: 'block', type: 'pin_digital_read' },
{ kind: 'block', type: 'arduino_builtin_led' },
],
},
{
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 } } },
},
},
],
},
{
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 } } },
},
},
],
},
{
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' },
],
},
{
kind: 'category',
name: 'Time',
colour: '120',
contents: [
{
kind: 'block',
type: 'sleep_seconds',
inputs: {
SECONDS: { shadow: { type: 'math_number', fields: { NUM: 1 } } },
},
},
{
kind: 'block',
type: 'sleep_ms',
inputs: {
MS: { shadow: { type: 'math_number', fields: { NUM: 100 } } },
},
},
{ kind: 'block', type: 'ticks_ms' },
],
},
{
kind: 'category',
name: 'Serial / Print',
colour: '60',
contents: [
{
kind: 'block',
type: 'print_text',
inputs: {
TEXT: { shadow: { type: 'text', fields: { TEXT: 'Hello!' } } },
},
},
],
},
],
});

View File

@ -5,6 +5,7 @@ import {
clearAddonCategories, clearAddonCategories,
getAddonCategories, getAddonCategories,
getDeviceId, getDeviceId,
registerDevice,
} from '../devices/registry.js'; } from '../devices/registry.js';
const STORAGE_KEY = 'esp32block_addons'; const STORAGE_KEY = 'esp32block_addons';
@ -26,14 +27,19 @@ export function getInstalledAddons() {
/** /**
* The API object passed to every addon's init(). * The API object passed to every addon's init().
* refreshToolbox is injected later by setRefreshCallback(). * refreshToolbox / refreshDeviceList are injected later by set*Callback().
*/ */
let refreshToolboxFn = () => {}; let refreshToolboxFn = () => {};
let refreshDeviceListFn = () => {};
export function setRefreshCallback(fn) { export function setRefreshCallback(fn) {
refreshToolboxFn = fn; refreshToolboxFn = fn;
} }
export function setDeviceListRefreshCallback(fn) {
refreshDeviceListFn = fn;
}
function makeAddonApi() { function makeAddonApi() {
return { return {
Blockly, Blockly,
@ -43,6 +49,11 @@ function makeAddonApi() {
registerAddonCategories(categories); registerAddonCategories(categories);
refreshToolboxFn(); refreshToolboxFn();
}, },
registerDevice(profile) {
registerDevice(profile);
refreshDeviceListFn();
refreshToolboxFn();
},
}; };
} }

View File

@ -8,9 +8,11 @@ import {
getDevice, getDevice,
canFlashInBrowser, canFlashInBrowser,
buildToolbox, buildToolbox,
getAllDevices,
} from './devices/registry.js'; } from './devices/registry.js';
import { import {
setRefreshCallback, setRefreshCallback,
setDeviceListRefreshCallback,
loadAllSavedAddons, loadAllSavedAddons,
installAddonFromFile, installAddonFromFile,
removeAddon, removeAddon,
@ -43,6 +45,23 @@ setRefreshCallback(() => {
workspace.updateToolbox(buildToolbox(getDeviceId())); workspace.updateToolbox(buildToolbox(getDeviceId()));
}); });
// Rebuild the device <select> from the registry (called when addons register devices)
function rebuildDeviceSelect() {
const devices = getAllDevices();
const current = getDeviceId();
const select = document.getElementById('device-select');
select.innerHTML = '';
for (const [id, profile] of Object.entries(devices)) {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = profile.label;
select.appendChild(opt);
}
select.value = current;
}
setDeviceListRefreshCallback(rebuildDeviceSelect);
// ─── Live Code Preview ─────────────────────────────────── // ─── Live Code Preview ───────────────────────────────────
const codeOutput = document.getElementById('code-output'); const codeOutput = document.getElementById('code-output');