| name | library-migration-guide |
| description | Complete guide for converting Arduino/ESP32 hardware libraries into Aily Blockly compatible format. Covers the full workflow: source analysis, block.json design, generator.js implementation, toolbox.json configuration, bus initialization (Serial/I2C/SPI), board adaptation, and packaging. |
| metadata | {"version":"4.0.0","author":"aily-team","scope":"global","agents":"mainAgent","auto-activate":false,"tags":"library,migration,conversion,block-json,generator,serial,i2c,spi,board-config"} |
Blockly Library Conversion Guide
A systematic guide for converting Arduino libraries into Aily Blockly libraries,
based on real conversion cases (ArduinoJson, OneButton, MQTT/PubSubClient, DHT, INA219, VL53L0X, etc.).
Conversion Workflow
Prerequisites
- Use the existing environment/project context first to determine whether a project is already open.
- If no project exists, use
project with action="create" to create one, then continue with the new project path.
⚠️ CRITICAL: Library Working Directory
All library files MUST be created in <projectPath>/<library-name>/, NOT in node_modules/.
- ✅ Correct:
<projectPath>/lib-grove_motor/block.json
- ❌ Wrong:
<projectPath>/node_modules/@aily-project/lib-grove_motor/block.json
The node_modules/ directory is managed by npm and is write-protected.
After creating the library locally, install it with: npm install ./<library-name>
If a file write operation returns a path/permission error, check your target path — you are likely
writing to node_modules/ instead of <projectPath>/<library-name>/. Fix the path and retry.
Do NOT attempt to bypass by using terminal commands (mkdir, echo, Out-File, etc.) to write into protected directories.
Step-by-step Process
-
Source Analysis: Analyze the Arduino library header files to identify public APIs. Classify by operation type: initialization, connection, communication, status, maintenance, quick operations.
-
Block Design: Design user-friendly blocks following block type mapping rules (see Section "Block Design Rules" below).
-
Create Library Files in <projectPath>/<library-name>/:
block.json — Block definitions
generator.js — Code generator
toolbox.json — Toolbox configuration
package.json — Library metadata
-
Copy Source Files: Use run_terminal to copy the Arduino source:
- If
src/ folder exists → copy to <projectPath>/<library-name>/src/<library-name>/
- If no
src/ folder → copy .c, .cpp, .h, .hpp files to <projectPath>/<library-name>/src/<library-name>/
-
Write README.md: First read Blockly_Library_README_Conventions.md (fetch from https://blockly.diandeng.tech/files/Blockly_Library_README_Conventions.md), then follow its format.
-
Post-conversion: Ask the user if they need help with:
- Installing:
npm i <library-path> (must specify the local library path)
- Testing the converted library
- Opening the library folder location
Library Directory Structure
library-name/
├─ block.json // Block definitions
├─ generator.js // Code generator
├─ toolbox.json // Toolbox configuration
├─ package.json // Library metadata
├─ README.md // Human-readable documentation
├─ README_AI.md // LLM-readable documentation
└─ src/
└─ library-name/ // Copied Arduino source files
Detailed Code Specification
IMPORTANT: For complete code specification with detailed examples, read the companion file
Blockly_Library_CODE_Conventions.md located in this skill's folder. It covers:
- Full block.json design rules with templates for every block type
- Complete generator.js implementation patterns with real-world examples
- toolbox.json shadow blocks and organization
- package.json configuration with board compatibility
- Board adaptation patterns and WiFi library selection
The sections below summarize the critical rules that MUST be followed.
Block Design Rules
Block Type Mapping
| Arduino Pattern | Block Type | Connection | Field Type |
|---|
| Object creation/init | Statement | prev/next | field_input (user enters new var name) |
| Global object method | Statement | prev/next | No variable field (direct call) |
| Object method call | Statement | prev/next | field_variable (select existing var) |
| Global object query | Value | output | No variable field |
| Quick operation | Statement/Value | standard | No variable field, direct params |
| Event callback | Hat block | No prev/next | field_variable + input_statement |
| Conditional callback | Hybrid | prev/next | input_value + input_statement |
| Status query | Value | output | field_variable |
field_input vs field_variable
field_input: For initialization blocks — user enters a NEW variable name
field_variable: For method call blocks — user selects an EXISTING variable (set variableTypes and defaultType)
- Global objects (Serial, WiFi, Wire, SPI, httpUpdate, SPIFFS, ESP, EEPROM): No variable field needed
Reading Variable Names in generator.js
const varName = block.getFieldValue('VAR') || 'defaultVar';
const varField = block.getField('VAR');
const varName = varField ? varField.getText() : 'defaultVar';
const serialPort = block.getFieldValue('SERIAL') || 'Serial';
Board Config Template Variables (block.json)
Use these in field_dropdown options — auto-populated at runtime:
| Variable | Usage |
|---|
${board.i2c} | I2C interface list (Wire selector) |
${board.digitalPins} | Digital pin list |
${board.analogPins} | Analog pin list |
${board.serialPort} | Serial port list |
${board.serialSpeed} | Baud rate list |
${board.interruptPins} | Interrupt pin list |
${board.interruptMode} | Interrupt mode list |
Extensions
Register dynamic extensions in generator.js. Always unregister before registering:
if (Blockly.Extensions.isRegistered('ext_name')) {
Blockly.Extensions.unregister('ext_name');
}
Blockly.Extensions.register('ext_name', function() { });
Code Generation Rules
Injection Methods & Execution Order
All injection methods take (tag, code) and auto-deduplicate by tag.
#include <Lib.h>
#define MACRO val
Type globalVar;
MyClass obj;
void helper() {}
void setup() {
Serial.begin(9600);
sensor.begin();
attachCb(handler);
}
void loop() {
btn.tick();
}
Bus Initialization (MANDATORY)
Serial — Always use ensureSerialBegin(), NEVER write Serial.begin() directly:
ensureSerialBegin('Serial', generator);
ensureSerialBegin('Serial', generator, 115200);
ensureSerialBegin(serialPort, generator, baud);
generator.addSetupBegin('serial_begin', 'Serial.begin(9600);');
I2C — Use wire_${wireName}_begin key for deduplication:
const wire = block.getFieldValue('WIRE') || 'Wire';
generator.addLibrary('Wire', '#include <Wire.h>');
const wireBeginKey = `wire_${wire}_begin`;
if (!generator.setupCodes_ || !generator.setupCodes_[wireBeginKey]) {
generator.addSetup(wireBeginKey, wire + '.begin();\n');
}
SPI — Use spi_${spiName}_begin key for deduplication:
const spi = block.getFieldValue('SPI') || 'SPI';
generator.addLibrary('SPI', '#include <SPI.h>');
generator.addSetup(`spi_${spi}_begin`, spi + '.begin();\n');
Variable Management
Initialization blocks with field_input MUST implement a rename listener:
if (!block._varMonitorAttached) {
block._varMonitorAttached = true;
block._varLastName = block.getFieldValue('VAR') || 'defaultVar';
registerVariableToBlockly(block._varLastName, 'VarType');
const varField = block.getField('VAR');
if (varField) {
const orig = varField.onFinishEditing_;
varField.onFinishEditing_ = function(newName) {
if (typeof orig === 'function') orig.call(this, newName);
const ws = block.workspace || Blockly.getMainWorkspace?.();
const oldName = block._varLastName;
if (ws && newName && newName !== oldName) {
renameVariableInBlockly(block, oldName, newName, 'VarType');
block._varLastName = newName;
}
};
}
}
Generator Return Values
- Statement blocks:
return 'code;\n';
- Value blocks:
return [expr, generator.ORDER_ATOMIC];
- Hat / event blocks:
return ''; (empty string — event-driven, not in main flow)
- Hybrid blocks:
return 'conditional_code;\n'; (returned code runs inside parent callback)
valueToCode & ORDER Constants
const val = generator.valueToCode(block, 'INPUT', generator.ORDER_ATOMIC) || '0';
return [varName + '.read()', generator.ORDER_FUNCTION_CALL];
Board Adaptation
Access runtime board config via window['boardConfig']:
const boardConfig = window['boardConfig'];
NEVER modify window['boardConfig'] directly — use independent storage like window['customXxx'].
toolbox.json Rules
All input_value slots MUST have shadow blocks:
{
"kind": "block",
"type": "sensor_read",
"inputs": {
"TIMEOUT": {"shadow": {"type": "math_number", "fields": {"NUM": 1000}}}
}
}
Organize blocks by user cognitive flow using label separators:
{
"kind": "category",
"name": "SensorLib",
"contents": [
{"kind": "label", "text": "Setup"},
{"kind": "block", "type": "sensor_init"},
{"kind": "label", "text": "Read Data"},
{"kind": "block", "type": "sensor_read"}
]
}
package.json Configuration
{
"name": "@aily-project/lib-libname",
"nickname": "Display Name",
"description": "Brief description (<50 chars)",
"version": "1.0.0",
"compatibility": {
"core": [],
"voltage": [3.3, 5]
},
"keywords": ["aily", "blockly"],
"tested": true,
"url": "original library URL"
}
Board compatibility shorthand:
- Universal:
"core": [] (empty array = all boards)
- ESP32 only:
"core": ["esp32:esp32"]
- Classic Arduino:
"core": ["arduino:avr", "arduino:megaavr"]
- IoT boards:
"core": ["esp32:esp32", "esp8266:esp8266", "renesas_uno:unor4wifi"]
Common Anti-patterns