上一篇主要讲的是PC端调试lettuce-Sea以及部署lettuce-Sea到树莓派并与华为OC平台进行联调而这一篇将要讲lettuce的对外服务lettuce-Air服务端java代码解析。
lettuce-Air服务端java代码讲解
想必上节课,大家已经在设备侧完成了对华为OC平台的集成,并且有了可观的效果。可以说已经胜利在望了。而这节课,我将给大家讲解如何接入华为OC平台的北向接口。并转换为对外服务的能力。
lettuce-Air的源代码
https://github.com/lipuqi/lettuce-Air
首先我们要有一个云服务器,开放8013端口。
其次云服务器上要安装JDK1.8的环境。
lettuce-Air服务端主要提供2种服务:
- 服务端后台对总体情况的监控。监控内容包括设备的当前状态和命令下发的当前状态。
- 服务端后台对客户端lettuce-Land的服务提供,例如在客户端开关灯的操作,和客户端查询设备在线情况的操作。
lettuce-Air服务端主要使用的是spring boot的框架,因为便于大家演示操作,使用单例模式的Map作为缓存来存储数据,与华为OC平台采用API的方式对接。
接下来我就先讲解lettuce-Air服务端是与华为OC平台怎样对接的。
主要分3种接口:
- 订阅接口:这种接口是我们提供给平台的,用于平台向我们主动提供的数据。例如设备端的数据上报。
- 主动访问式接口:这种接口是平台提供给我们的,用于我们主动触发相关操作。例如我们向设备发送一条命令。
- 回调接口:这种接口是平台回调我们的接口,主要用于一些状态的返回等。例如我们刚才下发命令时,传给平台命令状态的回调地址。
所以一个完整的命令下发操作是这样的流程
首先我们先来看订阅平台的设备数据变化的通知。
在华为OC平台的控制板上我们选择订阅调试
然后大家可以看到,可以订阅很多种通知。我们这里只订阅了设备数据变化。
在订阅时需要填写服务器的接口。
这个添写的接口就是我们提供给平台的。
注意这里因为我们只做演示,采用的是HTTP的协议,如果想用HTTPS的,需要将证书信息设置到华为OC平台里(对接信息中设置)。
填写接口以后,平台会发送一条虚拟数据给接口自动检测接口是否通过。
下面我们来看看接口的入参
接口要使用POST方式
这是平台给我们的数据是怎样的结构
相关java代码片段
DeviceController.java
/**
* 上报数据
* @param result
* @return
*/
@PostMapping(value = "/deviceDataChanged", produces = { "application/json;charset=UTF-8" })
public GenericResponse deviceDataChanged(@RequestBody JSONObject result){
try {
deviceService.getDeviceData(result);
} catch (CustomException ex) {
throw ex;
} catch (Exception e) {
throw new BasicException(1000, e);
}
return ResponseFormat.retParam(200, "OK");
}
DeviceServiceImpl.java
@Override
public void getDeviceData(JSONObject result) {
BasicDevice device = null;
//判断通知类型是否为上报数据类型
if (result.containsKey("notifyType") && PushStatus.DEVICE_DATA_CHANGED.equals(result.getString("notifyType"))) {
JSONObject service = result.getJSONObject("service");
//判断是灯的服务还是设备的服务
switch (service.getString("serviceId")) {
case ServiceConstant.SwitchBulb:
SwitchBulb_status switchBulb_status = new SwitchBulb_status();
switchBulb_status.packaging(service);//解析消息内容
//放到设备类中
device = new Bulb();
device.setStatus(switchBulb_status.getStatus());
device.setUpdateTime();
break;
case ServiceConstant.OperationPi:
OperationPi_status sperationPi_status = new OperationPi_status();
sperationPi_status.packaging(service);//解析消息内容
//放到设备类中
device = new OperationPi();
device.setStatus(sperationPi_status.getStatus());
device.setUpdateTime();
LOGGER.info("----------------------设备上报心跳----------------------");
break;
default:
throw new CustomException(DeviceServiceImpl.class, "获取数据上报信息类型没有匹配");
}
//将设备参数更新到缓存中
mapCache.put(device.getDeviceKey(), device);
}
}
解析消息
SwitchBulb_status.java
@Override
public void packaging(JSONObject service) {
try {
if (service.containsKey("serviceId"))
setServiceId(service.getString("serviceId"));
if (service.containsKey("serviceType"))
setServiceType(service.getString("serviceType"));
if (service.containsKey("data")) {
JSONObject serviceData = service.getJSONObject("data");
if (serviceData.containsKey("status"))
setStatus(serviceData.getInt("status"));
}
} catch (Exception e) {
throw new CustomException(SwitchBulb_status.class, "SwitchBulb-status上传数据解析服务出现问题", e);
}
}
接下来我们来看一看命令下发的接口
首先我们要先了解一下平台命令下发有两种机制
其实之所以有2种机制,就是为了保证设备的低功耗。如果看过前面的对这个就会有更深的理解,有点类似于模组的PSM低功耗策略。
这里要注意,此接口采用的是HTTPS的方式,服务端调用此接口要使用HTTPS的方式调用。
这里lettuce-Air中已经集成华为API DEMO了HTTPS的调用工具,其中还需要华为的证书文件,这个证书文件我是外挂到程序外目录的,这里要注意一下。
Authorization就是需要提供一个凭证,这个凭证需要调用相关接口获取,这里我在lettuce-Air做好了一个获取凭证的工具。
callbackUrl就是我们服务端提供的对命令状态通知的接口。
expireTime就是前面说的两种命令下发的机制,0为立即下发,其他为缓存下发的超时时间。如果超过了这个时间,命令还没达到下发条件的话,就会废弃这条下发指令。
maxRetransmit如果失败就会重试的次数。
这个是指令的相关参数
这是响应给服务器的参数,
这里注意的是status
这个参数就是指令下发的状态。
相关java代码片段
AppController.java
/**
* 发送一条指令
* @param method
* @param value
* @return
*/
@GetMapping("/sendCommand")
public GenericResponse sendCommand(String method, Integer value){
try {
deviceService.sendCommand(method, value);
} catch (CustomException ex) {
throw ex;
} catch (BasicException exc) {
throw exc;
} catch (Exception e) {
throw new BasicException(1000, e);
}
return ResponseFormat.retParam(200, null);
}
DeviceServiceImpl.java
@Override
public void sendCommand(String method, Integer value) throws Exception {
if(StringUtils.isEmpty(method) || value == null){
throw new BasicException(10002, new CustomException(DeviceServiceImpl.class, "下发命令参数为空"));
}
/* if(getOperationPiStatus() == 0){
throw new BasicException(30001, new CustomException(DeviceServiceImpl.class, "设备已下线"));
}*/
JSONObject commandData = new JSONObject();
JSONObject command = null;
//根据响应的命令,封装不同的上报命令参数
switch (method) {
case ServiceConstant.ON_OFF:
if(getBulbStatus() == value){
return;
}
command = new SwitchBulb_ON_OFF(value).unpack();
break;
case ServiceConstant.QUERY_STATUS:
command = new SwitchBulb_QUERY_STATUS(value).unpack();
break;
case ServiceConstant.QUIT_PYTHON:
command = new OperationPi_QUIT_PYTHON(value).unpack();
break;
default:
throw new BasicException(10001, new CustomException(DeviceServiceImpl.class, "下发命令类型没有匹配"));
}
//封装命令下发基础参数
commandData.put("deviceId", huaweiIotProperties.getDeviceId());
commandData.put("command", command);
commandData.put("expireTime", 0);
commandData.put("maxRetransmit", 3);
commandData.put("callbackUrl", huaweiIotProperties.getCommandCallbackUrl());
String result = huaweiIotApiUrl.getDeviceCommandsUrl(huaweiIotProperties.getAppID(), tokenUtil.getToken(),
commandData);
JSONObject resultJson = JSONObject.fromObject(result);
//命令下发成功后,创建命令任务状态
CommandTask commandTask = new CommandTask();
commandTask.setCommandId(resultJson.getString("commandId"));
commandTask.setMethod(method);
commandTask.setStatus(resultJson.getString("status"));
commandTask.setExpiresIn(System.currentTimeMillis() + (huaweiIotProperties.getCommandExecuteTime() + 1) * 1000);
//创建任务状态系列
mapCache.put(commandTask.getCommandId(), commandTask);
}
封装参数的相关代码
SwitchBulb_ON_OFF.java
@Override
public JSONObject unpack() {
JSONObject command = new JSONObject();
command.put("serviceId", getServiceId());
command.put("method", getMethod());
JSONObject paras = new JSONObject();
paras.put("toggleBulb", getToggleBulb());
command.put("paras", paras);
return command;
}
调用API的相关代码
HuaweiIotApiUrl.java
/**
* 执行命令
* @param appID
* @param token
* @param paramCreateDeviceCommand
* @return
* @throws Exception
*/
public String getDeviceCommandsUrl(String appID, String token, JSONObject paramCreateDeviceCommand) throws Exception {
HttpsUtil httpsUtil = new HttpsUtil();
httpsUtil.initSSLConfigForTwoWay();
Map<String, String> header = new HashMap<>();
header.put("app_key", appID);
header.put("Authorization", "Bearer" + " " + token);
HttpResponse responseCreateDeviceCommand = httpsUtil.doPostJson(deviceCommandsUrl, header, paramCreateDeviceCommand.toString());
return httpsUtil.getHttpResponseBody(responseCreateDeviceCommand);
}
还有回调命令状态的接口
resultCode使用的就是前面下发命令状态的相关常量。
这里注意的是如果有响应参数,那么当resultCode为SUCCESSFUL状态时,会将解析后的json消息放在resultDetail中。
相关java代码片段
DeviceController.java
/**
* 返回命令下发状态
* @param result
* @return
*/
@PostMapping(value = "/commandStatus", produces = { "application/json;charset=UTF-8" })
public GenericResponse commandStatus(@RequestBody JSONObject result){
try {
deviceService.getCommandStatus(result);
} catch (CustomException ex) {
throw ex;
} catch (Exception e) {
throw new BasicException(1000, e);
}
return ResponseFormat.retParam(200, "OK");
}
DeviceServiceImpl.java
@Override
public void getCommandStatus(JSONObject result) {
if (result.containsKey("commandId")) {
//根据消息标识获取任务
CommandTask commandTask = (CommandTask) mapCache.get(result.getString("commandId"));
if (commandTask == null) {
throw new CustomException(DeviceServiceImpl.class, "获取命令状态时的命令标识不在序列中");
}
//解析数据
JSONObject commandResult = result.getJSONObject("result");
String resultCode = commandResult.getString("resultCode");
commandTask.setStatus(resultCode);
//如果任务状态为成功
if("SUCCESSFUL".equals(resultCode)){
//解析响应内容
getCommandRsp(commandTask.getMethod(), commandResult.getJSONObject("resultDetail").getInt("result"));
}
//更新任务状态
mapCache.put(commandTask.getCommandId(), commandTask);
}
}
/**
* 解析响应内容
* @param method
* @param value
*/
private void getCommandRsp(String method, Integer value) {
BasicDevice device = null;
if (method != null && value != null) {
//判断命令,将响应后的内容更新相应设备状态
switch (method) {
case ServiceConstant.ON_OFF:
device = new Bulb();
device.setStatus(value);
device.setUpdateTime();
break;
case ServiceConstant.QUERY_STATUS:
device = new Bulb();
device.setStatus(value);
device.setUpdateTime();
break;
case ServiceConstant.QUIT_PYTHON:
device = new OperationPi();
device.setStatus(value);
device.setUpdateTime();
break;
default:
throw new CustomException(DeviceServiceImpl.class, "获取命令响应类型没有匹配");
}
//更新设备状态
mapCache.put(device.getDeviceKey(), device);
}
}
其他的还有给lettuce-Land提供的接口
/**
* 获取灯的当前状态
* @return
*/
@GetMapping(value = "/getBulbStatus", produces = { "application/json;charset=UTF-8" })
public GenericResponse getBulbStatus(){
return ResponseFormat.retParam(200, deviceService.getBulbStatus());
}
/**
* 获取设备当前状态
* @return
*/
@GetMapping(value = "/getOperationPiStatus", produces = { "application/json;charset=UTF-8" })
public GenericResponse getOperationPiStatus(){
return ResponseFormat.retParam(200, deviceService.getOperationPiStatus());
}
和给监控提供的接口
/**
* 获取设备列表
* @return
*/
@GetMapping(value = "/getDeviceList", produces = { "application/json;charset=UTF-8" })
public GenericResponse getDeviceList(){
return ResponseFormat.retParam(200, mapCache.getDeviceList());
}
/**
* 获取任务列表
* @return
*/
@GetMapping(value = "/getTaskList", produces = { "application/json;charset=UTF-8" })
public GenericResponse getTaskList(){
return ResponseFormat.retParam(200, mapCache.getTaskList());
}
监控是每5秒刷新一次。