UDS Demo
这里我们以一个完整的demo程序做示例,通过真实代码介绍如何基于ACService开发用户自己的服务程序。demo场景不一定完全符合常识,其目的是为了尽量简单而全面的演示服务框架提供的功能。
场景介绍
本示例场景有一智能灯,可以通过手机端向云端发消息远程控制灯的开/关,云端服务把这些APP发来的控制行为记录到AbleCloud提供的云存储中。同时,智能灯也可能由用户通过机械开关起停,智能灯将这类开/关事件也主动汇报到服务端,我们写的服务程序将这类主动汇报的开/关数据也存到云存储中。所有存储在云存储中的数据,可提供给APP查询,比如可用于统计用户的作息习惯等。
实现步骤
先简单分析下需求,然后梳理出实现步骤。
- 要开发服务程序,需从
ACService
派生子类来实现自己的服务框架。本示例中,DemoService
就是继承自ACService
的子类;- 服务要接收来自APP对灯的远程控制命令,也会接收来自APP的数据查询请求,因此必须为
handleMsg
提供具体的实现handler;- 服务要向智能灯发送控制命令,因此我们需要和灯以及APP端定义具体的控制消息格式
LightMsg
;- 服务要接收智能灯汇报的开/关消息,因此必须为
handleDeviceMsg
提供具体的实现handler。
具体实现
新建设备属性
为了能够查询开关历史纪录,需要持久化存储设备上报的开关记录数据。
通过开发者管理控制台的【产品管理=>智能灯Demo=>属性管理】完成设备上报属性参数的定义。
定义好的属性如下图所示:
DemoService
DemoService
为自定义服务的主要逻辑处理类,通过handleMsg
处理APP端发来的消息,通过handleDeviceMsg
处理设备上报上来的消息。
本Demo的逻辑比较简单,在DemoService
中实现了两个具体的处理函数handleControlLight
(用于响应开关灯的指令)和handleQueryData
(用于响应查询开关灯历史记录的指令)。
在DemoService
中只实现了一个具体的处理函数handleLightReport
(用于处理智能灯上报的消息)。开发者可以根据业务需求任意扩展handler。
public class DemoService extends ACService {
private static final Logger logger = LoggerFactory.getLogger(DemoService.class);
/**
* 重载init函数。
*
* @throws Exception
*/
public void init() throws Exception {
}
/**
* 处理来自APP或其它service发来消息的入口函数
*
* @param req 请求消息
* @param resp 响应消息
* @throws Exception
*/
public void handleMsg(ACMsg req, ACMsg resp) throws Exception {
String name = req.getName();
switch (name) {
case "controlLight":
handleControlLight(req, resp);
break;
case "fetchHistoryPropertyData":
fetchHistoryPropertyData(req, resp);
break;
case "test":
logger.info(req.toString());
resp.put("result", "userId[" + req.getContext().getUserId() + "] is testing, return ack");
resp.setAck();
break;
default:
logger.warn("got an invalid request, method[" + name + "] is not implemented.");
resp.setErr(Errors.ERR_MSG_NOT_SUPPORTED.code, Errors.ERR_MSG_NOT_SUPPORTED.error);
break;
}
}
/**
* 处理来自设备上报消息的入口函数
*
* @param reportInfo 设备的属性
* @param req 设备上报消息
* @throws Exception
*/
public void handleDeviceMsg(ACDeviceReportInfo reportInfo, ACDeviceMsg req) throws Exception {
int msgCode = req.getCode();
switch (msgCode) {
case LightMsg.REPORT_CODE:
handleLightReport(reportInfo, req.getContent());
break;
default:
logger.warn("got an unknown report, opcode[" + msgCode + "]");
}
}
/**
* 处理匿名请求。某些特殊请求,如用户注册,没有有效用户的信息,可以考虑使用匿名请求的方式执行。
*
* @param req 请求消息体
* @param resp 响应消息体
* @throws Exception
*/
public void handleAnonymousMsg(ACMsg req, ACMsg resp) throws Exception {
String name = req.getName();
switch (name) {
case "register": // 用户注册。
handleRegister(req, resp);
break;
default:
logger.warn("got an invalid request, method[" + name + "] is not implemented.");
resp.setErr(Errors.ERR_MSG_NOT_SUPPORTED.code, Errors.ERR_MSG_NOT_SUPPORTED.error);
break;
}
}
//////////////////////////////////////
// 具体的私有handler
/**
* 处理来自APP端的智能灯控制命令,再将命令发往具体的设备
* <p/>
* 实际上,厂商在实现后端服务的时候,通常情况下自定义服务不用处理APP端发来的设备控制请求也
* 能实现远程控制。因为ablecloud在云端提供了设备管理服务,APP通过APP端的sendToDevice
* 接口可以将控制命令远程发往ablecloud的设备管理服务,设备管理服务再将控制命令发给设备。
* <p/>
* 本示例在开发者自定义的这个服务中实现对灯的控制,一方面是为了展示后端服务的灵活性,可以作
* 各种事情,包括对设备的控制,比如后端服务在多设备联动的时候,可能会主动往设备发控制命令。
*
* @param req 请求消息
* @param resp 响应消息
* @throws Exception
*/
private void handleControlLight(ACMsg req, ACMsg resp) throws Exception {
long userId = req.getContext().getUserId();
long lightId = req.getLong("deviceId");
String action = req.get("action");
byte deviceAction;
if (action.equalsIgnoreCase("on")) {
deviceAction = LightMsg.ON;
} else
deviceAction = LightMsg.OFF;
ACDeviceMsg deviceReqMsg = new ACDeviceMsg(LightMsg.CODE, new byte[]{deviceAction, 0, 0, 0});
ACDeviceMsg deviceRespMsg;
try {
// 获取子域
// (1) 此处通过已过时的方法 ACConfig.getSubDomain 来指定设备所属的子域名。这是旧的子域级别UDS中的使用方法。
// 新版的云应用引擎不支持从配置项(ACConfig)中获取子域名(返回值为空字符串)。此时应通过其它方法指定子域,如从请求的上下文中获取(如果存在)、从请求参数中获取等。
// String subDomainName = this.getAc().getACConfig().getSubDomain();
// (2) 从请求上下文中获取子域
String subDomainName = req.getContext().getSubDomainName();
// (3) 从请求参数中获取子域
// String subDomainName = req.get("subDomain");
// 通过ac框架的sendToDevice接口,向灯发送控制命令
deviceRespMsg = ac.bindMgr(req.getContext()).sendToDevice(subDomainName, lightId, deviceReqMsg, userId);
// 获取控制开关结果
byte[] payload = deviceRespMsg.getContent();
if (payload.length > 0 && payload[0] == 1)
resp.put("result", "success");
else
resp.put("result", "fail");
resp.setAck();
logger.info("handle control light ok, action[" + action + "].");
} catch (ACServiceException e) {
resp.setErr(e.getErrorCode(), e.getErrorMsg());
logger.warn("send to device[" + lightId + "] error:", e);
}
}
/**
* 处理来自APP端的查询智能灯历史属性数据请求
* @param req 请求消息
* @param resp 响应消息
* @throws Exception
*/
private void fetchHistoryPropertyData(ACMsg req, ACMsg resp) throws Exception {
long userId = req.getContext().getUserId();
long lightId = req.getLong("deviceId");
long startTime = req.getLong("startTime"); // 查询开始时间戳
long endTime = req.getLong("endTime"); // 查询结束时间戳
String subDomainName = req.getContext().getSubDomainName(); // 被查询的设备所属的子域的名字。
//查找所有开灯记录
List<ACObject> historyData = ac.dstore(subDomainName)
.scanHistory(lightId)
.where(ac.filter().whereEqualTo(LightMsg.KEY_SWITCH, 1))
.startTime(true, startTime) //true为代表闭区间包含的意思即: >= startTime
.endTime(true, endTime)
.execute();
resp.put("data", historyData);
resp.put("result", "success");
resp.setAck();
}
/**
* 处理智能灯汇报的消息,在该函数中,服务还将收到的汇报数据写入ablecloud提供的云端存储中。
*
* @param reportInfo 汇报数据的设备的信息。
* @param payload 汇报的具体消息内容。
* @throws Exception
*/
private void handleLightReport(ACDeviceReportInfo reportInfo, byte[] payload) throws Exception {
try {
LightMsg lightMsg = new LightMsg(payload);
// 通过ac框架,将智能灯汇报的数据存入云端存储
ac.dstore(reportInfo.getContext().getSubDomainName())
.create(reportInfo.getDeviceId(), System.currentTimeMillis())
.put(LightMsg.KEY_SWITCH, lightMsg.getLedOnOff())
.put(LightMsg.KEY_SOURCE, lightMsg.getSource())
.execute(true); //true:为存储属性并发布推送 false:只存储属性数据
} catch (ACServiceException e) {
logger.warn("handle light report error:", e);
}
}
/**
* 处理APP端发来的用户注册请求。用户注册的请求使用匿名访问的方式。
*
* @param req 请求消息
* @param resp 响应消息
* @throws Exception
*/
private void handleRegister(ACMsg req, ACMsg resp) throws Exception {
// 实现用户注册的逻辑,并设置响应的结果。
resp.setAck(); // 注册成功返回OK。
// 注册失败是返回错误消息。
// resp.setErr(errCode, errMessage);
}
}
LightMsg
LightMsg
是控制灯开/关的消息(命令),需要设备/APP/服务三方共同确定。如果服务需要和其它类型的智能设备交互,则再定义其它的message即可。
public class LightMsg {
public static final int CODE = 68;
public static final int RESP_CODE = 102;
public static final int REPORT_CODE = 203;
public static final String KEY_SWITCH = "switch";
public static final String KEY_SOURCE = "source";
//0代表关,1代表开
public static final byte ON = 1;
public static final byte OFF = 0;
//控制类型,0代表app控制,1代表物理开关控制
public static final byte FROM_APP = 0;
public static final byte FROM_SWITCH = 1;
//开关状态
private byte ledOnOff;
//操作来源
private byte source;
public LightMsg(byte[] payload) {
this.ledOnOff = payload[0];
this.source = payload[1];
}
public byte getLedOnOff() {
return ledOnOff;
}
public byte getSource() {
return source;
}
}
前文的代码实现了本示例的全部功能。在终端运行mvn package
即可编译成jar包。你可以开发更多好玩的逻辑,比如多设备联动:当某些设备上报的数据达到设置的规则时,触发另外的设备做出响应。
对该服务的测试见后文的相关章节。