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写空字符串:
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¶
I2C 读 — i2cReadOnce¶
设置 I2C 时钟 — i2cSetClock¶
i2cSetClock(frequency)
配置串口 — serialConfig¶
硬串口 ID:HW_SERIAL0–HW_SERIAL7 → 0x00–0x07
软串口 ID:SW_SERIAL0–SW_SERIAL3 → 0x08–0x0B
串口写 / 读¶
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 之间传递数据。