diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt new file mode 100644 index 0000000..521c353 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt @@ -0,0 +1,37 @@ +package com.casic.endoscope.widgets + +import android.content.Context +import android.widget.TextView +import com.casic.endoscope.R +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF + +class LineChartMarkerView(context: Context?) : + MarkerView(context, R.layout.popu_line_chart_marker) { + + private val dayView: TextView = findViewById(R.id.dayView) + private val dataView: TextView = findViewById(R.id.dataView) + private var xAxisDate: MutableList = ArrayList() + + fun setXAxisDate(date: MutableList) { + this.xAxisDate = date + } + + //每次重绘,会调用此方法刷新数据 + override fun refreshContent(e: Entry, highlight: Highlight) { + super.refreshContent(e, highlight) + try { + dataView.text = String.format("${e.y.toInt()}ppm·m") + dayView.text = xAxisDate[(e.x).toInt()] + } catch (e1: Exception) { + e1.printStackTrace() + } + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width shr 1)).toFloat(), (-height).toFloat()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt new file mode 100644 index 0000000..521c353 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt @@ -0,0 +1,37 @@ +package com.casic.endoscope.widgets + +import android.content.Context +import android.widget.TextView +import com.casic.endoscope.R +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF + +class LineChartMarkerView(context: Context?) : + MarkerView(context, R.layout.popu_line_chart_marker) { + + private val dayView: TextView = findViewById(R.id.dayView) + private val dataView: TextView = findViewById(R.id.dataView) + private var xAxisDate: MutableList = ArrayList() + + fun setXAxisDate(date: MutableList) { + this.xAxisDate = date + } + + //每次重绘,会调用此方法刷新数据 + override fun refreshContent(e: Entry, highlight: Highlight) { + super.refreshContent(e, highlight) + try { + dataView.text = String.format("${e.y.toInt()}ppm·m") + dayView.text = xAxisDate[(e.x).toInt()] + } catch (e1: Exception) { + e1.printStackTrace() + } + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width shr 1)).toFloat(), (-height).toFloat()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml new file mode 100644 index 0000000..471e378 --- /dev/null +++ b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt new file mode 100644 index 0000000..521c353 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt @@ -0,0 +1,37 @@ +package com.casic.endoscope.widgets + +import android.content.Context +import android.widget.TextView +import com.casic.endoscope.R +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF + +class LineChartMarkerView(context: Context?) : + MarkerView(context, R.layout.popu_line_chart_marker) { + + private val dayView: TextView = findViewById(R.id.dayView) + private val dataView: TextView = findViewById(R.id.dataView) + private var xAxisDate: MutableList = ArrayList() + + fun setXAxisDate(date: MutableList) { + this.xAxisDate = date + } + + //每次重绘,会调用此方法刷新数据 + override fun refreshContent(e: Entry, highlight: Highlight) { + super.refreshContent(e, highlight) + try { + dataView.text = String.format("${e.y.toInt()}ppm·m") + dayView.text = xAxisDate[(e.x).toInt()] + } catch (e1: Exception) { + e1.printStackTrace() + } + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width shr 1)).toFloat(), (-height).toFloat()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml new file mode 100644 index 0000000..471e378 --- /dev/null +++ b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_tag_blue.xml b/app/src/main/res/drawable/bg_text_tag_blue.xml new file mode 100644 index 0000000..5878a74 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_tag_blue.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt new file mode 100644 index 0000000..521c353 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt @@ -0,0 +1,37 @@ +package com.casic.endoscope.widgets + +import android.content.Context +import android.widget.TextView +import com.casic.endoscope.R +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF + +class LineChartMarkerView(context: Context?) : + MarkerView(context, R.layout.popu_line_chart_marker) { + + private val dayView: TextView = findViewById(R.id.dayView) + private val dataView: TextView = findViewById(R.id.dataView) + private var xAxisDate: MutableList = ArrayList() + + fun setXAxisDate(date: MutableList) { + this.xAxisDate = date + } + + //每次重绘,会调用此方法刷新数据 + override fun refreshContent(e: Entry, highlight: Highlight) { + super.refreshContent(e, highlight) + try { + dataView.text = String.format("${e.y.toInt()}ppm·m") + dayView.text = xAxisDate[(e.x).toInt()] + } catch (e1: Exception) { + e1.printStackTrace() + } + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width shr 1)).toFloat(), (-height).toFloat()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml new file mode 100644 index 0000000..471e378 --- /dev/null +++ b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_tag_blue.xml b/app/src/main/res/drawable/bg_text_tag_blue.xml new file mode 100644 index 0000000..5878a74 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_tag_blue.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 287cc58..8819a0e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -140,7 +140,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="未启用" - android:textColor="@color/black" + android:textColor="@color/red" android:textSize="@dimen/sp_16" /> diff --git a/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt new file mode 100644 index 0000000..7e3a01d --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/ByteArray.kt @@ -0,0 +1,14 @@ +package com.casic.endoscope.extensions + +/** + * 浓度值转换 + * */ +fun ByteArray.convertValue(): Int { + val response = StringBuilder() + this.forEach { + response.append(it.toInt().toChar()) + } + + val data = response.split(" ") + return data[2].toInt(16) * 65536 + data[3].toInt(16) * 256 + data[4].toInt(16) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt new file mode 100644 index 0000000..d968d0e --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/extensions/LineChart.kt @@ -0,0 +1,44 @@ +package com.casic.endoscope.extensions + +import android.content.Context +import android.graphics.Color +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.Chart +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.pengxh.kt.lite.extensions.dp2px + + +fun LineChart.init(context: Context) { + this.setNoDataText("无数据,无法渲染...") + this.setNoDataTextColor(Color.RED) + this.getPaint(Chart.PAINT_INFO).textSize = 12f.dp2px(context) + this.setDrawGridBackground(false) + this.setDrawBorders(false) + this.animateY(1200, Easing.EaseInOutQuad) + //设置样式 + val rightAxis: YAxis = this.axisRight + //设置图表右边的y轴禁用 + rightAxis.isEnabled = false + val leftAxis: YAxis = this.axisLeft + leftAxis.axisMinimum = 0f + leftAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + leftAxis.setDrawZeroLine(true) + this.isScaleXEnabled = true //X轴可缩放 + this.isScaleYEnabled = false //Y轴不可缩放 + //设置x轴 + val xAxis: XAxis = this.xAxis + xAxis.textSize = 10f + xAxis.setDrawLabels(true) //绘制标签 指x轴上的对应数值 + xAxis.setDrawAxisLine(true) //是否绘制轴线 + xAxis.setDrawGridLines(false) //设置x轴上每个点对应的线 + xAxis.granularity = 1f //禁止放大后x轴标签重绘 + xAxis.position = XAxis.XAxisPosition.BOTTOM + this.extraTopOffset = 20f + this.extraBottomOffset = 10f //解决X轴显示不完全问题 + //去掉描述 + this.description.isEnabled = false + //去掉图例 + this.legend.isEnabled = false +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java index 76125b5..5d4fa30 100644 --- a/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java +++ b/app/src/main/java/com/casic/endoscope/greendao/DaoMaster.java @@ -13,18 +13,23 @@ // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. + /** * Master of DAO (schema version 1): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { public static final int SCHEMA_VERSION = 1; - /** Creates underlying database table using DAOs. */ + /** + * Creates underlying database table using DAOs. + */ public static void createAllTables(Database db, boolean ifNotExists) { CameraPointBeanDao.createTable(db, ifNotExists); } - /** Drops underlying database table using DAOs. */ + /** + * Drops underlying database table using DAOs. + */ public static void dropAllTables(Database db, boolean ifExists) { CameraPointBeanDao.dropTable(db, ifExists); } @@ -75,7 +80,9 @@ } } - /** WARNING: Drops all table on Upgrade! Use only during development. */ + /** + * WARNING: Drops all table on Upgrade! Use only during development. + */ public static class DevOpenHelper extends OpenHelper { public DevOpenHelper(Context context, String name) { super(context, name); diff --git a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt index b60ea2c..5949c10 100644 --- a/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt +++ b/app/src/main/java/com/casic/endoscope/utils/ProjectConstant.kt @@ -54,4 +54,6 @@ //待转码视频路径 val VIDEO_PATH_STACK = Stack() + + const val SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" //连接设备的UUID } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt new file mode 100644 index 0000000..9810bd7 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/BleDeviceManager.kt @@ -0,0 +1,380 @@ +package com.casic.endoscope.utils.ble + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Message +import android.util.Log +import androidx.core.app.ActivityCompat +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.Constant +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import java.util.UUID + + +class BleDeviceManager(private val context: Context) : Handler.Callback { + private val kTag = "BleDeviceManager" + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private var bluetoothAdapter: BluetoothAdapter + private var isConnecting = false + private lateinit var bleDiscoveryListener: OnDeviceDiscoveredListener + private lateinit var bleConnectListener: OnDeviceConnectListener + private lateinit var serviceUuid: UUID + private lateinit var bluetoothGatt: BluetoothGatt + + override fun handleMessage(msg: Message): Boolean { + return true + } + + init { + val bluetoothManager = context.getSystemService()!! + bluetoothAdapter = bluetoothManager.adapter + } + + /** + * 打开蓝牙 + */ + fun openBluetooth() { + if (!bluetoothAdapter.isEnabled) { + if (checkConnectPermission()) { + context.startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) + } + } + } + + /** + * 蓝牙是否已打开 + */ + fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter.isEnabled + } + + /** + * 本地蓝牙是否处于正在扫描状态 + * @return true false + */ + fun isDiscovering(): Boolean { + return if (checkScanPermission()) { + bluetoothAdapter.isDiscovering + } else false; + } + + /** + * 开始扫描设备 + */ + fun startScanDevice(listener: OnDeviceDiscoveredListener, scanTime: Long) { + this.bleDiscoveryListener = listener + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback) + //设定最长扫描时间 + weakReferenceHandler.postDelayed(stopScanRunnable, scanTime) + } + } + + /** + * 停止扫描设备 + */ + fun stopDiscoverDevice() { + weakReferenceHandler.removeCallbacks(stopScanRunnable) + } + + @SuppressLint("MissingPermission") + private val stopScanRunnable = Runnable { + bleDiscoveryListener.onDeviceDiscoveryEnd() + //scanTime之后还没有扫描到设备,就停止扫描。 + if (checkScanPermission()) { + bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + } + } + + /** + * 扫描设备回调 + */ + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.apply { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if (!device.name.isNullOrBlank()) { + bleDiscoveryListener.onDeviceFound(device) + } + } + } + + override fun onBatchScanResults(results: List) { + + } + + override fun onScanFailed(errorCode: Int) { + Log.d(kTag, "onScanFailed: errorCode ===> $errorCode") + } + } + + /****连接蓝牙*********************************************************************/ + fun connectBleDevice( + bluetoothDevice: BluetoothDevice, serviceUuid: String, outTime: Long, + connectListener: OnDeviceConnectListener + ) { + if (isConnecting) { + Log.d(kTag, "connectBleDevice() ===> isConnecting = true") + return + } + this.serviceUuid = UUID.fromString(serviceUuid) + this.bleConnectListener = connectListener + if (checkConnectPermission()) { + Log.d(kTag, "开始准备连接:" + bluetoothDevice.name + "-->" + bluetoothDevice.address) + try { + bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback) + bluetoothGatt.connect() + } catch (e: Exception) { + e.printStackTrace() + } + //设置连接超时时间 + weakReferenceHandler.postDelayed(connectTimeoutRunnable, outTime) + } + } + + private val bluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + if (checkConnectPermission()) { + gatt?.apply { + Log.d(kTag, "连接的设备:" + this.device.name + " " + this.device.address) + isConnecting = true + //移除连接超时 + weakReferenceHandler.removeCallbacks(connectTimeoutRunnable) + when (newState) { + BluetoothGatt.STATE_CONNECTING -> { + Log.d(kTag, "正在连接...") + bleConnectListener.onConnecting(this) //正在连接回调 + } + + BluetoothGatt.STATE_CONNECTED -> { + Log.d(kTag, "连接成功") + //连接成功去发现服务 + discoverServices() + //设置发现服务超时时间 + weakReferenceHandler.postDelayed( + discoverServiceTimeoutRunnable, Constant.MAX_CONNECT_TIME + ) + bleConnectListener.onConnectSuccess(this, status) + } + + BluetoothGatt.STATE_DISCONNECTING -> { + Log.d(kTag, "正在断开...") + bleConnectListener.onDisConnecting(this) //正在断开回调 + } + + BluetoothGatt.STATE_DISCONNECTED -> { + when (status) { + 133 -> {//133连接异常,无法连接 + bleConnectListener.onConnectFailure(this, "连接异常!", status) + Log.d(kTag, "${this.device.address}连接失败") + } + + 62 -> {//62没有发现服务 异常断开 + bleConnectListener.onConnectFailure( + this, "没有发现服务,异常断开!", status + ) + } + + else -> { + //0:正常断开 + //8:因为距离远或者电池无法供电断开连接 + //34:断开 + //其他断开 + Log.d(kTag, "断开连接,status = $status") + bleConnectListener.onDisConnectSuccess(this, status) + } + } + close() + isConnecting = false + } + } + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + isConnecting = false + //移除发现超时服务 + weakReferenceHandler.removeCallbacks(discoverServiceTimeoutRunnable) + //配置服务信息 + gatt?.apply { + if (configBleService(this, serviceUuid)) { + //成功发现服务回调 + bleConnectListener.onServiceDiscoverySuccess(this, status) + } else { + bleConnectListener.onServiceDiscoveryFailed(this, "获取服务特征异常") + } + } + + } + + //读取蓝牙设备发出来的数据回调 + override fun onCharacteristicRead( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + Log.d(kTag, "onCharacteristicRead => $status") + } + + //向蓝牙设备写入数据结果回调 + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + //将收到的字节数组转换成十六进制字符串 + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + bleConnectListener.onWriteSuccess(gatt, characteristic?.value) + } + + BluetoothGatt.GATT_FAILURE -> bleConnectListener.onWriteFailed(gatt, "写入失败") + + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "没有写入设备权限".show(context) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + //接收数据 + Log.d(kTag, "收到数据:" + value.toList()) + bleConnectListener.onReceiveMessage(gatt, value) //接收数据回调 + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray + ) { + super.onDescriptorRead(gatt, descriptor, status, value) + Log.d(kTag, "onDescriptorRead => 开启监听成功,可以读取设备") + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + //开启监听成功,可以向设备写入命令了 + Log.d(kTag, "onDescriptorWrite => 开启监听成功,可以写入设备") + } + + override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) { + super.onReadRemoteRssi(gatt, rssi, status) + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.d(kTag, "onReadRemoteRssi => RSSI值: $rssi") + bleConnectListener.onReadRssi(gatt, rssi, status) //成功读取连接的信号强度回调 + } + + BluetoothGatt.GATT_FAILURE -> Log.d(kTag, "读取RSSI值失败,status: $status") + } + } + } + + @SuppressLint("MissingPermission") + private val connectTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //连接超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "连接超时", -1) + } + } + + @SuppressLint("MissingPermission") + private val discoverServiceTimeoutRunnable = Runnable { + isConnecting = false + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + //发现服务超时当作连接失败回调 + bleConnectListener.onConnectFailure(bluetoothGatt, "发现服务超时", -1) + } + } + + private fun configBleService(gatt: BluetoothGatt, serviceUuid: UUID): Boolean { + var notifyCharacteristic: BluetoothGattCharacteristic? = null + gatt.services.forEach { service -> + Log.d(kTag, "configBleService => ${service.uuid}") + if (service.uuid == serviceUuid) { + service.characteristics.forEach { characteristic -> + val charaProp = characteristic.properties + if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + val notifyServiceUuid = service.uuid + val notifyCharacteristicUuid = characteristic.uuid + notifyCharacteristic = bluetoothGatt.getService(notifyServiceUuid) + .getCharacteristic(notifyCharacteristicUuid) + } + } + } else { + Log.d(kTag, "configBleService => 未匹配到uuid") + } + } + //打开读通知,打开的是notifyCharacteristic,不然不走onCharacteristicChanged回调 + if (checkConnectPermission()) { + bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true) + //一定要重新设置 + notifyCharacteristic?.apply { + descriptors.forEach { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + bluetoothGatt.writeDescriptor(it) + } + } + //延迟2s,保证所有通知都能及时打开 + weakReferenceHandler.postDelayed({ }, 2000) + return true + } + return false + } + + fun disConnectDevice() { + if (checkConnectPermission()) { + bluetoothGatt.disconnect() + isConnecting = false + } + } + + private fun checkConnectPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙连接权限".show(context) + false + } else { + true + } + } + + private fun checkScanPermission(): Boolean { + return if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + "缺少蓝牙扫描权限".show(context) + return false + } else { + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt new file mode 100644 index 0000000..53c6fc6 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceConnectListener.kt @@ -0,0 +1,29 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothGatt + +interface OnDeviceConnectListener { + fun onConnecting(bluetoothGatt: BluetoothGatt?) //正在连接 + + fun onConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) //连接成功 + + fun onConnectFailure(bluetoothGatt: BluetoothGatt?, exception: String, status: Int) //连接失败 + + fun onDisConnecting(bluetoothGatt: BluetoothGatt?) //正在断开 + + fun onDisConnectSuccess(bluetoothGatt: BluetoothGatt?, status: Int) // 断开连接 + + fun onServiceDiscoverySuccess(bluetoothGatt: BluetoothGatt?, status: Int) //发现服务成功 + + fun onServiceDiscoveryFailed(bluetoothGatt: BluetoothGatt?, msg: String) //发现服务失败 + + fun onReceiveMessage(bluetoothGatt: BluetoothGatt?, value: ByteArray) //收到消息 + + fun onReceiveError(errorMsg: String) //接收数据出错 + + fun onWriteSuccess(bluetoothGatt: BluetoothGatt?, msg: ByteArray?) //写入成功 + + fun onWriteFailed(bluetoothGatt: BluetoothGatt?, errorMsg: String) //写入失败 + + fun onReadRssi(bluetoothGatt: BluetoothGatt?, rssi: Int, status: Int) //成功读取到连接信号强度 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt new file mode 100644 index 0000000..b49e96f --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/utils/ble/OnDeviceDiscoveredListener.kt @@ -0,0 +1,9 @@ +package com.casic.endoscope.utils.ble + +import android.bluetooth.BluetoothDevice + +interface OnDeviceDiscoveredListener { + fun onDeviceFound(device: BluetoothDevice) //搜索到设备 + + fun onDeviceDiscoveryEnd() //扫描结束 +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt index 57e2197..90bc653 100644 --- a/app/src/main/java/com/casic/endoscope/view/MainActivity.kt +++ b/app/src/main/java/com/casic/endoscope/view/MainActivity.kt @@ -1,7 +1,10 @@ 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.os.Bundle import android.os.Handler @@ -15,18 +18,29 @@ import com.casic.endoscope.adapter.CameraPointAdapter import com.casic.endoscope.bean.CameraPointBean import com.casic.endoscope.databinding.ActivityMainBinding +import com.casic.endoscope.extensions.convertValue import com.casic.endoscope.extensions.createHorizontalCommand import com.casic.endoscope.extensions.createImageFileDir import com.casic.endoscope.extensions.createVerticalCommand import com.casic.endoscope.extensions.createVideoFileDir 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.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.AddCameraPointDialog +import com.casic.endoscope.widgets.BluetoothDeviceDialog +import com.casic.endoscope.widgets.LineChartMarkerView +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 @@ -37,6 +51,8 @@ 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.utils.LoadingDialogHub import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.SteeringWheelView import kotlinx.coroutines.Dispatchers @@ -56,9 +72,18 @@ 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 selectedSpeed = 7 private val messageCode = 2024021901 + private val bluetoothDevices = ArrayList() + private val xAxisLabels = ArrayList() + private val densityEntries = ArrayList() + private val lineDataSets = ArrayList() + + //趋势线起点X坐标 + private var i = 0 + private var isConnected = false private var clickTime = 0L private var previewHandle = -1 private var selectChannel = -1 @@ -87,6 +112,8 @@ private lateinit var weakReferenceHandler: WeakReferenceHandler private lateinit var dataAdapter: CameraPointAdapter private lateinit var serviceIntent: Intent + private lateinit var dataSet: LineDataSet + private lateinit var lineData: LineData override fun initViewBinding(): ActivityMainBinding { return ActivityMainBinding.inflate(layoutInflater) @@ -101,6 +128,13 @@ 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() { @@ -132,6 +166,48 @@ navigatePageTo() } + 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.imageButton.setOnClickListener { if (isPreviewSuccess) { @@ -223,11 +299,8 @@ } else { dataBeans.last().step } - AddCameraPointDialog.Builder() - .setContext(this) - .setLastStep(step) - .setNegativeButton("取消") - .setPositiveButton("添加") + AddCameraPointDialog.Builder().setContext(this).setLastStep(step) + .setNegativeButton("取消").setPositiveButton("添加") .setOnDialogButtonClickListener(object : AddCameraPointDialog.OnDialogButtonClickListener { override fun onConfirmClick(step: Int, hAngle: Int, vAngle: Int) { @@ -282,8 +355,8 @@ startAChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() - dChannelNum = deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + - deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 + dChannelNum = + deviceInfo.deviceInfoV40_jna.struDeviceV30.byIPChanNum + deviceInfo.deviceInfoV40_jna.struDeviceV30.byHighDChanNum * 256 startDChannel = deviceInfo.deviceInfoV40_jna.struDeviceV30.byStartChan.toInt() @@ -451,6 +524,33 @@ } } + 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() + } + } + /** * 角度执行 * */ @@ -478,10 +578,7 @@ e.printStackTrace() } hkSDK.NET_DVR_SerialSend( - serialHandle, - 1, - vertical.createVerticalCommand(), - vertical.createVerticalCommand().size + serialHandle, 1, vertical.createVerticalCommand(), vertical.createVerticalCommand().size ) try { @@ -552,4 +649,102 @@ } 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) { + //解析返回值 + lifecycleScope.launch(Dispatchers.Main) { + val density = value.convertValue() + /***将值渲染到曲线图****************************************************************/ + + //时间作为X轴 + xAxisLabels.add(System.currentTimeMillis().timestampToTime()) + + //浓度作为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() + } + } + + 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") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt index 6ae91ba..0d8a791 100644 --- a/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt +++ b/app/src/main/java/com/casic/endoscope/widgets/BluetoothDeviceDialog.kt @@ -27,14 +27,12 @@ private val ctx = builder.context private var beans = builder.beans private val negativeBtn = builder.negativeBtn - private val positiveBtn = builder.positiveBtn private val listener = builder.listener class Builder { lateinit var context: Context lateinit var beans: ArrayList lateinit var negativeBtn: String - lateinit var positiveBtn: String lateinit var listener: OnDialogButtonClickListener fun setContext(context: Context): Builder { @@ -52,11 +50,6 @@ return this } - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { this.listener = listener return this @@ -84,12 +77,6 @@ listener.onCancelClick() } - binding.confirmButton.text = positiveBtn - binding.confirmButton.setOnClickListener { - dismiss() - listener.onRetryClick() - } - binding.listView.adapter = ItemListAdapter(ctx) binding.listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> @@ -114,7 +101,7 @@ val holder: ItemViewHolder if (convertView == null) { holder = ItemViewHolder() - view = inflater.inflate(com.pengxh.kt.lite.R.layout.item_action_sheet, null) + view = inflater.inflate(R.layout.item_action_sheet, null) holder.itemView = view.findViewById(R.id.sheetItemView) view.tag = holder } else { @@ -140,8 +127,6 @@ } interface OnDialogButtonClickListener { - fun onRetryClick() - fun onItemClick(position: Int) fun onCancelClick() diff --git a/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt new file mode 100644 index 0000000..521c353 --- /dev/null +++ b/app/src/main/java/com/casic/endoscope/widgets/LineChartMarkerView.kt @@ -0,0 +1,37 @@ +package com.casic.endoscope.widgets + +import android.content.Context +import android.widget.TextView +import com.casic.endoscope.R +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF + +class LineChartMarkerView(context: Context?) : + MarkerView(context, R.layout.popu_line_chart_marker) { + + private val dayView: TextView = findViewById(R.id.dayView) + private val dataView: TextView = findViewById(R.id.dataView) + private var xAxisDate: MutableList = ArrayList() + + fun setXAxisDate(date: MutableList) { + this.xAxisDate = date + } + + //每次重绘,会调用此方法刷新数据 + override fun refreshContent(e: Entry, highlight: Highlight) { + super.refreshContent(e, highlight) + try { + dataView.text = String.format("${e.y.toInt()}ppm·m") + dayView.text = xAxisDate[(e.x).toInt()] + } catch (e1: Exception) { + e1.printStackTrace() + } + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width shr 1)).toFloat(), (-height).toFloat()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml new file mode 100644 index 0000000..471e378 --- /dev/null +++ b/app/src/main/res/drawable/bg_stroke_layout_blue_5.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_tag_blue.xml b/app/src/main/res/drawable/bg_text_tag_blue.xml new file mode 100644 index 0000000..5878a74 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_tag_blue.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 287cc58..8819a0e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -140,7 +140,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="未启用" - android:textColor="@color/black" + android:textColor="@color/red" android:textSize="@dimen/sp_16" /> diff --git a/app/src/main/res/layout/dialog_bluetooth_device.xml b/app/src/main/res/layout/dialog_bluetooth_device.xml index a2e5e8d..0073cbe 100644 --- a/app/src/main/res/layout/dialog_bluetooth_device.xml +++ b/app/src/main/res/layout/dialog_bluetooth_device.xml @@ -41,37 +41,14 @@ android:layout_height="@dimen/lib_px_1" android:background="@color/lib_line_color" /> - - -