跳转至

Mind+ 上传模式扩展开发手册

本文档将指导开发者创建适用于 Mind+ 上传模式(Arduino/Micropython)的硬件扩展。

1. 扩展包文件结构

一个标准的扩展包目录结构如下:

extensionName/                  # 扩展库根目录
├── func.js                     # 生成代码
├── index.js                    # 图形块定义主文件
├── public/                     # 静态资源目录
│   ├── cover.png               # 扩展封面图
│   └── config.json             # 扩展配置文件(元数据)
│   ├── libraries/              # Arduino 库文件目录 (可选)
│   │   └── DFRobot_xxxx        # Arduino 库文件
│   │   │   └── library.properties           # 库配置文件
├── icon/                       # 图标资源目录
│   ├── blockIcon.svg           # 积木图标
│   └── menuIcon.svg            # 分类菜单图标
├── locales/                    # 国际化语言包
│   ├── zh-cn.json              # 简体中文翻译
│   ├── en.json                 # 英文翻译
│   └── index.js                # 翻译入口文件

2. 配置文件 (public/config.json)

config.json 是扩展的核心配置文件,定义了扩展的基本信息、支持的设备以及依赖库。

示例配置

{
    "id": "test",
    "author": "yourUserId",
    "mode": "upload",
    "version": "0.0.1",
    "name": {
        "zh-cn": "I2C级联扩展器",
        "en": "I2C cascade extender"
    },
    "description": {
        "zh-cn": "用于解决I2C器件地址的冲突",
        "en": "Used to resolve I2C device address conflicts"
    },
    "cover": "cover.png",
    "isDevice": false,
    "main": "main.js",
    "libraryConfig": ["DFRobot_Mindplus_Multiplexer@1.0.0"],
    "supportDevices": {
        "dev-DFRobot-dfrobotKitRob0135": "^0.0.1",
        "dev-DFRobot-arduinoUno": "^0.0.1",
        "dev-DFRobot-microbitV2": "^0.0.1"
    },
    "supportArch": ["arduino:avr"],
    "meta": {
        "runtimeVersion": "0.0.2"
    }
}

关键字段说明

字段 类型 说明
id string 扩展唯一标识符,仅包含字母和数字,勿使用空格或特殊符号。
author string 开发者名称,仅包含字母和数字,勿使用空格或特殊符号,与id一起组成扩展唯一标识。。
version string 扩展版本号,三段数字形式。
name object 扩展库卡片标题,需多语言支持。
description object 扩展库卡片描述,需多语言支持。
cover string 封面图片文件名,需位于 public 目录下。
isDevice boolean false 表示这是一个功能扩展,true 表示这是主控板扩展。
libraryConfig array 依赖的 Arduino 库列表,格式为 库名称@版本号
supportDevices object 支持的主控板列表及其版本要求。
supportArch array (可选)仅 upload 模式:支持的主板架构列表。如 ["arduino:avr","arduino:samd"],配置后会覆盖 supportDevices。谨慎设置,一般使用指定主板方式设置兼容。
mode string 模式,此处应为 upload
meta.runtimeVersion string 扩展 runtime 接口版本(用于控制 API 兼容性)。

meta.runtimeVersion

runtimeVersion 表示开发扩展时依赖的 runtime API 最低版本。比如 runtime.getBoardPinsByType0.0.2 才支持,则使用该 API 的扩展不能在 runtime 0.0.1 的 Mind+ 中加载;但也不应随意设置一个很大的版本号,否则扩展只能在高版本中加载,兼容性会变差。

supportArch(仅 upload + isDevice: false)

supportArch 用于按“主板架构”声明兼容范围,方便小模块扩展一次适配一类主板,而不是枚举所有 supportDevices

  • 架构取自主板 boardConfig.board 的前两段:例如 "arduino:avr:uno" 的架构为 "arduino:avr"
  • supportArch 存在时,会覆盖 supportDevices
  • 谨慎设置,一般使用指定主板方式设置兼容。

3. 图形块描述文件 (index.js)

index.js 是扩展的入口文件,负责定义图形块(Block)的外观、参数以及与 Mind+ 运行时的交互。

类结构定义

import ArgumentType from '../utils/argument-type';
import BlockType from '../utils/block-type';
import DataType from '../utils/data-type';
import blockIconURI from './icon/blockIcon.svg';
import menuIconURI from './icon/menuIcon.svg';
import Func from './func'; // 引入生成代码类
import {setLocaleData, formatMessage, setLocale} from '../utils/translation';
import LocaleData from "./locales";

// 注册翻译数据
setLocaleData(LocaleData);

class Extension {
    constructor(runtime, extensionId) {
        this.runtime = runtime;
        // 初始化生成代码的实例
        this.funcs = new Func(runtime, extensionId);
    }

    // 切换语言的钩子
    setLocale(locale) {
        setLocale(locale);
    }

    // 必须实现:返回生成代码的实例
    getCodePrimitives() {
        return this.funcs;
    }

    // 必须实现:返回扩展的详细信息
    getInfo() {
        return {
            name: formatMessage({id:"ext.dfr0576.name", default:"I2C Cascade Extender"}),
            blockIconURI: blockIconURI,
            menuIconURI: menuIconURI,
            blockIconWidth: 36,
            blockIconHeight: 36,
            color1: "#EB2F96", // 主色
            color2: "#C41D7F", // 边框色
            color3: "#C41D7F", // 阴影色
            blocks: [
                {
                    opcode: 'begin', // 对应 func.js 中的函数名
                    blockType: BlockType.COMMAND,
                    text: formatMessage({
                        id: "gui.blocklyText.dfr0576.begin",
                        default: 'Initialize [ADDR]'
                    }),
                    arguments: {
                        ADDR: {
                            type: ArgumentType.STRING,
                            menu: 'iicAddrMenu',
                            defaultValue: '0x70'
                        }
                    }
                }
            ],
            menus: {
                iicAddrMenu: {
                    items: [
                        { text: '0x70', value: '0x70' },
                        { text: '0x71', value: '0x71' }
                    ]
                }
            }
        };
    }
}

export default Extension;

关键方法说明

  • getCodePrimitives(): 必须。返回包含生成代码逻辑的对象实例(即 Func 类的实例)。
  • getInfo(): 必须。定义扩展的元数据,包括名称、图标、颜色、积木列表(blocks)和菜单(menus)。
  • setLocale(locale): 必须。用于响应Mind+设置中的切换语言操作。

4. 代码生成文件 (func.js)

func.js 负责将图形块转换为实际的 Arduino C++ 代码。

核心结构

class Func {
    constructor() {
    }

    // 对应 index.js 中定义的 opcode: 'begin'
    begin(generator, block, parameter) {
        let addr = parameter.ADDR.code;

        // 添加头文件引用
        generator.addInclude('DFRobot_I2CMultiplexer.h');

        // 添加对象定义
        generator.addObject('I2CMulti', `DFRobot_I2CMultiplexer I2CMulti;`);

        // 添加 Setup 中的初始化代码
        // 第4个参数 true 表示即使有多个相同积木,也只生成一次
        generator.addSetup(`I2CMulti.begin`, `I2CMulti.begin(${addr});`, 0, true);

        // 返回 Loop 中的执行代码
        return `I2CMulti.begin(${addr});`;
    }

    // 对应 index.js 中定义的 opcode: 'selectPort'
    selectPort(generator, block, parameter) {
        let port = parameter.PORT.code;

        generator.addInclude('DFRobot_I2CMultiplexer.h');
        generator.addObject('I2CMulti', `DFRobot_I2CMultiplexer I2CMulti;`);

        // 如果 setup 代码是固定的,可以直接传入字符串
        generator.addSetup(`I2CMulti.begin`, `I2CMulti.begin(0x70);`);

        return `I2CMulti.selectPort(${port});`;
    }
}

export default Func;

Generator API 常用方法

1. 生成头文件 addInclude

addInclude(id: string, code?: string)

  • 功能:插入/覆盖头文件
  • 参数
  • id - 头文件标识符(如 <MPython.h>
  • code - 实际生成代码(可选,默认使用 #include <id>
  • 覆盖规则:相同 id 时覆盖旧代码
  • 示例
    generator.addInclude('MPython.h'); // 生成: #include <MPython.h>
    generator.addInclude('MPython.h', 'lib/MPython.h'); // 生成: #include "lib/MPython.h"
    
  • 生成效果
    // 添加的头文件
    #include "lib/MPython.h"
    
    void setup() { ... }
    void loop() { ... }
    

2. 生成 define 形式的宏 addDefine

addDefine(name: string, value: string, cover: boolean = false)

  • 功能:生成 #define
  • 参数
  • name - 宏名称(作为唯一ID)
  • value - 宏值
  • cover - 是否覆盖同名宏(默认 false
  • 示例
    generator.addDefine('LED_PIN', '3'); // 生成: #define LED_PIN 3
    generator.addDefine('LED_PIN', '5', true); // 覆盖为: #define LED_PIN 5
    
  • 生成效果
    #include ...
    // 添加的对象
    #define LED_PIN 5
    
    void setup() { ... }
    void loop() { ... }
    

3. 创建全局对象 addObject

addObject(id: string, code: string, cover: boolean = false)

  • 功能:声明全局对象(如 Servo servo;
  • 参数
  • id - 对象类型标识符(如 Servo
  • code - 实际声明代码
  • cover - 是否覆盖同名对象(默认 false
  • 示例
    generator.addObject('Servo', 'Servo servo;'); // 生成: Servo servo;
    generator.addObject('Servo', 'Servo servo1;', true); // 覆盖为: Servo servo1;
    
  • 生成效果
    #include ...
    // 添加的对象
    Servo servo1;
    
    void setup() { ... }
    void loop() { ... }
    

4. 创建全局变量 addVariable

addVariable(name: string, type: 'number' | 'string' | 'list'): string

  • 功能:声明全局变量,自动生成前缀
  • 参数
  • name - 变量名(作为ID)
  • type - 类型决定前缀:
    • numbervolatile float mind_n_<name>
    • stringString mind_s_<name>
    • listSimpleList<String> mind_l_<name>
  • 返回:生成的变量名
  • 示例
    const varName = generator.addVariable("my_float_variable", 'number');  // 生成: volatile float mind_n_my_float_variable;
    //const varName = generator.addVariable("my_string_variable", 'string'); // 生成: String mind_s_my_string_variable;
    //const varName = generator.addVariable("my_list", 'list'); // 生成: SimpleList<String> mind_l_my_list;
    
  • 生成效果
    // 变量
    volatile float mind_n_my_float_variable;
    
    void setup() { ... }
    void loop() { ... }
    

5. 创建静态常量 addConst

addConst(name: string, type: string, content: string, isArray: boolean = true, cover: boolean = false): string

  • 功能:声明常量,支持数组自动后缀
  • 参数
  • name - 常量名(同名不同内容时自动追加 _1
  • type - 常量类型(如 uint8_t
  • content - 初始化值
  • isArray - 是否为数组(默认 true,生成 name[]
  • cover - 是否覆盖(默认 false
  • 返回:实际常量名
  • 示例
    // 方法1 普通类型常量,效果:const uint8_t led = 5;
    const ledName = generator.addConst("led", "uint8_t", "5", false);
    
    // 方法2 数组类型常量,效果:const uint8_t bbcBitmap[] = {B01010,B10101,B10001,B01010,B00100};
    const bitmapName = generator.addConst("bbcBitmap", "uint8_t", "{B01010,B10101,B10001,B01010,B00100}");
    
    // name 相同、content 不同,效果:const uint8_t bbcBitmap_1[] = {B00100,B00010,B11111,B00010,B00100};
    const bitmapName1 = generator.addConst("bbcBitmap", "uint8_t", "{B00100,B00010,B11111,B00010,B00100}");
    
  • 生成效果
    // 常量
    const uint8_t led = 5;
    const uint8_t bbcBitmap[] = {B01010,B10101,B10001,B01010,B00100};
    const uint8_t bbcBitmap_1[] = {B00100,B00010,B11111,B00010,B00100};
    
    void setup() { ... }
    void loop() { ... }
    

6. 创建静态函数 addFunction

addFunction(code: string, cover: boolean = false)

  • 功能:插入函数声明/定义(函数名作为ID)
  • 参数
  • code - 完整函数代码
  • cover - 是否覆盖同名函数(默认 false
  • 示例
    generator.addFunction(`uint32_t rgbToColor(uint8_t r, uint8_t g, uint8_t b) {
      return (uint32_t)((((uint32_t)r<<16) | ((uint32_t)g<<8)) | (uint32_t)b);
    }`);
    
  • 生成效果
    // ArduinoCli会自动处理函数声明
    // 1.添加函数声明
    uint32_t rgbToColor(uint8_t r, uint8_t g, uint8_t b);
    
    void setup() { ... }
    void loop() { ... }
    
    // 2.添加函数定义
    uint32_t rgbToColor(uint8_t r, uint8_t g, uint8_t b) {
      return (uint32_t)((((uint32_t)r<<16) | ((uint32_t)g<<8)) | (uint32_t)b);
    }
    

7. 在 setup 中生成 code

addSetup(id: string, code: string, priority: number = 0, cover: boolean = false)

  • 功能:向 setup() 函数插入代码
  • 参数
  • id - 唯一标识符
  • code - 实际代码
  • priority - 优先级(0-9,9最靠前,默认 0)
  • cover - 是否覆盖(默认 false
  • 示例
    generator.addSetup("init", "mPython.begin();", 9); // 插入到 setup 顶部
    generator.addSetup("display.fillInLine", "display.fillInLine(1, 0);");
    
  • 生成效果
    void setup() {
        // setupTop (优先级 9)
        mPython.begin();
        // setup (优先级 0)
        display.fillInLine(1, 0);
    }
    

8. 在积木位置生成 code (Loop中)

  • 位置loop() 或积木当前位置
  • 规则
  • 返回字符串直接插入代码
  • 返回数组 [code, priority] 处理优先级(如运算符顺序)
  • 示例
    // 方法1 返回生成代码 (COMMAND类型)
    return `digitalWrite(${pin}, ${level});`;
    
    // 方法2 返回多条生成代码 (COMMAND类型)
    return `code1();\n\tcode2();`;
    
    // 方法3 返回数组 (REPORTER/BOOLEAN类型)
    // 第二个参数用于添加优先级,默认为 generator.ORDER_ATOMIC
    // generator.ORDER_UNARY_POSTFIX 加括号
    return [`analogRead(P0)`, generator.ORDER_UNARY_POSTFIX];
    
  • 生成效果
    void setup() { ... }
    void loop() {
        // block返回的生成代码
        digitalWrite(P0, LOW);
    }
    

9. 进阶用法

  • 添加注释 addComment(id: string, comment: any, position: number, coverage: boolean)
  • 添加错误提示 addErrorPrompt(message: string)
  • 重写内置积木执行方法
    // 重写内置 随机数block的执行方法
    operator_random(generator, block, parameter) {
        let code = `random(${parameter.FROM.code}, ${parameter.TO.code}+1)`;
        generator.addSetup(`dfrobotRandomSeed`, `dfrobotRandomSeed();`, 9);
        return ([code, generator.ORDER_UNARY_POSTFIX]);
    }
    

5. 翻译资源文件 (locales)

使用 locales/index.js 统一管理多语言资源。

locales/index.js:

// 自动加载该目录下所有 json 文件
const req = require.context('./', true, /\.json$/);
const locales = {};

req.keys().forEach((key) => {
    const name = key.match(/\/([\w-]+)\.json$/)[1];
    locales[name] = req(key);
});

export default locales;

locales/zh-cn.json:

{
    "ext.dfr0576.name": "级联扩展器",
    "gui.blocklyText.dfr0576.begin": "初始化I2C级联模块地址为 [ADDR]",
    "gui.blocklyText.dfr0576.selectPort": "选通端口 [PORT]"
}

6. 引脚规范(仅上传模式支持)

目的:将小模块与主板解耦。主板通过统一规范暴露引脚能力,小模块通过 runtime API 获取并构建积木,从而减少小模块为适配不同主板而频繁更新的问题。

6.1 引脚类型(features)

  • 模拟
  • PWM: 模拟输出,占空比变化的高低电平输出(timer 定时器,硬件 PWM)
  • ADC: 模拟输入
  • DAC: 模拟输出,连续变化的模拟电压
  • 数字
  • DIGITAL_IN
  • DIGITAL_OUT
  • I2C
  • I2C_SDA: 数据
  • I2C_SCL: 时钟
  • SPI
  • SPI_MOSI: 主 -> 从
  • SPI_MISO: 从 -> 主
  • SPI_SCK: 时钟
  • SPI_CS: 片选
  • UART
  • UART_TX: 发送
  • UART_RX: 接收
  • 中断
  • EXT_INTERRUPT: 外部中断引脚(软串口的 RX 引脚需要)
  • 可触摸的金手指
  • TOUCH_PAD
  • 内部外设占用
  • RESERVED: 内部外设占用(点阵、传感器等)

说明:按钮/LED 这类板载外设通常也作为 GPIO 开放给用户;但板载传感器、点阵等开放出去会造成困扰的外设,可以标记为 RESERVED

6.2 主板暴露引脚(Device.getPins)

主板扩展的 device.js 实现 getPins(),返回主板引脚列表与能力描述:

class Device {
    getPins() {
        return [
            {
                features: [
                    "UART_TX", "UART_RX",
                    "EXT_INTERRUPT",
                    "DIGITAL_IN", "DIGITAL_OUT",
                    "ADC", "PWM"
                ],
                text: "P0",
                value: "P0"
            },
        ];
    }
}

6.3 获取引脚(runtime.getBoardPinsByType)

小模块通过 runtime API 获取当前主板满足某类能力的引脚列表,并构建积木下拉框。

版本要求meta.runtimeVersion >= 0.0.2

使用限制:仅上传模式支持该 API

6.3.1 设备参数传递

主板在初始化过程中需要调用 getInfo(),但此时主板未成功注册,runtime 可能无法获取 device 对象,所以需要手动传入 device 对象。

// 主板扩展:初始化阶段需手动传入 device
runtime.getBoardPinsByType("DIGITAL_OUT", this.device);

// 小模块扩展:直接调用,runtime 会自动获取当前主板 device
runtime.getBoardPinsByType("DIGITAL_OUT");

6.3.2 type 类型参数说明

type 值 过滤条件 说明
ALL 返回所有引脚,不做任何过滤
DIGITAL_READ DIGITAL_IN 且非 RESERVED 数字输入引脚
DIGITAL_WRITE DIGITAL_OUT 且非 RESERVED 数字输出引脚
ANALOG_READ ADC 且非 RESERVED 模拟输入引脚
ANALOG_WRITE PWMDAC 且非 RESERVED 模拟输出引脚
自定义类型 指定 type I2C_SDAI2C_SCLUART_TXUART_RXEXT_INTERRUPT

说明: - 四种常见场景(DIGITAL_READ/DIGITAL_WRITE/ANALOG_READ/ANALOG_WRITE)会自动过滤 RESERVED(内部外设占用)引脚 - 自定义类型(如 I2C/UART/EXT_INTERRUPT)不会过滤 RESERVED,按实际能力返回

6.3.3 返回值示例

// 获取所有引脚
runtime.getBoardPinsByType("ALL", this.device);
// 返回: [{ features: ["UART_TX", "UART_RX", ...], text: "P0", value: "P0" }, ...]

// 获取数字输入引脚(排除内部占用)
runtime.getBoardPinsByType("DIGITAL_READ", this.device);
// 返回: [{ features: ["DIGITAL_IN", "DIGITAL_OUT", ...], text: "P0", value: "P0" }, ...]

// 获取 PWM/模拟输出引脚
runtime.getBoardPinsByType("ANALOG_WRITE", this.device);
// 返回: [{ features: ["PWM", ...], text: "P0", value: "P0" }, ...]

// 获取 I2C 引脚(不排除 RESERVED)
runtime.getBoardPinsByType("I2C_SDA", this.device);