Score
0
Watch 18 Star 64 Fork 18

10km / dtalkJavaBSD-2-Clause

Join us
Explore and code with more than 2 million developers,Free private repositories !:)
Sign up
dtalk(Device Talk),基于Redis 发布订阅(pub/sub)系统实现的前端设备控制框架 spread retract

  • Java 66.2%
  • CSS 17.9%
  • HTML 10.9%
  • JavaScript 4.6%
  • Shell 0.2%
  • Other 0.2%
Clone or download
MANUAL.md 23.09 KB
Copy Edit Web IDE Raw Blame History
10km authored 2019-09-20 09:37 . update manual/MANUAL.md manual/MANUAL.html

dtalk(Device Talk)开发手册(更新中)

术语

术语 描述
(菜单)条目,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),则设备端将对应的菜单内容(通过命令响应频道)返回给管理端,如下就是一条管理端发送给设备端的菜单(menu)命令.

{"catalog":"MENU","path":"/"}

该命令只有两个字段:

  1. path代表要执行的命令(在设备端菜单树形结构中)的全路径。"/"即为根菜单。
  2. catalog指定了该命令的类型为菜单(menu),这个字段可以省略,设备端根据path就可以找到对应的menu,并将menu数据作为响应数据(ack)通过命令响应频道发送给管理端

OPTION

如果菜单命令是一个选项(option),则设备端会修改指定option的值,如下就是一条管理端发送给设备端的option菜单命令.

{"catalog":"OPTION","path":"/menu1/menu1_1/option1","value":"HELLO"}
#NOTE: 'catalog'字段可以省略

设备端收到这条命令后,就会将搜索path指定的选项,将该选项的值设置为HELLO

CMD

如果菜单命令是一个(设备)命令(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

选项(OPTION)类型说明

TYPE 说明 Java类
STRING 任意字符串 gu.dtalk.StringOption
INTEGER 整数 gu.dtalk.IntOption
FLOAT 浮点数 gu.dtalk.FloatOption
BOOL 布尔型 true/false 0/1 gu.dtalk.BoolOption
DATE 日期 支持的格式:yyyy-MM-dd HH:mm:ss gu.dtalk.DateOption
URL url字符串 gu.dtalk.UrlOption
PASSWORD 密码字符串 gu.dtalk.PasswordOption
EMAIL e-mail地址 gu.dtalk.StringOption
MPHONE 手机号码(11位) gu.dtalk.StringOption
IDNUM 身份证号(15位、18位数字) gu.dtalk.StringOption
BASE64 base64 格式二进制数据 gu.dtalk.Base64Option
MAC 网卡MAC地址二进制数据 gu.dtalk.MACOption
IP IP地址二进制数据 gu.dtalk.IPv4Option
IMAGE base64 格式JPEG/BMP/PNG格式图像 gu.dtalk.ImageOption
MULTICHECK 多选列表 gu.dtalk.CheckOption
SWITCH 单选列表 gu.dtalk.SwitchOption

验证器(validator)

选项(OPTION)允许应用层设置验证器对拟设置的值进行有效性验证,如果验证器返回false则会抛出异常。具体做法是通过gu.dtalk.BaseOptionsetValidator方法设置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;
		}
	});

(菜单)条目定义字段

字段名 说明 MENU OPTION CMD
catalog item分类类型,可选的值MENU,OPTION,CMD,参见《(菜单)条目类型说明》 Y Y Y
name 条目名称([a-zA-Z0-9_],不允许有空格) Y Y Y
uiName 条目的用户界面显示名称,如果不指定则使用{@link #name} Y Y Y
path 当前对象在整个菜单树形结构中的全路径 Y Y Y
container 是否为容器(可包含item),当catalog为MENU,CMD时为true Y Y Y
description 对当前条目的说明文字,默认值:空 Y Y Y
disable 当前条目是否禁用,默认值:false Y Y Y
hide 当前条目是否在用户界面中隐藏(不显示),默认值:false Y Y Y
childs 所有子条目,当catalog为OPTION时,恒为空 Y Y Y
empty 是否有子条目,即childs中元素不为0,当catalog为OPTION时,恒为0 Y Y Y
type 选项的类型,可选的值参见《OPTION类型说明》 Y
readOnly 是否为只读的选项,当catalog为CMD时,恒为false Y
needReset 会不会导致应用重启 Y Y
required 是否为必须的选项,默认值:false Y
value 选项值 Y
defaultValue 选项默认值 Y

NOTE:上表中后三列为Y,代表此字段适合该类型的菜单条目

创建菜单

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();
}

http连接

为了增加dtalk的易用性,dtalk增加了http直接连接的功能。也就是设备端启动时启动一个基于nanohttpd的微型http服务。接收来自管理端的服务请求。

在此运行模式下,管理端与设备端的连接不再需要借助redis中转,而是直接送http请求到设备端。

dtalk http服务由gu.dtalk.engine.DtalkHttpServer实现 调用示例参见 gu.dtalk.engine.demo.DemoHttpd

http接口说明

与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菜单请求

任务队列

(待续)

Comment ( 0 )

Sign in for post a comment

Java
1
https://gitee.com/l0km/dtalk.git
git@gitee.com:l0km/dtalk.git
l0km
dtalk
dtalk
master

Help Search