前言

Mind+从V1.6.5开始开放实时模式用户库,内部兼容Scratch扩展语法,,本文档将介绍如何开发Mind+实时模式用户库,阅读此文档需要你掌握JavaScript的基础知识以及了解一些ES6的新特性。

如果你在阅读过程中产生疑惑, 请加入官方交流群(671877416)。

在开发之前, 你需要知道能做出什么东西.

准备

开发包说明


---


开发流程

准备工作你已经做好了吧! 跟着下面的步骤, 一步一步创建属于你的用户库.

1.导入Mind+

接下来介绍和尝试修改这个库。

2. 配置用户库

   ==template 文件夹==是一个用户库模板, 在这里配置你的用户库

  1. 配置config.json

    调整config.json中的字段为自己想修改的内容。

{
    "name": {
        "zh-cn": "示例",
        "en": "Example"
    },
    "description": {
        "zh-cn": "示例程序的描述",
        "en": "Description of the example"
    },
    "author": "dfrobot",
    "email": "xxxx@dfrobot.com",
    "license": "MIT",
    "isBoard": false,
    "id": "example0729",
    "version": "0.0.1",
    "platform": ["win"],
    "asset": {
        "javascript": {
            "version": "0.0.1",
            "dir": "javascript/",
            "board": ["arduino", "microbit"],
            "main": "main.js"

        }
    }
}
  1. 添加扩展图片 将图片复制到到template/_images文件夹, ==必须命名为featured.png==(600372, png格式) *没有图片, 则显示默认图片**

  2. 添加积木icon 将svg格式icon复制到到template/_images文件夹, ==必须命名为icon.svg==,推荐在https://www.iconfont.cn/[iconfont](https://www.iconfont.cn/ "iconfont")下载白色的icon文件。

3. 定义扩展(Extension)

template/javascript/index.js 文件中编辑你的代码.

扩展可以被定义成一个class, 必须有一个getInfo函数.

class YourExtension {
    constructor(runtime) {
        this.runtime = runtime;
    }

    getInfo () {
        // ...
    }
}
module.exports = YourExtension;

getInfo返回一个包含积木块和扩展本身配置信息的对象

import blockIconURI from './image/icon.svg';
class YourExtension {
    // ...
    getInfo () {
        return {
            name: 'Extension Test',
            blockIconURI: blockIconURI,
            blocks: [
                // ...
            ]
        }
    }
}

getInfo 可返回的参数:

定义积木块

const ArgumentType = require('./extension-support/argument-type');
const BlockType = require('./extension-support/block-type');
import blockIconURI from './image/icon.svg';
class YourExtension {
    // ...
    getInfo () {
        return {
            // ...
            blocks: [
                {   
                    opcode: 'myAdd',
                    blockType: BlockType.REPORTER,
                    // block描述: []包裹的是参数
                    text: '[NUM1] + [NUM2]',
                    arguments: {
                        NUM1: {
                            // 参数类型
                            type: ArgumentType.STRING,
                            // 默认显示的值
                            defaultValue: 1
                        },
                        NUM2: {
                            type: ArgumentType.STRING,
                            defaultValue: 2
                        }
                    }
                }
            ]
        }
    }
}
module.exports = YourExtension;

还要为每个opcode定义一个功能同名函数,积木被执行的时候调用此函数:

class YourExtension {
    // ...
    myAdd(args) {
        return parseInt(args.NUM1) + parseInt(args.NUM2);
    }
}

blockType: 积木类型详细文档. ArgumentType: 参数类型详细文档.

定义下拉菜单(menu)

return {
    // ...
    blocks: [
        {   
            // ...
            arguments: {
                WEEK: {
                    type: ArgumentType.STRING,
                    menu: 'week',
                    defaultValue: '+'
                }
            }
        }
    ],
    menus: {
        week: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',]
    }
}

当下拉菜单支持多国语言, 下拉菜单的显示会随用户的语言环境改变, 但实际的值必须保持不变.

return {
    // ...
    menus: {
        week: [
            {
                text: formatMessage('Sunday')
                value: 'Sunday'
            },
            {
                text: formatMessage('Monday')
                value: 'Monday'
            },
            {
                text: formatMessage('Tuesday')
                value: 'Tuesday'
            },
            // ...
        ]
    }
}

完整的例子请参考examples/下的例子.

4. 打包导入

开发包中环境已经配置好了, 直接使用即可.

  1. 编译。每次修改代码后均需要重新编译,在cmd命令行或vscode终端下运行npm run build

    如果编译后没有生成build目录说明有错误产生请查看错误信息调整代码。
    小技巧:输入过一次命令后,再次按键盘上键即可自动填充历史命令。

  2. 导入。编译完成后会在build目录下生成一个用户库,在Mind+中导入build目录下项目中的config.json,这里前面已经导入则点击刷新即可。


---


积木类型

HAT 帽子型

class HatBlcok {
    // ...
    getInfo () {
        return {
            // ...
            blocks: [
                {
                    opcode: 'myEvent',
                    blockType: BlockType.HAT,
                    // 默认值为true
                    isEdgeActivated: false,
                    shouldRestartExistingThreads: false,
                    // ...
                }
            ]
        }
    }
    myEvent (args) {
        // ...
        // 返回true: 运行该事件头下的积木块
        return true;
    }
}

isEdgeActivated: 如果为true, runtime每隔17ms调用一次myEvent函数; 如果为false, 必须调用 runtime.extensionStartHats 函数触发myEvent.

shouldRestartExistingThreads: 如果为false, 该事件头下的积木块正在运行时, 不会被下一次事件触发所打断.

BOOLEAN 菱形

布尔类型, 返回布尔值

REPORTER 圆形

报告类型, 返回 StringNumber

COMMAND 方形

命令类型, 没有返回值.

输入控件类型

STRING 文本

代码示例:

    // ...    
    STRING: {
        type: ArgumentType.STRING,
        defaultValue: "文本输入框",        
    }

参数说明:
type:必填。固定格式;
defaultValue: 可选。默认显示内容;

NUMBER 数字

代码示例:

    // ...        
    NUMBER: {
        type: ArgumentType.NUMBER,
        defaultValue: 123,        
    }

参数说明:
type:必填。固定格式;
defaultValue: 可选。默认显示内容;

RANGE 带输入范围的数字

代码示例:

COL2:{
   type: ArgumentType.RANGE,
   defaultValue: 123,
   inputParams:{
   rangeMax:200,
   rangeMin:100
   }

}

参数说明:
type:必填。固定格式;
defaultValue: 可选。默认显示内容;

BOOLEAN 布尔值

代码示例:

    // ...    
    BOOLEAN: {
         type: ArgumentType.BOOLEAN
    }

参数说明:
type:必填。固定格式;

下拉框

代码示例:

    const menu_test = [["QQ", "1"],["WW", "2"],["EE", "3"], ["RR",  "4"]];
    // ...    
    MENUDROP: {
        type: ArgumentType.STRING,
        onlyField: true,
        options: menu_test,
        defaultValue: menu_test[1][1]
    }

参数说明:
type:必填。固定格式;
onlyField: 必填。固定为true;
options:下拉框数据,数组。前者为显示内容,后者为对应的值(数字类型请使用引号);
defaultValue :默认选中的数据;

可拖入下拉框

代码示例:

    return {
    // ...
    blocks: [
        {
            // ...
            MENUDROPROUND: {
                type: ArgumentType.NUMBER,
                menu: 'exMenu',
                defaultValue: b
            }
        }
    ],
    menus: {
        exMenu: {
            items: [{text: "选项1" ,value:'a'}, {text: "选项2" ,value:'b'}, {text: "选项3" ,value:'c'}]
        }
    }
}

参数说明:
type:必填。固定格式;

ANGLE 角度

代码示例:

    // ...    
    ANGLE: {
         type: ArgumentType.ANGLE,
         defaultValue: 90
    }

参数说明:
type:必填。固定格式;
defaultValue:可选。默认参数;

COLORPICKER 取色器

代码示例:

    // ...    
    COLORPICKER: {
        type: ArgumentType.COLORPICKER,
        defaultValue: "#FF0000"
    }    

参数说明:
type:必填。固定格式;
defaultValue:可选。默认颜色,颜色格式为十六进制;

COLORPALETTE 色板

代码示例:

    // ...    
    COLORPALETTE: {
        type: ArgumentType.COLORPALETTE,
        defaultValue: "#0000FF",
        inputParams: {
            //colours: ["#fff", "#f00", "#0f0", "#00f", "#ff0", "#0ff", "#f0f", "#000"],
            columns: 6
        },
    }

参数说明:
type:必填。固定格式;
defaultValue:可选。默认颜色,颜色格式为十六进制;
inputParams:

colours:可选。可以选择的颜色数组,颜色格式为十六进制。默认为mind+提供的色块。
columns:可选。每行显示的色块个数。

NOTE 内置琴键

代码示例:

    // ...    
    PIANO: {
        type: ArgumentType.NOTE,
        defaultValue: 60,
    }

参数说明:
type:必填。固定格式;

defaultValue:可选。默认值。

PIANO 钢琴键

参数说明:
type:必填。固定格式;9.琴键

代码示例:

    // ...    
    PIANO: {
        type: ArgumentType.PIANO
    }

参数说明:
type:必填。固定格式;

MATRIXICONS 点阵

代码示例:

    const matrixList = [{matrix: "0101011111111110111000100", value: "HEART", isBuiltIn: true }];
    // ...    
    MATRIXICONS: {
        type: ArgumentType.MATRIXICONS,
        defaultValue: "0101010101100010101000100",
        inputParams: {
            colour: "#ff0000",
            row: 8,
            column: 8,
            isSplit: false,
            builtinMatrixs: matrixList
        }
    }

参数说明:
type:必填。固定格式;
defaultValue:必填。点阵列表中,1表示点亮0熄灭;
inputParams:

color: 可选。控件默认的底色;
row: 可选。点阵的列数,取值范围为[5, 16],默认为5;
column: 可选。点阵的行数,取值范围为[5, 32],默认为5;
isSplit:可选。按列数平分,左右隔开显示,true或false;
builtinMatrixs:可选。控件内置的点阵。使用mind+默认提供的图案(V1.7.1以上版本),参数固定为arguments[0]。如需设置为自定义的点阵,具体格式参照代码中的matrixList; builtinFunc:可选。控件内置的点阵函数。如设置,需返回数组,格式与matrixList一致;

SETTINGS 设置框

代码示例:

   const getComponentInfoForLoad = ()=>{
        return {
            //icon: "SETTING.SVG",
            content: [
                {                   
                    description: "名字",
                    saveId: 'name_id',
                    type: 'text',
                    value: '张衡',
                    IFAdd: true,
                    addLength: 6
                },
                {                   
                    description: "年龄",
                    saveId: 'age_id',
                    type: 'text',
                    value: 23
                }
            ]
        }
    }
    // ...    
    SETTING: {
        type: ArgumentType.SETTINGS,
        inputParams: {
            componentInfo: getComponentInfoForLoad(),
            color: "#FF0000"
        }
    }

参数说明:
type:必填。固定格式;
inputParams:

componentInfo: 必填。函数,返回值为文本框结构描述。
color: 控件的基础色。

函数getComponentInfoForLoad说明:
icon: 可选。控件图标,传入格式为base64或url,仅支持svg,jpg和svg图片。默认为mind+提供的齿轮图标。
content: 必填。包含的输入框,数组中的元素描述文本框。

description: 文本框名称;
saveId: 必填。可根据id获取对应输入框的值;
type: 必填。文本框类型,支持text,password,file(文件夹), single_file(文件),number和textarea;
value: 默认值;
IFAdd: 可选。控制增加输入框,默认为false。 addLength: IFAdd为true时必填。控制增加输入框的最多个数。

设置框控件返回各元素ID与值组成的JSON格式的字符串。

API

1.runtime

runtime 提供了积木运行、获取内置主板和串口的方法.

  1. runtime.getLocale()

    • 返回: <string>

    获取 Mind+ 当前设置的语言环境.

    console.log(runtime.getLocale()); // 'en' 'zh-cn' ...

    在使用时将其绑定到 formatMessage 就可以了.

    formatMessage = formatMessage.bind(null, runtime.getLocale);
  2. runtime.extensionStartHats(requestedHatOpcode, optMatchFields, optTarget)

    • requestedHatOpcode <string>idopcode 构成
    • optMatchFields <object> 传递给block的参数, 若没有参数则为空
    • optTarget <string> 运行指定target中的block, 若为空, 运行所有target中的block(精灵和舞台都作为target)

    触发HAT(帽子形状)积木块的运行

    {
        id: 'extensionTest1',
        ...
        blocks: [
            {  
                // 事件类型
                opcode: 'whenReceive',
                blockType: BlockType.HAT,
                ...
                arguments: {
                    TEXT: {
                        ...
                    }
                }
            }
        ]
    }
    ...
    runtime.extensionStartHats(`${this.getInfo().id}.whenReceive`, {TEXT: '123'})

    获取id的方法 考虑到用户命名id会有重名的情况, 所以 Mind+ 在内部生成一个唯一id, 并通过构造函数传参以及重新封装getInfo方法.

    this.getInfo().id

    或:

    class YourExtension {
        constructor (runtime, id) {
            ...
        }
    }
  1. runtime.getBoardName()

    • 返回: <string> 当前选择的主板或套件名称, 若没有返回''
    console.log(runtime.getBoardName()); 
    // 'microbit' 'maqueen' 'arduino' 'esp32' 'maixduino' 'arduinonano'
    // 'leonardo' 'arduinounor3' 'max' 'maxbot' 'romeo' 'vortex'
    // 'mega2560' 'firebeetleesp32' 'telloesp32' 'calliope'
  2. runtime.getBoard()

    • 返回: <object> 当前选择的主板或套件对象, 若没有返回null
  1. runtime.isBuildinBoardConnected()

    • 返回: <boolean> 当前主板或套件是否连接
  1. runtime.requestBreakThreads()util.yield() 配合使用, 跳过本次执行, 再下一次事件循环继续执行

    doSomething () {
        if (...) { 
            do ... // 串口未被占用, 执行某些操作
        } else { // 串口被占用
            util.yield(); // yield出去, 等待下次事件循环执行
            runtime.requestBreakThreads();
        }
    }

    底层会以17ms的间隔循环扫描工作区的所有block, 运行符合条件(被点击/事件被触发)的顶部block, 当遇到yieldpromise时, 就暂停执行该block后面的程序, 等待下一次循环再来判断条件是否达成, 执行后面的block或继续等待下一次循环.

  1. runtime.registerPeripheralExtension(extensionId, extension)

    • extensionId <string> 扩展的唯一id
    • extension <object> 外设对象

    注册外设对象, 该对象必须包含连接处理的相关方法

    class YourExtension {
        constructor (runtime, id) {
            runtime.registerPeripheralExtension(id, extension);
            ...
        }
    }

    extension对象必须包含scan, connect, disconnect, isConnected四个方法

    class Extension {
        constructor (runtime, id) {
            runtime.registerPeripheralExtension(id, extension);
            ...
        }
        scan () {
            // 扫描设备
            ...
            // 扫描到的设备列表
            const peripherals = [{
                name: `xxx`, // 显示的名称
                peripheralId: 'xxxx' // 传递的参数
            }, ...]
            this._runtime.changePeripheralStatus(PERIPHERAL_LIST_UPDATE, peripherals);
        }
        connect (peripheralId) {
            // peripheralId 接口的id(COM2 ...)
            ...
        }
    
        disconnect () {
            // 断开当前设备的连接
            ...
        }
    
        isConnected () {
            // 当前设备是否连接
            ...
        }
    }
  2. runtime.getSerialport()

    • 返回: <object> 返回 Mind+ 中内置的 serialport 对象 Mind+ 中内置的 serialport5.0.0 版本

    详细文档

  1. runtime.changePeripheralStatus(event, ...args)

    • event <object> 触发相应事件, 改变设备连接状态
      • 'PERIPHERAL_CONNECTED' 设备已连接
      • 'PERIPHERAL_DISCONNECTED' 设备断开连接
      • 'PERIPHERAL_SCAN_TIMEOUT' 扫描设备超时
      • 'PERIPHERAL_LIST_UPDATE' 更新设备列表
      • 'PERIPHERAL_REQUEST_ERROR' 设备连接出错

    详细用法查看 /examples/util-serialport 例程

2.board对象

  1. board.digitalRead(pin)

    • pin <number> 引脚
    • 返回: <number> 高电平为1 低电平为 0
  1. board.analogRead(analogPin)

    • analogPin <number> A00
    • 返回: <number>
  1. board.digitalWrite(pin, value)

    • pin <number> 引脚
    • value <number> 高电平为1 低电平为 0
  1. board.analogWrite(pin, value)

    • analogPin <number> A00
    • value <number>
  1. board.serialWriteIsAvailable()

    • 返回: <boolean> 串口写是否被占用 当两段积木块同时运行且都有串口操作时, 就会发生混乱, 所以需要 serialWriteIsAvailablesetSerialWriteBusy 来保证串口写互斥使用.
  1. board.setSerialWriteBusy(flag)

    • flag <boolean> 设置串口写被占用, true 为占用, false 解除占用
  1. board.i2cConfig(options)

    • options <object>
      • delay <number>
      • address <number>
    board.i2cConfig(); // 可以忽略参数
  1. board.i2cWrite(address, registerOrData, inBytes)

    • address <number> i2c地址
    • registerOrData <number|array> 寄存器地址或数据buf
    • inBytes <array>

    i2c 写数据

    board.i2cConfig(); // 可以忽略参数
    board.i2cWrite(addr, reg/*寄存器地址*/, [0x01, 0x02, ...]);
    // 或:
    board.i2cWrite(addr, [0x01, 0x02, ...]);
  1. board.sendI2CReadRequest(address, numBytes, callback)
    • address <number> i2c地址
    • numBytes <number> 请求的字节数
    • callback <function> 收到数据的回调函数
 ```JavaScript
 board.i2cConfig(); // 可以忽略参数
 board.sendI2CReadRequest(addr, 8, (data) => {
     ...
 });
 ```
  1. board.i2cWriteReg(address, register, byte)
    • address <number> i2c地址
    • register <number> 寄存器地址
    • byte <number> 写入的字节
  1. board.i2cReadOnce(address, register, bytesToRead, callback)

    • address <number> i2c地址
    • register <number> 寄存器地址
    • bytesToRead <number> 请求的字节数
    • callback <function> 收到数据的回调函数
  2. board.i2cSetClock(frequency)

    • frequency <number> 时钟频率

翻译

如何使用翻译

  1. 导入翻译模块
  import * as translation from './translation/index';
  let { setLocaleData, formatMessage } = translation;

这种导入方式是为了给 formatMessage 绑定参数

  1. 初始化翻译内容 setLocaleData(localesData) 初始化翻译内容 localesData 结构

     {
         "en": {
             "say": "hello"
         },
         "zh-cn": {
             "say": "你好"
         }
     }
  2. formatMessage 翻译 formatMessage(getLocale, message)

    • 第一个参数是获取当前语言环境的方法, 第二个参数是待翻译的文本对象, 如: {"id": "say", "default": "hello"}

    • 但是每次传入getLocale方法不太方便, 所以推荐使用前先绑定getLocale formatMessage = formatMessage.bind(null, getLocale)

翻译例子

有少量语句需要翻译
import BlockType from './extension-support/block-type';
// 导入翻译模块的方法
let { setLocaleData, formatMessage } = require('./translation/index');

// 初始化翻译内容
setLocaleData({
    "en": {
        "extensionName": "Extension Test",
        "say": "hello!"
    },
    "zh-cn": {
        "extensionName": "扩展测试",
        "say": "你好!"
    }
});

class YourExtension {
    constructor(runtime) {
        this.runtime = runtime;
        // 绑定获取语言环境的方法, 这个步骤不可缺少
        formatMessage = formatMessage.bind(null, runtime.getLocale);
    }

    getInfo () {
        return {
            id: 'extensionTest',
            name: formatMessage({
                id: 'extensionName',
                default: 'Extension Test' // 默认值, 没有翻译时返回此默认值, 可忽略
            }),
            blocks: [
                {
                    opcode: 'say',
                    text: formatMessage({
                        id: 'say',
                        default: 'hello'
                    }),
                    blockType: BlockType.REPORTER,
                }
            ],
        }
    }

    say () {
        return 'hello';
    }
}
export default YourExtension;
有很多语句需要翻译

当有很多语句需要翻译时, 这种添加翻译语句的方式就不适用了, 不方便管理翻译文件

setLocaleData({
    en: {
        'extensionName': 'Extension Test',
        'say': 'hello!'
    },
    zh: {
        'extensionName': '扩展测试',
        'say': '你好!'
    }
});

推荐下面的用法:

    en.json

{     'extensionName': 'Extension Test',     'say': 'hello!' }

    zh-cn.json

{     'extensionName': '扩展测试',     'say': '你好!' }

// 导入translation/locales/index.js
import locales from './translation/locales';
// 初始化翻译内容
setLocaleData(locales);

语言环境(locales)

const locales = {
    'ab': {name: 'Аҧсшәа'},
    'ar': {name: 'العربية'},
    'am': {name: 'አማርኛ'},
    'az': {name: 'Azeri'},
    'id': {name: 'Bahasa Indonesia'},
    'be': {name: 'Беларуская'},
    'bg': {name: 'Български'},
    'ca': {name: 'Català'},
    'cs': {name: 'Česky'},
    'cy': {name: 'Cymraeg'},
    'da': {name: 'Dansk'},
    'de': {name: 'Deutsch'},
    'et': {name: 'Eesti'},
    'el': {name: 'Ελληνικά'},
    'en': {name: 'English'},
    'es': {name: 'Español'},
    'es-419': {name: 'Español Latinoamericano'},
    'eu': {name: 'Euskara'},
    'fa': {name: 'فارسی'},
    'fr': {name: 'Français'},
    'ga': {name: 'Gaeilge'},
    'gd': {name: 'Gàidhlig'},
    'gl': {name: 'Galego'},
    'ko': {name: '한국어'},
    'hy': {name: 'Հայերեն'},
    'he': {name: 'עִבְרִית'},
    'hr': {name: 'Hrvatski'},
    'zu': {name: 'isiZulu'},
    'is': {name: 'Íslenska'},
    'it': {name: 'Italiano'},
    'ka': {name: 'ქართული ენა'},
    'sw': {name: 'Kiswahili'},
    'ht': {name: 'Kreyòl ayisyen'},
    'ku': {name: 'Kurdî'},
    'ckb': {name: 'کوردیی ناوەندی'},
    'lv': {name: 'Latviešu'},
    'lt': {name: 'Lietuvių'},
    'hu': {name: 'Magyar'},
    'mi': {name: 'Māori'},
    'nl': {name: 'Nederlands'},
    'ja': {name: '日本語'},
    'ja-Hira': {name: 'にほんご'},
    'nb': {name: 'Norsk Bokmål'},
    'nn': {name: 'Norsk Nynorsk'},
    'uz': {name: 'Oʻzbekcha'},
    'th': {name: 'ไทย'},
    'km': {name: 'ភាសាខ្មែរ'},
    'pl': {name: 'Polski'},
    'pt': {name: 'Português'},
    'pt-br': {name: 'Português Brasileiro'},
    'rap': {name: 'Rapa Nui'},
    'ro': {name: 'Română'},
    'ru': {name: 'Русский'},
    'sr': {name: 'Српски'},
    'sk': {name: 'Slovenčina'},
    'sl': {name: 'Slovenščina'},
    'fi': {name: 'Suomi'},
    'sv': {name: 'Svenska'},
    'vi': {name: 'Tiếng Việt'},
    'tr': {name: 'Türkçe'},
    'uk': {name: 'Українська'},
    'zh-cn': {name: '简体中文'},
    'zh-tw': {name: '繁體中文'}
};

---


常见问题