diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt new file mode 100644 index 0000000..4117f0a --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt @@ -0,0 +1,130 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.view.MethaneActivity +import com.google.gson.Gson + +class SocketManager : ISocketListener { + + companion object { + //Kotlin委托模式双重锁单例 + val get: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + SocketManager() + } + } + + private val kTag = "SocketManager" + private val nettyClient by lazy { SocketClient() } + + 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, "channelRead0 ===> " + data.contentToString()) + /** + * 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] + * [FF 01 01 37 E6 00 00 B2 35 0A 3D 05 6F C1] + * 甲烷浓度值为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]; + */ + val bytes = bytesToUnsigned(data) + val hashMap = HashMap() + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + hashMap["methane"] = covertDataValue(methaneBytes) + hashMap["methaneState"] = covertState(bytes[5]) + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + hashMap["laser"] = covertDataValue(laserBytes) + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + hashMap["horizontal"] = covertAngleValue(horizontalBytes) + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + hashMap["vertical"] = covertAngleValue(verticalBytes) + + //{"horizontal":26.21,"laser":-19915,"methaneState":"正常","methane":79590,"vertical":13.91} + Log.d(kTag, "onMessageResponse ===> " + Gson().toJson(hashMap)) + } + + private fun covertDataValue(bytes: IntArray): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return bytes[0] * 65536 + bytes[1] * 256 + bytes[2] + } + + private fun covertState(b: Int): String { + var state = "" + when (b) { + 0 -> state = "正常" + 1 -> state = "温控故障" + 2 -> state = "激光未打开" + else -> {} + } + return state + } + + private fun covertAngleValue(bytes: IntArray): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (bytes[0] * 256 + bytes[1]).toDouble() / 100 + return if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + } + + override fun onServiceStatusConnectChanged(statusCode: Int) { + if (statusCode == LocaleConstant.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.CONNECT_SUCCESS) + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.RECONNECT) + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } + + private fun bytesToUnsigned(data: ByteArray): IntArray { + val array = IntArray(data.size) + for (i in data.indices) { + val datum = data[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 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt new file mode 100644 index 0000000..4117f0a --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt @@ -0,0 +1,130 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.view.MethaneActivity +import com.google.gson.Gson + +class SocketManager : ISocketListener { + + companion object { + //Kotlin委托模式双重锁单例 + val get: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + SocketManager() + } + } + + private val kTag = "SocketManager" + private val nettyClient by lazy { SocketClient() } + + 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, "channelRead0 ===> " + data.contentToString()) + /** + * 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] + * [FF 01 01 37 E6 00 00 B2 35 0A 3D 05 6F C1] + * 甲烷浓度值为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]; + */ + val bytes = bytesToUnsigned(data) + val hashMap = HashMap() + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + hashMap["methane"] = covertDataValue(methaneBytes) + hashMap["methaneState"] = covertState(bytes[5]) + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + hashMap["laser"] = covertDataValue(laserBytes) + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + hashMap["horizontal"] = covertAngleValue(horizontalBytes) + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + hashMap["vertical"] = covertAngleValue(verticalBytes) + + //{"horizontal":26.21,"laser":-19915,"methaneState":"正常","methane":79590,"vertical":13.91} + Log.d(kTag, "onMessageResponse ===> " + Gson().toJson(hashMap)) + } + + private fun covertDataValue(bytes: IntArray): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return bytes[0] * 65536 + bytes[1] * 256 + bytes[2] + } + + private fun covertState(b: Int): String { + var state = "" + when (b) { + 0 -> state = "正常" + 1 -> state = "温控故障" + 2 -> state = "激光未打开" + else -> {} + } + return state + } + + private fun covertAngleValue(bytes: IntArray): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (bytes[0] * 256 + bytes[1]).toDouble() / 100 + return if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + } + + override fun onServiceStatusConnectChanged(statusCode: Int) { + if (statusCode == LocaleConstant.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.CONNECT_SUCCESS) + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.RECONNECT) + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } + + private fun bytesToUnsigned(data: ByteArray): IntArray { + val array = IntArray(data.size) + for (i in data.indices) { + val datum = data[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/operationsite/view/MethaneActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt index aa7e298..f46c9ee 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt @@ -1,20 +1,121 @@ package com.casic.br.operationsite.view +import android.view.View import com.casic.br.operationsite.R +import com.casic.br.operationsite.extensions.createStartCommand +import com.casic.br.operationsite.extensions.createStopCommand import com.casic.br.operationsite.extensions.initLayoutImmersionBar +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.netty.SocketManager import com.gyf.immersionbar.ImmersionBar import com.pengxh.kt.lite.base.KotlinBaseActivity -import kotlinx.android.synthetic.main.activity_main.* +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.widget.SteeringWheelController +import kotlinx.android.synthetic.main.activity_methane.* import kotlinx.android.synthetic.main.include_base_title.* class MethaneActivity : KotlinBaseActivity() { - override fun initData() { + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + private var isConnectSuccess = false + + //点击中间开关次数 + private var clickCount = 0 + + //手指是否已经从方向控制盘抬起 + private var isActionUp = true + + override fun initData() { + weakReferenceHandler = WeakReferenceHandler { + when (it.what) { + LocaleConstant.CONNECT_SUCCESS -> { + notConnectedLayout.visibility = View.GONE + "激光甲烷控制云台连接成功".show(this) + isConnectSuccess = true + } + LocaleConstant.RECONNECT -> { + notConnectedLayout.visibility = View.VISIBLE + isConnectSuccess = false + } + } + true + } + + val host = configSpinner.selectedItem.toString().split(":") + + SocketManager.get.connectNetty(host[0], host[1].toInt()) } override fun initEvent() { leftBackView.setOnClickListener { finish() } + + wheelController.setOnWheelTouchListener(object : + SteeringWheelController.OnWheelTouchListener { + override fun onActionTurnUp(dir: SteeringWheelController.Direction) { + if (dir == SteeringWheelController.Direction.LEFT || + dir == SteeringWheelController.Direction.TOP || + dir == SteeringWheelController.Direction.RIGHT || + dir == SteeringWheelController.Direction.BOTTOM + ) { + SocketManager.get.sendData(createStopCommand()) + isActionUp = true + } else { + if (isConnectSuccess) { + clickCount++ + if (clickCount % 2 == 0) { + openInstructionsLight() + } else { + closeInstructionsLight() + } + } + } + } + + override fun onCenterTurn() { + + } + + override fun onLeftTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.LEFT.createStartCommand()) + isActionUp = false + } + } + + override fun onTopTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.TOP.createStartCommand()) + isActionUp = false + } + } + + override fun onRightTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.RIGHT.createStartCommand()) + isActionUp = false + } + } + + override fun onBottomTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.BOTTOM.createStartCommand()) + isActionUp = false + } + } + }) + } + + private fun closeInstructionsLight() { + lightStateView.setBackgroundColor(R.color.red.convertColor(this)) + } + + private fun openInstructionsLight() { + lightStateView.setBackgroundColor(R.color.green.convertColor(this)) } override fun initLayoutView(): Int = R.layout.activity_methane @@ -30,4 +131,9 @@ titleView.text = "云台参数" } + + override fun onDestroy() { + super.onDestroy() + SocketManager.get.close() + } } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt new file mode 100644 index 0000000..4117f0a --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt @@ -0,0 +1,130 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.view.MethaneActivity +import com.google.gson.Gson + +class SocketManager : ISocketListener { + + companion object { + //Kotlin委托模式双重锁单例 + val get: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + SocketManager() + } + } + + private val kTag = "SocketManager" + private val nettyClient by lazy { SocketClient() } + + 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, "channelRead0 ===> " + data.contentToString()) + /** + * 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] + * [FF 01 01 37 E6 00 00 B2 35 0A 3D 05 6F C1] + * 甲烷浓度值为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]; + */ + val bytes = bytesToUnsigned(data) + val hashMap = HashMap() + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + hashMap["methane"] = covertDataValue(methaneBytes) + hashMap["methaneState"] = covertState(bytes[5]) + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + hashMap["laser"] = covertDataValue(laserBytes) + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + hashMap["horizontal"] = covertAngleValue(horizontalBytes) + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + hashMap["vertical"] = covertAngleValue(verticalBytes) + + //{"horizontal":26.21,"laser":-19915,"methaneState":"正常","methane":79590,"vertical":13.91} + Log.d(kTag, "onMessageResponse ===> " + Gson().toJson(hashMap)) + } + + private fun covertDataValue(bytes: IntArray): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return bytes[0] * 65536 + bytes[1] * 256 + bytes[2] + } + + private fun covertState(b: Int): String { + var state = "" + when (b) { + 0 -> state = "正常" + 1 -> state = "温控故障" + 2 -> state = "激光未打开" + else -> {} + } + return state + } + + private fun covertAngleValue(bytes: IntArray): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (bytes[0] * 256 + bytes[1]).toDouble() / 100 + return if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + } + + override fun onServiceStatusConnectChanged(statusCode: Int) { + if (statusCode == LocaleConstant.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.CONNECT_SUCCESS) + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.RECONNECT) + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } + + private fun bytesToUnsigned(data: ByteArray): IntArray { + val array = IntArray(data.size) + for (i in data.indices) { + val datum = data[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/operationsite/view/MethaneActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt index aa7e298..f46c9ee 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt @@ -1,20 +1,121 @@ package com.casic.br.operationsite.view +import android.view.View import com.casic.br.operationsite.R +import com.casic.br.operationsite.extensions.createStartCommand +import com.casic.br.operationsite.extensions.createStopCommand import com.casic.br.operationsite.extensions.initLayoutImmersionBar +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.netty.SocketManager import com.gyf.immersionbar.ImmersionBar import com.pengxh.kt.lite.base.KotlinBaseActivity -import kotlinx.android.synthetic.main.activity_main.* +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.widget.SteeringWheelController +import kotlinx.android.synthetic.main.activity_methane.* import kotlinx.android.synthetic.main.include_base_title.* class MethaneActivity : KotlinBaseActivity() { - override fun initData() { + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + private var isConnectSuccess = false + + //点击中间开关次数 + private var clickCount = 0 + + //手指是否已经从方向控制盘抬起 + private var isActionUp = true + + override fun initData() { + weakReferenceHandler = WeakReferenceHandler { + when (it.what) { + LocaleConstant.CONNECT_SUCCESS -> { + notConnectedLayout.visibility = View.GONE + "激光甲烷控制云台连接成功".show(this) + isConnectSuccess = true + } + LocaleConstant.RECONNECT -> { + notConnectedLayout.visibility = View.VISIBLE + isConnectSuccess = false + } + } + true + } + + val host = configSpinner.selectedItem.toString().split(":") + + SocketManager.get.connectNetty(host[0], host[1].toInt()) } override fun initEvent() { leftBackView.setOnClickListener { finish() } + + wheelController.setOnWheelTouchListener(object : + SteeringWheelController.OnWheelTouchListener { + override fun onActionTurnUp(dir: SteeringWheelController.Direction) { + if (dir == SteeringWheelController.Direction.LEFT || + dir == SteeringWheelController.Direction.TOP || + dir == SteeringWheelController.Direction.RIGHT || + dir == SteeringWheelController.Direction.BOTTOM + ) { + SocketManager.get.sendData(createStopCommand()) + isActionUp = true + } else { + if (isConnectSuccess) { + clickCount++ + if (clickCount % 2 == 0) { + openInstructionsLight() + } else { + closeInstructionsLight() + } + } + } + } + + override fun onCenterTurn() { + + } + + override fun onLeftTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.LEFT.createStartCommand()) + isActionUp = false + } + } + + override fun onTopTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.TOP.createStartCommand()) + isActionUp = false + } + } + + override fun onRightTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.RIGHT.createStartCommand()) + isActionUp = false + } + } + + override fun onBottomTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.BOTTOM.createStartCommand()) + isActionUp = false + } + } + }) + } + + private fun closeInstructionsLight() { + lightStateView.setBackgroundColor(R.color.red.convertColor(this)) + } + + private fun openInstructionsLight() { + lightStateView.setBackgroundColor(R.color.green.convertColor(this)) } override fun initLayoutView(): Int = R.layout.activity_methane @@ -30,4 +131,9 @@ titleView.text = "云台参数" } + + override fun onDestroy() { + super.onDestroy() + SocketManager.get.close() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_red.xml b/app/src/main/res/drawable/bg_solid_layout_red.xml new file mode 100644 index 0000000..fc1785e --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt new file mode 100644 index 0000000..4117f0a --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt @@ -0,0 +1,130 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.view.MethaneActivity +import com.google.gson.Gson + +class SocketManager : ISocketListener { + + companion object { + //Kotlin委托模式双重锁单例 + val get: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + SocketManager() + } + } + + private val kTag = "SocketManager" + private val nettyClient by lazy { SocketClient() } + + 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, "channelRead0 ===> " + data.contentToString()) + /** + * 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] + * [FF 01 01 37 E6 00 00 B2 35 0A 3D 05 6F C1] + * 甲烷浓度值为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]; + */ + val bytes = bytesToUnsigned(data) + val hashMap = HashMap() + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + hashMap["methane"] = covertDataValue(methaneBytes) + hashMap["methaneState"] = covertState(bytes[5]) + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + hashMap["laser"] = covertDataValue(laserBytes) + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + hashMap["horizontal"] = covertAngleValue(horizontalBytes) + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + hashMap["vertical"] = covertAngleValue(verticalBytes) + + //{"horizontal":26.21,"laser":-19915,"methaneState":"正常","methane":79590,"vertical":13.91} + Log.d(kTag, "onMessageResponse ===> " + Gson().toJson(hashMap)) + } + + private fun covertDataValue(bytes: IntArray): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return bytes[0] * 65536 + bytes[1] * 256 + bytes[2] + } + + private fun covertState(b: Int): String { + var state = "" + when (b) { + 0 -> state = "正常" + 1 -> state = "温控故障" + 2 -> state = "激光未打开" + else -> {} + } + return state + } + + private fun covertAngleValue(bytes: IntArray): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (bytes[0] * 256 + bytes[1]).toDouble() / 100 + return if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + } + + override fun onServiceStatusConnectChanged(statusCode: Int) { + if (statusCode == LocaleConstant.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.CONNECT_SUCCESS) + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.RECONNECT) + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } + + private fun bytesToUnsigned(data: ByteArray): IntArray { + val array = IntArray(data.size) + for (i in data.indices) { + val datum = data[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/operationsite/view/MethaneActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt index aa7e298..f46c9ee 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt @@ -1,20 +1,121 @@ package com.casic.br.operationsite.view +import android.view.View import com.casic.br.operationsite.R +import com.casic.br.operationsite.extensions.createStartCommand +import com.casic.br.operationsite.extensions.createStopCommand import com.casic.br.operationsite.extensions.initLayoutImmersionBar +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.netty.SocketManager import com.gyf.immersionbar.ImmersionBar import com.pengxh.kt.lite.base.KotlinBaseActivity -import kotlinx.android.synthetic.main.activity_main.* +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.widget.SteeringWheelController +import kotlinx.android.synthetic.main.activity_methane.* import kotlinx.android.synthetic.main.include_base_title.* class MethaneActivity : KotlinBaseActivity() { - override fun initData() { + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + private var isConnectSuccess = false + + //点击中间开关次数 + private var clickCount = 0 + + //手指是否已经从方向控制盘抬起 + private var isActionUp = true + + override fun initData() { + weakReferenceHandler = WeakReferenceHandler { + when (it.what) { + LocaleConstant.CONNECT_SUCCESS -> { + notConnectedLayout.visibility = View.GONE + "激光甲烷控制云台连接成功".show(this) + isConnectSuccess = true + } + LocaleConstant.RECONNECT -> { + notConnectedLayout.visibility = View.VISIBLE + isConnectSuccess = false + } + } + true + } + + val host = configSpinner.selectedItem.toString().split(":") + + SocketManager.get.connectNetty(host[0], host[1].toInt()) } override fun initEvent() { leftBackView.setOnClickListener { finish() } + + wheelController.setOnWheelTouchListener(object : + SteeringWheelController.OnWheelTouchListener { + override fun onActionTurnUp(dir: SteeringWheelController.Direction) { + if (dir == SteeringWheelController.Direction.LEFT || + dir == SteeringWheelController.Direction.TOP || + dir == SteeringWheelController.Direction.RIGHT || + dir == SteeringWheelController.Direction.BOTTOM + ) { + SocketManager.get.sendData(createStopCommand()) + isActionUp = true + } else { + if (isConnectSuccess) { + clickCount++ + if (clickCount % 2 == 0) { + openInstructionsLight() + } else { + closeInstructionsLight() + } + } + } + } + + override fun onCenterTurn() { + + } + + override fun onLeftTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.LEFT.createStartCommand()) + isActionUp = false + } + } + + override fun onTopTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.TOP.createStartCommand()) + isActionUp = false + } + } + + override fun onRightTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.RIGHT.createStartCommand()) + isActionUp = false + } + } + + override fun onBottomTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.BOTTOM.createStartCommand()) + isActionUp = false + } + } + }) + } + + private fun closeInstructionsLight() { + lightStateView.setBackgroundColor(R.color.red.convertColor(this)) + } + + private fun openInstructionsLight() { + lightStateView.setBackgroundColor(R.color.green.convertColor(this)) } override fun initLayoutView(): Int = R.layout.activity_methane @@ -30,4 +131,9 @@ titleView.text = "云台参数" } + + override fun onDestroy() { + super.onDestroy() + SocketManager.get.close() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_red.xml b/app/src/main/res/drawable/bg_solid_layout_red.xml new file mode 100644 index 0000000..fc1785e --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_no_net.xml b/app/src/main/res/drawable/ic_no_net.xml new file mode 100644 index 0000000..a5a0c5c --- /dev/null +++ b/app/src/main/res/drawable/ic_no_net.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/build.gradle b/app/build.gradle index 34e9ec3..df28eb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,4 +121,6 @@ implementation "androidx.camera:camera-camera2:${CameraX_version}" implementation "androidx.camera:camera-lifecycle:${CameraX_version}" implementation 'androidx.camera:camera-view:1.2.0-alpha02' + //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/operationsite/extensions/Direction.kt b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt new file mode 100644 index 0000000..4774c89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/extensions/Direction.kt @@ -0,0 +1,36 @@ +package com.casic.br.operationsite.extensions + +import com.pengxh.kt.lite.widget.SteeringWheelController + +fun SteeringWheelController.Direction.createStartCommand(): ByteArray { + when (this) { + SteeringWheelController.Direction.LEFT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x04.toByte(), 0xFF.toByte(), 0x00, 0x04.toByte() + ) + } + SteeringWheelController.Direction.TOP -> { + //Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x08.toByte(), 0x00, 0xFF.toByte(), 0x08.toByte() + ) + } + SteeringWheelController.Direction.RIGHT -> { + //Byte 5 (Data 1) - 水平速度, 值从 00 (停止) 到 3F (高速) + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x02.toByte(), 0xFF.toByte(), 0x00, 0x02.toByte() + ) + } + else -> { + //下。Byte 6 (Data 2) - 竖直速度, 值从 00 (停止) 到 3F + return byteArrayOf( + 0xFF.toByte(), 0x01, 0x00, 0x10.toByte(), 0x00, 0xFF.toByte(), 0x10.toByte() + ) + } + } +} + +fun createStopCommand(): ByteArray { + return byteArrayOf(0xFF.toByte(), 0x01, 0x00, 0x00, 0x00, 0x00, 0x01) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index bdd97ae..7d58e00 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -14,7 +14,7 @@ Manifest.permission.READ_PHONE_STATE ) -// const val SERVER_BASE_URL = "http://192.168.43.66:12210" + // const val SERVER_BASE_URL = "http://192.168.43.66:12210" const val SERVER_BASE_URL = "http://111.198.10.15:12210" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" @@ -31,4 +31,11 @@ const val HK_NET_PORT = "8000" const val HK_NET_USERNAME = "admin" const val HK_NET_PASSWORD = "1234qwer" + + const val STATUS_CONNECT_SUCCESS = 1 //连接成功 + const val STATUS_CONNECT_CLOSED = 0 //关闭连接 + const val STATUS_CONNECT_ERROR = 0 //连接失败 + + const val CONNECT_SUCCESS = 2023042501 + const val RECONNECT = 2023042502 } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt new file mode 100644 index 0000000..f718b89 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/ISocketListener.kt @@ -0,0 +1,13 @@ +package com.casic.br.operationsite.utils.netty + +interface ISocketListener { + /** + * 当接收到系统消息 + */ + fun onMessageResponse(data: ByteArray) + + /** + * 当连接状态发生变化时调用 + */ + fun onServiceStatusConnectChanged(statusCode: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt new file mode 100644 index 0000000..c837635 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketChannelHandle.kt @@ -0,0 +1,54 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +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(LocaleConstant.STATUS_CONNECT_SUCCESS) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + Log.e(kTag, "channelInactive: 连接断开") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + } + + //TODO 心跳 + 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?) { + if (data == null) { + return + } + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + Log.d(kTag, "exceptionCaught ===> $cause") + listener.onServiceStatusConnectChanged(LocaleConstant.STATUS_CONNECT_ERROR) + cause.printStackTrace() + ctx.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt new file mode 100644 index 0000000..70c7870 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketClient.kt @@ -0,0 +1,160 @@ +package com.casic.br.operationsite.utils.netty + +import android.os.SystemClock +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +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 lateinit var hostname: String + private var port = 0 + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + private var listener: ISocketListener? = null + private var reconnectNum = Int.MAX_VALUE + private var isNeedReconnect = true + private var isConnecting = false + private var reconnectIntervalTime: Long = 15000 + var connectStatus = false + + //重连时间 + fun setReconnectNum(reconnectNum: Int) { + this.reconnectNum = reconnectNum + } + + fun setReconnectIntervalTime(reconnectIntervalTime: Long) { + this.reconnectIntervalTime = reconnectIntervalTime + } + + fun setSocketListener(listener: ISocketListener?) { + this.listener = listener + } + + fun connect(hostname: String, port: Int) { + this.hostname = hostname + this.port = port + Log.d(kTag, "connect ===> 开始连接TCP服务器") + if (isConnecting) { + return + } + //起个线程 + val clientThread = object : Thread("client-Netty") { + override fun run() { + super.run() + isNeedReconnect = true + reconnectNum = Int.MAX_VALUE + connectServer() + } + } + clientThread.start() + } + + private fun connectServer() { + synchronized(this) { + 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(hostname, 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(LocaleConstant.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) { + if (bytes.isNotEmpty()) { + try { + channel!!.writeAndFlush(bytes).addListener(ChannelFutureListener { future -> + if (future.isSuccess) { + Log.d(kTag, "onClick ===> 发送成功") + } else { + // 关闭连接,节约资源 + Log.d(kTag, "onClick ===> 关闭连接,节约资源") + future.channel().close() + nioEventLoopGroup!!.shutdownGracefully() + } + }) + } catch (e: NullPointerException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt new file mode 100644 index 0000000..4117f0a --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/netty/SocketManager.kt @@ -0,0 +1,130 @@ +package com.casic.br.operationsite.utils.netty + +import android.util.Log +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.view.MethaneActivity +import com.google.gson.Gson + +class SocketManager : ISocketListener { + + companion object { + //Kotlin委托模式双重锁单例 + val get: SocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + SocketManager() + } + } + + private val kTag = "SocketManager" + private val nettyClient by lazy { SocketClient() } + + 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, "channelRead0 ===> " + data.contentToString()) + /** + * 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] + * [FF 01 01 37 E6 00 00 B2 35 0A 3D 05 6F C1] + * 甲烷浓度值为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]; + */ + val bytes = bytesToUnsigned(data) + val hashMap = HashMap() + val methaneBytes = IntArray(3) + System.arraycopy(bytes, 2, methaneBytes, 0, 3) + hashMap["methane"] = covertDataValue(methaneBytes) + hashMap["methaneState"] = covertState(bytes[5]) + val laserBytes = IntArray(3) + System.arraycopy(bytes, 6, laserBytes, 0, 3) + hashMap["laser"] = covertDataValue(laserBytes) + val horizontalBytes = IntArray(2) + System.arraycopy(bytes, 9, horizontalBytes, 0, 2) + hashMap["horizontal"] = covertAngleValue(horizontalBytes) + val verticalBytes = IntArray(2) + System.arraycopy(bytes, 11, verticalBytes, 0, 2) + hashMap["vertical"] = covertAngleValue(verticalBytes) + + //{"horizontal":26.21,"laser":-19915,"methaneState":"正常","methane":79590,"vertical":13.91} + Log.d(kTag, "onMessageResponse ===> " + Gson().toJson(hashMap)) + } + + private fun covertDataValue(bytes: IntArray): Int { + //数据码1* 65536 + 数据码2 * 256 + 数据码3 + return bytes[0] * 65536 + bytes[1] * 256 + bytes[2] + } + + private fun covertState(b: Int): String { + var state = "" + when (b) { + 0 -> state = "正常" + 1 -> state = "温控故障" + 2 -> state = "激光未打开" + else -> {} + } + return state + } + + private fun covertAngleValue(bytes: IntArray): Double { + //首先计算Tangle=[数据码10 * 256 + 数据码11]/100,单位为°,精确到0.01。 + //若Tangle在0~90范围内,则垂直角度值=Tangle;若Tangle在-1~-90范围内,则垂直角度值=Tangle-360) + val tangle = (bytes[0] * 256 + bytes[1]).toDouble() / 100 + return if (tangle in 0.0..90.0) { + tangle + } else { + tangle - 360 + } + } + + override fun onServiceStatusConnectChanged(statusCode: Int) { + if (statusCode == LocaleConstant.STATUS_CONNECT_SUCCESS) { + if (nettyClient.connectStatus) { + Log.d(kTag, "连接成功") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.CONNECT_SUCCESS) + } + } else { + if (!nettyClient.connectStatus) { + Log.e(kTag, "onServiceStatusConnectChanged:$statusCode,连接断开,正在重连") + MethaneActivity.weakReferenceHandler.sendEmptyMessage(LocaleConstant.RECONNECT) + } + } + } + + fun sendData(data: ByteArray) { + nettyClient.sendData(data) + } + + fun close() { + nettyClient.disconnect() + } + + private fun bytesToUnsigned(data: ByteArray): IntArray { + val array = IntArray(data.size) + for (i in data.indices) { + val datum = data[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/operationsite/view/MethaneActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt index aa7e298..f46c9ee 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/MethaneActivity.kt @@ -1,20 +1,121 @@ package com.casic.br.operationsite.view +import android.view.View import com.casic.br.operationsite.R +import com.casic.br.operationsite.extensions.createStartCommand +import com.casic.br.operationsite.extensions.createStopCommand import com.casic.br.operationsite.extensions.initLayoutImmersionBar +import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.netty.SocketManager import com.gyf.immersionbar.ImmersionBar import com.pengxh.kt.lite.base.KotlinBaseActivity -import kotlinx.android.synthetic.main.activity_main.* +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.widget.SteeringWheelController +import kotlinx.android.synthetic.main.activity_methane.* import kotlinx.android.synthetic.main.include_base_title.* class MethaneActivity : KotlinBaseActivity() { - override fun initData() { + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + private var isConnectSuccess = false + + //点击中间开关次数 + private var clickCount = 0 + + //手指是否已经从方向控制盘抬起 + private var isActionUp = true + + override fun initData() { + weakReferenceHandler = WeakReferenceHandler { + when (it.what) { + LocaleConstant.CONNECT_SUCCESS -> { + notConnectedLayout.visibility = View.GONE + "激光甲烷控制云台连接成功".show(this) + isConnectSuccess = true + } + LocaleConstant.RECONNECT -> { + notConnectedLayout.visibility = View.VISIBLE + isConnectSuccess = false + } + } + true + } + + val host = configSpinner.selectedItem.toString().split(":") + + SocketManager.get.connectNetty(host[0], host[1].toInt()) } override fun initEvent() { leftBackView.setOnClickListener { finish() } + + wheelController.setOnWheelTouchListener(object : + SteeringWheelController.OnWheelTouchListener { + override fun onActionTurnUp(dir: SteeringWheelController.Direction) { + if (dir == SteeringWheelController.Direction.LEFT || + dir == SteeringWheelController.Direction.TOP || + dir == SteeringWheelController.Direction.RIGHT || + dir == SteeringWheelController.Direction.BOTTOM + ) { + SocketManager.get.sendData(createStopCommand()) + isActionUp = true + } else { + if (isConnectSuccess) { + clickCount++ + if (clickCount % 2 == 0) { + openInstructionsLight() + } else { + closeInstructionsLight() + } + } + } + } + + override fun onCenterTurn() { + + } + + override fun onLeftTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.LEFT.createStartCommand()) + isActionUp = false + } + } + + override fun onTopTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.TOP.createStartCommand()) + isActionUp = false + } + } + + override fun onRightTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.RIGHT.createStartCommand()) + isActionUp = false + } + } + + override fun onBottomTurn() { + if (isConnectSuccess && isActionUp) { + SocketManager.get.sendData(SteeringWheelController.Direction.BOTTOM.createStartCommand()) + isActionUp = false + } + } + }) + } + + private fun closeInstructionsLight() { + lightStateView.setBackgroundColor(R.color.red.convertColor(this)) + } + + private fun openInstructionsLight() { + lightStateView.setBackgroundColor(R.color.green.convertColor(this)) } override fun initLayoutView(): Int = R.layout.activity_methane @@ -30,4 +131,9 @@ titleView.text = "云台参数" } + + override fun onDestroy() { + super.onDestroy() + SocketManager.get.close() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_red.xml b/app/src/main/res/drawable/bg_solid_layout_red.xml new file mode 100644 index 0000000..fc1785e --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_no_net.xml b/app/src/main/res/drawable/ic_no_net.xml new file mode 100644 index 0000000..a5a0c5c --- /dev/null +++ b/app/src/main/res/drawable/ic_no_net.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_methane.xml b/app/src/main/res/layout/activity_methane.xml index 9426fc0..b813094 100644 --- a/app/src/main/res/layout/activity_methane.xml +++ b/app/src/main/res/layout/activity_methane.xml @@ -10,6 +10,31 @@ + + + + + + + - - -