package com.casic.endoscope.view import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.content.Intent import android.graphics.Color import android.graphics.PixelFormat import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment import android.os.Handler import android.os.Message import android.provider.Settings import android.util.Log import android.view.KeyEvent import android.view.MotionEvent import android.view.SurfaceHolder import android.view.View import androidx.lifecycle.lifecycleScope import com.casic.endoscope.R import com.casic.endoscope.databinding.ActivityMainBinding import com.casic.endoscope.extensions.check import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.getChannel import com.casic.endoscope.extensions.init import com.casic.endoscope.extensions.toTime import com.casic.endoscope.service.VideoTranscodeService import com.casic.endoscope.utils.DataBaseManager import com.casic.endoscope.utils.FileManager import com.casic.endoscope.utils.ProjectConstant import com.casic.endoscope.utils.ble.BleDeviceManager import com.casic.endoscope.utils.ble.OnDeviceConnectListener import com.casic.endoscope.utils.ble.OnDeviceDiscoveredListener import com.casic.endoscope.utils.hk.MessageCodeHub import com.casic.endoscope.utils.hk.SDKGuider import com.casic.endoscope.widgets.AlertControlDialog import com.casic.endoscope.widgets.BluetoothDeviceDialog import com.casic.endoscope.widgets.LineChartMarkerView import com.casic.endoscope.widgets.PreviewImageDialog import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.interfaces.datasets.ILineDataSet import com.gyf.immersionbar.ImmersionBar import com.hikvision.netsdk.HCNetSDK import com.hikvision.netsdk.NET_DVR_JPEGPARA import com.hikvision.netsdk.NET_DVR_PREVIEWINFO import com.hikvision.netsdk.NET_DVR_SERIALSTART_V40 import com.hikvision.netsdk.PTZCommand import com.pengxh.kt.lite.adapter.NormalRecyclerAdapter import com.pengxh.kt.lite.adapter.ViewHolder import com.pengxh.kt.lite.base.KotlinBaseActivity import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets import com.pengxh.kt.lite.extensions.dp2px import com.pengxh.kt.lite.extensions.getStatusBarHeight import com.pengxh.kt.lite.extensions.navigatePageTo import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.extensions.timestampToTime import com.pengxh.kt.lite.extensions.toJson import com.pengxh.kt.lite.extensions.writeToFile import com.pengxh.kt.lite.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.Timer import java.util.TimerTask @SuppressLint("all") class MainActivity : KotlinBaseActivity<ActivityMainBinding>(), SurfaceHolder.Callback, Handler.Callback { private val kTag = "MainActivity" private val context = this@MainActivity private val hkSDK by lazy { HCNetSDK.getInstance() } private val bleDeviceManager by lazy { BleDeviceManager(this) } private val timeFormat by lazy { SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA) } private val marginOffset by lazy { 10.dp2px(this) } private val selectedSpeed = 7 private val messageCode = 2024021901 private val requestManageFilesAccess = 2024041301 private val bluetoothDevices = ArrayList<BluetoothDevice>() private val xAxisLabels = ArrayList<String>() private val densityEntries = ArrayList<Entry>() private val lineDataSets = ArrayList<ILineDataSet>() private val recyclerViewImages = ArrayList<String>() //趋势线起点X坐标 private var i = 0 private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 private var returnUserId = -1 private var aChannelNum = 0 private var startAChannel = 0 private var dChannelNum = 0 private var startDChannel = 0 private var isPreviewSuccess = false //手指是否已经从方向控制盘抬起 private var isActionUp = true //焦距按钮是否已松开 private var isScaleButtonUp = true private var timer: Timer? = null private var timerTask: TimerTask? = null private var seconds = 0L private var videoPath = "" private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var serviceIntent: Intent private lateinit var dataSet: LineDataSet private lateinit var lineData: LineData private lateinit var imageAdapter: NormalRecyclerAdapter<String> override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == requestManageFilesAccess) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { FileManager.getRootDirectory() } } } override fun initOnCreate(savedInstanceState: Bundle?) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.parse("package:${packageName}") startActivityForResult(intent, requestManageFilesAccess) } else { FileManager.getRootDirectory() } } //显示数据 weakReferenceHandler = WeakReferenceHandler(this) weakReferenceHandler.sendEmptyMessage(messageCode) serviceIntent = Intent(this, VideoTranscodeService::class.java) //初始化浓度趋势折线图和Marker binding.lineChart.init(this) val markerView = LineChartMarkerView(this) markerView.chartView = binding.lineChart markerView.setXAxisDate(xAxisLabels) binding.lineChart.marker = markerView } override fun onResume() { super.onResume() //启动视频转码服务 startService(serviceIntent) } override fun initEvent() { binding.openAlbumButton.setOnClickListener { if (isPreviewSuccess) { "设备预览画面中,无法切换".show(this) return@setOnClickListener } navigatePageTo<AlbumActivity>() } binding.searchBleButton.setOnClickListener { if (!bleDeviceManager.isBluetoothEnabled()) { bleDeviceManager.openBluetooth() } if (bleDeviceManager.isDiscovering()) { bleDeviceManager.stopDiscoverDevice() } if (isConnected) { bleDeviceManager.disConnectDevice() } else { LoadingDialogHub.show(this, "设备搜索中...") bleDeviceManager.startScanDevice(object : OnDeviceDiscoveredListener { override fun onDeviceFound(device: BluetoothDevice) { if (bluetoothDevices.isEmpty()) { bluetoothDevices.add(device) } else { //0表示未添加到list的新设备,1表示已经扫描并添加到list的设备 var judge = 0 for (temp in bluetoothDevices) { if (temp.address == device.address) { judge = 1 break } } if (judge == 0) { bluetoothDevices.add(device) } } } override fun onDeviceDiscoveryEnd() { LoadingDialogHub.dismiss() //显示搜索到设备列表 showScanResult() } }, 10 * 1000) } } binding.outputDataView.setOnClickListener { val valueBeans = DataBaseManager.get.loadAll() //导出到本地文件 val rootDirectory = FileManager.getRootDirectory() val file = File("${rootDirectory}/GasDensityValue.json") if (file.exists()) { file.delete() } valueBeans.toJson().writeToFile(file) "数据导出成功,请在设备文件管理根目录【Endoscope】查看".show(this) } binding.upScaleButton.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { if (isPreviewSuccess && isScaleButtonUp) { hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.ZOOM_IN, 0) isScaleButtonUp = false } } MotionEvent.ACTION_UP -> { isScaleButtonUp = true hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.ZOOM_IN, 1) } } false } binding.downScaleButton.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { if (isPreviewSuccess && isScaleButtonUp) { hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.ZOOM_OUT, 0) isScaleButtonUp = false } } MotionEvent.ACTION_UP -> { isScaleButtonUp = true hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.ZOOM_OUT, 1) } } false } binding.topButton.setOnTouchListener { _, motionEvent -> when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { if (isPreviewSuccess && isActionUp) { hkSDK.NET_DVR_PTZControlWithSpeed( previewHandle, PTZCommand.TILT_UP, 0, selectedSpeed ) isActionUp = false } } MotionEvent.ACTION_UP -> { hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.TILT_UP, 1) isActionUp = true } } false } binding.centerButton.setOnClickListener { if (!isPreviewSuccess) { val deviceItem = SDKGuider.sdkGuider.devManageGuider.DeviceItem() deviceItem.szDevName = "" deviceItem.devNetInfo = SDKGuider.sdkGuider.devManageGuider.DevNetInfo( ProjectConstant.HK_NET_IP, ProjectConstant.HK_NET_PORT, ProjectConstant.HK_NET_USERNAME, ProjectConstant.HK_NET_PASSWORD ) if (deviceItem.szDevName.isEmpty()) { deviceItem.szDevName = deviceItem.devNetInfo.szIp } val loginV40Jna = SDKGuider.sdkGuider.devManageGuider.login_v40_jna( deviceItem.szDevName, deviceItem.devNetInfo ) if (loginV40Jna) { //配置设备通道 try { val deviceInfo = SDKGuider.sdkGuider.devManageGuider.devList[0] returnUserId = deviceInfo.szUserId aChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byChanNum.toInt() startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() var iAnalogStartChan = startAChannel var iDigitalStartChan = startDChannel val channelList = ArrayList<String>() for (i in 0 until aChannelNum) { channelList.add("ACamera_$iAnalogStartChan") iAnalogStartChan++ } for (i in 0 until dChannelNum) { channelList.add("DCamera_$iDigitalStartChan") iDigitalStartChan++ } selectChannel = Integer.valueOf(channelList[0].getChannel()) val streamList = ArrayList<String>() streamList.add("main_stream") streamList.add("sub_stream") streamList.add("third_stream") //开始预览 if (previewHandle != -1) { SDKGuider.sdkGuider.devPreviewGuider.RealPlay_Stop_jni(previewHandle) } val strutPlayInfo = NET_DVR_PREVIEWINFO() strutPlayInfo.lChannel = selectChannel strutPlayInfo.dwStreamType = 1 strutPlayInfo.bBlocked = 1 strutPlayInfo.hHwnd = binding.surfaceView.holder previewHandle = SDKGuider.sdkGuider.devPreviewGuider.RealPlay_V40_jni( returnUserId, strutPlayInfo, null ) if (previewHandle < 0) { Log.d(kTag, "initEvent: Err:${MessageCodeHub.getErrorCode()}") return@setOnClickListener } isPreviewSuccess = true "预览开启成功".show(context) //开始计时 timer = Timer() timerTask = object : TimerTask() { override fun run() { seconds++ lifecycleScope.launch(Dispatchers.Main) { binding.runningTimeView.text = seconds.toTime() } } } timer?.schedule(timerTask, 0, 1000) } catch (e: IndexOutOfBoundsException) { e.printStackTrace() "设备未正常连接,无法开启预览".show(context) } } else { "设备登陆失败".show(context) } } else { if (!SDKGuider.sdkGuider.devPreviewGuider.RealPlay_Stop_jni(previewHandle)) { return@setOnClickListener } previewHandle = -1 isPreviewSuccess = false "预览关闭成功".show(context) //停止计时 timerTask?.cancel() timer?.cancel() } } binding.bottomButton.setOnTouchListener { _, motionEvent -> when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { if (isPreviewSuccess && isActionUp) { hkSDK.NET_DVR_PTZControlWithSpeed( previewHandle, PTZCommand.TILT_DOWN, 0, selectedSpeed ) isActionUp = false } } MotionEvent.ACTION_UP -> { hkSDK.NET_DVR_PTZControl(previewHandle, PTZCommand.TILT_DOWN, 1) isActionUp = true } } false } //单张拍照 binding.imageButton.setOnClickListener { if (isPreviewSuccess) { binding.imageButton.isEnabled = false val strJpeg = NET_DVR_JPEGPARA() strJpeg.wPicQuality = 1 strJpeg.wPicSize = 2 val imagePath = "/${FileManager.getImageFileDir()}/${timeFormat.format(Date())}.png" hkSDK.NET_DVR_CaptureJPEGPicture( returnUserId, selectChannel, strJpeg, imagePath ) if (MessageCodeHub.getErrorCode() == 0) { "画面抓取成功".show(this) binding.imageButton.isEnabled = true //刷新右侧预览界面 recyclerViewImages.add(imagePath) imageAdapter.notifyDataSetChanged() } } else { "摄像头预览未打开,无法拍照".show(this) } } //视频录制 binding.videoButton.setOnClickListener { if (isPreviewSuccess) { videoPath = "/${FileManager.getVideoFileDir()}/${timeFormat.format(Date())}.mp4" hkSDK.NET_DVR_SaveRealData(returnUserId, videoPath) binding.videoStateView.visibility = View.VISIBLE } else { "摄像头预览未打开,无法录制视频".show(this) } } binding.videoButton.setOnLongClickListener { //停止视频抓取 hkSDK.NET_DVR_StopSaveRealData(returnUserId) binding.videoStateView.visibility = View.INVISIBLE ProjectConstant.VIDEO_PATH_STACK.push(videoPath) "视频录制成功".show(this) true } binding.resetButton.setOnClickListener { //建立透明通道 val serialStart = NET_DVR_SERIALSTART_V40() //串口类型:1- 232 串口,2- 485 串口 serialStart.dwSerialPort = 2 //串口编号 serialStart.wPort = 0 val serialHandle = hkSDK.NET_DVR_SerialStart_V40(returnUserId, serialStart) { _, _, _, _ -> } //链式执行 lifecycleScope.launch(Dispatchers.IO) { flow { //向透明通道发送数据,水平 val horizontalCommand = 60.createHorizontalCommand() val serialSend = hkSDK.NET_DVR_SerialSend( serialHandle, 1, horizontalCommand, horizontalCommand.size ) delay(500) emit(serialSend) }.onCompletion { //关闭通道 hkSDK.NET_DVR_SerialStop(serialHandle) }.collect { value -> if (value) { //向透明通道发送数据,垂直 val verticalCommand = 0.createVerticalCommand() val serialSend = hkSDK.NET_DVR_SerialSend( serialHandle, 1, verticalCommand, verticalCommand.size ) if (!serialSend) { withContext(Dispatchers.Main) { "垂直角度指令执行失败".show(context) } } delay(500) } else { withContext(Dispatchers.Main) { "水平角度指令执行失败".show(context) } } } } } } override fun handleMessage(msg: Message): Boolean { if (msg.what == messageCode) { //绑定数据 imageAdapter = object : NormalRecyclerAdapter<String>( R.layout.item_image_rv_g, recyclerViewImages ) { override fun convertView(viewHolder: ViewHolder, position: Int, item: String) { viewHolder.setImageResource(R.id.imageView, item) viewHolder.setOnLongClickListener(R.id.imageView) { AlertControlDialog.Builder() .setContext(context) .setTitle("温馨提示") .setMessage("是否保存此图片?") .setNegativeButton("取消") .setPositiveButton("确定") .setOnDialogButtonClickListener(object : AlertControlDialog.OnDialogButtonClickListener { override fun onConfirmClick() { } override fun onCancelClick() { //删除图片 recyclerViewImages.remove(item) File(item).delete() imageAdapter.notifyDataSetChanged() } }).build().show() true } viewHolder.setOnClickListener(R.id.imageView) { //查看大图Dialog PreviewImageDialog.Builder() .setContext(context) .setImagePath(item) .build() .show() } } } binding.recyclerView.addItemDecoration( RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) ) binding.recyclerView.adapter = imageAdapter } return true } private fun showScanResult() { BluetoothDeviceDialog.Builder().setContext(this).setDeviceArray(bluetoothDevices) .setNegativeButton("取消").setOnDialogButtonClickListener(object : BluetoothDeviceDialog.OnDialogButtonClickListener { override fun onItemClick(position: Int) { //连接点击的设备 startConnectDevice(bluetoothDevices[position]) } override fun onCancelClick() { } }).build().show() } private fun startConnectDevice(device: BluetoothDevice) { // 当前蓝牙设备 if (!isConnected) { LoadingDialogHub.show(this, "正在连接...") bleDeviceManager.connectBleDevice( device, ProjectConstant.SERVICE_UUID, 10 * 1000, bleConnectListener ) } else { bleDeviceManager.disConnectDevice() } } override fun observeRequestState() { } override fun surfaceCreated(holder: SurfaceHolder) { binding.surfaceView.holder.setFormat(PixelFormat.TRANSLUCENT) if (-1 == previewHandle) { return } val surface = holder.surface if (surface.isValid) { if (-1 == SDKGuider.sdkGuider.devPreviewGuider.RealPlaySurfaceChanged_jni( previewHandle, 0, holder ) ) { Log.d(kTag, "surfaceCreated: ${MessageCodeHub.getErrorCode()}") } } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder) { if (-1 == previewHandle) { return } if (holder.surface.isValid) { if (-1 == SDKGuider.sdkGuider.devPreviewGuider.RealPlaySurfaceChanged_jni( previewHandle, 0, null ) ) { Log.d(kTag, "surfaceDestroyed: ${MessageCodeHub.getErrorCode()}") } } } override fun setupTopBarLayout() { ImmersionBar.with(this).statusBarDarkFont(false).init() //根据不同设备状态栏高度设置statusBarView高度 val statusBarHeight = getStatusBarHeight() binding.rootView.setPadding(0, statusBarHeight, 0, 0) binding.rootView.requestLayout() } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK) { if (System.currentTimeMillis() - clickTime > 2000) { stopService(serviceIntent) "再按一次退出应用".show(this) clickTime = System.currentTimeMillis() return true } else { super.onKeyDown(keyCode, event) } } return super.onKeyDown(keyCode, event) } /***低功耗蓝牙********************************************/ private val bleConnectListener = object : OnDeviceConnectListener { override fun onConnecting(bluetoothGatt: BluetoothGatt?) { Log.d(kTag, "onConnecting => ") } override fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) { Log.d(kTag, "onConnectSuccess => $status") } override fun onConnectFailure( bluetoothGatt: BluetoothGatt?, exception: String, status: Int ) { Log.d(kTag, "onConnectFailure => $status") isConnected = false } override fun onDisConnecting(bluetoothGatt: BluetoothGatt?) { Log.d(kTag, "onDisConnecting => ") } override fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) { lifecycleScope.launch(Dispatchers.Main) { Log.d(kTag, "onDisConnectSuccess => $status") isConnected = false binding.searchBleButton.text = "搜索蓝牙" binding.stateView.text = "未连接" binding.stateView.setTextColor(Color.RED) } } override fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) { lifecycleScope.launch(Dispatchers.Main) { Log.d(kTag, "onServiceDiscoverySuccess => $status") LoadingDialogHub.dismiss() "连接成功".show(context) isConnected = true binding.searchBleButton.text = "断开连接" binding.stateView.text = "已连接" binding.stateView.setTextColor(Color.GREEN) } } override fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) { Log.d(kTag, "onServiceDiscoveryFailed => ") isConnected = false } override fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) { //[97, 97, 32, 48, 49, 32, 48, 48, 32, 48, 48, 32, 48, 48, 32, 48, 100, 32, 53, 53, 32] if (value.check()) { /***将值渲染到曲线图****************************************************************/ lifecycleScope.launch(Dispatchers.Main) { val currentTime = System.currentTimeMillis().timestampToTime() //解析返回值 val density = value.convertValue() withContext(Dispatchers.IO) { //协程切换为子线程将数据保存在数据库 DataBaseManager.get.saveGasDensity(density, currentTime) } //时间作为X轴 xAxisLabels.add(currentTime) //浓度作为Y轴 val entry = Entry(i++.toFloat(), density.toFloat(), "浓度") densityEntries.add(entry) //转化为控件需要的Data dataSet = LineDataSet(densityEntries, "") dataSet.setDrawCircles(false) dataSet.mode = LineDataSet.Mode.CUBIC_BEZIER //线条颜色 dataSet.color = Color.RED lineDataSets.add(dataSet) lineData = LineData(lineDataSets) lineData.setDrawValues(false) //解决折线点太多导致卡顿问题 if (lineData.entryCount > 50) { lineData.removeDataSet(0) lineData.notifyDataChanged() } binding.lineChart.data = lineData binding.lineChart.invalidate() } } else { Log.d(kTag, "onReceiveMessage: 数据校验失败") } } override fun onReceiveError(errorMsg: String) { Log.d(kTag, "onReceiveError => $errorMsg") } override fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) { Log.d(kTag, "onWriteSuccess => ${msg.contentToString()}") } override fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) { Log.d(kTag, "onWriteFailed => $errorMsg") } override fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) { Log.d(kTag, "onReadRssi => $rssi") } } }