From a9564bb441bde7d9088925ab9dff3268823c1a66 Mon Sep 17 00:00:00 2001 From: MrApple100 Date: Tue, 20 Aug 2024 19:15:25 +0300 Subject: [PATCH] practise BLE yes --- app/build.gradle.kts | 7 +- app/src/main/AndroidManifest.xml | 46 ++- .../ru/myitschool/work/ActivityViewModel.kt | 12 + .../ru/myitschool/work/BluetoothScanner.kt | 126 ++++++ .../kotlin/ru/myitschool/work/MainActivity.kt | 389 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 88 ++++ build.gradle.kts | 1 + 7 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 app/src/main/kotlin/ru/myitschool/work/ActivityViewModel.kt create mode 100644 app/src/main/kotlin/ru/myitschool/work/BluetoothScanner.kt create mode 100644 app/src/main/kotlin/ru/myitschool/work/MainActivity.kt create mode 100644 app/src/main/res/layout/activity_main.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ab9e22..906a5f7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { androidApplication + kotlinAndroid } val packageName = "ru.myitschool.work" @@ -18,14 +19,18 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - buildFeatures.viewBinding = true + buildFeatures.dataBinding = true compileOptions { sourceCompatibility = Version.Kotlin.javaSource targetCompatibility = Version.Kotlin.javaSource } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { + implementation("androidx.core:core-ktx:1.7.0") defaultLibrary() } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ee5c40..c09c2d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,35 @@ + + + + + + + + + + + + + + + + + + + + + + + tools:targetApi="31" > + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/ru/myitschool/work/ActivityViewModel.kt b/app/src/main/kotlin/ru/myitschool/work/ActivityViewModel.kt new file mode 100644 index 0000000..0ac40d0 --- /dev/null +++ b/app/src/main/kotlin/ru/myitschool/work/ActivityViewModel.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work + +import android.bluetooth.BluetoothDevice +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ActivityViewModel( + +): ViewModel() { + val foundDevice = MutableLiveData(); + val statemotors = MutableLiveData("OFF"); +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/myitschool/work/BluetoothScanner.kt b/app/src/main/kotlin/ru/myitschool/work/BluetoothScanner.kt new file mode 100644 index 0000000..4ac2637 --- /dev/null +++ b/app/src/main/kotlin/ru/myitschool/work/BluetoothScanner.kt @@ -0,0 +1,126 @@ +package ru.myitschool.work + +import android.Manifest +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.widget.Toast +import androidx.core.app.ActivityCompat + +class BluetoothScanner(activityViewModel: ActivityViewModel, private val context: Context) { + private val mBluetoothAdapter: BluetoothAdapter + private val mBluetoothLeScanner: BluetoothLeScanner + private var mScanCallback: ScanCallback? = null + private val mHandler: Handler + private var mScanning = false + private var bluetoothDevice: BluetoothDevice? = null + private val activityViewModel: ActivityViewModel + + init { + mHandler = Handler(Looper.getMainLooper()) + val bluetoothManager = + context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + + } + mBluetoothAdapter = bluetoothManager.adapter + mBluetoothAdapter.startDiscovery() + mBluetoothLeScanner = mBluetoothAdapter.bluetoothLeScanner + this.activityViewModel = activityViewModel + } + + fun createScanSettings():ScanSettings{ + TODO()//Реализовать метод создания ScanSetting с настройками Балансированного режима сканирования и немедленной показе результатов сканирования + return ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) + .setReportDelay(0) + .build() + } + fun startScan(filters: ArrayList) { + if (!mScanning) { + Log.i(TAG, "Starting scan...") + val settings = createScanSettings() + mScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + super.onScanResult(callbackType, result) + val device = result.device + println("RESULT") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + Log.i(TAG, "Found device: " + device.name + " (" + device.address + ")") + bluetoothDevice = device + activityViewModel.foundDevice.value = device + } + + override fun onBatchScanResults(results: List) { + super.onBatchScanResults(results) + for (result in results) { + val device = result.device + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + Log.i(TAG, "Found device: " + device.name + " (" + device.address + ")") + } + } + + override fun onScanFailed(errorCode: Int) { + super.onScanFailed(errorCode) + Log.e( + TAG, + "Scan failed with error code: $errorCode" + ) + Toast.makeText(context, "Scan failed with error code: ", Toast.LENGTH_SHORT) + .show() + } + } + mBluetoothLeScanner.startScan(filters, settings, mScanCallback) + mHandler.postDelayed({ stopScan() }, SCAN_PERIOD) + mScanning = true + } else { + Log.i(TAG, "Scan already in progress") + Toast.makeText(context, "Scan already in progress", Toast.LENGTH_SHORT).show() + } + } + + fun stopScan() { + if (mScanning) { + Log.i(TAG, "Stopping scan...") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + context, Manifest.permission.BLUETOOTH_SCAN + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + mBluetoothLeScanner.stopScan(mScanCallback) + mScanning = false + } else { + Log.i(TAG, "No scan in progress") + } + } + + companion object { + private val TAG = BluetoothScanner::class.java.simpleName + private const val SCAN_PERIOD: Long = 4000 // 4 seconds + } +} diff --git a/app/src/main/kotlin/ru/myitschool/work/MainActivity.kt b/app/src/main/kotlin/ru/myitschool/work/MainActivity.kt new file mode 100644 index 0000000..48e172e --- /dev/null +++ b/app/src/main/kotlin/ru/myitschool/work/MainActivity.kt @@ -0,0 +1,389 @@ +package ru.myitschool.work + +import android.Manifest +import android.bluetooth.* +import android.bluetooth.BluetoothDevice.* +import android.bluetooth.le.ScanFilter +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider + +import kotlinx.coroutines.* +import ru.myitschool.work.databinding.ActivityMainBinding +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer +import kotlin.collections.ArrayList + + +class MainActivity : AppCompatActivity() { + private val BLP_SERVICE_UUID: UUID? = UUID.fromString("e7112e6c-c396-11ed-afa1-0242ac120002");//e7112e6c-c396-11ed-afa1-0242ac120002 + private val names:Array? = arrayOf("RDB-1") + private val mac:Array? = arrayOf("0C:B8:15:F6:0D:52") + + + val TAG ="CONTROLLER" + + + private lateinit var bluetoothGattCallback: BluetoothGattCallback + private lateinit var bluetoothScanner: BluetoothScanner + private lateinit var activityViewModel: ActivityViewModel + + private lateinit var service:BluetoothGattService; + + + companion object{ + private var status:Int=-1; + private var newState:Int=-1; + private lateinit var gatt:BluetoothGatt; + private var isOffMotors=false; + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + activityViewModel = ViewModelProvider(this).get( + ActivityViewModel::class.java + ) + val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) + + binding.vm =activityViewModel; + binding.setLifecycleOwner(this); + setContentView(binding.root) + + if (allPermissionGranted()) { + + } else { + askPermissions() + } + + bluetoothScanner = BluetoothScanner(activityViewModel,this); + + val scanningButton = binding.Scan + scanningButton.setOnClickListener(View.OnClickListener { + Scanning() + }) + val connectButton = binding.Connect + connectButton.setOnClickListener(View.OnClickListener { + Connect() + }) + val OnOffMotorsButton = binding.OnOffMotors + OnOffMotorsButton.setOnClickListener(View.OnClickListener { + OnOffMotors() + }) + val position1Button = binding.Position1 + position1Button.setOnClickListener(View.OnClickListener { + Position1() + }) + val position2Button = binding.Position2 + position2Button.setOnClickListener(View.OnClickListener { + Position2() + }) + + + bluetoothGattCallback = object : BluetoothGattCallback(){ + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + Companion.newState = newState + Companion.status = status + if(newState==BluetoothProfile.STATE_CONNECTED) + Companion.gatt = gatt!! + + } + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + + for(ser in gatt!!.services){ + println(" "+ ser.uuid+" "+ ser.characteristics.toString()+" "+ser.type+" "+ser.includedServices) + } + + service = gatt!!.getService(BLP_SERVICE_UUID) + for(chara in service.characteristics) { + println(TAG + " " + chara.uuid + " " + chara.properties + " " + chara.permissions + " ") + } + val characteristic0 = service.getCharacteristic(service.characteristics[0].uuid) + Log.d(TAG,characteristic0.toString()) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + return + } + + CoroutineScope(Dispatchers.Main).launch { + activityViewModel.statemotors.value = "Calibration" + } + gatt.readCharacteristic(service.characteristics[0]) + + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, status) + + if (status === BluetoothGatt.GATT_SUCCESS) { + // Получаем данные характеристики + var value = characteristic!!.value.clone() + + if(value[0]==(0).toByte() && value[1]==(0).toByte()) {//Если все двигатели выключены + isOffMotors = true; + //и включаем двигатели + value = byteArrayOf(0b11111111.toByte(), 0b00001111.toByte()) + val characteristic0 = service.characteristics[0]; + characteristic0.value = value!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + this@MainActivity, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + gatt!!.writeCharacteristic(characteristic0) // перезаписываем состояния двигателей чтобы их включить + + + }else{ + CoroutineScope(Dispatchers.Main).launch { + Toast.makeText(this@MainActivity, "Двигатели уже включены!", Toast.LENGTH_SHORT).show() + activityViewModel.statemotors.value = "ON" + } + } + // Делаем что-то с полученными данными + } else { + // Чтение характеристики не удалось + } + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + + if (status === BluetoothGatt.GATT_SUCCESS) { + if (characteristic == service.characteristics[0]){// данная характиристика отвечает за состояния вкл/выкл двигателя + + CoroutineScope(Dispatchers.Main).launch { + activityViewModel.statemotors.value = "ON" + } + } else if (characteristic == service.characteristics[2]) { // данная характиристика отвечает за положение + + println("VALUE "+Arrays.toString(characteristic!!.value)) + val floats = FloatArray(characteristic.value.size/4); + var i = 0 + while (i*4 < characteristic.value.size) { + val myFloat = ByteBuffer.wrap(characteristic.value).getFloat(i*4) + floats.set(i,myFloat) + i += 1 + } + println("FLOAT "+Arrays.toString(floats)) + } + } + + + } + } + + } + + override fun onStart() { + super.onStart() + if (allPermissionGranted()) { + bluetoothScanner = BluetoothScanner(activityViewModel,this); + } else { + askPermissions() + } + } + + fun createScanFilter(name:String):ScanFilter{ + TODO()//Реализовать createScanFilter - Создание scan filter по имени + return ScanFilter.Builder() + .setDeviceName(name) + .build() + } + fun Scanning(){ + println("Scanning") + var filters: ArrayList = ArrayList(); + + if (names != null) { + for (name in names) { + println(name) + val filter = createScanFilter(name) + filters.add(filter) + } + } + bluetoothScanner.startScan(filters) + + } + + + + + fun Connect(){ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED + ) { + return + } + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + pairDevice(activityViewModel.foundDevice.value) + } + + if(activityViewModel.foundDevice.value!=null) { + val gatt: BluetoothGatt = activityViewModel.foundDevice.value!! + .connectGatt(this, false, bluetoothGattCallback, TRANSPORT_AUTO) + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + gatt.readPhy() + } + } + + } + fun OnOffMotors(){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this@MainActivity,Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + } + if(status == BluetoothGatt.GATT_SUCCESS) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + // Мы подключились, можно запускать обнаружение сервисов + gatt?.discoverServices(); + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + // Мы успешно отключились (контролируемое отключение) + gatt?.close(); + } else { + // мы или подключаемся или отключаемся, просто игнорируем эти статусы + } + } else { + // Произошла ошибка... разбираемся, что случилось! + gatt?.close(); + } + } + + fun floatAAToByteArray(floatAA: ArrayList>):ByteArray{ + TODO()//Реализовать метод перевода из массива вещественных чисел к массиву байтов + var value :ByteArray? = null + value = ByteArray(floatAA.size*floatAA[0].size*Float.SIZE_BYTES) + val buffer = ByteBuffer.wrap(value) + for (i in floatAA.indices){ + for(j in floatAA[0].indices){ + buffer.putFloat(floatAA[i][j]) + } + } + return value + } + + fun Position1(){ + val pos1 = arrayListOf>(arrayListOf(0.8f, 12f, 0f, 10f, 0f )) + val characteristic = service.getCharacteristic(service.characteristics[2].uuid) + Log.d(TAG,characteristic.uuid.toString()) + var value :ByteArray? = floatAAToByteArray(pos1) + + characteristic.value = value!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + this@MainActivity, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + gatt!!.writeCharacteristic(characteristic) + + } + fun Position2(){ + TODO()//Реализать метод Position2 для передачи массива вещественных чисел на (третью характеристику) [0.3, 2, 4.5, 2, 0.4] + val pos1 = arrayListOf>(arrayListOf(0.3f, 2f, 4.5f, 2f, 0.4f )) + val characteristic = service.getCharacteristic(service.characteristics[2].uuid) + Log.d(TAG,characteristic.uuid.toString()) + var value :ByteArray? = floatAAToByteArray(pos1) + + characteristic.value = value!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + this@MainActivity, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + gatt!!.writeCharacteristic(characteristic) + } + + fun createPairingIntent(device:BluetoothDevice?):Intent{ + TODO()//метод должен отправить intent для запроса присоединения к устройству с вводом пин кода + val intent = Intent(BluetoothDevice.ACTION_PAIRING_REQUEST) + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device) + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PIN) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + return intent + } + + + fun pairDevice(device: BluetoothDevice?) { + val intent = createPairingIntent(device) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( + this, + Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) { + + return + } + + this.startActivity(intent) + } + + + private val PERMISSIONS = arrayOf(Manifest.permission.BLUETOOTH_SCAN,Manifest.permission.BLUETOOTH_CONNECT) + + private val requestLauncher = registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions: Map -> + val permissionGranted = + AtomicBoolean(false) + permissions.forEach { (key: String, value: Boolean) -> + if (Arrays.asList(*PERMISSIONS).contains(key) && !value) { + Toast.makeText(this, "$key $value", Toast.LENGTH_SHORT).show() + permissionGranted.set(false) + } + } + if (!permissionGranted.get()) { + Toast.makeText( + this, + "Permission request denied", + Toast.LENGTH_SHORT + ).show() + } else { + //do something + } + } + + private fun askPermissions() { + requestLauncher.launch(PERMISSIONS) + } + + fun allPermissionGranted(): Boolean { + val granted = AtomicBoolean(true) + Arrays.asList(*PERMISSIONS).forEach(Consumer { it: String? -> + if (ContextCompat.checkSelfPermission( + this, + it!! + ) == PackageManager.PERMISSION_DENIED + ) { + granted.set(false) + } + }) + return granted.get() + } +} \ 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 new file mode 100644 index 0000000..03f0040 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 860f2c8..abbfbf5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { androidApplication version Version.gradle apply false kotlinJvm version Version.Kotlin.language apply false + kotlinAndroid version Version.Kotlin.language apply false } \ No newline at end of file