implemented device addons
parent
fd5a39db77
commit
f2196f0c2a
|
|
@ -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!' } } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
clearAddonCategories,
|
||||
getAddonCategories,
|
||||
getDeviceId,
|
||||
registerDevice,
|
||||
} from '../devices/registry.js';
|
||||
|
||||
const STORAGE_KEY = 'esp32block_addons';
|
||||
|
|
@ -26,14 +27,19 @@ export function getInstalledAddons() {
|
|||
|
||||
/**
|
||||
* 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 refreshDeviceListFn = () => {};
|
||||
|
||||
export function setRefreshCallback(fn) {
|
||||
refreshToolboxFn = fn;
|
||||
}
|
||||
|
||||
export function setDeviceListRefreshCallback(fn) {
|
||||
refreshDeviceListFn = fn;
|
||||
}
|
||||
|
||||
function makeAddonApi() {
|
||||
return {
|
||||
Blockly,
|
||||
|
|
@ -43,6 +49,11 @@ function makeAddonApi() {
|
|||
registerAddonCategories(categories);
|
||||
refreshToolboxFn();
|
||||
},
|
||||
registerDevice(profile) {
|
||||
registerDevice(profile);
|
||||
refreshDeviceListFn();
|
||||
refreshToolboxFn();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
19
src/main.js
19
src/main.js
|
|
@ -8,9 +8,11 @@ import {
|
|||
getDevice,
|
||||
canFlashInBrowser,
|
||||
buildToolbox,
|
||||
getAllDevices,
|
||||
} from './devices/registry.js';
|
||||
import {
|
||||
setRefreshCallback,
|
||||
setDeviceListRefreshCallback,
|
||||
loadAllSavedAddons,
|
||||
installAddonFromFile,
|
||||
removeAddon,
|
||||
|
|
@ -43,6 +45,23 @@ setRefreshCallback(() => {
|
|||
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 ───────────────────────────────────
|
||||
|
||||
const codeOutput = document.getElementById('code-output');
|
||||
|
|
|
|||
Loading…
Reference in New Issue