跳转至

Mind+ 实时模式扩展开发手册

本文档将指导开发者创建适用于 Mind+ 实时模式(Realtime)的模块扩展和舞台扩展。

模块扩展复用已有主控提供的设备接口;舞台扩展可使用 runtime 提供的系统能力。


1. 扩展包文件结构

本文介绍模块(Module)与舞台(Stage)两类扩展,Mind+ V2 扩展库基于模板仓库 mindplus-ext2-builder 开发,克隆或下载该仓库得到的目录既是开发工程,也是最终提交收录的仓库,结构如下:

ext2-rt-name/                              # 仓库名称可参考,以ext2-rt-为前缀
├── extension/                              # 扩展库源码目录
│   ├── func.js                             # 积木实时执行逻辑
│   ├── index.js                            # 图形块定义主文件
│   ├── public/                             # 静态资源目录
│   │   ├── cover.png                       # 封面图
│   |   └── config.json                     # 配置文件
│   ├── icon/                               # 图标资源目录
│   │   ├── blockIcon.svg                   # 积木图标 (建议使用白色)
│   │   └── menuIcon.svg                    # 分类菜单图标 (建议使用积木颜色)
│   └── locales/                            # 国际化语言包
│       ├── zh-cn.json                      # 简体中文翻译
│       ├── en.json                         # 英文翻译
│       └── index.js                        # 翻译入口文件 (构建时自动生成)
├── scripts/                                             
├── utils/                                                   
├── ...               
└── README.md

extension/ 及 README.md 需要自己编写,其余部分是固定的构建环境,无需修改


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

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

2.1 模块扩展示例

{
  "id": "SEN0001",
  "author": "yourUserId",
  "version": "0.0.1",
  "name": {
    "zh-cn": "超声波测距传感器",
    "en": "Ultrasonic Sensor"
  },
  "description": {
    "zh-cn": "准确测距,量程2~800cm,兼容urm及SR04超声波",
    "en": "Accurate distance detection with the range 2~800cm"
  },
  "cover": "cover.png",
  "isDevice": false,
  "mode": "realtime",
  "main": "main.js",
  "supportDevices": {
    "dev-DFRobot-arduinoUno": "^0.0.1",
    "dev-DFRobot-microbitV2": "^0.0.1"
  },
  "meta": {
    "runtimeVersion": "0.0.1"
  },
  "isSupportStage": false,
  "sku": "SEN0001"
}

2.2 舞台扩展示例

{
  "id": "udp",
  "author": "yourUserId",
  "version": "0.0.1",
  "name": {
    "zh-cn": "UDP",
    "en": "UDP broadcast"
  },
  "description": {
    "zh-cn": "让设备与设备通过UDP通信",
    "en": "UDP broadcast communication"
  },
  "cover": "cover.png",
  "isDevice": false,
  "mode": "realtime",
  "main": "main.js",
  "meta": {
    "runtimeVersion": "0.0.1"
  },
  "isSupportStage": true
}

关键字段说明

字段 必填 类型 说明
id 必填 string 扩展唯一标识符,建议仅包含字母和数字
author 必填 string 作者 ID
version 必填 string 扩展版本号
name 必填 object 多语言名称
description 必填 object 多语言描述
cover 必填 string 封面图片文件名,需位于 public 目录下
isDevice 必填 boolean 模块扩展和舞台扩展固定为 false
mode 必填 string 模式,此处应为 realtime
main 必填 string 入口文件路径,一般为编译后的 main.js
supportDevices 条件必填 object 模块扩展(isSupportStage: false)必填,声明支持的主控板及版本要求;舞台扩展省略此字段
meta.runtimeVersion 必填 string 扩展 runtime 接口版本(用于控制 API 兼容性)
isSupportStage 可选 boolean 是否可作为舞台扩展(无主控也可使用);不写时按 false 处理,舞台扩展须显式写 true
sku 可选 string 硬件产品型号,多个型号用|隔开;没有则留空
compatibilityId 可选 string Mind+V1.x用户库兼容字段;没有则留空

不需要使用可选字段时,sku、compatibilityId 写空字符串:

{
  ...
  "sku": "",
  "compatibilityId": ""
}

meta.runtimeVersion

runtimeVersion 表示开发扩展时依赖的 runtime API 最低版本,以 通用参考-Runtime接口参考 为准

不可随意设置一个很大的版本号,否则扩展只能在高版本中加载,兼容性会变差

compatibilityId(仅需兼容V1.x库时使用)

如果当前开发的库由Mind+V1.x库转换而来,或者需要在Mind+V2.x中打开Mind+V1.x包含同样用户库的项目文件,则可以使用此字段指定V1.x用户库的id。

注意:如需兼容V1.x版本扩展,需要保证V2扩展库index.js中积木的opcode与V1.x版本扩展库main.ts中对应积木的函数名称一致,并保证积木类型、数量、输入框数量和类型、兼容主板等信息保持一致,并在Mind+V1版本中加载旧扩展的所有积木保存项目,然后在Mind+V2中需要保证可以正常打开。

参考示例:
- V1用户库:https://gitee.com/liliang9693/ext-ds1302
- V2用户库:https://gitee.com/mind-plus/ext2-ds1302

格式:"compatibilityId": "[author]-[id]-thirdex"

例如:

  • 在V1的扩展库中config.json中,"id": "oled12864","author": "DFRobot",则在V2的扩展库中的config.json中设置 "compatibilityId": "dfrobot-oled12864-thirdex"
  • 另外也可以直接查看V1导出的.mpext文件名,例如 dfrobot-oled12864-thirdex-V0.0.3.mpext,不带版本号和后缀即为compatibilityId的值;或者也可以在Mind+V2中打开包含这个库的V1的项目文件,会提示“项目中存在尚未兼容的扩展: dfrobot-oled12864-thirdex”,提示语中的字段即为compatibilityId的值。

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

index.js 是扩展入口,负责定义积木外观、参数,并挂载实时执行方法。

类结构定义

模块扩展通过 runtime.getDevice() 获取已有主控提供的设备对象,再调用其硬件接口。

import ArgumentType from '../utils/argument-type';
import BlockType from '../utils/block-type';
import Func from './func';
import blockIconURI from './icon/blockIcon.svg';
import menuIconURI from './icon/menuIcon.svg';
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);
    }

    getPrimitives() {
        return this.funcs;
    }

    getInfo() {
        return {
            name: formatMessage({
                id: 'gui.extensionName.sen0001',
                default: 'ultrasonic'
            }),
            blockIconURI: blockIconURI,
            menuIconURI: menuIconURI,
            color1: '#FF9F06',
            color2: '#DD8A04',
            color3: '#BE7705',
            showStatusButton: false,
            blocks: [
                {
                    opcode: 'readUltrasound',
                    blockType: BlockType.REPORTER,
                    text: formatMessage({
                        id: 'gui.blocklyText.sen0001.readUltrasound',
                        default: 'read ultrasonic unit [UNIT] trig [TRIG] echo [ECHO]'
                    }),
                    arguments: {
                        UNIT: {
                            type: ArgumentType.STRING,
                            menu: 'unitMenu',
                            defaultValue: 'cm'
                        },
                        TRIG: {
                            type: ArgumentType.STRING,
                            menu: 'pinMenu',
                            defaultValue: '4'
                        },
                        ECHO: {
                            type: ArgumentType.STRING,
                            menu: 'pinMenu',
                            defaultValue: '5'
                        }
                    }
                }
            ],
            menus: {
                unitMenu: [
                    { text: 'cm', value: 'cm' },
                    { text: 'inch', value: 'inch' }
                ],
                // 动态菜单:items 为扩展实例上的方法名
                pinMenu: {
                    items: 'getDynamicPinMenu'
                }
            }
        };
    }

    getDynamicPinMenu() {
        const deviceId = this.runtime.getCurrentDeviceId();
        // 按当前主控返回不同引脚列表
        return ['2', '3', '4', '5'].map(pin => ({ text: pin, value: pin }));
    }
}

export default Extension;

关键方法说明

方法 作用 是否必须
getPrimitives() 返回积木实时执行方法对象(Func 实例) 必须
getInfo() 返回扩展积木元信息(名称、图标、颜色、blocks、menus) 必须
setLocale(locale) 响应 IDE 语言切换 必须

4. 积木执行文件 (func.js)

func.js 负责在实时模式下立刻执行积木逻辑。函数名必须与 index.js 中积木的 opcode 一一对应。

4.1 方法签名

opcodeName(args, util) {
    // args: 积木参数,键名与 arguments 中定义一致(如 args.PIN)
    // util: 运行时工具(如 util.yield() 用于等待串口空闲)
}
  • 入参:(args, util)
  • 返回值:同步值、Promise,或通过 util.yield() 等待下一帧重试
  • 作用:立即与硬件交互,不生成代码

4.2 模块扩展示例(含串口占用等待)

模块扩展通过 runtime.getDevice() 获取主控 Device,并建议在写串口前检查 serialWriteIsAvailable();若串口忙则调用 util.yield() 等待下一帧重试。

class Func {
    constructor(runtime, extensionId) {
        this.runtime = runtime;
        this.extensionId = extensionId;
        this.device = this.runtime.getDevice();
    }

    readUltrasound(args, util) {
        if (!this.device.isConnected()) {
            this.runtime.emit('DEVICE_NOT_CONNECTED');
            return Promise.reject();
        }

        // 串口写占用时,等待资源释放
        if (this.device.firmata.serialWriteIsAvailable()) {
            const trig = this.device.firmata.pinPortConversionTab[args.TRIG];
            const echo = this.device.firmata.pinPortConversionTab[args.ECHO];
            const isInch = args.UNIT !== 'cm';

            return (async () => {
                let val = await this.device.firmata.reportSonar(trig, echo);
                if (isInch) val = val / 2.54;
                return val.toFixed(2);
            })();
        } else {
            util.yield();
        }
    }
}

export default Func;

4.3 使用注意

  • 每个积木的 opcode 必须在 Func 中有同名方法。
  • 硬件操作前应检查 device.isConnected()。
  • 引脚名到 Firmata 引脚号的转换使用 firmata.pinPortConversionTab[pin]。
  • 并发写串口时使用 serialWriteIsAvailable() + util.yield(),避免指令冲突。
  • 异步操作返回 Promise,运行时会自动等待结果。

5. 翻译资源文件 (locales)

locales/zh-cn.json:

{
  "gui.extensionName.sen0001": "超声波测距传感器",
  "gui.blocklyText.sen0001.readUltrasound": "读取超声波 单位 [UNIT] trig [TRIG] echo [ECHO]"
}

6. Firmata 相关 API

实时模式模块扩展通过 runtime.getDevice() 获取已有主控的设备对象,并在该主控支持时通过 device.firmata 访问硬件。以下为常用标准协议与 DFRobot 私有协议说明;具体可用方法取决于目标主控提供的实现。

6.1 标准协议 API

设置引脚模式 pinMode

pinMode(pin, mode)

模式 MODES
数字输入 firmata.MODES.INPUT
数字输出 firmata.MODES.OUTPUT
模拟输入 firmata.MODES.ANALOG
PWM firmata.MODES.PWM
舵机 firmata.MODES.SERVO
移位寄存器 firmata.MODES.SHIFT
I2C firmata.MODES.I2C
单总线 firmata.MODES.ONEWIRE
步进电机 firmata.MODES.STEPPER
串口 firmata.MODES.SERIAL
内部上拉 firmata.MODES.PULLUP
忽略 firmata.MODES.IGNORE

数字读 digitalRead

digitalRead(pin) — 内部已自动设置模式。

  • 参数:pin — 引脚号
  • 返回:0 或 1
digitalRead(pin, util) {
    if (!this.device.isConnected()) return Promise.reject(formatMessage('deviceUnconnected'));
    if (this.device.firmata.serialWriteIsAvailable()) {
        return this.device.firmata.digitalRead(this.getPinNum(pin));
    } else {
        util.yield();
    }
}

数字写 digitalWrite

digitalWrite(pin, value) — 内部已自动设置模式。

  • 参数:pin,value(0 或 1)

模拟读 analogRead

analogRead(pin) — 内部已自动设置模式。

  • 参数:pin — 模拟通道值(通常取 firmata.pins[pinNum].analogChannel)
  • 返回:模拟量读数
analogRead(pin, util) {
    if (!this.device.isConnected()) return Promise.reject(formatMessage('deviceUnconnected'));
    if (this.device.firmata.serialWriteIsAvailable()) {
        return this.device.firmata.analogRead(
            this.device.firmata.pins[this.getPinNum(pin)].analogChannel
        );
    } else {
        util.yield();
    }
}

模拟写(PWM)pwmWrite

pwmWrite(pin, value) — 内部已自动设置模式。

  • 参数:pin,value(PWM 占空比,常见范围 0–255)

配置 I2C — i2cConfig

i2cConfig(options)

  • 方式 1:delay 数字(可选读延迟)
  • 方式 2:{ delay: 1000 }

I2C 写 — i2cWrite / i2cWriteReg

i2cWrite(address, registerOrData, inBytes?)
i2cWriteReg(address, register, byte)

I2C 读 — i2cReadOnce

i2cReadOnce(address, register?, bytesToRead, callback)

设置 I2C 时钟 — i2cSetClock

i2cSetClock(frequency)

配置串口 — serialConfig

serialConfig({
  portId,   // 串口 ID,见下表
  baud,     // 波特率
  rxPin,    // 仅软串口
  txPin     // 仅软串口
})

硬串口 ID:HW_SERIAL0–HW_SERIAL7 → 0x00–0x07
软串口 ID:SW_SERIAL0–SW_SERIAL3 → 0x08–0x0B

串口写 / 读

serialWrite(portId, bytes)
serialRead(portId, maxBytesToRead?, callback)

6.2 引脚转换与串口占用

// 逻辑引脚名 → Firmata 物理引脚号
const pinNum = this.device.firmata.pinPortConversionTab[pin]; // 如 'A0' → 14

// 写串口前检查是否空闲
if (this.device.firmata.serialWriteIsAvailable()) {
    // 执行读写
} else {
    util.yield(); // 下一帧重试
}

6.3 DFRobot 私有协议(节选)

基于 Firmata 扩展的私有命令,常见用途如下:

不同主控版本的方法名和参数可能不同,以当前主控实际接口为准

功能 命令码 常用方法示例
红外传感器 0x00 sendIR(type, pin, value, bit);reportIR(pin, key) ; reportOnceIR(pin)
超声波 0x01 reportSonar(trig, echo)
蜂鸣器音调 0x02 reportTone(pin, tone, beat)
引脚脉冲宽度 0x03 reportPulseIn(pin, level, timeout)
microbit 点阵 0x05 showBitmap(bitmap);showString(string);showImage(image);drawPixel(x, y, light);matrixClear();stopAnimation();setBrightness(brightness)
蜂鸣器音乐 0x09 reportTone(pin, tone, beat) ; playSound(pin, sound, flag);playNote(pin, note, beat);playNoteOff();setTicksTempo(pin, ticks, tempo);setSpeed(speed)
DHT11/DHT22 0x0D readDHT(pin, dht);reportDHT(pin, dht)
I2C 地址扫描 0x0E i2cScan(),返回 Promise,结果为扫描响应字符串
无线通信 0x10 openWireless(type);setWirelessChannel(channel);sendStrByWireless(string)
DS18B20 0x11 getDs18b20(pin),返回 Promise,结果为解析后的温度数值
电机 0x13 setMotor(index, dir, speed)
WS2812 0x14 initWs2812(pin, num, bright);setWs2812Rgb(num, color);setWs2812Rotate(num);Ws2812Clear();Ws2812SetBrightness(brightness)
OLED12864 0x16 oled12864PrintTextXY(x, y, text, textLen);oled12864imageDrawing(x, y, width, height, image, imgLen);oled12864FillScreen(color);oled12864PointDrawing(x, y);oled12864LineDrawing(x1, y1, x2, y2);oled12864ClearInLine(line)
指南针 0x17 calibrate(),启动指南针校准
加速度范围 0x19 accelerationSetRange(range)
数据上报标志 0x20 setBusy1(boo);reportMicrobitLightness(boo),属于主控通信及上报控制,模块扩展不建议自行调用
复位 0x7F 收到后恢复引脚状态并触发 reset 事件;未提供专用发送方法。reset() 发送的是标准 Firmata 复位命令 0xFF

示例(超声波 / 蜂鸣器 / 红外 / 脉冲):

// 超声波距离 (cm) 
this.device.firmata.reportSonar(trigPin, echoPin); 

// 蜂鸣器 
this.device.firmata.reportTone(pin, parseInt(tone), parseInt(beat)); 

// 红外一次读取(当前示例接口) 
this.device.firmata.reportOnceIR(pin); 

// 脉冲宽度 (us) 
this.device.firmata.reportPulseIn( 
    pin, 
    level === 'HIGH' ? 1 : 0, 
    parseInt(timeout) 
); 

// 系统运行时间 (ms),来自上报数据 
this.device.firmata.reportData.millis;


7. 舞台扩展补充说明

舞台扩展(isSupportStage: true)一般不依赖 Firmata,可使用 runtime 提供的系统能力,例如:

  • runtime.createDgram() — UDP 套接字
  • runtime.os.networkInterfaces() — 网卡信息
  • runtime.extensionStartHats(hatId) — 触发 HAT 积木事件

HAT 积木示例要点:

{
  opcode: 'udpWhenServerRecvMsg',
  blockType: BlockType.HAT,
  isEdgeActivated: false,
  shouldRestartExistingThreads: false,
  text: formatMessage({
    id: 'gui.blocklyText.udp.udpWhenServerRecvMsg',
    default: 'when udp server recv [RMSGS]'
  }),
  arguments: { /* ... */ }
}

在 func.js 中,HAT 积木通常返回 true / false 表示是否触发;可用 util.initSpecialBlockParams()、util.pushSpecialBlockParam()、util.getSpecialBlockParam() 在事件头与内部 reporter 之间传递数据。