practise BLE yes

This commit is contained in:
MrApple100 2024-08-20 19:15:25 +03:00
parent 3e6fe310e0
commit a9564bb441
7 changed files with 667 additions and 2 deletions

View File

@ -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()
}

View File

@ -2,6 +2,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30"/>
<!-- Needed only if your app looks for Bluetooth devices.
If your app doesn't use Bluetooth scan results to derive physical
location information, you can strongly assert that your app
doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- Needed only if your app makes the device discoverable to Bluetooth
devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Needed only if your app communicates with already-paired Bluetooth
devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Needed only if your app uses Bluetooth scan results to derive physical location. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -11,6 +40,21 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Default"
tools:targetApi="31" />
tools:targetApi="31" >
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>

View File

@ -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<BluetoothDevice>();
val statemotors = MutableLiveData<String>("OFF");
}

View File

@ -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<ScanFilter>) {
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<ScanResult>) {
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
}
}

View File

@ -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<String>? = arrayOf("RDB-1")
private val mac:Array<String>? = 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<ScanFilter> = 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<ArrayList<Float>>):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<kotlin.collections.ArrayList<Float>>(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<kotlin.collections.ArrayList<Float>>(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<Array<String>, Map<String, Boolean>>(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions: Map<String, Boolean> ->
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()
}
}

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="vm"
type="ru.myitschool.work.ActivityViewModel" />
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/Scan"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="Scan"
></Button>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:hint="DEVICE"
android:text="@{vm.foundDevice.name}"
android:gravity="center">
</TextView>
</LinearLayout>
<Button
android:id="@+id/Connect"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="Connect"
></Button>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/OnOffMotors"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:text="OnOffMotors"
></Button>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:hint="state motors"
android:text="@{vm.statemotors}"
android:gravity="center">
</TextView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
>
<Button
android:id="@+id/Position1"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:text="Position1"
></Button>
<Button
android:id="@+id/Position2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:text="Position2"
></Button>
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -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
}