术语 | 描述 |
---|---|
(菜单)条目,item | dtalk设备端菜单数据组织的基本单元 |
(参数)选项,option | 存储指定数据类型的数据条目对象,没有子条目 |
(设备)命令,cmd | 设备端执行的指定动作的条目,可以包含一个或多个由option对象描述的参数,可以有返回值 |
菜单,menu | 包含一个或多个opton或cmd的菜单条目 |
菜单命令 | 由管理端发送的一个item |
以下为上面术语中在对应的java类,及层次关系图:
我们希望通过管理软件向前端设备发指令做一些事性,比如开门,比如升级设备端的软件版本,比如汇报当前设备状态,这些事情都可以定义为设备命令。管理端通过dtalk,发送设备命令给前端,前端收到命令并执行然后返回执行结果。这就是基本的设备命令发送执行流程。
dtalk就是用来定义整个设备命令发送和执行的一个开发框架,是管理端和设备端和对话协议框架,所以叫它device talk,
dtalk是基于redis的频道发布订阅功能来实现管理端和设备端的交互通信。管理端和设备端之间建立通信,就是在redis上注册侦听两个消息频道,一个用于管理端向设备端发送命令请求(request channel),另一个是反向的,叫命令响应频道(ack channel),用于设备端向管理端发送命令请求的响应消息,由管理端侦听。
管理端让设备端执行一个命令,不论执行的成功或失败,设备端总要吱一声让管理端知道呀--这就是命令响应频道的作用。
管理端如何知道设备有哪些命令或管理选项呢?
这就是dtalk的核心协议:基于树形结构的管理选项定义---直观说就是一个层次结构的菜单
在管理端输入数字2,设备端将
faceloge 服务器
这个菜单项中的数据发送给管理端,在管理端呈现该菜单的详细内容
如上所示的,设备端通过一个树形结构菜单定义自己能执行的设备命令和管理选项(在发送过程中以json字符串形式传递)。
上图中设备端发送给管理端的faceloge 服务器
菜单的JSON数据
{
"catalog": "MENU",
"container": true,
"description": "",
"disable": false,
"empty": false,
"name": "facelog",
"path": "/facelog",
"uiName": "facelog \xe6\x9c\x8d\xe5\x8a\xa1\xe5\x99\xa8"
"childs":
[
{"catalog": "CMD","childs": [],"container": true,"description": "","disable": false,"empty": true,"name": "back","path": "/","taskQueue": null, "uiName": "back"},
{"catalog": "OPTION","childs": [],"container": false,"defaultValue": null,"description": "", "disable": false,"empty": true,"name": "host","path": "/facelog/host","readOnly": false,"required": false,"type": "STRING","uiName": "\xe4\xb8\xbb\xe6\x9c\xba\xe5\x90\x8d\xe7\xa7\xb0","value": "localhost"},
{"catalog": "OPTION","childs": [],"container": false,"defaultValue": 0,"description": "","disable": false,"empty": true,"name": "port","path": "/facelog/port","readOnly": false,"required": false,"type": "INTEGER","uiName": "\xe7\xab\xaf\xe5\x8f\xa3\xe5\x8f\xb7","value": 26411}
],
}
管理端收到这个菜单数据后,可以以自己想要的形式呈现给用户。在dtalk实现的简单字符终端中,以简单的列表形式显示设备端管理菜单 。如果在浏览器上用js实现,可以实现更好看的呈现方式。
管理端有了这个菜单,就可以知道该设备有哪些参数可以设置修改,有哪些命令可以执行,每个命令的含义是啥,都需要什么样的参数。
我们把运行在设备端,响应管理的dtalk请求的模块称为dtalk引擎或设备命令引擎。
我们把运行在管理端,向用户呈现管理菜单,向设备端发送dtlk请求,并将收到的响应显示给用户的模块称为dtalk console,或命令console。
设备端有哪些命令和选项,可以根据项目的需要定义,定制设备命令菜单。dtalk engine(命令引擎)负责向管理端发送这个菜单,并执行管理端的设备命令请求。
也就是说,设备端的命令和参数选项完全是自由灵活的,动态定义的。
如下图,设备端运行后,会订阅只属于自己的连接请求频道,这个频道的频道名是常量,名字格式-- ${设备端MAC地址}_dtalk_connect
,管理端要连接某台设备,必须要知道这台设备的MAC地址,有了MAC地址,就可以向这台设备发送连接请求,设备端收到连接请求后,会验证连接请求是否有效,如果有效则允许管理连接,就会向命令响应频道发送命令请求频道名(request channel),这个频道名是动态生成的,名字格式--${设备端MAC地址}_dtalk_[\d]+}
(后缀是个随机数)。
管理端启动后,会订阅只属于自己的命令响应频道名,这个频道的频道名是常量,名字格式---${管理端MAC地址}_dtalk_ack
。管理端通过响应频道收到连接成功的信号后,也同时会收到命令请求频道名(request channel),有了这个request channel,管理端才能向设备端发送设备命令请求。
管理端到设备端的连接是独占的,设备端以管理端连接时提供的管理端MAC地址来区分管理端,不允许有两个不同的管理端同时连接到同一台设备。如果已经有一个管理端电脑A连接到了设备端,那么另一台管理端电脑B尝试连接该设备时就会报错:ANOTHER CLIENT LOCKED
设备如何验证管理端的连接请求是否合法呢?
dtalk默认的实现方式是密码验证
管理端连接设备端时,向连接请求频道发送如下连接请求数据:
{
"mac": "58fb842d294f",/** 管理端自己的MAC地址 */
"pwd": "33902b064aab3e1c0db64827d8496fce" /** 管理端提供的连接密码(已加密) */
}
gu.dtalk.ConnectReq是上述连接请求字符串反序列化后的实现类
设备端根据管理端提供的连接密码,与本机保存的密码相比较,如果匹配则允许连接,否则报错INVALID REQUEST PASSWORD
。
关于建立连接的设备端实现参见gu.dtalk.engine.SampleConnector
这只是dtalk提供的默认连接实现,应用程序在使用dtalk的时候,可以根据业务需求实现gu.dtalk.engine.RequestValidator 接口,实现不同的连接验证方式。
比如facelog项目在使用dtalk时就重写了此方法,参见net.gdface.facelog.client.dtalk.TokenRequestValidator
关于建立连接的管理端实现参见 gu.dtalk.client.SampleConsole#authorize()方法
管理端与设备端命令交互的过程,就是管理发送菜单请求,设备端响应菜单请求的过程。
下面的json字符是一个完整的menu菜单示例
{"catalog":"MENU","name":"","path":"/","uiName":"","container":true,"description":"","disable":false,"empty":false,
"childs":[
{"catalog":"CMD","name":"quit","path":"/quit","childs":[],"container":true,"description":"","disable":false,"empty":true,"taskQueue":null,"uiName":"quit"},
{"catalog":"MENU","name":"menu1","path":"/menu1","uiName":"菜单1","container":true,"description":"","disable":false,"empty":false,
"childs":[
{"catalog":"CMD","childs":[],"container":true,"description":"","disable":false,"empty":true,"name":"back","path":"/","taskQueue":null,"uiName":"back"},
{"catalog":"MENU","name":"menu1_1","path":"/menu1/menu1_1","uiName":"菜单1.1",,"container":true,"description":"","disable":false,"empty":false
"childs":[
{"catalog":"CMD","name":"back","path":"/","taskQueue":null,"uiName":"back","childs":[],"container":true,"description":"","disable":false,"empty":true},
{"catalog":"OPTION""name":"option1","uiName":"选项1","path":"/menu1/menu1_1/option1","childs":[],"container":false,"defaultValue":null,"description":"","disable":false,"empty":true,,"readOnly":false,"required":false,"type":"STRING","value":null},
{"catalog":"OPTION","name":"option2","uiName":"选项2","path":"/menu1/menu1_1/option2","childs":[],"container":false,"defaultValue":0,"description":"","disable":false,"empty":true,"readOnly":false,"required":false,"type":"INTEGER","value":null}
]}]},
{"catalog":"MENU","container":true,"description":"","disable":false,"empty":false,"name":"menu2","path":"/menu2","uiName":"菜单2",
"childs":[
{"catalog":"CMD","name":"back","path":"/","uiName":"back","childs":[],"container":true,"description":"","disable":false,"empty":true,"taskQueue":null},
{"catalog":"MENU","name":"menu2_1","path":"/menu2/menu2_1","uiName":"菜单2.1","container":true,"description":"","disable":false,"empty":false,
"childs":[
{"catalog":"CMD","name":"back","uiName":"back","path":"/","childs":[],"container":true,"description":"","disable":false,"empty":true,"taskQueue":null},
{"catalog":"CMD","name":"cmd1","uiName":"命令1","path":"/menu2/menu2_1/cmd1","container":true,"description":"","disable":false,"empty":true,"taskQueue":null,
"childs":[
{"catalog": "OPTION","name": "param1","uiName": "命令参数1","path": "/menu2/menu2_1/cmd1/param1","childs": [],"container": false,"defaultValue": null,"description": "","disable": false,"empty": true,"readOnly": false,"required": false,"type": "STRING","value": null}
]
}
]}]}
]
}
如下图,管理端通过命令请求频道发送菜单命令,设备端收到菜单命令后,根据菜单命令的类型执行相应的动作
如果菜单命令是一个菜单(menu),则设备端将对应的菜单内容(通过命令响应频道)返回给管理端,如下就是一条管理端发送给设备端的菜单(menu)命令.
{"catalog":"MENU","path":"/"}
该命令只有两个字段:
path
代表要执行的命令(在设备端菜单树形结构中)的全路径。"/"即为根菜单。catalog
指定了该命令的类型为菜单(menu),这个字段可以省略,设备端根据path
就可以找到对应的menu,并将menu数据作为响应数据(ack)通过命令响应频道发送给管理端如果菜单命令是一个选项(option),则设备端会修改指定option的值,如下就是一条管理端发送给设备端的option菜单命令.
{"catalog":"OPTION","path":"/menu1/menu1_1/option1","value":"HELLO"}
#NOTE: 'catalog'字段可以省略
设备端收到这条命令后,就会将搜索path
指定的选项,将该选项的值设置为HELLO
如果菜单命令是一个(设备)命令(cmd),则设备端会执行指定的设备命令,如下就是一条管理端发送给设备端的cmd菜单命令.
{"catalog":"CMD","path":"/menu2/menu2_1/cmd1" "childs":[{"name": "param1","value":"HELLO"}]}
#NOTE: 'catalog'字段可以省略
设备端收到这条命令后,会执行指定的动作,设备命令的内容由应用程序实现,比如设备重启,比如远程升级
上面这个例子中,cmd1
这个设备命令定义了一个参数param1
作为子节点
关于命令交互的设备端实现参见 gu.dtalk.engine.BaseItemEngine
关于命令交互的管理端实现参见 gu.dtalk.client.BaseConsole#cmdInteractive()方法
TYPE | 说明 | Java类/基类 |
---|---|---|
OPTION | 参数选项,对应《术语》中的(参数)选项 | gu.dtalk.BaseOption |
CMD | (设备)命令,cmd | gu.dtalk.CmdItem |
MENU | 菜单,menu | gu.dtalk.MenuItem |
TYPE | JSON type for value/defaultValue field |
说明 | Java类 |
---|---|---|---|
STRING | string | 任意字符串 | gu.dtalk.StringOption |
INTEGER | number | 整数 | gu.dtalk.IntOption |
FLOAT | number | 浮点数 | gu.dtalk.FloatOption |
BOOL | bool | 布尔型 true/false 0/1 | gu.dtalk.BoolOption |
DATE | string | 日期 支持的格式:yyyy-MM-dd HH:mm:ss | gu.dtalk.DateOption |
URL | string | url字符串 | gu.dtalk.UrlOption |
PASSWORD | string | 密码字符串 | gu.dtalk.PasswordOption |
string | e-mail地址 | gu.dtalk.StringOption | |
MPHONE | string | 手机号码(11位) | gu.dtalk.StringOption |
IDNUM | string | 身份证号(15位、18位数字) | gu.dtalk.StringOption |
BASE64 | string | base64 格式二进制数据 | gu.dtalk.Base64Option |
MAC | string | 6字节网卡MAC地址,保存为base64字符串即8 characters,如 'ODg4ODg4',不是'192.168.0.100' | gu.dtalk.MACOption |
IP | string | 4字节IP地址,保存为base64字符串即8 characters,如 'ODg4OA==',不是'D0:17:C2:D2:3F:AE' | gu.dtalk.IPv4Option |
IMAGE | string | base64 格式JPEG/BMP/PNG格式图像 | gu.dtalk.ImageOption |
MULTICHECK | array | 多选列表 | gu.dtalk.CheckOption |
SWITCH | number | 单选列表 | gu.dtalk.SwitchOption |
选项(OPTION)允许应用层设置验证器对拟设置的值进行有效性验证,如果验证器返回false则会抛出异常。具体做法是通过gu.dtalk.BaseOption的setValidator
方法设置Predicate
接口实例,示例如下:
OptionBuilder.builder(IntOption.class).name("status").readonly(true).value(0).instance().setValidator(new Predicate<Integer>() {
@Override
public boolean apply(Integer input) {
return input != null && input > 100;
}
});
字段名 | JSON type | 说明 | MENU | OPTION | CMD |
---|---|---|---|---|---|
catalog | string | item分类类型,可选的值MENU,OPTION,CMD ,参见《(菜单)条目类型说明》 |
Y | Y | Y |
name | string | 条目名称([a-zA-Z0-9_],不允许有空格) | Y | Y | Y |
uiName | string | 条目的用户界面显示名称,如果不指定则使用name
|
Y | Y | Y |
path | string | 当前条目在整个菜单树形结构中以'/'分割子节点名字(name)的全路径,如'/device/mac',顶级根菜单(忽略name )的路径为'/', |
Y | Y | Y |
container | bool | 是否为容器(可包含item),当catalog 为MENU,CMD 时为true |
Y | Y | Y |
description | string | 对当前条目的说明文字,默认值:空 | Y | Y | Y |
disable | bool | 当前条目是否禁用,默认值:false | Y | Y | Y |
hide | bool | 当前条目是否在用户界面中隐藏(不显示),默认值:false | Y | Y | Y |
childs | bool | 所有子条目,当catalog 为OPTION 时,恒为空 |
Y | Y | Y |
empty | bool | 是否有子条目,没有childs 或childs 中元素为0时为true,当catalog 为OPTION 时,恒为true |
Y | Y | Y |
canceled | bool | 取消正在执行的设备命令,为true时,指示取消正在执行的设备命令,仅对支持交互的设备命令有效 | Y | ||
needReset | bool | 执行CMD /修改OPTION 的值会不会导致设备重启 |
Y | Y | |
needRefresh | bool | 修改OPTION 值后前端要不要重新获取数据 |
Y | ||
type | string | 选项的类型,可选的值参见《OPTION类型说明》 | Y | ||
readOnly | bool | 是否为只读的选项,当catalog 为CMD 时,恒为false |
Y | ||
required | bool | 是否为必须的选项,默认值:false | Y | ||
value | any | 选项值,根据OPTION 的类型不同可以有不同的类型 |
Y | ||
defaultValue | any | 选项默认值,根据OPTION 的类型不同可以有不同的类型 |
Y | ||
regex | string | 当前选项的字符串合法性检查正则表达式 | Y | ||
fieldRequire | number |
DATE Option 设备端要求的日期类型 |
Y | ||
precision | number |
FLOAT Option 显示精度:小数点位数 |
Y | ||
options | object |
MULTICHECK ,SWITCH 可选项列表 |
Y | ||
available | array | 已知可用的的值列表 | Y | ||
width | number |
IMAGE Option 图像宽度 |
Y | ||
height | number |
IMAGE Option 图像高度 |
Y | ||
suffix | number |
IMAGE Option 图像格式 |
Y |
NOTE:
options
目前使用允许的类型是object
,未来可能会允许使用array
,以保持列表元素顺序dtalk提供了gu.dtalk.ItemBuilder用于创建menu和cmd,提供了gu.dtalk.OptionBuilder
如下是创建前面的示例的菜单的代码:
public void test6Menu(){
MenuItem menu1 = ItemBuilder.builder(MenuItem.class).name("menu1").uiName("菜单1").addChilds(
ItemBuilder.builder(MenuItem.class).name("menu1_1").uiName("菜单1.1").addChilds(
OptionType.STRING.builder().name("option1").uiName("选项1").instance(),
OptionType.INTEGER.builder().name("option2").uiName("选项2").instance()
).instance()
).instance();
MenuItem menu2 = ItemBuilder.builder(MenuItem.class).name("menu2").uiName("菜单2").addChilds(
ItemBuilder.builder(MenuItem.class).name("menu2_1").uiName("菜单2.1").addChilds(
ItemBuilder.builder(CmdItem.class).name("cmd1").uiName("命令1").instance().addParameters(
OptionType.STRING.builder().name("param1").uiName("命令参数1").instance()
)
).instance()
).instance();
RootMenu root = new RootMenu();
root.addChilds(menu1,menu2);
logger.info(BaseJsonEncoder.getEncoder().toJsonString(root));
}
完整代码参见 gu.dtalk.ItemTest
根据设备命令执行方式的不同,分为立即(执行)设备命令和交互设备命令,立即设备命令执行后立即返回结果或不返回结果,立即命令执行的结果只有成功或异常两种状态。 交互命令适用于可能要长时间执行的命令,允许在设备命令执行教程中,管理端取消设备命令执行,也允许设备在设备命令执行过程,向管理端发送完成进度或人为取消。交互命令执行的结果状态更加复杂:成功,被拒绝,异常,超时。
dtalk的立即设备命令由立即设备命令执行接口(gu.dtalk.ICmdImmediateAdapter)定义.
/**
* 设备命令执行接口
* @author guyadong
*
*/
public static interface ICmdImmediateAdapter {
/**
* 执行设备命令
* @param input 以值对(key-value)形式提供的输入参数
* @return 命令返回值,没有返回值则返回{@code null}
* @throws CmdExecutionException 命令执行失败
*/
Object apply(Map<String, Object> input) throws CmdExecutionException;
}
应用程序实现了设备命令执行接口后,通过gu.dtalk.CmdItem.setCmdAdapter
方法绑定到指定的设备命令。当设备端收到这个设备命令时就会执行对应的ICmdImmediateAdapter
实例.
该接口实例在CmdItem
实例中被gu.dtalk.CmdItem.runCmd
方法调用
/**
* 执行立即命令
* @return
* @throws CmdExecutionException 设备命令执行异常
*/
public final Object runImmediateCmd() throws CmdExecutionException{
checkState(cmdAdapter instanceof ICmdImmediateAdapter,"type of cmdAdapter must be %s",ICmdImmediateAdapter.class.getSimpleName());
synchronized (items) {
if(cmdAdapter !=null){
try {
// 将 parameter 转为 Map<String, Object>
Map<String, Object> objParams = Maps.transformValues(items, TO_VALUE);
return ((ICmdImmediateAdapter)cmdAdapter).apply(checkRequired(objParams));
} finally {
reset();
}
}
return null;
}
}
dtalk的交互设备命令由交互设备命令执行接口(gu.dtalk.ICmdInteractiveAdapter)定义.
/**
* 交互设备命令接口
* @author guyadong
*
*/
public interface ICmdInteractiveAdapter extends ICmdUnionAdapter {
/**
* 执行设备命令
* @param input 以值对(key-value)形式提供的输入参数
* @param listener 状态侦听器,用于向管理端发送命令状态
* @return 命令返回值,没有返回值则返回{@code null}
* @throws InteractiveCmdStartException 当设备命令被拒绝或不支持或其他出错时抛出此异常,通过{@link InteractiveCmdStartException#getStatus() }获取状态类型
*/
void apply(Map<String, Object> input,ICmdInteractiveStatusListener listener) throws InteractiveCmdStartException;
/**
* 取消当前执行的设备命令
* @throws UnsupportedOperationException 设备命令不支持取消
*/
void cancel() throws UnsupportedOperationException;
}
交互设备命令启动时,调用者会提供一个ICmdInteractiveStatusListener
接口实例,用于设备命令执行时向调用层报告状态
/**
* 命令状态侦听器,用于交互命令向管理端发送命令执行状态
* @author guyadong
*/
public interface ICmdInteractiveStatusListener{
/**
* 返回设备命令完成进度<br>
* 设备命令在执行过程中应该定时调用此方法,以作为心跳发送给管理端,直到任务结束
* @param progress 完成进度(0-100),可为{@code null}
* @param statusMessage 附加状态消息,可为{@code null}
*/
void onProgress(Integer progress,String statusMessage);
/**
* 任务结束,设备命令成功执行完成
* @param value 命令执行返回值,没有返回值则为{@code null}
*/
void onFinished(Object value);
/**
* 任务结束,执行中的设备命令被取消
*/
void onCaneled();
/**
* 任务结束,调用抛出异常
* @param errorMessage 错误信息,可为{@code null}
* @param throwable 异常对象,可为{@code null}
*/
void onError(String errorMessage, Throwable throwable);
/**
* 此方法用于设备命令发送方控制设备端定时报告进度的间隔(秒),
* 设备端调用此方法获取数值后,用于控制调用{@link #onProgress(Integer, String)}方法的调用间隔
* @return 返回要求的{@link #onProgress(Integer, String)}方法调用间隔(秒),<=0时,使用设备自定义的默认值
*/
int getProgressInternal();
}
为了增加dtalk的易用性,dtalk增加了http直接连接的功能。也就是设备端启动时启动一个基于nanohttpd的微型http服务。接收来自管理端的服务请求。
在此运行模式下,管理端与设备端的连接不再需要借助redis中转,而是直接送http请求到设备端。
dtalk http服务由gu.dtalk.engine.DtalkHttpServer实现 调用示例参见 gu.dtalk.engine.demo.DemoHttpd
与Redis连接实现一样,http连接的菜单协议没有任何改变,不同的就是Redis实现时,菜单命令是从redis消息订阅频道接收,而http实现时,菜单命令直接来自http请求。
GET | POST | 路径 | 参数 | 描述 |
---|---|---|---|---|
Y | Y | / /index.html /index.htm |
首页(静态页面) | |
Y | Y | /login | password:连接密码(默认为MAC地址后4位,比如3fbf) isMd5(true/false):密码是否为MD5加密,默认为'true' |
安全连接密码验证 |
Y | Y | /logout | 关闭连接 | |
Y | /dtalk | 所有参数须为json格式, 指定菜单条目的'path'参数是必不可少的,比如: {path:'/device'} |
dtalk菜单请求 | |
Y | Y | /dtalk${path} | POST请求时参数须为json格式, 指定菜单条目的'path'参数由${path}定义,比如 '/device/name' |
dtalk菜单请求 |
(待续)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )