miot

miot 是一个基于 MQTT 协议实现的物联网平台框架。通过它,你可以快速地搭建属于你自己的物联网平台。

概述

miot 是一个基于 MQTT 协议的物联网平台框架。通过它,你可以快速地搭建属于你自己的物联网平台。通过下图,你可以大概地了解该平台的结构。

此平台分为四层,其主要名称和功能如下:

  • 视图层 : 该层给用户提供操作界面
  • 外网网关 : 外网网关连接内网网关并为用户层提供服务
  • 内网网关 : 内网网关连接配件且与外网网关直接通信
  • 配件层 : 配件通过内网网关组织起来,对外提供操作接口以及数据服务

此外,为了便于后面的叙述,下面对涉及平台相关的一些名词做出解释:

  • 视图 : 视图层的组成单元,WEB 应用,通过 MQTT 协议与外网网关相连
  • 配件 : 配件层的组成单元,终端应用,通过 MQTT 协议与内网网关相连
  • 中间件 : 与视图及配件相匹配,含两个文件,一个为视图提供服务,另一个为配件提供服务。
  • 微服务 : 中间件的别称
  • 应用 : 由视图,中间件以及配件按 1:1:1 组成,其中任何一部分都是可选的

外网网关

如果你已经安装了 npm 客户端,可以通过 npm 安装服务端程序 miot:

$ npm install miot

此服务程序即外网网关,同时它内置了视图服务以及中间件服务,下面给出的是项目的基本结构:

miot/
├── miot.js            // 主文件
├── config.json        // 配置文件
├── middle/            // 中间件目录
└── static/
    ├── views/         // 视图层目录
    └── index.html     // 视图层入口

这里仅对配置文件做些说明,对于其它内容,你只需要稍微了解就可以,当后面各章节会有详细的说明。

{
    "gw_view": {
        "http": {"port": 8080, "static": "__dirname/static"}
        //"https": { "port": 443, bundle: true, "static": "__dirname/static" }, 
        //"secure": { "keyPath": "__dirname/secure/tls-key.pem", "certPath": "__dirname/secure/tls-cert.pem" } },
    },
    "gw_local": {
        "port": 1883
        //"secure": { "port": 8443, "keyPath": "__dirname/secure/tls-key.pem",  "certPath": "__dirname/secure/tls-cert.pem" }
    },
    "logger": {
        "lever": "info",
        "appender" "default"
    }
}

上面配置中,gw_view 是提供给视图连接的配置,你可以根据需要来决定是否提供 https 服务。gw_local 是提供给内网网关连接的配置,你可以根据需要来决定是否启用 lts 安全连接。logger 是日志打印配置。

视图层与中间件

在上节给出的项目的基本结构中,视图层文件位于 /miot/static/views 目录中。其中,index.html 是主文件,旗下目录 views 存放了相关的视图目录。

在视图层主文件 index.html 的开头是外网网关的 MQTT 服务器的地址配置,你可以按需修改该内容:

<meta name="mqtt-server" content="ws://localhost:8080">

上面 content 中的 8080 即来自上节中配置文件的 port 值。当一切配置就绪后,即可使用如下命令启动项目:

$ node miot.js

项目启动后,用浏览器访问 index.html,使用初始用户名 admin 和密码 123456 即可登录后台进行配置管理。

与视图类似,中间件也依附于外网网关,位于 /miot/middles/ 目录中。中间件又叫微服务,中间件可能含两个文件,一个为视图提供服务,它实例化为一个独立的子线程,另一个为配件提供服务,它最终也实例化为一个独立的子线程。

内网网关

首先,安装内网网关服务程序 miot-local:

$ npm install miot-local

此服务程序包含了内网网关的相关文件,下面仅列出最要紧的两个文件:

miot-local/
├── miot-local.js            // 主文件
└── config.json              // 配置文件

相对于外网网关,内网网关的配置文件如下:

{
    "remote": {
        "port": 1883,                       // 若开启 tls,则使用 8443 端口
        "host": "localhost",                // 主机
        "clientId": "be1aa660-2b48-11ec-a191-4dbcbb23f97f", // 连接到外网网关的客户端标识符
        "protocol": "mqtt",                 // 若开启 tls,则使用 mqtts 并打开下面的注释
        //"rejectUnauthorized": true,       // 开启授权
        //"ca": "dir/secure/tls-cert.pem"   // 自签名证书
    },
    "gateway": {
        "port": 1883,
        //"secure": { "port": 8443, "keyPath": "dir/secure/tls-key.pem",  "certPath": "dir/secure/tls-cert.pem" }
    },
    "parts": [
        { "id": "d9ae5656-9e5e-4991-b4e4-343897a11f28", "path": "/system" },
        { "id": "35e64bc0-1268-477a-9327-94e880e67866", "path": "/player" }
    ]
}

上面配置中,remote 是连接到外网网关的配置,你可以根据需要来决定是否使用 lts 连接。gateway 是提供给内网配件连接的配置,你可以根据需要来决定是否开启 lts 安全连接。

另外,上述 parts 项是连接到内网网关的配件列表,parts 中的 path 参数用于唯一地命名配件,其描述方式类似于操作系统的文件定位。

当一切配置就绪后,即可使用如下命令启动网关服务:

$ node miot-local.js

配件层

任何支持 MQTT 客户端的软硬件平台都可以非常方便地建立配件以接入内网网关。下面仅以 Node.js 环境示例。首先安装 miot-part 模块。

$ npm install miot-part

下面一个是配件的示例,由此示例可知,配件想要连接到内网网关,提供的参数与上节中连接到外网网关的 remote 配置类似。

let xmlplus = require("miot-part");     // 模块引入
let config = {
    "port": 1883,                       // 若开启 tls,则使用 8443 端口
    "host": "localhost",
    "partId": "d9ae5656-9e5e-4991-b4e4-343897a11f28", // 连接到外网网关的客户端标识符
    "protocol": "mqtt",                 // 若开启 tls,则使用 mqtts
    //"rejectUnauthorized": true,       // 开启授权
    //"ca": "dir/secure/tls-cert.pem"   // 自签名证书
};

xmlplus("part-demo", (xp, $_) => {
$_().imports({
    Index: {
        cfg: { index: config },
        xml: "<i:Client id='index' xmlns:i='//miot-part'/>",
        fun: function (sys, items, opts) {
            this.watch("/hi/alice", (e, body)=> {
                this.trigger("to-users", "/hi/bob");
            });
        }
    }
});
}).startup("//part-demo/Index");

与中间件不同,配件是一个独立的终端应用程序,它通过 MQTT 协议与内网网关相联系。

管理后台

本章节通过介绍管理后台的使用,概要地说明如何建立平台应用。这里并不涉及更具体的细节,详细内容将由后续章节逐个展开。

概览

登录用户界面后,可以看到如下的七个与应用管理相关的模块。这些模块本身作为平台应用内置于系统。下面的图示描述了这些模块之间的依赖关系。

                +----------+
                |          |
                | 区域管理 |
                |          |
                +----+-----+
                     |
                     v
+----------+    +----+-----+    +----------+
|          |    |          |    |          |
| 用户管理 |    | 网关管理 |    | 系统状态 |
|          |    |          |    |          |
+----+-----+    +----+-----+    +----------+
     |               |
     v               v
+----+-----+    +----+-----+    +----------+
|          |    |          |    |          |
| 授权管理 | <--+ 应用管理 | <--+ 服务管理 |
|          |    |          |    |         |
+----------+    +----------+    +----------+

从图中可以看出,要创建一个平台应用,可以分四步走:

  1. 注册依赖模块,包括与目标应用相关的区域管理、网关管理以及服务管理等模块。
  2. 注册应用的目标用户
  3. 根据注册好的依赖模块,在应用管理模块中注册一个平台应用
  4. 将注册好的平台应用通过授权管理模块授权给目标客户

在上述步骤中,如果创建的应用只是管理员自己使用,步骤 2 和步骤 4 都可以省略。

区域管理

区域管理模块是网关管理模块的上一层级。在该管理模块内可以对区域进行注册、修改或者删除。如下图所示:

要注册一个区域,请点击上图所示页面的注册按钮,即可进入注册页面。要修改或者删除区域,请将目标区域向左滑动,即可看到修改和删除的操作按钮。

区域名称可以任意修改,但删除区域时需要小心,删除区域会连带删除其下一级的网关以及网关下面的所有应用,当然相关的应用授权也会一并移除。

网关管理

网关管理模块是应用管理模块的上一层级。在该管理模块内可以对网关进行注册、修改或者删除。如下图所示:

要注册一个网关,请点击上图所示页面的注册按钮,即可进入注册页面。要修改或者删除区域,请将目标区域向左滑动,即可看到修改和删除的操作按钮。

与区域不同,网关注册或者修改时都需要提供该网关所属的区域,如下图所示:

另一个与区域不同的地方在于:网关存在状态属性(离线或者在线)。如果有客户端(内网网关)以该网关标识符连接至该系统(外网网关),则该网关处于在线状态,否则该网关处于离线状态。下图是当网关离线时的提示,即在网关名后显示一个星号,当网关在线时则没有此提示。

服务管理

服务管理模块是平台应用的组成部分,在该管理模块内可以对服务进行注册、修改或者删除。如下图所示:

服务由服务名称和标识符组成,这两个成份都可以修改的,但要确保标识符是唯一的。该标识符在注册平台应用时需要用到。

每一个应用都会关联一个服务标识符,而一个服务标识符都会关联相应的视图层文件和中间件文件。

注意,为安全起见,在管理模块内对服务进行注册、修改或者删除并不影响视图层目录或者中间目录。

在服务管理的编辑页面,还可以对服务进行重启操作。如下图所示:

上面说过一个服务标识符都会关联相应的视图层文件和中间件文件。系统在启动时,会以线程的方式启动中间件文件。这里所谓的重启服务指的就是对中间件线程的重启操作。

用户管理

下图是用户管理页面。在该管理模块内可以对用户进行注册、修改或者删除。

从图中可以看出,每个用户条目包含登录系统的有效期,用户名以及最后登录时间。有效期从最后登录时间开始计算。

系统默认内置一管理员用户,其默认用户名是 admin,默认密码是 123456。对于管理员用户只能修改而不能删除。

我们再来看用户修改页面,有一个是否允许重复登录的选项。如果允许用户重复登录,那么在同一时间,就会有多个相同用户名同时在线。

应用管理

当平台应用的依赖全部建立好之后,就可以开始着手创建平台应用。该模块的主页面如下所示。

主页面列出了所有区域旗下的所有网关,想注册、修改或者删除应用,需要点开目标网关。下面是一个示例页面。

对于上面介绍的其它管理模块,应用管理包含更丰富的内容。我们现在打开注册应用的页面:

请看注册页面的类型选项,这里把平台应用分为两种类型,含配件的以及不含配件的。它们之间的区别在于,含配件的应用存在在线和离线两种状态。当平台与服务端的之间未断开连接时,不含配件的应用一直都是在线的,反映到桌面图标就是,前者离线时图标处于灰色状态,在线时图标处于高亮状态。而含配件的应图标则一直以高亮显示。

我们现在再打开平台应用的修改页面:

从上图中还可以看到,每一个平台应用都包含一个应用标识符,它是不可修改的。另外,我们还可以看到一个叫配件标识符的项目。每创建一个平台应用,都会自动生成一个可修改的配件标识符。配件标识符用于标识一个配件,在创建配件时需要用到。

授权管理

建立了用户以及平台应用后,就可以对平台应用进行授权。该模块的主页面如下所示:

授权步骤如下:

首先,选择需要授权的用户(页面的第一选项)。

其次,点开目标网关。下面是某一网关点开后的页面:

最后,将要授权的平台应用勾上或者对不想授权的平台应用取消勾选即可。

系统状态

该模块与平台应用无直接关系,但通过该模块可以查看与系统相关的运行信息。下面是一个系统状态截图:

视图

用户层落实到代码文件就是一个存放各用户界面的目录,为了便于陈述,这里再次引用前面章节中关于外网网关的项目结构图:

miot/
├── miot.js            // 主文件
├── config.json        // 配置文件
├── middle/            // 中间件目录
└── static/
    ├── views/         // 用户界面目录
    └── index.html     // 用户界面入口

注意到 miot/static/views/ 目录,该目录存放了各视图的代码文件,本章的目的就是弄清楚这个目录的来龙去脉,以及如果构建它们。

概述

视图是一个包含若干文件的文件夹,它的名称是已注册好的一个视图标识符。该文件夹包含一个名为 index.js 的主文件。下面是该文件的代码框架,其中顶层命名空间与包含目录名是一致的。组件 Index 是第一个被实例化的入口组件。最后的 if 语句是为了满足 require.js 规范而添加的。

// 02-01
xmlplus(视图标识符, (xp, $_) => {

$_().imports({
    Index: {  // 入口
    }
});

});
if ( typeof define === "function" ) {
    define( "xmlplus", [], function () { return xmlplus; } );
}

此文件夹还可以包含一个可选的名为 icon.js 图标文件,同理,其中顶层命名空间与包含目录名是一致的。前端系统会将其显示为应用图标。如果图标文件未提供,系统则使用默认图标。下面是一个图标文件的示例:

// 02-02
xmlplus(视图标识符, (xp, $_) => {

$_().imports({
    Icon: {
        xml: "<svg viewBox='0 0 1024 1024' width='200' height='200'>\
                <path d='M864.4 831.1V191.7c0-35.3...'/>\
                <path d='M161.1 383.5h703.3v63.9H1...'/>\
              </svg>"
    }
});

});

此文件夹中还可以包含图片或者其它资源文件,那么在主文件 index.js 是如何对其进行引用呢?请看下面的示例:

// 02-03
$_().imports({
    Index: {
        xml: "<div id='index' class='page-content'>\
                <h3>请扫描二维码</h3>\
                <img id='img' src='/views/视图标识符/pic.jpg'/>\
              </div>\
    }
});

从上面的代码可见,主文件对相关资源的引用方式为 /views/视图标识符/资源名。当然也可以在主文件中交叉引用其它应用的资源,但不建议这么做。

初始化

作为第一个被实例化的入口组件,当 Index 的函数项被执行时,函数的参数 opts 包含了初始化数据 name,它是该应用的名称标识。你可以把该函数项理解为面向对象编程语言的类的构造函数。

// 02-04
Index: {
    fun: function (sys, items, opts) {
        console.log(opts); // opts 含 name 选项
    }
}

在页面设计时,name 可以作为标题名置顶。当然,你也可以忽略它。

向中间件或者配件发送消息

向中间件或者配件发送消息,不需要明确指出具体的端,这是网关的工作。通过触发 publish 事件即可完成消息的发送。

// 02-05
Index: {
    xml: "<button id='index'>submit</button>",
    fun: function (sys, items, opts) {
        this.on("click", () => {
            this.trigger("publish", ["/commit", {vol: 88}]);
        });
    }
}

如上述代码所描述的,发送的内容是一个数组,数组首位是一个用于标识数据主题的字符串。数组的次位是负载,可以为空或者是一个 PlainObject 类型的对象。如果没有负载,也可以忽略该实参,那么提交语句可以下面这样写:

this.trigger("publish", "/commit");

接收来自中间件或者配件的消息

要接收来自中间件或者配件的消息,需要建立一个侦听目标主题的消息侦听器。如下面的示例所示:

// 02-06
Index: {
    xml: "<button id='index'>submit</button>",
    fun: function (sys, items, opts) {
        this.watch("/receive", (e, data) => {
            console.log(data);
        });
    }
}

消息侦听器的第二个参数是自中间件或者配件发送的负载。注意,由于用户端与中间件或者配件之间的数据交互是建立在 MQTT 协议的 QoS 1 机制上的,所以在一些应用场合应该注意消息的重入问题。

最后,给出一个非强制性的消息格式的使用建议,所有的视图、中间件以及配件之间的通信消息主题统一以返斜杆 / 开头。比如,前面示例中的 /commit/receive

中间件

与视图相对应,中间件也是一个包含若干文件的文件夹,它的名称与对应的视图的名称一致,是一个已注册好的视图标识符。中间件也不是必须的。中间件按接收信息的来源,可以分为两个模块文件 uindex.js 和 pindex.js。

接收的信息来自视图

中间件模块 uindex.js 接收的信息来自视图,下面是该文件的代码框架,其中顶层命名空间与包含目录名是一致的,即视图标识符。

// 03-01
xmlplus(视图标识符, (xp, $_) => {

$_().imports({
    Index: {
        fun: function (sys, items, opts) {
            this.watch("/from-user", (e, p) => console.log(p));
        }
    }
});

该模块接收的信息来自视图,它是一个 PlainObject 对象,其包含内容如下:

- `mid`: String 会话标识符
- `cid`: String 客户端的连接标识符
- `topic`: String 消息的主题
- `data`: AnyThing 负载

模块完成数据处理后可以回传数据给视图,回传的数据和来源内容不必相同,但必须包含 midtopic 以及 data 3 个字段且 mid 字段必须与来源的内容一致。

// 03-02
Index: {
    fun: function (sys, items, opts) {
        this.watch("/from-user", (e, p) => {
            this.trigger("to-users", {mid: p.mid, topic: "/hi", data: "Alice"});
        });
    }
}

虽然数据请求来源于一个试图,但同时在线使用该中间件服务的视图可以有多个,所以上面事例中出现的事件名采用的是复数形式: to-users

如果只想回复发送消息的视图,在回复信息中可以加入 cid 字段。否则,回复的信息会发送给使用该中间件的所有在线视图。

模块完成数据处理后也可以发送数据给配件端,发送的数据和来源内容不必相同,但必须包含下面所述的 3 个字段且 mid 字段必须与来源的内容一致。

- `mid`: String 会话标识符
- `topic`: String 消息的主题
- `body`: AnyThing 负载

下面的示例演示了如何从视图接收数据以及如何向配件端发送数据。

// 03-03
Index: {
    fun: function (sys, items, opts) {
        this.watch("/from-user", (e, p) => {
            this.trigger("to-local", {mid: p.mid, topic: "/hi", body: "Bob"});
        });
    }
}

这里解释下为什么将发送给配件端的事件命名为 to-local,而不是 to-part。因为与外网网关直连的是局域内网网关而不是配件。当然,这只是设计使然。

由于中间件不是必需的,如果目标文件 uindex.js 不存在,那么系统会试图将消息直接发送给配件端。

接收的数据来自配件端

中间件模块 pindex.js 接收的信息来自配件端,下面是该文件的代码框架,其中顶层命名空间与包含目录名是一致的,即视图标识符。

// 03-04
xmlplus(试图标识符, (xp, $_) => {

$_().imports({
    Index: {
        fun: function (sys, items, opts) {
            this.watch("/from-part", (e, p) => console.log(p));
        }
    }
});

该模块接收的的信息来自配件端,它是一个 PlainObject 对象,其包含内容如下:

- `mid`: String 会话标识符
- `topic`: String 消息的主题
- `data`: AnyThing 负载

模块完成数据处理后可以将数据回传给配件端,回传的数据和来源内容不必相同,但必须包含下面的 3 个字段且 mid 字段必须与来源的内容一致。

- `mid`: String 会话标识符
- `topic`: String 消息的主题
- `body`: AnyThing 负载

下面的示例演示了如何从配件端接收数据然后再回传给配件端。

// 03-05
Index: {
    fun: function (sys, items, opts) {
        this.watch("/from-part", (e, p) => {
            this.trigger("to-part", {mid: p.mid, topic: "/hi", body: "Alice"});
        });
    }
}

模块完成数据处理后也可以发送给视图。发送的数据和来源内容不必相同,但必须包含下面的 3 个字段且 mid 字段必须与来源的内容一致。

- `mid`: String 会话标识符
- `topic`: String 消息的主题
- `data`: AnyThing 负载

下面的示例演示了如何从配件端接收数据以及如何向视图发送数据。

// 03-06
Index: {
    fun: function (sys, items, opts) {
        this.watch("/from-part", (e, p) => {
            this.trigger("to-users", {mid: p.mid, topic: "/hi", body: "Bob"});
        });
    }
}

与来源于视图的消息不同,来源于配件的消息不含 cid 字段。所以,当来自配件的消息要发往视图,任何被授权的在线用户都可以接收到该消息。

应该注意的事项

出于设计上的考量,中间件不是一个独立的进程或者线程,为了避免 miot 主程序崩溃,在编写中间件的过程中,不应该使用未经捕获处理的异常抛出语句。或者,对于可能抛出异常的语句,应该要有捕获该异常的处理语句。

内网网关

内网网关是连接网内配件与外网网关的桥梁,同时也是内网配件之间通信的媒介。

安装

如果你已经安装了 npm 客户端,可以通过 npm 安装局域网关程序 miot-local:

$ npm install miot-local

配置文件

内网网关项目中最关键的是下面两个文件:

miot-local/
├── miot-local.js            // 主文件
└── config.json              // 配置文件

主文件是启动文件,在此着重对配置文件做些说明,下面是配置文件的一个示例:

// 04-01
{
    "proxy": {
        "port": 1883,                       // 若开启 tls,则使用 8443 端口
        "host": "localhost",                // 主机
        "clientId": "be1aa660-2b48-11ec-a191-4dbcbb23f97f", // 连接到外网网关的客户端标识符
        "protocol": "mqtt",                 // 若开启 tls,则使用 mqtts
        //"rejectUnauthorized": true,       // 开启授权
        //"ca": "dir/secure/tls-cert.pem"   // 自签名证书
    },
    "mosca": {
        "port": 1883,
        //"secure": { "port": 8443, "keyPath": "dir/secure/tls-key.pem",  "certPath": "dir/secure/tls-cert.pem" }
    },
    "parts": [
        { "id": "d9ae5656-9e5e-4991-b4e4-343897a11f28", "path": "/system" },
        { "id": "35e64bc0-1268-477a-9327-94e880e67866", "path": "/player" }
    ]
}

上面配置中,proxy 是连接到外网网关的配置,你可以根据需要来决定是否使用 lts 连接。mosca 是提供给内网配件连接的配置,你可以根据需要来决定是否开启 lts 安全连接。注意 clientId 必须已经在外网网关已经注册过的,否则连接将被拒绝。

上述的 parts 项是连接到内网网关的配件列表。配件描述中的参数 id 是配件的唯一标识符,网关只允许接入 parts 列表中存在的配件。参数 path 也用于唯一地命名配件,其描述方式类似于操作系统的文件定位。该参数存在的目的在于方便网内配件之间的访问控制。

配件

配件是一个包含了 MQTT 协议的客户端应用,任何支持 MQTT 协议的软硬件环境都可以接入配件。下面先以 Node.js 环境为例来说明如何编写配件应用。最后,再给出一个在 esp8266 上使用 lua 编程语言编写配件的示例。

安装与示例

首先需要安装 miot-part 模块:

npm install miot-part

如下面示例所示,该模块封装了 MQTT 协议的客户端,配件应用在开发时只需要引入组件 //miot-part/Client 即可以方便使用。

// 05-01
let xmlplus = require("miot-part");     // 模块引入
let config = {
    "port": 1883,                       // 若开启 tls,则使用 8443 端口
    "host": "localhost",
    "partId": "d9ae5656-9e5e-4991-b4e4-343897a11f28", // 连接到外网网关的客户端标识符
    "protocol": "mqtt",                 // 若开启 tls,则使用 mqtts
    //"rejectUnauthorized": true,       // 开启授权
    //"ca": "dir/secure/tls-cert.pem"   // 自签名证书
};

xmlplus("part-demo", (xp, $_) => {
$_().imports({
    Index: {
        cfg: { index: config },
        xml: "<i:Client id='index' xmlns:i='//miot-part'/>",
        fun: function (sys, items, opts) {
            this.watch("/hi/alice", (e, body)=> {
                this.trigger("to-users", "/hi/bob");
            });
        }
    }
});
}).startup("//part-demo/Index");

配件可以接收来自用户端或者其它局域配件发来的消息,无论是哪一种,只需要在配件里面指定相应的侦听器即可。如上例所示,该侦听器侦听了名为 /hi/alice 的消息。

同样,配件可以通过派发 to-users 事件向用户端发送消息。向用户端派发消息需要提供两个参数,如上面的示例,该配件向用户端派发了消息 /hi/bob,其中 data 是负载。下面是该 API 的格式:

trigger("publish", [messageType, data]);
  • messageType : String 消息名,比如 /hi/bob
  • data : AnyThing 负载

向内网配件发送消息则需要派发 to-parts 事件,与 to-users 事件相比,该语句会多一个 targets 参数,该参数用于指明所接受的目标配件集合。

trigger("to-parts", [targets, messageType, data]);
  • targets : String 指明所接受的目标配件集合,如 /:key
  • messageType : String 消息名,比如 /hi/bob
  • data : AnyTing 负载

为了演示内网配件之间的通信,我们需要先来看下如何描述目标配件集。

目标配件集

目标配件集的写法类似于正则表达式,其描述的字符串由开源模块 path-to-regexp 来解析,下面是一些常用的目标配件表达式的通配符:

  • 命名参数:由符号 : 加参数名来定义,如 /:key
  • 可选后缀:由符号 ? 紧跟参数定义,表示参数为可选,如 /:key?
  • 零至多个:由符号 * 紧跟参数定义,表示允许参数为零个或多个,如 /:key*
  • 一至多个:由符号 + 紧跟参数定义,表示允许参数为一个或多个,如 /:key+
  • 自定义参数:可以是任何的合法的正则表达式的字符串表示,如 /:key(\\w+)
  • 星号:星号 * 用于匹配一切子级路径,如 /:key/*

下面是一个向局域配件集发送消息的示例,该示例在接收到 /close 命令后,会向局域内所有的机器下达关闭命令。

// 05-02
Index: {
    cfg: { ... } },
    xml: "<i:Client id='index' xmlns:i='//miot-parts'/>",
    fun: function (sys, items, opts) {
        this.watch("/close", (e, body)=> {
            this.trigger("to-parts", ["/machine/*", "/close"]);
        });
    }
}

在 esp8266 上编写配件

下面的示例是用 lua 编程语言写的一个简易的配件,它连接内网网关,并向目标视图派发消息。

-- 05-03
partId = "d9ae5656-9e5e-4991-b4e4-343897a11f28"
m = mqtt.Client(partId,120)
m:on("connect",function(m)
    print("connection "..node.heap()) 
    m:subscribe(partId,0,function(m) print("sub done") end)
end )
m:on('offline', function(client) print('offline') end)
m:on('message', function(client, topic, data) 
    t = sjson.decode(data)
    message = {}
    message.pid = partId
    message.data = "hi,bob"
    message = sjson.encode(message)
    m:publish("to-gateway",message,1,1, function(client) print("sent") end)
end)
m:connect('192.168.0.1',1883,0,1)

如果要向内网配件派发消息,请添加如下语句,并把派发消息类型改为 "to-parts"。

message.targets = "/machine/*"

数据流

这一章节主要综合前面的内容,总结下配件端、外网网关、内网网关以及用户端之间的数据是如何流动的。

本章对于想深入了解平台运转机制的读者,不妨一读。下面分两个方向进行,其中涉及一些网关数据库的内部数据,读者可以自行参阅。

配件端到用户端

配件端 <=> 中间件 [=> 用户端]

  1. 配件端通过局域网关将 (pid,topic,data) 发送给外网网关。

  2. 外网网关根据 part <= pid, link <= client.id 在表 apps 中查出唯一的 mid 记录。

  3. 若存在中间件,中间件可选择回传数据 (mid,topic,body) 给配件端。或者不向用户端发送消息,那么下面几个步骤就没有了。

  4. 外网网关在授权表 auths 中根据 mid 查出已授权的用户列表。

  5. 外网网关根据步骤 4 中查到的用户列表,在状态表 status 中查找在线视图 (client_id)

  6. 外网网关将 (mid,topic,data) 发送给已查询出的在线视图。

用户端到配件端

用户端 <=> 中间件 [=> 配件端]

  1. 用户端将 (mid,topic,body) 发送给外网网关。

  2. 外网网关根据 mid 在表 parts 中查出唯一的 mid 记录。

  3. 若存在中间件,中间件可选择回传数据 (mid,topic,data) 给用户端。或者不向配件端发送消息,那么下面几个步骤就没有了。

  4. 外网网关在记录中提取 link <= client.idpart = pid

  5. 外网网关根据已经查到的 client.id(pid,topic,data) 发送给局域网关。

  6. 局域网关根据 pid(pid,topic,data) 发送给目标配件。