“实现基于CTWing(AIOT)平台的TCP透传协议应用订阅端(北向应用):Springboot Netty”

之前实现了使用Springboot+Netty基于天翼物联网平台CTWing(AIOT)终端TCP协议(透传模式)-设备终端(南向设备),模拟设备发送的数据给到了AIOT平台,那么在第三方应用也需要去订阅AIOT平台的数据,以及对设备进行下发指令(消息),订阅设备消息可以使用http,对整个产品进行设备消息订阅。

 订阅方地址可以先用接口来接收json字符串,他的格式是json的(接收设备订阅的数据后,必须要有返回,否则AIOT平台会认为没有推送到位,还会推送直到触发禁制)

package boot.ctwing.tcp.app.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 	蚂蚁舞
 */
@Controller
@RequestMapping("/receive")
public class CtWingReceiveController {

	private final Logger log =  LoggerFactory.getLogger(this.getClass());

	@PostMapping(value = "/tcp-transfer/data", produces = "application/json;charset=UTF-8")
	@ResponseBody
	public Map<String, Object> receiveTcpTransferController(@RequestBody String str) {
		log.info(str);
		Map<String, Object> mapResult = new HashMap<>();
		mapResult.put("state", true);
		return mapResult;
	}

}

如果说不创建应用开发,只需要设备推送上来的数据,那么此刻就可以去启动模拟的设备,尝试发消息,北向应用也启动,这样的话就能订阅到从模拟设备发送过来的数据,下下行数据,需要先在应用开发-应用管理里面创建应用得到APP_KEY和APP_SECRET。

 

 在这里我们能看到AIOT的接口文档和在线调试还有sdk下载,在这里我只需要下发指令的操作,那么就去找下发指令的文档信息,在线调试是最快调通下发的,我这里已经把它转换成程序了。

 新建Springboot的maven项目,pom.xml文件导入依赖包(用到了swagger来测试下发数据)

    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>boot.ctwing.tcp.app</groupId>
    <artifactId>boot-example-ctwing-tcp-app-2.0.5</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>boot-example-ctwing-tcp-app-2.0.5</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.20</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ctg.ag</groupId>
            <artifactId>ctg-ag-sdk-core</artifactId>
            <version>2.7.0-20221208.015855-5</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/doc/lib/ctg-ag-sdk-core-2.7.0-20221208.015855-5.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.ctg.ag</groupId>
            <artifactId>ag-sdk-biz-108549.tar.gz</artifactId>
            <version>20221221.143437-SNAPSHOT</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/doc/lib/ag-sdk-biz-108549.tar.gz-20221221.143437-SNAPSHOT.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>boot.ctwing.tcp.app.BootCtWingTcpApp</mainClass>
                    <includeSystemScope>true</includeSystemScope><!--外部进行打包-->
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Springboot启动类,Netty启动

package boot.ctwing.tcp.app;


import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 *  蚂蚁舞
 */
@SpringBootApplication
public class BootCtWingTcpApp implements CommandLineRunner
{
    public static void main( String[] args ) throws Exception {
    	SpringApplication.run(BootCtWingTcpApp.class, args);
        System.out.println( "Hello World!" );

    }

    @Override
    public void run(String... args) throws Exception {

    }
}

SwaggerConfig配置
package boot.ctwing.tcp.app.config;

import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 *  蚂蚁舞
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                .paths(Predicates.not(PathSelectors.regex("/error.*")))
                .paths(PathSelectors.regex("/.*"))
                .build().apiInfo(apiInfo());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("天翼物联网CtWing北向接收订阅数据端")
                .description("接收订阅数据端需要的下行测试接口")
                .version("0.01")
                .build();
    }

    /**
     * http://localhost:8178/doc.html  地址和端口根据实际项目查看
     */


}
CtWingConstant
package boot.ctwing.tcp.app.config;

/**
 *  蚂蚁舞
 */
public class CtWingConstant {

    //  产品Id
    public static final int PRODUCT_ID = 15506850;

    //  应用开发-应用管理-具体应用的App Key
    public static final String APP_KEY = "XXXXXXX";

    //  应用开发-应用管理-具体应用的App Secret
    public static final String APP_SECRET = "XXXXXXXX";

    //  产品信息里的Master-APIkey
    public static final String MASTER_KEY = "XXXXXXXXXXXXXXXXXXXX";



    //  数据订阅通知
    public static final String dataReport = "dataReport";

    //  设备上线下线订阅通知
    public static final String deviceOnlineOfflineReport = "deviceOnlineOfflineReport";

    //  指令下发结果订阅
    public static final String commandResponse = "commandResponse";

}
CtWingReceiveController
package boot.ctwing.tcp.app.controller;

import boot.ctwing.tcp.app.config.CtWingConstant;
import boot.ctwing.tcp.app.utils.CtWingUtils;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * 	蚂蚁舞
 */
@Controller
@RequestMapping("/receive")
public class CtWingReceiveController {

	private final Logger log =  LoggerFactory.getLogger(this.getClass());

	@PostMapping(value = "/tcp-transfer/data", produces = "application/json;charset=UTF-8")
	@ResponseBody
	public Map<String, Object> receiveTcpTransferController(@RequestBody String str) {
		log.info(str);
		JSONObject jsonObject = JSONObject.parseObject(str);
		String messageType = jsonObject.getString("messageType");
		switch (messageType){  // 这里只是例举了三种
			case CtWingConstant.dataReport:
				String APPdata = jsonObject.getJSONObject("payload").getString("APPdata");
				byte[] decoded2 = Base64.getDecoder().decode(APPdata);
				String hex2 = CtWingUtils.bytesToHexStr(decoded2);
				System.out.println(CtWingUtils.hexStrToStr(hex2));
				break;
			case CtWingConstant.deviceOnlineOfflineReport:
				String eventType = jsonObject.getString("eventType");
				System.out.println(eventType);
				break;
			case CtWingConstant.commandResponse:
				String taskId = jsonObject.getString("taskId");
				//JSONObject jsonResult = jsonObject.getJSONObject("result");
				System.out.println(taskId);
				break;
			default:
				break;
		}
		Map<String, Object> mapResult = new HashMap<>();
		mapResult.put("state", true);
		return mapResult;
	}

}
CtWingDownCmdController
package boot.ctwing.tcp.app.controller;

import boot.ctwing.tcp.app.config.CtWingConstant;
import boot.ctwing.tcp.app.utils.CtWingDownSendUtils_1;
import boot.ctwing.tcp.app.utils.CtWingDownSendUtils_2;
import boot.ctwing.tcp.app.utils.CtWingDownSendUtils_3;
import com.alibaba.fastjson.JSONObject;
import com.ctg.ag.sdk.biz.aep_device_command.CreateCommandResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * 	蚂蚁舞 下行指令
 */
@Controller
@RequestMapping("/down")
public class CtWingDownCmdController {

	private final Logger log =  LoggerFactory.getLogger(this.getClass());

	@PostMapping(value = "/cmd_send_1")
	@ResponseBody
	public String cmd_send_1(@RequestParam(name="imei",required = true) String imei, @RequestParam(name="payload",required = true) String payload) throws Exception {
		JSONObject jsonContent = new JSONObject();
		jsonContent.put("dataType", 1);  // TCP和LWM2M协议透传的content内容, payload:指令内容,数据格式为十六进制时需要填十六进制字符串,
		jsonContent.put("payload",payload);	//	dataType:数据类型:1字符串,2十六进制

		JSONObject jsonObject = new JSONObject();
		jsonObject.put("content", jsonContent);	//	指令内容,必填,格式为Json
		jsonObject.put("deviceId", CtWingConstant.PRODUCT_ID+imei);  //	设备ID,(当指令级别为设备级时必填,为设备组级时则不填)
		jsonObject.put("operator", "myw");	//	操作者,必填
		jsonObject.put("productId", CtWingConstant.PRODUCT_ID);	//	产品ID,必填
		jsonObject.put("ttl", 0);	//	设备指令缓存时长,选填。单位:秒,取值范围:0-864000,不携带则默认值:7200 如不需缓存请填0
		jsonObject.put("level", 1);	//	指令级别,1或2为设备级别,3为设备组级别,选填。不填默认设备级

		CreateCommandResponse response = CtWingDownSendUtils_1.sendApi(jsonObject.toString());
		log.info(response.toString());
		return response.toString();
	}

	@PostMapping(value = "/cmd_send_2")
	@ResponseBody
	public String cmd_send_2(@RequestParam(name="imei",required = true) String imei, @RequestParam(name="payload",required = true) String payload) throws Exception {
		JSONObject jsonContent = new JSONObject();
		jsonContent.put("dataType", 1);  // TCP和LWM2M协议透传的content内容, payload:指令内容,数据格式为十六进制时需要填十六进制字符串,
		jsonContent.put("payload",payload);	//	dataType:数据类型:1字符串,2十六进制

		JSONObject jsonObject = new JSONObject();
		jsonObject.put("content", jsonContent);	//	指令内容,必填,格式为Json
		jsonObject.put("deviceId", CtWingConstant.PRODUCT_ID+imei);  //	设备ID,(当指令级别为设备级时必填,为设备组级时则不填)
		jsonObject.put("operator", "myw");	//	操作者,必填
		jsonObject.put("productId", CtWingConstant.PRODUCT_ID);	//	产品ID,必填
		jsonObject.put("ttl", 0);	//	设备指令缓存时长,选填。单位:秒,取值范围:0-864000,不携带则默认值:7200 如不需缓存请填0
		jsonObject.put("level", 1);	//	指令级别,1或2为设备级别,3为设备组级别,选填。不填默认设备级

		String str = CtWingDownSendUtils_2.httpPostExample(jsonObject.toString());
		log.info(str);
		return str;
	}

	@PostMapping(value = "/cmd_send_3")
	@ResponseBody
	public String cmd_send_3(@RequestParam(name="imei",required = true) String imei, @RequestParam(name="payload",required = true) String payload) throws Exception {
		JSONObject jsonContent = new JSONObject();
		jsonContent.put("dataType", 1);  // TCP和LWM2M协议透传的content内容, payload:指令内容,数据格式为十六进制时需要填十六进制字符串,
		jsonContent.put("payload",payload);	//	dataType:数据类型:1字符串,2十六进制

		JSONObject jsonObject = new JSONObject();
		jsonObject.put("content", jsonContent);	//	指令内容,必填,格式为Json
		jsonObject.put("deviceId", CtWingConstant.PRODUCT_ID+imei);  //	设备ID,(当指令级别为设备级时必填,为设备组级时则不填)
		jsonObject.put("operator", "myw");	//	操作者,必填
		jsonObject.put("productId", CtWingConstant.PRODUCT_ID);	//	产品ID,必填
		jsonObject.put("ttl", 0);	//	设备指令缓存时长,选填。单位:秒,取值范围:0-864000,不携带则默认值:7200 如不需缓存请填0
		jsonObject.put("level", 1);	//	指令级别,1或2为设备级别,3为设备组级别,选填。不填默认设备级

		String strResult = CtWingDownSendUtils_3.send_post(jsonObject.toString());
		log.info(strResult);
		return strResult;

	}


	@PostMapping(value = "/cmd_send_test")
	@ResponseBody
	public String cmd_send_test() throws Exception {
		JSONObject jsonContent = new JSONObject();
		jsonContent.put("dataType", 1);  // TCP和LWM2M协议透传的content内容, payload:指令内容,数据格式为十六进制时需要填十六进制字符串,
		jsonContent.put("payload","hello"); //	dataType:数据类型:1字符串,2十六进制

		JSONObject jsonObject = new JSONObject();
		jsonObject.put("content", jsonContent);	//	指令内容,必填,格式为Json
		jsonObject.put("deviceId", CtWingConstant.PRODUCT_ID+"869401041201815"); //	设备ID,(当指令级别为设备级时必填,为设备组级时则不填)
		jsonObject.put("operator", "myw");	//	操作者,必填
		jsonObject.put("productId", CtWingConstant.PRODUCT_ID);	//	产品ID,必填
		jsonObject.put("ttl", 0);	//	设备指令缓存时长,选填。单位:秒,取值范围:0-864000,不携带则默认值:7200 如不需缓存请填0
		jsonObject.put("level", 1);	//	指令级别,1或2为设备级别,3为设备组级别,选填。不填默认设备级

//		CreateCommandResponse response = CtWingDownSendUtils_1.sendApi(jsonObject.toString());
//		log.info(response.toString());
//		return response.toString();
//
//		String str = CtWingDownSendUtils_2.httpPostExample(jsonObject.toString());
//		log.info(str);
//		return str;

		String strResult = CtWingDownSendUtils_3.send_post(jsonObject.toString());
		log.info(strResult);
		return strResult;
	}





}

CtWingDownSendUtils_1 

package boot.ctwing.tcp.app.utils;


import boot.ctwing.tcp.app.config.CtWingConstant;
import com.ctg.ag.sdk.biz.AepDeviceCommandClient;
import com.ctg.ag.sdk.biz.aep_device_command.CreateCommandRequest;
import com.ctg.ag.sdk.biz.aep_device_command.CreateCommandResponse;

/**
 *  蚂蚁舞
 */
public class CtWingDownSendUtils_1 {

    /**
     *  ctwing提供的sdk方式下发指令,方便快速,在后台创建应用后可获取sdk
     */
    public static CreateCommandResponse sendApi(String jsonStr) throws Exception {

        AepDeviceCommandClient client = AepDeviceCommandClient.newClient().appKey(CtWingConstant.APP_KEY).appSecret(CtWingConstant.APP_SECRET).build();

        CreateCommandRequest request = new CreateCommandRequest();
        // set your request params here
        request.setParamMasterKey(CtWingConstant.MASTER_KEY);	// single value
        request.setBody(jsonStr.getBytes());	//具体格式
        CreateCommandResponse commandResponse = client.CreateCommand(request);
        client.shutdown();
        return commandResponse;
    }




}
CtWingDownSendUtils_2
package boot.ctwing.tcp.app.utils;

import boot.ctwing.tcp.app.config.CtWingConstant;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;

public class CtWingDownSendUtils_2 {


    //  官方提供的example
    public static String httpPostExample(String bodyString) throws Exception {
        String secret = CtWingConstant.APP_SECRET;//密钥,到控制台->应用管理打开应用可以找到此值
        String application = CtWingConstant.APP_KEY;//appKey,到控制台->应用管理打开应用可以找到此值
        String version = "20190712225145";//api版本,到文档中心->使能平台API文档打开要调用的api可以找到版本值
        String MasterKey = CtWingConstant.MASTER_KEY;//MasterKey,在产品中心打开对应的产品查看此值
        // 下面以增加设备的API为例【具体信息请以使能平台的API文档为准】。
        //请求BODY,到文档中心->使能平台API文档打开要调用的api中,在“请求BODY”中查看
        //String bodyString = "{\"deviceName\":\"testDevice\",\"deviceSn\":\"\",\"imei\":123456789012345,\"operator\":\"admin\",\"productId\":\"9392\"}";

        CloseableHttpClient httpClient = null;
        HttpResponse response = null;
        httpClient = HttpClientBuilder.create().build();
        long offset = getTimeOffset();// 获取时间偏移量,方法见前面
        // 构造请求的URL,具体参考文档中心->使能平台API文档中的请求地址和访问路径
        URIBuilder uriBuilder = new URIBuilder();
        uriBuilder.setScheme("https");
        uriBuilder.setHost("ag-api.ctwing.cn/aep_device_command"); //请求地址
        uriBuilder.setPath("/command"); //访问路径,可以在API文档中对应API中找到此访问路径
        // 在请求的URL中添加参数,具体参考文档中心->API文档中请求参数说明
        // (如果有MasterKey,将MasterKey加到head中,不加在此处)
        //uriBuilder.addParameter("productId", "9392");//如果没有其他参数,此行不要
        HttpPost httpPost = new HttpPost(uriBuilder.build());//构造post请求
        long timestamp = System.currentTimeMillis() + offset;// 获取时间戳
        Date date = new Date(timestamp);
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        String dataString = dateFormat.format(date);// 生成格式化的日期字符串
        // head中添加公共参数
        httpPost.addHeader("MasterKey", MasterKey);// MasterKey加在此处head中
        httpPost.addHeader("application", application);
        httpPost.addHeader("timestamp", "" + timestamp);
        httpPost.addHeader("version", version);
        httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
        httpPost.addHeader("Date", dataString);
        // 下列注释的head暂时未用到
        // httpPost.addHeader("sdk", "GIT: a4fb7fca");
        // httpPost.addHeader("Accept", "gzip,deflate");
        // httpPost.addHeader("User-Agent", "Telecom API Gateway Java SDK");
        // 构造签名需要的参数,如果参数中有MasterKey,则添加来参与签名计算,
        // 其他参数根据实际API从URL中获取,如有其他参数,写法参考get示例
        Map<String, String> param = new HashMap<String, String>();
        param.put("MasterKey", MasterKey);
        // 添加签名
        httpPost.addHeader("signature", sign(param, timestamp, application, secret, bodyString.getBytes()));
        //请求添加body部分
        httpPost.setEntity(new StringEntity(bodyString,"utf-8"));
        try {
            // 发送请求
            response = httpClient.execute(httpPost);
            // 从response获取响应结果
            String result = new String(EntityUtils.toByteArray(response.getEntity()));
            httpClient.close();
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @param param    api 配置参数表
     * @param timestamp UNIX格式时间戳
     * @param application appKey,到应用管理打开应用可以找到此值
     * @param secret 密钥,到应用管理打开应用可以找到此值
     * @param body 请求body数据,如果是GET请求,此值写null
     * @return 签名数据
     */
    public static String sign(Map<String, String> param, long timestamp, String application, String secret, byte[] body) throws Exception {
        // 连接系统参数
        StringBuffer sb = new StringBuffer();
        sb.append("application").append(":").append(application).append("\n");
        sb.append("timestamp").append(":").append(timestamp).append("\n");
        // 连接请求参数
        if (param != null) {
            TreeSet<String> keys = new TreeSet<String>(param.keySet());
            for (String s : keys) {
                String val = param.get(s);
                sb.append(s).append(":").append(val == null ? "" : val).append("\n");
            }
        }
        //body数据写入需要签名的字符流中
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(sb.toString().getBytes("utf-8"));
        if (body != null && body.length > 0) {
            baos.write(body);
            baos.write("\n".getBytes("utf-8"));
        }
        // 得到需要签名的字符串
        String string = baos.toString("utf-8");
        System.out.println("Sign string: " + string);
        // hmac-sha1编码
        byte[] bytes = null;
        SecretKey secretKey = new SecretKeySpec(secret.getBytes("utf-8"), "HmacSha1");
        Mac mac = Mac.getInstance(secretKey.getAlgorithm());
        mac.init(secretKey);
        bytes = mac.doFinal(string.getBytes("utf-8"));
        // base64编码
        // 得到需要提交的signature签名数据
        return new String(Base64.encodeBase64(bytes));
    }

    public static long getTimeOffset() {
        long offset = 0;
        HttpResponse response = null;
        //构造httpGet请求
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet httpTimeGet = new HttpGet("https://ag-api.ctwing.cn/echo");
        try {
            long start = System.currentTimeMillis();
            response = httpClient.execute(httpTimeGet);
            long end = System.currentTimeMillis();
            //时间戳在返回的响应的head的x-ag-timestamp中
            Header[] headers = response.getHeaders("x-ag-timestamp");
            if (headers.length > 0) {
                long serviceTime = Long.parseLong(headers[0].getValue());
                offset = serviceTime - (start + end) / 2L;
            }
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return offset;
    }



}
CtWingDownSendUtils_3
package boot.ctwing.tcp.app.utils;

import boot.ctwing.tcp.app.config.CtWingConstant;
import cn.hutool.http.HttpRequest;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 *  蚂蚁舞
 */
public class CtWingDownSendUtils_3 {

    //  使用hutool request
    private static final String url_time = "https://ag-api.ctwing.cn/echo";

    private static final String url_cmd = "http://ag-api.ctwing.cn/aep_device_command/command";

    public static String send_post(String data) throws Exception {
        long offset = getTimeOffset();// 获取时间偏移量
        long timestamp = System.currentTimeMillis() + offset;// 获取时间戳
        Date date = new Date(timestamp);
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        String dataString = dateFormat.format(date);// 生成格式化的日期字符串

        Map<String, String> param = new HashMap<>();
        param.put("MasterKey", CtWingConstant.MASTER_KEY);

        String signature = sign(param, timestamp, CtWingConstant.APP_KEY, "if8hEBlPIs", data.getBytes());
        //System.out.println(signature);
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json; charset=UTF-8");
        headers.put("application", CtWingConstant.APP_KEY);
        headers.put("MasterKey", CtWingConstant.MASTER_KEY);// MasterKey加在此处head中
        headers.put("timestamp",timestamp+"");
        headers.put("signature", signature);
        headers.put("Date", dataString);
        headers.put("version", "20190712225145"); //版本  需要根据实际来确定
        return HttpRequest.post(url_cmd).headerMap(headers, true).body(data).timeout(6000).execute().body();
    }

    public static String sign(Map<String, String> param, long timestamp, String application, String secret, byte[] body) throws Exception {
        // 连接系统参数
        StringBuffer sb = new StringBuffer();
        sb.append("application").append(":").append(application).append("\n");
        sb.append("timestamp").append(":").append(timestamp).append("\n");
        // 连接请求参数
        if (param != null) {
            TreeSet<String> keys = new TreeSet<String>(param.keySet());
            for (String s : keys) {
                String val = param.get(s);
                sb.append(s).append(":").append(val == null ? "" : val).append("\n");
            }
        }
        //body数据写入需要签名的字符流中
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(sb.toString().getBytes(StandardCharsets.UTF_8));
        if (body != null && body.length > 0) {
            baos.write(body);
            baos.write("\n".getBytes(StandardCharsets.UTF_8));
        }
        // 得到需要签名的字符串
        String string = baos.toString("utf-8");
        // hmac-sha1编码
        byte[] bytes = null;
        SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSha1");
        Mac mac = Mac.getInstance(secretKey.getAlgorithm());
        mac.init(secretKey);
        bytes = mac.doFinal(string.getBytes(StandardCharsets.UTF_8));
        // base64编码
        // 得到需要提交的signature签名数据
        return new String(Base64.encodeBase64(bytes));
    }

    public static long getTimeOffset() {
        long offset = 0;
        long start = System.currentTimeMillis();
        cn.hutool.http.HttpResponse res = HttpRequest.get(url_time).execute();
        long end = System.currentTimeMillis();
        //时间戳在返回的响应的head的x-ag-timestamp中
        String headers = res.header("x-ag-timestamp");
        if (headers.length() > 0) {
            long serviceTime = Long.parseLong(headers);
            offset = serviceTime - (start + end) / 2L;
        }
        return offset;
    }


}
CtWingUtils
package boot.ctwing.tcp.app.utils;

import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *  蚂蚁舞
 */
public class CtWingUtils {

    private static final Logger log =  LoggerFactory.getLogger(CtWingUtils.class);

    private static final char[] HEXES = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    public static String bytesToHexStr(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        StringBuilder hex = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            hex.append(HEXES[(b >> 4) & 0x0F]);
            hex.append(HEXES[b & 0x0F]);
        }
        return hex.toString().toUpperCase();
    }

    public static byte[] hexStrToBytes(String hex) {
        if (hex == null || hex.length() == 0) {
            return null;
        }
        char[] hexChars = hex.toCharArray();
        byte[] bytes = new byte[hexChars.length / 2];   // 如果 hex 中的字符不是偶数个, 则忽略最后一个
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt("" + hexChars[i * 2] + hexChars[i * 2 + 1], 16);
        }
        return bytes;
    }

    public static String strToHexStr(String str) {
        StringBuilder sb = new StringBuilder();
        byte[] bs = str.getBytes();
        int bit;
        for (int i = 0; i < bs.length; i++) {
            bit = (bs[i] & 0x0f0) >> 4;
            sb.append(HEXES[bit]);
            bit = bs[i] & 0x0f;
            sb.append(HEXES[bit]);
        }
        return sb.toString().trim();
    }

    public static String hexStrToStr(String hexStr) {
        //能被16整除,肯定可以被2整除
        byte[] array = new byte[hexStr.length() / 2];
        try {
            for (int i = 0; i < array.length; i++) {
                array[i] = (byte) (0xff & Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16));
            }
            hexStr = new String(array, StandardCharsets.UTF_8);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
        return hexStr;
    }


}

代码目录结构

├─boot-example-ctwing-tcp-app-2.0.5
│  │  pom.xml
│  │  
│  ├─doc
│  │  │  108549_sdk.tar.gz
│  │  │  Readme.md
│  │  │  
│  │  ├─demo
│  │  │      AepCommandModbusDemo.java
│  │  │      AepDeviceCommandDemo.java
│  │  │      AepDeviceCommandLwmProfileDemo.java
│  │  │      AepDeviceControlDemo.java
│  │  │      AepDeviceEventDemo.java
│  │  │      AepDeviceGroupManagementDemo.java
│  │  │      AepDeviceManagementDemo.java
│  │  │      AepDeviceModelDemo.java
│  │  │      AepDeviceStatusDemo.java
│  │  │      AepEdgeGatewayDemo.java
│  │  │      AepFirmwareManagementDemo.java
│  │  │      AepModbusDeviceManagementDemo.java
│  │  │      AepMqSubDemo.java
│  │  │      AepNbDeviceManagementDemo.java
│  │  │      AepProductManagementDemo.java
│  │  │      AepPublicProductManagementDemo.java
│  │  │      AepRuleEngineDemo.java
│  │  │      AepSoftwareManagementDemo.java
│  │  │      AepSoftwareUpgradeManagementDemo.java
│  │  │      AepStandardManagementDemo.java
│  │  │      AepSubscribeNorthDemo.java
│  │  │      AepUpgradeManagementDemo.java
│  │  │      TenantAppStatisticsDemo.java
│  │  │      TenantDeviceStatisticsDemo.java
│  │  │      UsrDemo.java
│  │  │      
│  │  ├─doc
│  │  │      ApiDocument_AEPNB设备管理_aep_nb_device_management.md
│  │  │      ApiDocument_AEP事件上报_aep_device_event.md
│  │  │      ApiDocument_AEP产品管理_aep_product_management.md
│  │  │      ApiDocument_AEP公共产品管理_aep_public_product_management.md
│  │  │      ApiDocument_AEP分组管理_aep_device_group_management.md
│  │  │      ApiDocument_AEP升级包管理_aep_software_management.md
│  │  │      ApiDocument_AEP固件管理_aep_firmware_management.md
│  │  │      ApiDocument_AEP固件远程升级管理_aep_upgrade_management.md
│  │  │      ApiDocument_AEP指令下发_aep_device_command.md
│  │  │      ApiDocument_AEP指令下发_lwm2m有profile_aep_device_command_lwm_profile.md
│  │  │      ApiDocument_AEP指令下发_modbus_aep_command_modbus.md
│  │  │      ApiDocument_AEP数据查询_aep_device_status.md
│  │  │      ApiDocument_AEP标准物模型管理_aep_standard_management.md
│  │  │      ApiDocument_AEP物模型管理_aep_device_model.md
│  │  │      ApiDocument_AEP规则引擎_aep_rule_engine.md
│  │  │      ApiDocument_AEP订阅管理_aep_subscribe_north.md
│  │  │      ApiDocument_AEP设备管理_aep_device_management.md
│  │  │      ApiDocument_AEP设备管理_modbus_aep_modbus_device_management.md
│  │  │      ApiDocument_AEP软件升级管理_aep_software_upgrade_management.md
│  │  │      ApiDocument_AEP边缘网关接入_aep_edge_gateway.md
│  │  │      ApiDocument_AEP远程控制_aep_device_control.md
│  │  │      ApiDocument_MQ订阅推送管理_aep_mq_sub.md
│  │  │      ApiDocument_User_usr.md
│  │  │      ApiDocument_感知终端数据分析_tenant_device_statistics.md
│  │  │      ApiDocument_物联网应用数据分析_tenant_app_statistics.md
│  │  │      
│  │  ├─lib
│  │  │      ag-sdk-biz-108549.tar.gz-20221221.143437-SNAPSHOT.jar
│  │  │      ag-sdk-biz-108549.tar.gz-20221221.143437-SNAPSHOT.pom.xml
│  │  │      ctg-ag-sdk-core-2.7.0-20221208.015855-5.jar
│  │  │      ctg-ag-sdk-core-2.7.0-20221208.015855-5.pom.xml
│  │  │      
│  │  └─src
│  │          ag-sdk-biz-108549.tar.gz-20221221.143437-source.jar
│  │          
│  └─src
│      ├─main
│      │  ├─java
│      │  │  └─boot
│      │  │      └─ctwing
│      │  │          └─tcp
│      │  │              └─app
│      │  │                  │  BootCtWingTcpApp.java
│      │  │                  │  
│      │  │                  ├─config
│      │  │                  │      CtWingConstant.java
│      │  │                  │      SwaggerConfig.java
│      │  │                  │      
│      │  │                  ├─controller
│      │  │                  │      CtWingDownCmdController.java
│      │  │                  │      CtWingReceiveController.java
│      │  │                  │      
│      │  │                  └─utils
│      │  │                          CtWingDownSendUtils_1.java
│      │  │                          CtWingDownSendUtils_2.java
│      │  │                          CtWingDownSendUtils_3.java
│      │  │                          CtWingUtils.java
│      │  │                          
│      │  └─resources
│      │          application.properties
│      │          logback-spring.xml
│      │          
│      └─test
│          └─java
│              └─boot
│                  └─ctwing
│                      └─tcp
│                          └─app
│                                  BootCtWingTcpAppTest.java
│                                  Test.java
│       

订阅需要部署在云服务器上或者其他方式,启动项目后,在启动模拟设备端的应用,操作上行下行交互,可以看到很不错的结果

在这里我用了三种方式视线指令的下发

第一种 AIOT提供的SDK 超好用

第二种 AIOT在官方文档提供的Demo 代码较多,方便理解,也可用

第三种 使用Hutool工具的请求来实现,实际也是参考AIOT官方提供demo实现的

 再看看AIOT平台的下发指令日志

订阅数据的json数据格式,这里记录一下

上下线消息通知

{"timestamp":1671885117746,"tenantId":"XXXXXX","protocol":"tcp","productId":"15506850","messageType":"deviceOnlineOfflineReport","ipv4Address":"39.144.17.238","iccid":"undefined","eventType":1,"deviceId":"15506850869401041201815"}


{"timestamp":1671886478951,"tenantId":"XXXXXX","protocol":"tcp","productId":"15506850","messageType":"deviceOnlineOfflineReport","eventType":0,"deviceId":"15506850869401041201815"}

 数据上报通知

{"upPacketSN":-1,"upDataSN":-1,"topic":"ad","timestamp":1671885539011,"tenantId":"XXXXXX","serviceId":"","protocol":"tcp","productId":"15506850","payload":{"APPdata":"6JqC6JqB6Iie"},"messageType":"dataReport","deviceType":"","deviceId":"15506850869401041201815","assocAssetId":"","IMSI":"","IMEI":""}


{"upPacketSN":-1,"upDataSN":-1,"topic":"ad","timestamp":1671885567941,"tenantId":"XXXXXX","serviceId":"","protocol":"tcp","productId":"15506850","payload":{"APPdata":"bXl5aHR3MTIzNDU0NjU0Njc0ZXF3amRjcW93ZWljcW93aXhjbmRjeA=="},"messageType":"dataReport","deviceType":"","deviceId":"15506850869401041201815","assocAssetId":"","IMSI":"","IMEI":""}

指令下发后通知

{"timestamp":1671932803885,"tenantId":"XXXXXX","taskId":2,"result":{"resultDetail":"","resultCode":"SENT"},"protocol":"tcp","productId":"15506850","messageType":"commandResponse","deviceId":"15506850869401041201815"}

物联沃分享整理
物联沃-IOTWORD物联网 » “实现基于CTWing(AIOT)平台的TCP透传协议应用订阅端(北向应用):Springboot Netty”

发表评论