diff --git a/public/atmega328p-addon.js b/public/atmega328p-addon.js new file mode 100644 index 0000000..9e5bbe3 --- /dev/null +++ b/public/atmega328p-addon.js @@ -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!' } } }, + }, + }, + ], + }, + ], +}); diff --git a/src/addons/loader.js b/src/addons/loader.js index b6df19d..e7ea86b 100644 --- a/src/addons/loader.js +++ b/src/addons/loader.js @@ -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(); + }, }; } diff --git a/src/main.js b/src/main.js index b3c53a5..6669661 100644 --- a/src/main.js +++ b/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