UDS Demo

这里我们以一个完整的demo程序做示例,通过真实代码介绍如何基于ACService开发用户自己的服务程序。demo场景不一定完全符合常识,其目的是为了尽量简单而全面的演示服务框架提供的功能。

场景介绍

本示例场景有一智能灯,可以通过手机端向云端发消息远程控制灯的开/关,云端服务把这些APP发来的控制行为记录到AbleCloud提供的云存储中。同时,智能灯也可能由用户通过机械开关起停,智能灯将这类开/关事件也主动汇报到服务端,我们写的服务程序将这类主动汇报的开/关数据也存到云存储中。所有存储在云存储中的数据,可提供给APP查询,比如可用于统计用户的作息习惯等。

实现步骤

先简单分析下需求,然后梳理出实现步骤。

  1. 要开发服务程序,需从ACService派生子类来实现自己的服务框架。本示例中,DemoService就是继承自ACService的子类;
  2. 服务要接收来自APP对灯的远程控制命令,也会接收来自APP的数据查询请求,因此必须为handleMsg提供具体的实现handler;
  3. 服务要向智能灯发送控制命令,因此我们需要和灯以及APP端定义具体的控制消息格式LightMsg
  4. 服务要接收智能灯汇报的开/关消息,因此必须为handleDeviceMsg提供具体的实现handler。

具体实现

新建设备属性

为了能够查询开关历史纪录,需要持久化存储设备上报的开关记录数据。

通过开发者管理控制台的【产品管理=>智能灯Demo=>属性管理】完成设备上报属性参数的定义。
定义好的属性如下图所示:

demoservice_property

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包。你可以开发更多好玩的逻辑,比如多设备联动:当某些设备上报的数据达到设置的规则时,触发另外的设备做出响应。 对该服务的测试见后文的相关章节。