diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt new file mode 100644 index 0000000..4af74cf --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt @@ -0,0 +1,110 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import com.casic.br.ktd.extensions.covertAngleValue +import com.casic.br.ktd.extensions.covertDataValue +import com.casic.br.ktd.extensions.toUnsignedByteArray +import com.casic.br.ktd.model.SensorDataModel +import java.util.* + +class SocketManager private constructor() : ISocketListener { + + private val kTag = "SocketManager" + private var nettyClient: SocketClient = SocketClient() + + companion object { + val instance: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SocketManager() } + } + + fun connectNetty(hostname: String, port: Int) { + Thread { + if (!nettyClient.connectStatus) { + nettyClient.setSocketListener(this) + nettyClient.connect(hostname, port) + } else { + nettyClient.disconnect() + } + }.start() + } + + override fun onMessageResponse(data: ByteArray?) { + Log.d(kTag, "onMessageResponse ===> " + Arrays.toString(data)) + /** + * 0xFF,0x01, + * 0x01,0x37,0xE6, 甲烷浓度值(数据码1* 65536 + 数据码2 * 256 + 数据码3) + * 0x00, 激光甲烷模块工作状态(00表示设备正常,01表示温控故障,02表示设备激光未打开) + * 0x00,0xB2,0x35, 激光强度值(数据码5* 65536 + 数据码6 * 256 + 数据码7) + * 0x0A,0x3D, 云台水平角度([数据码8 * 256 + 数据码9]/100,单位为°,精确到0.01) + * 0x05,0x6F, 云台垂直角度(首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + * 0xC1 + * + * [-1, 1, 1, 55, -26, 0, 0, -78, 53, 10, 61, 5, 111, -63] + * 甲烷浓度值为79638,计算为79638=0x01*65536+0x37*256+0xE6[0x01为数据码1,0x37为数据码2,0xE6为数据码3]; + * 激光甲烷设备状态值为0,表示状态正常,[0x00为数据码4]; + * 激光强度值为45621,计算为45621=0x00*65536+0xB2*256+0x35[0x00为数据码5,0xB2为数据码6,0x35为数据码7]; + * + * ========================================================================================= + * 实际数据(1800-1950左右) + * [-86, 1, 0, 7, 71, 0, 0, 62, -92, 57, 59, -118, -111, -64] + * */ + + if (data == null) { + return + } + val bytes = data.toUnsignedByteArray() + if (bytes.size == 14) { + val dataModel = SensorDataModel(0, "激光未打开", 0, 0.0, 0.0) + + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + dataModel.methane = methaneBytes.covertDataValue() + + dataModel.methaneState = when (bytes[5]) { + 1 -> "温控故障" + 2 -> "激光未打开" + else -> "正常" + } + + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + dataModel.laser = laserBytes.covertDataValue() + + /** + * 水平角度的实际范围为从水平零点开始,顺时针方向(从上往下看),0~360° + * 垂直角度的实际范围为从垂直零点开始,以向上为正,-90°~90°,控制精度为0.01° + * */ + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + dataModel.horizontal = horizontalBytes.covertAngleValue() + + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + dataModel.vertical = verticalBytes.covertAngleValue() + + //{"horizontal":26.21,"laser":45621,"methane":79846,"methaneState":"正常","vertical":13.91} +// BroadcastManager.obtainInstance(BaseApplication.get()).sendBroadcast( +// LocaleConstant.ACTION_UPDATE_DATA, dataModel.toJson() +// ) + } + } + + override fun onServiceStatusConnectChanged(statusCode: Byte) { + if (statusCode == ISocketListener.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt new file mode 100644 index 0000000..4af74cf --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt @@ -0,0 +1,110 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import com.casic.br.ktd.extensions.covertAngleValue +import com.casic.br.ktd.extensions.covertDataValue +import com.casic.br.ktd.extensions.toUnsignedByteArray +import com.casic.br.ktd.model.SensorDataModel +import java.util.* + +class SocketManager private constructor() : ISocketListener { + + private val kTag = "SocketManager" + private var nettyClient: SocketClient = SocketClient() + + companion object { + val instance: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SocketManager() } + } + + fun connectNetty(hostname: String, port: Int) { + Thread { + if (!nettyClient.connectStatus) { + nettyClient.setSocketListener(this) + nettyClient.connect(hostname, port) + } else { + nettyClient.disconnect() + } + }.start() + } + + override fun onMessageResponse(data: ByteArray?) { + Log.d(kTag, "onMessageResponse ===> " + Arrays.toString(data)) + /** + * 0xFF,0x01, + * 0x01,0x37,0xE6, 甲烷浓度值(数据码1* 65536 + 数据码2 * 256 + 数据码3) + * 0x00, 激光甲烷模块工作状态(00表示设备正常,01表示温控故障,02表示设备激光未打开) + * 0x00,0xB2,0x35, 激光强度值(数据码5* 65536 + 数据码6 * 256 + 数据码7) + * 0x0A,0x3D, 云台水平角度([数据码8 * 256 + 数据码9]/100,单位为°,精确到0.01) + * 0x05,0x6F, 云台垂直角度(首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + * 0xC1 + * + * [-1, 1, 1, 55, -26, 0, 0, -78, 53, 10, 61, 5, 111, -63] + * 甲烷浓度值为79638,计算为79638=0x01*65536+0x37*256+0xE6[0x01为数据码1,0x37为数据码2,0xE6为数据码3]; + * 激光甲烷设备状态值为0,表示状态正常,[0x00为数据码4]; + * 激光强度值为45621,计算为45621=0x00*65536+0xB2*256+0x35[0x00为数据码5,0xB2为数据码6,0x35为数据码7]; + * + * ========================================================================================= + * 实际数据(1800-1950左右) + * [-86, 1, 0, 7, 71, 0, 0, 62, -92, 57, 59, -118, -111, -64] + * */ + + if (data == null) { + return + } + val bytes = data.toUnsignedByteArray() + if (bytes.size == 14) { + val dataModel = SensorDataModel(0, "激光未打开", 0, 0.0, 0.0) + + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + dataModel.methane = methaneBytes.covertDataValue() + + dataModel.methaneState = when (bytes[5]) { + 1 -> "温控故障" + 2 -> "激光未打开" + else -> "正常" + } + + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + dataModel.laser = laserBytes.covertDataValue() + + /** + * 水平角度的实际范围为从水平零点开始,顺时针方向(从上往下看),0~360° + * 垂直角度的实际范围为从垂直零点开始,以向上为正,-90°~90°,控制精度为0.01° + * */ + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + dataModel.horizontal = horizontalBytes.covertAngleValue() + + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + dataModel.vertical = verticalBytes.covertAngleValue() + + //{"horizontal":26.21,"laser":45621,"methane":79846,"methaneState":"正常","vertical":13.91} +// BroadcastManager.obtainInstance(BaseApplication.get()).sendBroadcast( +// LocaleConstant.ACTION_UPDATE_DATA, dataModel.toJson() +// ) + } + } + + override fun onServiceStatusConnectChanged(statusCode: Byte) { + if (statusCode == ISocketListener.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt index 2a7648b..6579d57 100644 --- a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt @@ -26,17 +26,30 @@ /** * ============================================================================================= + * Long + * ============================================================================================= + * */ + //数据接收频率 + const val DATA_TIMER_PERIOD = 1000L + + /** + * ============================================================================================= * String * ============================================================================================= * */ const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val SERVER_BASE_URL = "http://111.198.10.15:21609" + //海康摄像头参数 const val HK_NET_IP = "192.168.1.64" const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "abcd1234" + //甲烷设备参数 + const val DEV_NET_IP = "192.168.1.21" + const val DEV_NET_PORT = "8000" + /** * ============================================================================================= * ByteArray diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt new file mode 100644 index 0000000..4af74cf --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt @@ -0,0 +1,110 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import com.casic.br.ktd.extensions.covertAngleValue +import com.casic.br.ktd.extensions.covertDataValue +import com.casic.br.ktd.extensions.toUnsignedByteArray +import com.casic.br.ktd.model.SensorDataModel +import java.util.* + +class SocketManager private constructor() : ISocketListener { + + private val kTag = "SocketManager" + private var nettyClient: SocketClient = SocketClient() + + companion object { + val instance: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SocketManager() } + } + + fun connectNetty(hostname: String, port: Int) { + Thread { + if (!nettyClient.connectStatus) { + nettyClient.setSocketListener(this) + nettyClient.connect(hostname, port) + } else { + nettyClient.disconnect() + } + }.start() + } + + override fun onMessageResponse(data: ByteArray?) { + Log.d(kTag, "onMessageResponse ===> " + Arrays.toString(data)) + /** + * 0xFF,0x01, + * 0x01,0x37,0xE6, 甲烷浓度值(数据码1* 65536 + 数据码2 * 256 + 数据码3) + * 0x00, 激光甲烷模块工作状态(00表示设备正常,01表示温控故障,02表示设备激光未打开) + * 0x00,0xB2,0x35, 激光强度值(数据码5* 65536 + 数据码6 * 256 + 数据码7) + * 0x0A,0x3D, 云台水平角度([数据码8 * 256 + 数据码9]/100,单位为°,精确到0.01) + * 0x05,0x6F, 云台垂直角度(首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + * 0xC1 + * + * [-1, 1, 1, 55, -26, 0, 0, -78, 53, 10, 61, 5, 111, -63] + * 甲烷浓度值为79638,计算为79638=0x01*65536+0x37*256+0xE6[0x01为数据码1,0x37为数据码2,0xE6为数据码3]; + * 激光甲烷设备状态值为0,表示状态正常,[0x00为数据码4]; + * 激光强度值为45621,计算为45621=0x00*65536+0xB2*256+0x35[0x00为数据码5,0xB2为数据码6,0x35为数据码7]; + * + * ========================================================================================= + * 实际数据(1800-1950左右) + * [-86, 1, 0, 7, 71, 0, 0, 62, -92, 57, 59, -118, -111, -64] + * */ + + if (data == null) { + return + } + val bytes = data.toUnsignedByteArray() + if (bytes.size == 14) { + val dataModel = SensorDataModel(0, "激光未打开", 0, 0.0, 0.0) + + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + dataModel.methane = methaneBytes.covertDataValue() + + dataModel.methaneState = when (bytes[5]) { + 1 -> "温控故障" + 2 -> "激光未打开" + else -> "正常" + } + + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + dataModel.laser = laserBytes.covertDataValue() + + /** + * 水平角度的实际范围为从水平零点开始,顺时针方向(从上往下看),0~360° + * 垂直角度的实际范围为从垂直零点开始,以向上为正,-90°~90°,控制精度为0.01° + * */ + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + dataModel.horizontal = horizontalBytes.covertAngleValue() + + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + dataModel.vertical = verticalBytes.covertAngleValue() + + //{"horizontal":26.21,"laser":45621,"methane":79846,"methaneState":"正常","vertical":13.91} +// BroadcastManager.obtainInstance(BaseApplication.get()).sendBroadcast( +// LocaleConstant.ACTION_UPDATE_DATA, dataModel.toJson() +// ) + } + } + + override fun onServiceStatusConnectChanged(statusCode: Byte) { + if (statusCode == ISocketListener.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt index 2a7648b..6579d57 100644 --- a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt @@ -26,17 +26,30 @@ /** * ============================================================================================= + * Long + * ============================================================================================= + * */ + //数据接收频率 + const val DATA_TIMER_PERIOD = 1000L + + /** + * ============================================================================================= * String * ============================================================================================= * */ const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val SERVER_BASE_URL = "http://111.198.10.15:21609" + //海康摄像头参数 const val HK_NET_IP = "192.168.1.64" const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "abcd1234" + //甲烷设备参数 + const val DEV_NET_IP = "192.168.1.21" + const val DEV_NET_PORT = "8000" + /** * ============================================================================================= * ByteArray diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt new file mode 100644 index 0000000..8e31d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt @@ -0,0 +1,44 @@ +package com.casic.br.ktd.utils + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import androidx.core.app.ActivityCompat + +object LocationHelper { + fun obtainCurrentLocation(context: Context, listener: ILocationListener) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + //位置变化时更新位置 + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, + 3000, 5f, object : LocationListener { + override fun onLocationChanged(location: Location) { + listener.onLocationGet(location) + } + + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + }) + } + + interface ILocationListener { + fun onLocationGet(location: Location?) //高德定位数据 + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt new file mode 100644 index 0000000..4af74cf --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt @@ -0,0 +1,110 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import com.casic.br.ktd.extensions.covertAngleValue +import com.casic.br.ktd.extensions.covertDataValue +import com.casic.br.ktd.extensions.toUnsignedByteArray +import com.casic.br.ktd.model.SensorDataModel +import java.util.* + +class SocketManager private constructor() : ISocketListener { + + private val kTag = "SocketManager" + private var nettyClient: SocketClient = SocketClient() + + companion object { + val instance: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SocketManager() } + } + + fun connectNetty(hostname: String, port: Int) { + Thread { + if (!nettyClient.connectStatus) { + nettyClient.setSocketListener(this) + nettyClient.connect(hostname, port) + } else { + nettyClient.disconnect() + } + }.start() + } + + override fun onMessageResponse(data: ByteArray?) { + Log.d(kTag, "onMessageResponse ===> " + Arrays.toString(data)) + /** + * 0xFF,0x01, + * 0x01,0x37,0xE6, 甲烷浓度值(数据码1* 65536 + 数据码2 * 256 + 数据码3) + * 0x00, 激光甲烷模块工作状态(00表示设备正常,01表示温控故障,02表示设备激光未打开) + * 0x00,0xB2,0x35, 激光强度值(数据码5* 65536 + 数据码6 * 256 + 数据码7) + * 0x0A,0x3D, 云台水平角度([数据码8 * 256 + 数据码9]/100,单位为°,精确到0.01) + * 0x05,0x6F, 云台垂直角度(首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + * 0xC1 + * + * [-1, 1, 1, 55, -26, 0, 0, -78, 53, 10, 61, 5, 111, -63] + * 甲烷浓度值为79638,计算为79638=0x01*65536+0x37*256+0xE6[0x01为数据码1,0x37为数据码2,0xE6为数据码3]; + * 激光甲烷设备状态值为0,表示状态正常,[0x00为数据码4]; + * 激光强度值为45621,计算为45621=0x00*65536+0xB2*256+0x35[0x00为数据码5,0xB2为数据码6,0x35为数据码7]; + * + * ========================================================================================= + * 实际数据(1800-1950左右) + * [-86, 1, 0, 7, 71, 0, 0, 62, -92, 57, 59, -118, -111, -64] + * */ + + if (data == null) { + return + } + val bytes = data.toUnsignedByteArray() + if (bytes.size == 14) { + val dataModel = SensorDataModel(0, "激光未打开", 0, 0.0, 0.0) + + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + dataModel.methane = methaneBytes.covertDataValue() + + dataModel.methaneState = when (bytes[5]) { + 1 -> "温控故障" + 2 -> "激光未打开" + else -> "正常" + } + + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + dataModel.laser = laserBytes.covertDataValue() + + /** + * 水平角度的实际范围为从水平零点开始,顺时针方向(从上往下看),0~360° + * 垂直角度的实际范围为从垂直零点开始,以向上为正,-90°~90°,控制精度为0.01° + * */ + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + dataModel.horizontal = horizontalBytes.covertAngleValue() + + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + dataModel.vertical = verticalBytes.covertAngleValue() + + //{"horizontal":26.21,"laser":45621,"methane":79846,"methaneState":"正常","vertical":13.91} +// BroadcastManager.obtainInstance(BaseApplication.get()).sendBroadcast( +// LocaleConstant.ACTION_UPDATE_DATA, dataModel.toJson() +// ) + } + } + + override fun onServiceStatusConnectChanged(statusCode: Byte) { + if (statusCode == ISocketListener.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt index 2a7648b..6579d57 100644 --- a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt @@ -26,17 +26,30 @@ /** * ============================================================================================= + * Long + * ============================================================================================= + * */ + //数据接收频率 + const val DATA_TIMER_PERIOD = 1000L + + /** + * ============================================================================================= * String * ============================================================================================= * */ const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val SERVER_BASE_URL = "http://111.198.10.15:21609" + //海康摄像头参数 const val HK_NET_IP = "192.168.1.64" const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "abcd1234" + //甲烷设备参数 + const val DEV_NET_IP = "192.168.1.21" + const val DEV_NET_PORT = "8000" + /** * ============================================================================================= * ByteArray diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt new file mode 100644 index 0000000..8e31d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt @@ -0,0 +1,44 @@ +package com.casic.br.ktd.utils + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import androidx.core.app.ActivityCompat + +object LocationHelper { + fun obtainCurrentLocation(context: Context, listener: ILocationListener) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + //位置变化时更新位置 + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, + 3000, 5f, object : LocationListener { + override fun onLocationChanged(location: Location) { + listener.onLocationGet(location) + } + + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + }) + } + + interface ILocationListener { + fun onLocationGet(location: Location?) //高德定位数据 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt b/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt index e07c21b..6f2d43c 100644 --- a/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt +++ b/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt @@ -2,17 +2,25 @@ import android.annotation.SuppressLint import android.graphics.PixelFormat +import android.location.Location import android.os.Bundle import android.os.Handler import android.os.Message import android.util.Log import android.view.MotionEvent import android.view.SurfaceHolder +import com.amap.api.maps.AMapUtils +import com.amap.api.maps.CameraUpdateFactory +import com.amap.api.maps.CoordinateConverter +import com.amap.api.maps.model.CameraPosition +import com.amap.api.maps.model.LatLng import com.casic.br.ktd.R import com.casic.br.ktd.extensions.createHorizontalCommand import com.casic.br.ktd.extensions.createVerticalCommand import com.casic.br.ktd.extensions.getChannel +import com.casic.br.ktd.netty.SocketManager import com.casic.br.ktd.utils.LocaleConstant +import com.casic.br.ktd.utils.LocationHelper import com.casic.br.ktd.widgets.AlertControlDialog import com.casic.br.ktd.widgets.SteeringWheelView import com.gyf.immersionbar.ImmersionBar @@ -24,6 +32,7 @@ import hcnetsdk.sdkhub.MessageCodeHub import hcnetsdk.sdkhub.SDKGuider import kotlinx.android.synthetic.main.activity_inspection.* +import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.* @@ -33,6 +42,10 @@ private val context = this@InspectionActivity private val hkSDK by lazy { HCNetSDK.getInstance() } private val timeFormat by lazy { SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA) } + private val latlngs = LinkedList() + private val speedTimer by lazy { Timer() } + private val tcpTimer by lazy { Timer() } + private val decimalFormat by lazy { DecimalFormat("##0.0") } private var isLoginSuccess = false private var previewHandle = -1 private var selectChannel = -1 @@ -52,6 +65,9 @@ //巡检是否已开始 private var isStartInspect = false + + //车速 + private var speed: Float = 0.0f private lateinit var weakReferenceHandler: WeakReferenceHandler private fun setDeviceConfig() { @@ -82,6 +98,82 @@ override fun initData(savedInstanceState: Bundle?) { weakReferenceHandler = WeakReferenceHandler(this) setDeviceConfig() + + /** + * 手机GPS定位 + * */ + val converter = CoordinateConverter(this) + LocationHelper.obtainCurrentLocation(this, object : LocationHelper.ILocationListener { + override fun onLocationGet(location: Location?) { + if (location == null) { + "当前信号弱,无法定位".show(context) + return + } + //WGS-84要转为高德坐标系 + converter.from(CoordinateConverter.CoordType.GPS) + converter.coord(LatLng(location.latitude, location.longitude)) + val latLng = converter.convert() + latlngs.add(latLng) + + //移动到指定经纬度 + val cameraPosition = CameraPosition(latLng, 13f, 0f, 0f) + val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition) +// aMap.animateCamera(cameraUpdate, 1500, null) + + //绘制线 +// aMap.addPolyline( +// PolylineOptions().addAll(latlngs).width(12f).color(Color.RED) +// ) + +// if (isStartInspect) { +// val route = LinkedList() +// latlngs.forEach { +// val routeModel = RouteModel() +// routeModel.lat = it.latitude +// routeModel.lng = it.longitude +// +// route.add(routeModel) +// } +// inspectPointModel?.latlngs = route.toJson() +// dataBaseManager.updateInspectPoint(inspectPointModel) +// } + } + }) + + /** + * 计算速度,微分原理,3s计算一次速度 + * */ + speedTimer.schedule(object : TimerTask() { + override fun run() { + //经纬度链表数据小于2,无法计算速度,默认为0 + speed = if (latlngs.size < 2) 0.0f else { + val temp = AMapUtils.calculateLineDistance( + latlngs.last(), latlngs[latlngs.size - 2] + ) / 3 + //转为 km/h + decimalFormat.format(temp * 3.6).toFloat() + } + + //切换到UI线程更新界面数据 + runOnUiThread { + carSpeedView.text = String.format("${speed}Km/h") + } + } + }, 0, 3000) + + /** + * TCP初始化 + * ***/ + SocketManager.instance.connectNetty( + LocaleConstant.DEV_NET_IP, LocaleConstant.DEV_NET_PORT.toInt() + ) + tcpTimer.schedule(object : TimerTask() { + override fun run() { + if (isPreviewSuccess) { + SocketManager.instance.sendData(LocaleConstant.QUERY_DATA_COMMAND) + } + } + }, 3000, LocaleConstant.DATA_TIMER_PERIOD) } @SuppressLint("ClickableViewAccessibility") diff --git a/app/build.gradle b/app/build.gradle index 5ba4945..04bc478 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,4 +109,6 @@ //腾讯Android UI框架 implementation 'com.qmuiteam:qmui:2.0.0-alpha10' implementation 'com.qmuiteam:arch:0.3.1' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt new file mode 100644 index 0000000..b90d5cb --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/ByteArray.kt @@ -0,0 +1,18 @@ +package com.casic.br.ktd.extensions + +/** + * 将有符号的ByteArray转为无符号的IntArray + * */ +fun ByteArray.toUnsignedByteArray(): IntArray { + val array = IntArray(this.size) + for (i in this.indices) { + val datum = this[i] + val temp = if (datum < 0) { + 0xFF and datum.toInt() + } else { + datum.toInt() + } + array[i] = temp + } + return array +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt new file mode 100644 index 0000000..83b20d4 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/extensions/IntArray.kt @@ -0,0 +1,34 @@ +package com.casic.br.ktd.extensions + +import java.text.DecimalFormat +import java.util.* + +fun IntArray.covertDataValue(): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return this[0] * 65536 + this[1] * 256 + this[2] +} + +fun IntArray.covertAngleValue(): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (this[0] * 256 + this[1]).toDouble() / 100 + val angle = if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + val decimalFormat = DecimalFormat("##0.00") + return decimalFormat.format(angle).toDouble() +} + +fun IntArray.toHexString(): String { + val builder = StringBuilder() + for (datum in this) { + val hex = Integer.toHexString(datum) + if (hex.length < 2) { + builder.append(0) + } + builder.append(hex) + } + return builder.toString().uppercase(Locale.ROOT) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt new file mode 100644 index 0000000..4153e07 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/model/SensorDataModel.kt @@ -0,0 +1,9 @@ +package com.casic.br.ktd.model + +data class SensorDataModel( + var methane: Int, + var methaneState: String, + var laser: Int, + var horizontal: Double, + var vertical: Double +) diff --git a/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt new file mode 100644 index 0000000..dab98e1 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/ISocketListener.kt @@ -0,0 +1,19 @@ +package com.casic.br.ktd.netty + +interface ISocketListener { + companion object { + const val STATUS_CONNECT_SUCCESS: Byte = 1 //连接成功 + const val STATUS_CONNECT_CLOSED: Byte = 0 //关闭连接 + const val STATUS_CONNECT_ERROR: Byte = 0 //连接失败 + } + + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray?) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Byte) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..42eb310 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketChannelHandle.kt @@ -0,0 +1,51 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleState +import io.netty.handler.timeout.IdleStateEvent +import java.nio.charset.StandardCharsets + +class SocketChannelHandle(private val listener: ISocketListener?) : + SimpleChannelInboundHandler() { + + private val kTag = "SocketChannelHandle" + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + Log.d(kTag, "channelActive ===> 连接成功") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + } + + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + super.userEventTriggered(ctx, evt) + if (evt is IdleStateEvent) { + if (evt.state() == IdleState.WRITER_IDLE) { + //写超时,此时可以发送心跳数据给服务器 + val temp = "FF" + ctx.writeAndFlush(temp.toByteArray(StandardCharsets.UTF_8)) + } else if (evt.state() == IdleState.READER_IDLE) { + //读超时,此时代表没有收到心跳返回可以关闭当前连接进行重连 + ctx.close() + } + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener?.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt new file mode 100644 index 0000000..d05d335 --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketClient.kt @@ -0,0 +1,153 @@ +package com.casic.br.ktd.netty + +import android.os.SystemClock +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.* +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class SocketClient { + + private val kTag = "SocketClient" + + private var host: String? = null + private var port = 8000 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + + //现在连接的状态 + var connectStatus = false //判断是否已连接 + private var reconnectNum = Int.MAX_VALUE //定义的重连到时候用 + private var isNeedReconnect = true //是否需要重连 + var isConnecting = false //是否正在连接 + private set + private var reconnectIntervalTime: Long = 15000 //重连的时间 + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(host: String, port: Int) { + this.host = host + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread: Thread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this@SocketClient) { + var channelFuture: ChannelFuture? = null //连接管理对象 + if (!connectStatus) { + isConnecting = true + nioEventLoopGroup = NioEventLoopGroup() //设置的连接group + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandle(listener)) + } + }) + try { + //连接监听 + channelFuture = bootstrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + connectStatus = true + channel = channelFuture.channel() + } else { + Log.e(kTag, "operationComplete: 连接失败") + connectStatus = false + } + isConnecting = false + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } finally { + connectStatus = false + listener?.onServiceStatusConnectChanged(ISocketListener.STATUS_CONNECT_CLOSED) //STATUS_CONNECT_CLOSED 这我自己定义的 接口标识 + if (null != channelFuture) { + if (channelFuture.channel() != null && channelFuture.channel().isOpen) { + channelFuture.channel().close() + } + } + nioEventLoopGroup?.shutdownGracefully() + reconnect() //重新连接 + } + } + } + } + + //断开连接 + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + isNeedReconnect = false + nioEventLoopGroup?.shutdownGracefully() + } + + //重新连接 + private fun reconnect() { + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + reconnectNum-- + SystemClock.sleep(reconnectIntervalTime) + if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { + Log.d(kTag, "reconnect ===> 重新连接") + connectServer() + } + } + } + + fun sendData(bytes: ByteArray) { + channel?.writeAndFlush(bytes)?.addListener(ChannelFutureListener { future -> + if (!future.isSuccess) { + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt new file mode 100644 index 0000000..4af74cf --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/netty/SocketManager.kt @@ -0,0 +1,110 @@ +package com.casic.br.ktd.netty + +import android.util.Log +import com.casic.br.ktd.extensions.covertAngleValue +import com.casic.br.ktd.extensions.covertDataValue +import com.casic.br.ktd.extensions.toUnsignedByteArray +import com.casic.br.ktd.model.SensorDataModel +import java.util.* + +class SocketManager private constructor() : ISocketListener { + + private val kTag = "SocketManager" + private var nettyClient: SocketClient = SocketClient() + + companion object { + val instance: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SocketManager() } + } + + fun connectNetty(hostname: String, port: Int) { + Thread { + if (!nettyClient.connectStatus) { + nettyClient.setSocketListener(this) + nettyClient.connect(hostname, port) + } else { + nettyClient.disconnect() + } + }.start() + } + + override fun onMessageResponse(data: ByteArray?) { + Log.d(kTag, "onMessageResponse ===> " + Arrays.toString(data)) + /** + * 0xFF,0x01, + * 0x01,0x37,0xE6, 甲烷浓度值(数据码1* 65536 + 数据码2 * 256 + 数据码3) + * 0x00, 激光甲烷模块工作状态(00表示设备正常,01表示温控故障,02表示设备激光未打开) + * 0x00,0xB2,0x35, 激光强度值(数据码5* 65536 + 数据码6 * 256 + 数据码7) + * 0x0A,0x3D, 云台水平角度([数据码8 * 256 + 数据码9]/100,单位为°,精确到0.01) + * 0x05,0x6F, 云台垂直角度(首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + * 0xC1 + * + * [-1, 1, 1, 55, -26, 0, 0, -78, 53, 10, 61, 5, 111, -63] + * 甲烷浓度值为79638,计算为79638=0x01*65536+0x37*256+0xE6[0x01为数据码1,0x37为数据码2,0xE6为数据码3]; + * 激光甲烷设备状态值为0,表示状态正常,[0x00为数据码4]; + * 激光强度值为45621,计算为45621=0x00*65536+0xB2*256+0x35[0x00为数据码5,0xB2为数据码6,0x35为数据码7]; + * + * ========================================================================================= + * 实际数据(1800-1950左右) + * [-86, 1, 0, 7, 71, 0, 0, 62, -92, 57, 59, -118, -111, -64] + * */ + + if (data == null) { + return + } + val bytes = data.toUnsignedByteArray() + if (bytes.size == 14) { + val dataModel = SensorDataModel(0, "激光未打开", 0, 0.0, 0.0) + + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + dataModel.methane = methaneBytes.covertDataValue() + + dataModel.methaneState = when (bytes[5]) { + 1 -> "温控故障" + 2 -> "激光未打开" + else -> "正常" + } + + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + dataModel.laser = laserBytes.covertDataValue() + + /** + * 水平角度的实际范围为从水平零点开始,顺时针方向(从上往下看),0~360° + * 垂直角度的实际范围为从垂直零点开始,以向上为正,-90°~90°,控制精度为0.01° + * */ + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + dataModel.horizontal = horizontalBytes.covertAngleValue() + + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + dataModel.vertical = verticalBytes.covertAngleValue() + + //{"horizontal":26.21,"laser":45621,"methane":79846,"methaneState":"正常","vertical":13.91} +// BroadcastManager.obtainInstance(BaseApplication.get()).sendBroadcast( +// LocaleConstant.ACTION_UPDATE_DATA, dataModel.toJson() +// ) + } + } + + override fun onServiceStatusConnectChanged(statusCode: Byte) { + if (statusCode == ISocketListener.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt index 2a7648b..6579d57 100644 --- a/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/ktd/utils/LocaleConstant.kt @@ -26,17 +26,30 @@ /** * ============================================================================================= + * Long + * ============================================================================================= + * */ + //数据接收频率 + const val DATA_TIMER_PERIOD = 1000L + + /** + * ============================================================================================= * String * ============================================================================================= * */ const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val SERVER_BASE_URL = "http://111.198.10.15:21609" + //海康摄像头参数 const val HK_NET_IP = "192.168.1.64" const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "abcd1234" + //甲烷设备参数 + const val DEV_NET_IP = "192.168.1.21" + const val DEV_NET_PORT = "8000" + /** * ============================================================================================= * ByteArray diff --git a/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt new file mode 100644 index 0000000..8e31d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/ktd/utils/LocationHelper.kt @@ -0,0 +1,44 @@ +package com.casic.br.ktd.utils + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import androidx.core.app.ActivityCompat + +object LocationHelper { + fun obtainCurrentLocation(context: Context, listener: ILocationListener) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + //位置变化时更新位置 + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, + 3000, 5f, object : LocationListener { + override fun onLocationChanged(location: Location) { + listener.onLocationGet(location) + } + + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + }) + } + + interface ILocationListener { + fun onLocationGet(location: Location?) //高德定位数据 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt b/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt index e07c21b..6f2d43c 100644 --- a/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt +++ b/app/src/main/java/com/casic/br/ktd/view/InspectionActivity.kt @@ -2,17 +2,25 @@ import android.annotation.SuppressLint import android.graphics.PixelFormat +import android.location.Location import android.os.Bundle import android.os.Handler import android.os.Message import android.util.Log import android.view.MotionEvent import android.view.SurfaceHolder +import com.amap.api.maps.AMapUtils +import com.amap.api.maps.CameraUpdateFactory +import com.amap.api.maps.CoordinateConverter +import com.amap.api.maps.model.CameraPosition +import com.amap.api.maps.model.LatLng import com.casic.br.ktd.R import com.casic.br.ktd.extensions.createHorizontalCommand import com.casic.br.ktd.extensions.createVerticalCommand import com.casic.br.ktd.extensions.getChannel +import com.casic.br.ktd.netty.SocketManager import com.casic.br.ktd.utils.LocaleConstant +import com.casic.br.ktd.utils.LocationHelper import com.casic.br.ktd.widgets.AlertControlDialog import com.casic.br.ktd.widgets.SteeringWheelView import com.gyf.immersionbar.ImmersionBar @@ -24,6 +32,7 @@ import hcnetsdk.sdkhub.MessageCodeHub import hcnetsdk.sdkhub.SDKGuider import kotlinx.android.synthetic.main.activity_inspection.* +import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.* @@ -33,6 +42,10 @@ private val context = this@InspectionActivity private val hkSDK by lazy { HCNetSDK.getInstance() } private val timeFormat by lazy { SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA) } + private val latlngs = LinkedList() + private val speedTimer by lazy { Timer() } + private val tcpTimer by lazy { Timer() } + private val decimalFormat by lazy { DecimalFormat("##0.0") } private var isLoginSuccess = false private var previewHandle = -1 private var selectChannel = -1 @@ -52,6 +65,9 @@ //巡检是否已开始 private var isStartInspect = false + + //车速 + private var speed: Float = 0.0f private lateinit var weakReferenceHandler: WeakReferenceHandler private fun setDeviceConfig() { @@ -82,6 +98,82 @@ override fun initData(savedInstanceState: Bundle?) { weakReferenceHandler = WeakReferenceHandler(this) setDeviceConfig() + + /** + * 手机GPS定位 + * */ + val converter = CoordinateConverter(this) + LocationHelper.obtainCurrentLocation(this, object : LocationHelper.ILocationListener { + override fun onLocationGet(location: Location?) { + if (location == null) { + "当前信号弱,无法定位".show(context) + return + } + //WGS-84要转为高德坐标系 + converter.from(CoordinateConverter.CoordType.GPS) + converter.coord(LatLng(location.latitude, location.longitude)) + val latLng = converter.convert() + latlngs.add(latLng) + + //移动到指定经纬度 + val cameraPosition = CameraPosition(latLng, 13f, 0f, 0f) + val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition) +// aMap.animateCamera(cameraUpdate, 1500, null) + + //绘制线 +// aMap.addPolyline( +// PolylineOptions().addAll(latlngs).width(12f).color(Color.RED) +// ) + +// if (isStartInspect) { +// val route = LinkedList() +// latlngs.forEach { +// val routeModel = RouteModel() +// routeModel.lat = it.latitude +// routeModel.lng = it.longitude +// +// route.add(routeModel) +// } +// inspectPointModel?.latlngs = route.toJson() +// dataBaseManager.updateInspectPoint(inspectPointModel) +// } + } + }) + + /** + * 计算速度,微分原理,3s计算一次速度 + * */ + speedTimer.schedule(object : TimerTask() { + override fun run() { + //经纬度链表数据小于2,无法计算速度,默认为0 + speed = if (latlngs.size < 2) 0.0f else { + val temp = AMapUtils.calculateLineDistance( + latlngs.last(), latlngs[latlngs.size - 2] + ) / 3 + //转为 km/h + decimalFormat.format(temp * 3.6).toFloat() + } + + //切换到UI线程更新界面数据 + runOnUiThread { + carSpeedView.text = String.format("${speed}Km/h") + } + } + }, 0, 3000) + + /** + * TCP初始化 + * ***/ + SocketManager.instance.connectNetty( + LocaleConstant.DEV_NET_IP, LocaleConstant.DEV_NET_PORT.toInt() + ) + tcpTimer.schedule(object : TimerTask() { + override fun run() { + if (isPreviewSuccess) { + SocketManager.instance.sendData(LocaleConstant.QUERY_DATA_COMMAND) + } + } + }, 3000, LocaleConstant.DATA_TIMER_PERIOD) } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/res/layout/activity_inspection.xml b/app/src/main/res/layout/activity_inspection.xml index ac15a6a..710ad8f 100644 --- a/app/src/main/res/layout/activity_inspection.xml +++ b/app/src/main/res/layout/activity_inspection.xml @@ -118,6 +118,7 @@ android:text="当前车速" />