应用开发手册

更新时间:

1 基本概念说明

本文提到的边缘应用,是指部署在物业管理一体机的边缘应用,是基于IoT的领域服务对接方案实现的边缘应用。领域服务对接方案里有服务模型定义和数据模型定义,下面介绍一些相关的基本概念:

服务模型:

是一组可提供完整业务功能的HTTP/HTTPS接口。边缘应用开发者可以根据对应的场景需求和业务需求进行这套接口定义。

服务提供方:

服务提供方即是可以提供服务模型里定义的服务功能的应用。服务提供方即可以是云端应用,也可以是边缘端应用

服务依赖方:

服务依赖方即是使用服务模型里定义的服务功能的应用。服务依赖方也可以是云端应用或边缘端应用

数据模型:

  • 使用场景:本地系统/设备上报的消息可以基于数据模型进行定义,例如人脸通行事件

  • 通过数据模型和IoT数据总线机制,可实现应用间的数据信息流转

  • (数据提供方)应用可以通过数据添加的API接口,将本地系统或设备的消息送入数据总线。

  • (数据消费方)应用可以通过查询数据模型的API接口获取消息,也可以通过数据变更消息订阅方式获取消息。

  • 数据模型支持图片文件上传,例如人脸通行事件的人脸照片

物业管理一体机框架:

image.png

物业管理一体机是基于K8s框架实现的,底层是托管底座,之上是基于docker的各种应用和服务。

LE组件是基于docker的服务程序,里面包含了支持各种设备接入驱动,例如门禁驱动,车行驱动,EBA设备驱动等

边缘应用也是基于docker的应用程序。边缘应用可通过编译出的jar包,打包成镜像,然后通过IoT云端平台将应用下发到指定的物业管理一体机。边缘应用的启动入口,可以通过应用jar包里的docker file指定。

2 整体架构

2.1 核心模块功能说明

云端应用:即SaaS应用,一般由ISV提供,是服务模型依赖方,负责服务模型的调用和数据订阅。

物联网云平台:即IoT云端平台。在领域服务对接方案里,与边缘端核心服务一起提供服务总线和数据总线框架服务。

边缘托管应用:即运行在物业管理一体机的边缘应用,是服务模型的提供方。该应用一方面接受云端应用通过IoT平台的服务模型调用,然后将调用转换成本地系统支持的接口调用,另外一方面该应用处理本地的事件上报,然后通过边缘数据总线,将消息安装数据模型格式要求,上传到IoT云端平台。

2.2 核心流程说明

基于边缘适配器应用的领域服务对接方案,核心流程包括两个:至上而下的服务模型调用,至下而上的数据上报。下面分别说明。下面的序号,与框架图中的序号一一对应。

服务调用:

(1) 云端SaaS应用调用服务模型提供的HTTP服务。 发起服务调用时,需提供项目Appkey, 路径名称(path)里需包含服务模型ID+接口方法名称。

(2) 边缘托管应用,侦听到对应的服务调用后,进行业务处理。

数据上报:

(5) 边缘托管应用处理事务后,可以将事件内容转化成数据模型要求的格式,然后根据(边缘)IoT平台提供的数据插入接口,上报数据到边缘数据总线,然后内部流转到云端数据总线。

(6) 云端SaaS应用,可以通过IoT平台提供的查询数据模型的API接口获取数据,也可以通过数据变更消息订阅方式获取数据。

3 开发指导

这里介绍的边缘应用,是服务提供方应用。

3.1 对接服务模型

边缘应用侦听到对应的服务调用后,进行适当的适配转换,再调用本地系统提供的HTTP服务。

边缘应用对接服务模型,可以参考IoT公开链接:服务总线:服务提供的开发示例:

https://help.aliyun.com/document_detail/114863.html?spm=a2c4g.11186623.6.579.30243aa12KgeDk#h2-7-2-8

3.2 对接数据模型

具体对接可参考IoT公开链接:边缘应用数据总线对接:https://help.aliyun.com/document_detail/145523.html?spm=a2c4g.11186623.6.595.81bc5ee4wgidUt

这里只补充说明一些需要关注的内容:

通过本地系统环境变量获取appkey and appSecrect:

  1. public static final String appkey =System.getenv("iot.hosting.appKey");

  2. public static final String appSecret =System.getenv("iot.hosting.appSecret");

  3. 边缘端数据模型服务路由

private static final String DATA_EDGE_PATH ="api.link.aliyun.com";

请求参数:

请求参数里的modelId,就是对应的数据模型Id。

  1. request.putParam("modelId","value1");

对于边缘端应用,下面两个请求参数可以忽略掉

  1. request.putParam("scopeId","value2");

  2. request.putParam("appId","value3");

上传文件数据模型接口说明:

下面链接文档提供的接口只是获得了需要上传的文件名称和URL:

https://help.aliyun.com/document_detail/145523.html?spm=a2c4g.11186623.6.595.56c2610adevJLp#h2-2-5-5

如返回接口示例:

{
"id":"6fr2c332-c1db-417c-aa15-8c5trg3r5d92",
"code":200,
"message":null,
"localizedMsg":null,
"data":{
"fileName":"5269712352e5.jpg",
"url":"https://xxxxx.xxx.xx.com/xxx/file/5269712352e5.jpg?Expires=1557902379&OSSAccessKeyId=uyedjYL******&Signature=sotMFFIq4RP%2BWJSDScE8SxvO******"
}

开发者还需将真正需要上传的文件,上传到接口返回值里指定的fileName 和 url,示例代码如下:

// response为上传文件数据模型接口的返回
result = new String(response.getBody(), "UTF-8");
UploadResult uploadResult = JSON.parseObject(result, UploadResult.class);
data nameAndPath = uploadResult.getData();
String url = nameAndPath.getUrl();
HttpClient httpClient = HttpClients.createDefault();
HttpResponse response;
HttpPut put = new HttpPut(url);
// byte[] fileBytes:为准备要上传的图片文件
HttpEntity reqEntity = EntityBuilder.create().setBinary(fileByte).build(); 
put.setEntity(reqEntity);
response = httpClient.execute(put);

4 应用自测

4.1 物联网应用服务平台应用调试接口

应用通过AIoT开放平台创建部署成功后,可以基于平台的应用调试接口,进行服务接口的调用验证:

进入应用管理,点击需调试的应用。注意这个应用需要处于已发布状态,如下图所示

image.png

点击实例管理-测试

image.png

点击服务提供测试,选择一个要调试验证的接口,点击右侧的进入接口调试界面

image.png

接口请求里,可以根据输入参数要求补充相关参数,然后点击发送,然后查看接口返回信息,如下图所示:

image.png

4.2 模拟本地系统事件上报:Postman

前置条件:

  • PC机安装了Postman软件

  • 物业管理一体机部署了适配器应用

  • PC机需要与物业管理一体机在相同的局域网内

模拟事件上报:

下面示例,以《停车场系统领域模型V3.1-数据模型定义》-车辆通行为例

路径:物业一体机ip:port/具体路径

请求Body内容示例如下:

{
    "carCode": "浙A5C393",
    "inTime": "2016-10-18 16:44:44",
    "passTime": "2016-10-28 16:44:44",
    "parkID": "88",
    "inOrOut": "1",
    "GUID": "134589c1d68d44d38dcb7f084b9cf8a1",
    "channelID": "1",
    "channelName": "北大门出口",
    "imagePath": "https://ss1.bdstatic.com\\70cFuXSh_Q1YnxGkpoWK1HF6hhy\\it\\u=3854694535,624476780&fm=11&gp=0.jpg"
}

Postman示例截图如下。其中192.168.1.40是物业一体机的内网IP地址,需根据实际情况填写,10060是固定的端口号。

image.png

4.3 模拟本地服务调用:Postman

前置条件:

  • PC机安装了Postman软件

  • 物业管理一体机部署了适配器应用

  • PC机需要与物业管理一体机在相同的局域网内

  • 本地部署了相应的系统,例如部署了立方停车系统

模拟服务调用:

下面示例,以《停车场系统领域服务V3.1-服务模型定义》-1.1 查询停车场信息为例

路径: 物业一体机IP:port/服务模型ID/服务接口path

特别注意Header内容:定义 Content-Type 为 application/octet-stream

请求Body内容示例如下:

{
 "id":"UniqueRequestId",
 "version":"1.0",
 "request":{
  "apiVer":"1.0"
  },
 "params":{
  }
}

Postman示例截图如下。其中192.168.1.40是物业一体机的内网IP地址,需根据实际情况填写,10060是固定的端口号。

image.png

4.4 如何查看边缘应用日志

应用开发完成并部署到物业一体机进行调试,可以登录到物业一体机,查看对应的应用日志:

进入应用管理,点击需调试的适配器应用。注意这个应用需要处于已发布状态,如下图所示

image.png

选择实例管理-管理

image.png

选择节点运维

image.png

点击SSH终端,选择容器(一般只有一个,点击选择即可):

image.png

选择完容器,将自动登录到物业一体机。当前目录即保持有该应用的日志

image.png

5 示例代码

5.1 对接服务模型

车辆加入安全黑/白名单服务为例

1、入口层

package com.aliyun.iotx.parkinglot.adapter.web.servicemodelcontroller;

import com.alibaba.fastjson.JSON;
import com.aliyun.iotx.common.base.service.IoTxResult;
import com.aliyun.iotx.parkinglot.adapter.enums.ParkingLotAdapterEnum;
import com.aliyun.iotx.parkinglot.adapter.service.BlackWhiteListService;
import com.aliyun.iotx.parkinglot.adapter.service.dto.VehiclePermissionalDTO;
import com.aliyun.iotx.parkinglot.adapter.service.serviceImpl.logicjudge.LogicJudge;
import com.aliyun.iotx.parkinglot.adapter.utils.IoTxResultUtils;
import com.aliyun.iotx.parkinglot.adapter.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 黑白名单
 */
@Slf4j
@RestController
@RequestMapping(value = "/iotx_parking_service_model", method = RequestMethod.POST)
public class BlackWhiteListController {

    @Autowired
    private BlackWhiteListService blackWhiteListService;

    /**
     * 7.1车辆加入安全黑/白名单
     * @param request
     * @return
     */
    @RequestMapping(value = "/parkingLotVehicleListAdd")
    public IoTxResult vehicleAddList(HttpServletRequest request) throws Exception {

        String json="";
        json= new String(readInputStream(request.getInputStream()),"UTF-8");
        
        //将json数据解析成vo对象接收
        BWListVo bwListVo = JSON.parseObject(json, BWListVo.class);
        BlackWhiteListVo blackWhiteList = bwListVo.getParams();
        
        //自己业务的处理逻辑
        blackWhiteListService.vehicleAddList(blackWhiteList);
        
        IoTxResult<Object> ioTxResult = new IoTxResult<>();
        ioTxResult.setData(null);
        IoTxResultUtils.ioTxResultSet(ParkingLotAdapterEnum.SUCCDESS_VEHICLEADDLIST, ioTxResult);
        return ioTxResult;
    }
}

/**
依赖类:
*/

@NoArgsConstructor
@AllArgsConstructor
@Data
class BWListVo implements Serializable {

    private static final long serialVersionUID = -8067179280515471493L;

    /**
     * request里的全局唯一id透传
     */
    private String id;
    
    /**
     * 请求协议版本
     */
    private String version;

    private Map<String, Object> request;

    private BlackWhiteListVo params;

}


/**
 * 车辆加入安全
 * 黑白名单入参
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@ValidateBean
class BlackWhiteListVo implements Serializable {

    private static final long serialVersionUID = 831783475630914****L;

    @NotBlank(message = "parkingLotId不能为空")
    private String parkingLotId;

    @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
    @EachValidate(constraint = NotBlank.class,message = "车牌号不能为空,不能有空字符串")
    private List<String> plateNumber;

    @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
    private List<String> areaId;

    @NotBlank(message = "type不能为空")
    private String type;

    private String effectiveDate;
    private String expiryDate;
}



5.2 对接数据模型

车辆通行数据模型为例

车辆通行接口入口

package com.aliyun.iotx.parkinglot.adapter.web.datamodelcontroller;

import com.alibaba.cloudapi.sdk.model.ApiResponse;
import com.alibaba.fastjson.JSON;
import com.aliyun.iotx.parkinglot.adapter.service.datamodelservice.IoTParkPassRecordService;
import com.aliyun.iotx.parkinglot.adapter.utils.UrlFormatUtil;
import com.aliyun.iotx.parkinglot.adapter.vo.InOutRecordVo;
import com.aliyun.iotx.parkinglot.adapter.vo.LFResultVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 数据模型
 * 车辆通行
 */
@Slf4j
@RestController
public class IoTParkPassRecordController {

    @Autowired
    private IoTParkPassRecordService parkPassRecordService;

    /**
     * 上传进出记录
     * 当代项目的图片地址格式是
     * @param inOutRecordVo
     * @return
     */
    @RequestMapping(value = "/reportInAndOutRecord", method = RequestMethod.POST)
    public LFResultVo inOutRecordReport(@RequestBody InOutRecordVo inOutRecordVo) {

        String newUrl = UrlFormatUtil.change(inOutRecordVo.getImagePath());
        inOutRecordVo.setImagePath(newUrl);

        //把记录转换为标准数据模型后上传,需要引用后一段通用的DOP上传代码片段。参考下面的边缘应用数据总线参考使用:
        ApiResponse apiResponse = parkPassRecordService.inOutRecordReport(inOutRecordVo);
        
        LFResultVo lfResultVo = new LFResultVo();
        if (apiResponse.getCode() == 200) {
            lfResultVo.setResCode(0);
            lfResultVo.setResMsg("数据上报成功");
        } else {
            //立方上报数据失败返回数字1
            lfResultVo.setResCode(1);
            lfResultVo.setResMsg("数据上报失败");
        }
        return lfResultVo;
    }
}

边缘应用数据总线参考使用:

public class BlackWhiteList {
    /**
     * 车辆通行上传
     * @param request
     * @return
     */
    public static void main(String[] args) {
        IoTApiRequest ioTApiRequest = new IoTApiRequest();
        String uuid = UUID.randomUUID().toString();
        String uuidOne = uuid.replace("-", "");
        
        ioTApiRequest.setId(uuidOne);
        ioTApiRequest.setApiVer("1.0");
        ioTApiRequest.putParam("modelId", "iot_park_pass_record");
        
        JSONObject properties = new JSONObject();
        properties.put("direction", "获取入参的对应的值");
        properties.put("openType", "获取入参的对应的值");
        properties.put("plateNumber","获取入参的对应的值");
        
        //首先获取到要上传的文件名和上传的路径
        ApiResponse response = getUploadFileNameAndPath();
        String result = null;
        try {
            result = new String(response.getBody(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        
        //将JSON字符串转化成对象
        UploadResult uploadResult = JSON.parseObject(result, UploadResult.class);
        data nameAndPath = uploadResult.getData();
        
        properties.put("plateNumberImage", nameAndPath.getFileName());
        
        //根据底层停车系统上报的文件路径下载文件
        String lfFileUrl = inOutRecordVo.getImagePath();
        //下载后文件的名字
        String name = nameAndPath.getFileName();
        //文件保存路径
        String savePath = fsp.getSavePathOne();

        if(!StringUtil.isEmpty(lfFileUrl)){
            String url = FileUtil.getURL(lfFileUrl);
            FileUtil.downLoadFromUrl(url,name,savePath);
        }
        
        //根据dop接口获取到的path将文件上传
        //上传文件的路径
        String url = nameAndPath.getUrl();
        //要上传的文件
        File file = new File(savePath + name);
        //判断上传是否成功
        //首先判断文件是否存在,存在才上传文件
        if(file.exists()){
            byte[] fileBytes = FileUtil.getFileBytes(file);
            boolean b1 = FileUtil.uploadFile(url, fileBytes);
            if (!b1) {
                log.info("文件上传失败");
                throw new ParkingLotAdapterException(502, "文件上传失败", "File upload failed");
            }
        }
        //文件上传成功后,清除本地缓存的文件
        FileUtil.deleteFile(savePath, name);

        properties.put("typePermission", "获取入参的对应的值");
        properties.put("plateColor", "获取入参的对应的值");
        properties.put("plateType", "获取入参的对应的值");
        properties.put("vehicleColor", "获取入参的对应的值");
        properties.put("vehicleType", "获取入参的对应的值");
        properties.put("barrierId", "获取入参的对应的值");

        if (StringUtils.isNotBlank(inOutRecordVo.getChannelName())) {
            properties.put("barrierName", "获取入参的对应的值");
        }
        
        properties.put("parkingLotId","获取入参的对应的值");
        properties.put("areaId", "未知");
        properties.put("orderNumber", "获取入参的对应的值");
        properties.put("recordId", "未知");
        
        //数据上报的时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String eventTime = dateFormat.format(date);
        properties.put("eventTime",eventTime);  //应该产生一个时间上报
        
        ioTApiRequest.putParam("properties", properties);
        ApiResponse apiResponse = syncApiClient.postBody(host, path, ioTApiRequest, "https".equalsIgnoreCase(schema));
                    
    }
    
    public static ApiResponse getUploadFileNameAndPath() {

        IoTApiRequest request = new IoTApiRequest();

        //设置api的版本
        request.setApiVer("0.0.1");

        // 接口参数
        String uuid = UUID.randomUUID().toString();
        request.setId(uuid.replace("-", ""));

        JSONObject param = new JSONObject();
        param.put("appId",  "应用id");
        param.put("version", "应用版本");

        request.putParam("properties", param);

        //这个参数对应于数据模型中需要上传文件的字段
        request.putParam("attrName", "plateNumberImage");
        
        //这个参数对应于数据模型的模型id
        request.putParam("modelId", "iot_park_pass_record");
        
        request.putParam("fileType", "文件类型");
        request.putParam("version", "版本);
        request.putParam("fileSize", "文件大小");

        try {
            ApiResponse apiResponse = syncApiClient.postBody(DATA_EDGE_PATH,
                    "/data/model/data/upload", request, false);
            return apiResponse;
        } catch (IOException e) {
            log.info("上传文件获取文件名和上传路径接口出现异常:{}", e.getMessage());
        }
        return null;
    }
    
}

pom依赖:

        <dependency>
            <groupId>com.aliyun.iotx</groupId>
            <artifactId>iotx-api-gateway-client</artifactId>
        </dependency>

        <dependency>
            <groupId>com.aliyun.iotx</groupId>
            <artifactId>common-base</artifactId>
        </dependency>