diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0fc3113..4515aa3 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 67a7214..120ff06 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
- id("kotlin-parcelize")
+ id("com.google.devtools.ksp").version("1.6.10-1.0.4")
}
android {
@@ -63,7 +63,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
implementation("androidx.navigation:navigation-compose:2.6.0")
- implementation("com.google.code.gson:gson:2.10.1")
+ implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
index 9fec070..66d511b 100644
Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ
diff --git a/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt b/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt
index 19b214a..3f8ce69 100644
--- a/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt
+++ b/app/src/main/java/com/helible/pilot/BluetoothDataTransferService.kt
@@ -12,14 +12,14 @@ import java.io.IOException
class TransferFailedException : IOException("Reading incoming data failed")
class BluetoothDataTransferService(
- private val socket: BluetoothSocket
+ private val socket: BluetoothSocket,
) {
fun listenForIncomingMessages(): Flow {
return flow {
- if(!socket.isConnected)
+ if (!socket.isConnected)
return@flow
val buffer = ByteArray(128)
- while(true) {
+ while (true) {
val byteCount: Int = try {
socket.inputStream.read(buffer)
} catch (e: IOException) {
diff --git a/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt b/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt
deleted file mode 100644
index f281c8d..0000000
--- a/app/src/main/java/com/helible/pilot/BluetoothViewModel.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.helible.pilot
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-
-class BluetoothViewModel(
- private val bluetoothController: BluetoothController
-) : ViewModel() {
-
- private val _selectedDevice: MutableStateFlow = MutableStateFlow(null)
- val selectedDevice: StateFlow
- get () = _selectedDevice.asStateFlow()
-
- private val _state: MutableStateFlow = MutableStateFlow(BluetoothUiState())
- val state: StateFlow = combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state)
- { scannedDevices, pairedDevices, state ->
- state.copy(
- scannedDevices = scannedDevices.toList(),
- pairedDevices = pairedDevices
- )
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
-
- init {
- bluetoothController.isConnected.onEach {
- isConnected -> _state.update { it.copy(isConnected = isConnected) }
- }.launchIn(viewModelScope)
- bluetoothController.errors.onEach { error ->
- _state.update {
- it.copy(errorMessage = error)
- }
- }.launchIn(viewModelScope)
- bluetoothController.isScanning.onEach { result ->
- _state.update {it.copy(
- isDiscovering = result,
- )}
- }.launchIn(viewModelScope)
- bluetoothController.isEnabled.onEach { result ->
- _state.update {it.copy(
- isEnabled = result,
- )}
- }.launchIn(viewModelScope)
- bluetoothController.isLocationEnabled.onEach { result ->
- _state.update { it.copy(
- isLocationEnabled = result
- ) }
- }.launchIn(viewModelScope)
- }
-
- private fun Flow.listen(): Job {
- return onEach { result ->
- when(result) {
- ConnectionResult.ConnectionEstablished -> {
- _state.update {
- it.copy(
- isConnected = true,
- isConnecting = false,
- errorMessage = null
- )
- }
- }
- is ConnectionResult.TransferSucceded -> {
- TODO("Telemetry not implemented")
- }
- is ConnectionResult.Error -> {
- _state.update { it.copy(
- isConnected = false,
- isConnecting = false,
- errorMessage = result.message
- ) }
- }
- }
- }
- .catch { _ ->
- bluetoothController.closeConnection()
- _state.update {
- it.copy(
- isConnected = false,
- isConnecting = false
- )
- }
- }
- .launchIn(viewModelScope)
- }
- private var deviceConnectionJob: Job? = null
-
- fun connectToDevice(device: Device) {
- _state.update {it.copy(isConnecting = true)}
- deviceConnectionJob = bluetoothController
- .connectToDevice(device)
- .listen()
- }
-
- fun disconnectFromDevice() {
- deviceConnectionJob?.cancel()
- bluetoothController.closeConnection()
- _state.update {
- it.copy(
- isConnecting = false,
- isConnected = false
- )
- }
- }
-
- fun selectDevice(selectedDevice: Device?) {
- _selectedDevice.update { selectedDevice }
- }
-
- fun onDestroy() {
- cancelScan()
- bluetoothController.onDestroy()
- }
-
- fun startScan(){
- selectDevice(null)
- bluetoothController.startDiscovery()
- }
-
- fun cancelScan() {
- bluetoothController.cancelDiscovery()
- }
-
- fun sendMessage(message: KMessage) {
- viewModelScope.launch {
- bluetoothController.trySendMessage(message)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/Kproto.kt b/app/src/main/java/com/helible/pilot/Kproto.kt
index a4634c4..aec2803 100644
--- a/app/src/main/java/com/helible/pilot/Kproto.kt
+++ b/app/src/main/java/com/helible/pilot/Kproto.kt
@@ -8,7 +8,7 @@ data class KMessage(
val r2: UShort,
val r3: UShort,
val emergStop: Boolean,
- val alarm: Boolean
+ val alarm: Boolean,
)
fun KMessage.toByteArray(): ByteArray {
diff --git a/app/src/main/java/com/helible/pilot/MainActivity.kt b/app/src/main/java/com/helible/pilot/MainActivity.kt
index 64a2b4a..c9c9aa2 100644
--- a/app/src/main/java/com/helible/pilot/MainActivity.kt
+++ b/app/src/main/java/com/helible/pilot/MainActivity.kt
@@ -1,105 +1,73 @@
package com.helible.pilot
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
-import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
-import androidx.navigation.NavType
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
import com.helible.pilot.components.BluetoothScannerScreen
-import com.helible.pilot.components.PreferencesCacheImpl
-import com.helible.pilot.components.SavedPreferences
+import com.helible.pilot.components.FlightControlScreen
+import com.helible.pilot.components.AppPreferences
+import com.helible.pilot.components.SavedPreferencesImpl
+import com.helible.pilot.permissions.PermissionsLauncher
+import com.helible.pilot.permissions.PermissionsRequest
+import com.helible.pilot.permissions.RequestHardwareFeatures
import com.helible.pilot.ui.theme.TestblueTheme
+import com.helible.pilot.viewmodels.BluetoothViewModel
+import com.helible.pilot.viewmodels.BluetoothViewModelFactory
+import com.helible.pilot.viewmodels.PermissionDialogViewModel
+import com.helible.pilot.viewmodels.PreferencesViewModel
class MainActivity : ComponentActivity() {
- // TODO: replace field bluetoothDevice in Device to deviceAddress field
- // TODO: share selected device via PersistentViewModel
- // TODO: add stub instead of the DevicesList, if there aren't nearby devices
- // TODO: add Bluetooth data transfer...
- // TODO: add text strings to resource
+ // TODO: device screen logic
+ // TODO: add Bluetooth telemetry...
+ // TODO: move text strings to resources
- private val bluetoothViewModel by lazy {
- BluetoothViewModel(AndroidBluetoothController(applicationContext))
- }
-
- private var permissionsViewModel = PermissionDialogViewModel()
-
- private val permissionsToRequest: Array by lazy {
- if (Build.VERSION.SDK_INT <= 30) {
- arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION
- )
- } else {
- arrayOf(
- Manifest.permission.BLUETOOTH_SCAN,
- Manifest.permission.BLUETOOTH_CONNECT
- )
- }
- }
-
- private val preferencesCache by lazy {
- PreferencesCacheImpl(getSharedPreferences(packageName, Context.MODE_PRIVATE))
+ private val preferences by lazy {
+ SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
}
private val preferencesViewModel by lazy {
- PersistentViewModel(preferencesCache)
+ PreferencesViewModel(preferences)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
setContent {
- val permissionLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.RequestMultiplePermissions(),
- onResult = { perms ->
- permissionsToRequest.forEach { permission ->
- permissionsViewModel.onPermissionResult(
- permission = permission,
- isGranted = perms[permission] == true
- )
- }
- if(hasAllPermissions() && !bluetoothViewModel.state.value.isDiscovering)
- bluetoothViewModel.startScan()
- }
+ val bluetoothViewModel =
+ viewModel(factory = BluetoothViewModelFactory(applicationContext))
+
+ val permissionsViewModel = viewModel()
+ val permissionLauncher = PermissionsLauncher()
+ permissionLauncher.setup(
+ onPermissionResult = { perm, isGranted ->
+ permissionsViewModel.onPermissionResult(perm, isGranted)
+ },
+ onGranted = { bluetoothViewModel.startScan() }
)
val bluetoothState by bluetoothViewModel.state.collectAsState()
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
LaunchedEffect(key1 = null) {
- permissionLauncher.launch(permissionsToRequest)
+ permissionLauncher.launch()
}
LaunchedEffect(key1 = bluetoothState) {
if (bluetoothState.isConnected) {
- Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_LONG)
+ Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_SHORT)
.show()
- bluetoothViewModel.sendMessage(KMessage(1u,2u,3u,false, false))
}
}
@@ -108,7 +76,6 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(key1 = bluetoothState.errorMessage) {
bluetoothState.errorMessage?.let { message ->
Toast.makeText(applicationContext, "Ошибка: $message", Toast.LENGTH_LONG).show()
- navController.navigate("scanner")
}
}
@@ -118,7 +85,7 @@ class MainActivity : ComponentActivity() {
dismissCurrentDialog = { permissionsViewModel.dismissDialog() },
visiblePermissionDialogQueue = permissionsViewModel.visiblePermissionDialogQueue,
activity = this,
- permissionLauncher = permissionLauncher
+ permissionLaunch = { perms -> permissionLauncher.launch(perms) }
)
RequestHardwareFeatures(
@@ -126,23 +93,31 @@ class MainActivity : ComponentActivity() {
bluetoothUiState = bluetoothState
)
- NavHost(navController = navController, startDestination = "scanner") {
+ NavHost(
+ navController = navController,
+ startDestination = "device"
+ ) {
composable("scanner") {
BluetoothScannerScreen(
bluetoothState = bluetoothState,
selectedDevice = selectedDevice,
startScan = { bluetoothViewModel.startScan() },
cancelScan = { bluetoothViewModel.cancelScan() },
- choiceDevice = {device -> bluetoothViewModel.selectDevice(device)},
+ choiceDevice = { device -> bluetoothViewModel.selectDevice(device) },
onScreenChanged = {
bluetoothViewModel.cancelScan()
- val deviceAddress = selectedDevice?.bluetoothDevice?.address
- preferencesViewModel.savePreferences(
- SavedPreferences(
- deviceAddress
+ val device = selectedDevice
+ if (device == null) {
+ preferencesViewModel.clearPreferences()
+ } else {
+ preferencesViewModel.savePreferences(
+ AppPreferences(
+ deviceName = device.name,
+ deviceAddress = device.macAddress
+ )
)
- )
- navController.navigate("flight/$deviceAddress")
+ }
+ navController.navigate("device")
Log.i(
"ScanActivity",
"Preferences: ${preferencesViewModel.preferences}"
@@ -150,44 +125,28 @@ class MainActivity : ComponentActivity() {
}
)
}
- composable(
- "flight/{device_address}",
- arguments = listOf(navArgument("device_address"){type = NavType.StringType})
- ) {
- backstackEntry ->
- LaunchedEffect(Unit) {
- val device: Device? = selectedDevice
- if(device == null){
- navController.navigate("scanner")
- } else {
- bluetoothViewModel.connectToDevice(device)
- }
- }
- BackHandler {
- bluetoothViewModel.disconnectFromDevice()
- Log.i("FlightScreen", "Disconnected from device")
- navController.navigate("scanner")
- }
- when {
- bluetoothState.isConnecting -> {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- CircularProgressIndicator()
- Text(text = "Подключение...", textAlign = TextAlign.Center)
- }
- }
-
- else -> {
- Text(
- text = "Device name: ${backstackEntry.arguments?.getString("device_address")}",
- modifier = Modifier.fillMaxSize(),
- textAlign = TextAlign.Center
+ composable("device")
+ {
+ FlightControlScreen(
+ bluetoothUiState = bluetoothState,
+ getPreferences = { preferencesViewModel.preferences },
+ navigateToScanner = { navController.navigate("scanner") },
+ connectToDevice = { device ->
+ bluetoothViewModel.connectToDevice(
+ device
)
- }
- }
+ },
+ sendRotorsState = { message ->
+ bluetoothViewModel.sendRotorsDutySpeed(
+ message
+ )
+ },
+ disconnectFromDevice = { bluetoothViewModel.disconnectFromDevice() },
+ sendEmergStop = { bluetoothViewModel.sendEmergStop() },
+ sendAlarm = { message -> bluetoothViewModel.sendAlarmState(message) },
+ sendR3Duty = { duty -> bluetoothViewModel.sendR3Duty(duty) }
+ )
+ if (preferencesViewModel.preferences != null) BackHandler {}
}
}
}
@@ -195,33 +154,6 @@ class MainActivity : ComponentActivity() {
}
}
- override fun onDestroy() {
- super.onDestroy()
- bluetoothViewModel.onDestroy()
- }
-
-
- override fun onStart() {
- super.onStart()
- bluetoothViewModel.startScan()
- }
-
- override fun onStop() {
- super.onStop()
- if(!hasAllPermissions()) return
- bluetoothViewModel.cancelScan()
- bluetoothViewModel.selectDevice(null)
- }
-
- private fun hasAllPermissions(): Boolean {
- permissionsToRequest.forEach { perm ->
- if(checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED){
- return false
- }
- }
- return true
- }
-
}
diff --git a/app/src/main/java/com/helible/pilot/MainViewModel.kt b/app/src/main/java/com/helible/pilot/MainViewModel.kt
deleted file mode 100644
index 6a5cfe7..0000000
--- a/app/src/main/java/com/helible/pilot/MainViewModel.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.helible.pilot
-
-import android.bluetooth.BluetoothDevice
-import androidx.compose.runtime.mutableStateListOf
-import androidx.lifecycle.ViewModel
-import com.helible.pilot.components.SavedPreferences
-import com.helible.pilot.components.SavedPreferencesCache
-
-data class Device(
- val bluetoothDevice: BluetoothDevice,
- val rssi: Short,
- val isPaired: Boolean = false
-)
-
-class PermissionDialogViewModel: ViewModel() {
- val visiblePermissionDialogQueue = mutableStateListOf()
-
- fun dismissDialog() {
- visiblePermissionDialogQueue.removeFirst()
- }
-
- fun onPermissionResult(permission: String, isGranted: Boolean) {
- if(!isGranted && !visiblePermissionDialogQueue.contains(permission)){
- visiblePermissionDialogQueue.add(permission)
- }
- }
-}
-
-class PersistentViewModel(
- private val preferencesCache: SavedPreferencesCache,
-) : ViewModel() {
- val preferences get() = preferencesCache.getPreferences()
- fun savePreferences(savedPreferences: SavedPreferences) {
- preferencesCache.savePreferences(
- preferences = savedPreferences
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt
index 8b0f6e9..2de9511 100644
--- a/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt
+++ b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt
@@ -3,7 +3,6 @@ package com.helible.pilot.components
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -12,7 +11,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
@@ -23,20 +21,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
-import com.helible.pilot.BluetoothUiState
-import com.helible.pilot.Device
+import com.helible.pilot.dataclasses.BluetoothUiState
+import com.helible.pilot.dataclasses.BluetoothDevice
@SuppressLint("MissingPermission")
@Composable
fun BluetoothScannerScreen(
bluetoothState: BluetoothUiState,
- selectedDevice: Device?,
+ selectedDevice: BluetoothDevice?,
startScan: () -> Unit,
cancelScan: () -> Unit,
- choiceDevice: (device: Device?) -> Unit,
+ choiceDevice: (device: BluetoothDevice?) -> Unit,
onScreenChanged: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
diff --git a/app/src/main/java/com/helible/pilot/components/DeviceItem.kt b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt
index 4fe7f1f..531d630 100644
--- a/app/src/main/java/com/helible/pilot/components/DeviceItem.kt
+++ b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt
@@ -1,7 +1,6 @@
package com.helible.pilot.components
import android.annotation.SuppressLint
-import android.widget.TextView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -16,23 +15,20 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.text.HtmlCompat
-import com.helible.pilot.Device
+import com.helible.pilot.dataclasses.BluetoothDevice
import com.helible.pilot.R
@SuppressLint("MissingPermission")
@Composable
fun DeviceItem(
- deviceInfo: Device,
- selectedDevice: Device?,
- choiceDevice: (device: Device?) -> Unit,
+ deviceInfo: BluetoothDevice,
+ selectedDevice: BluetoothDevice?,
+ choiceDevice: (device: BluetoothDevice?) -> Unit,
modifier: Modifier,
) {
ElevatedCard(
@@ -40,7 +36,7 @@ fun DeviceItem(
choiceDevice(deviceInfo)
},
colors = CardDefaults.elevatedCardColors(
- containerColor = if (deviceInfo.bluetoothDevice == selectedDevice?.bluetoothDevice)
+ containerColor = if (deviceInfo == selectedDevice)
MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surface
)
@@ -48,16 +44,16 @@ fun DeviceItem(
Row(modifier = Modifier.padding(8.dp)) {
Column(verticalArrangement = Arrangement.Center) {
Text(
- text = deviceInfo.bluetoothDevice.name,
+ text = deviceInfo.name,
fontWeight = FontWeight.Bold,
softWrap = true
)
Text(
- text = "MAC: ${deviceInfo.bluetoothDevice.address}",
+ text = "MAC: ${deviceInfo.macAddress}",
fontWeight = FontWeight.Thin
)
}
- if (!deviceInfo.isPaired) {
+ if (deviceInfo.isScanned) {
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
Icon(
painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)),
diff --git a/app/src/main/java/com/helible/pilot/components/DeviceScreen.kt b/app/src/main/java/com/helible/pilot/components/DeviceScreen.kt
new file mode 100644
index 0000000..89cd545
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/DeviceScreen.kt
@@ -0,0 +1,25 @@
+package com.helible.pilot.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DeviceScreen() {
+ Scaffold(
+ topBar = {
+ Text(text = "")
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier.padding(innerPadding)
+ ) {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt
index b583132..4ec4592 100644
--- a/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt
+++ b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt
@@ -14,15 +14,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.helible.pilot.BluetoothUiState
-import com.helible.pilot.Device
+import com.helible.pilot.dataclasses.BluetoothUiState
+import com.helible.pilot.dataclasses.BluetoothDevice
@Composable
fun DiscoveredDevicesList(
bluetoothState: BluetoothUiState,
- selectedDevice: Device?,
- choiceDevice: (device: Device?) -> Unit,
- modifier: Modifier = Modifier
+ selectedDevice: BluetoothDevice?,
+ choiceDevice: (device: BluetoothDevice?) -> Unit,
+ modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
@@ -33,7 +33,7 @@ fun DiscoveredDevicesList(
modifier = Modifier.padding(10.dp)
)
}
- items(bluetoothState.pairedDevices) { device ->
+ items(bluetoothState.pairedBluetoothDevices) { device ->
DeviceItem(
deviceInfo = device,
selectedDevice = selectedDevice,
@@ -45,7 +45,7 @@ fun DiscoveredDevicesList(
)
)
}
- if(bluetoothState.pairedDevices.isEmpty()){
+ if (bluetoothState.pairedBluetoothDevices.isEmpty()) {
item {
Text(
text = "Нет элементов для отображения",
@@ -64,7 +64,7 @@ fun DiscoveredDevicesList(
)
}
- items(bluetoothState.scannedDevices) { device ->
+ items(bluetoothState.scannedBluetoothDevices) { device ->
DeviceItem(
deviceInfo = device,
selectedDevice = selectedDevice,
@@ -76,12 +76,15 @@ fun DiscoveredDevicesList(
)
)
}
- if(bluetoothState.scannedDevices.isEmpty()) {
- if(bluetoothState.isDiscovering) {
+ if (bluetoothState.scannedBluetoothDevices.isEmpty()) {
+ if (bluetoothState.isDiscovering) {
item {
- Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
CircularProgressIndicator()
- Text(text = "Поиск устройств", modifier=Modifier.padding(10.dp))
+ Text(text = "Поиск устройств", modifier = Modifier.padding(10.dp))
}
}
} else {
diff --git a/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt b/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt
index 03af6c6..176d536 100644
--- a/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt
+++ b/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt
@@ -16,14 +16,14 @@ fun RequiredHardwareFeatures(
confirmButtonText: String,
featureState: Boolean,
requestFeature: () -> Unit,
- onDismissRequest: () -> Unit
+ onDismissRequest: () -> Unit,
) {
if (!featureState) {
AlertDialog(
confirmButton = {
Divider()
TextButton(onClick = requestFeature, modifier = Modifier.fillMaxWidth()) {
- Text(text=confirmButtonText)
+ Text(text = confirmButtonText)
}
},
onDismissRequest = onDismissRequest,
@@ -32,7 +32,7 @@ fun RequiredHardwareFeatures(
text = description
)
},
- title = { Text(text = title)}
+ title = { Text(text = title) }
)
}
}
diff --git a/app/src/main/java/com/helible/pilot/components/FlightControlScreen.kt b/app/src/main/java/com/helible/pilot/components/FlightControlScreen.kt
new file mode 100644
index 0000000..77c786e
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/FlightControlScreen.kt
@@ -0,0 +1,130 @@
+package com.helible.pilot.components
+
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.helible.pilot.dataclasses.AlarmStateMessage
+import com.helible.pilot.dataclasses.BluetoothUiState
+import com.helible.pilot.dataclasses.RotorsSpeedMessage
+import kotlin.math.roundToInt
+
+
+@Composable
+fun FlightControlScreen(
+ bluetoothUiState: BluetoothUiState,
+ getPreferences: () -> AppPreferences?,
+ navigateToScanner: () -> Unit,
+ connectToDevice: (String) -> Unit,
+ disconnectFromDevice: () -> Unit,
+ sendRotorsState: (RotorsSpeedMessage) -> Unit,
+ sendAlarm: (AlarmStateMessage) -> Unit,
+ sendEmergStop: () -> Unit,
+ sendR3Duty: (Int) -> Unit,
+) {
+ LaunchedEffect(Unit) {
+ val preferences: AppPreferences? = getPreferences()
+ if (preferences == null) {
+ navigateToScanner()
+ } else {
+ connectToDevice(preferences.deviceAddress)
+ }
+ }
+
+ var rotor1Duty by remember { mutableStateOf(0f) }
+ var rotor2Duty by remember { mutableStateOf(0f) }
+ var rotor3Duty by remember { mutableStateOf(0f) }
+
+ BackHandler {
+ disconnectFromDevice()
+ Log.i("FlightScreen", "Disconnected from the device")
+ navigateToScanner()
+ }
+ when {
+ bluetoothUiState.isConnecting -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ CircularProgressIndicator()
+ Text(text = "Подключение...", textAlign = TextAlign.Center)
+ }
+ }
+
+ else -> {
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ Text(
+ text = "Device name: ${getPreferences()?.deviceName ?: "(устройство отключено)"}",
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center
+ )
+ Text(text = "Rotor 1 value: $rotor1Duty", textAlign = TextAlign.Center)
+ Slider(
+ value = rotor1Duty,
+ onValueChange = { rotor1Duty = it.roundToInt().toFloat() },
+ valueRange = 0f..1000f,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ )
+ Text(text = "Rotor 2 value: $rotor2Duty", textAlign = TextAlign.Center)
+ Slider(
+ value = rotor2Duty,
+ onValueChange = { rotor2Duty = it.roundToInt().toFloat() },
+ valueRange = 0f..1000f,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ )
+ Text(text = "Rotor 3 value: $rotor1Duty", textAlign = TextAlign.Center)
+ Slider(
+ value = rotor3Duty,
+ onValueChange = { rotor3Duty = it.roundToInt().toFloat() },
+ valueRange = 0f..1000f,
+
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ )
+ FilledIconButton(
+ onClick = { sendEmergStop() },
+ modifier = Modifier.padding(10.dp)
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ contentDescription = null,
+ modifier = Modifier.padding(3.dp)
+ )
+ Text(
+ text = "СТОП",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(3.dp)
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt b/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt
index dce70db..cac8857 100644
--- a/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt
+++ b/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt
@@ -23,7 +23,7 @@ fun PermissionDialog(
onOkClick: () -> Unit,
onContinueClick: () -> Unit,
onGoToAppSettingsClick: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
AlertDialog(
onDismissRequest = onDismiss,
@@ -39,39 +39,44 @@ fun PermissionDialog(
} else {
"OK"
},
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center,
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- if (isPermanentDeclined) {
- onGoToAppSettingsClick()
- } else {
- onOkClick()
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ if (isPermanentDeclined) {
+ onGoToAppSettingsClick()
+ } else {
+ onOkClick()
+ }
}
- }
- .padding(16.dp)
+ .padding(16.dp)
)
}
},
- dismissButton = {if(isPermanentDeclined)
- Box(modifier=Modifier.fillMaxWidth()){
+ dismissButton = {
+ if (isPermanentDeclined)
+ Box(modifier = Modifier.fillMaxWidth()) {
Divider()
Text(
- text = "Снова проверить наличие разрешения",
- modifier = Modifier
- .fillMaxWidth()
- .padding(paddingValues = PaddingValues(top=10.dp))
- .clickable {onContinueClick()},
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center
- )}
+ text = "Снова проверить наличие разрешения",
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues = PaddingValues(top = 10.dp))
+ .clickable { onContinueClick() },
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
+ }
else
- Unit},
+ Unit
+ },
text = {
- Text(text = permissionTextProvider.getDescription(
- isPermanentDeclined = isPermanentDeclined
- ))
+ Text(
+ text = permissionTextProvider.getDescription(
+ isPermanentDeclined = isPermanentDeclined
+ )
+ )
},
modifier = modifier
)
@@ -83,21 +88,21 @@ interface PermissionTextProvider {
class LocationPermissionTextProvider : PermissionTextProvider {
override fun getDescription(isPermanentDeclined: Boolean): String {
- return if (isPermanentDeclined){
+ return if (isPermanentDeclined) {
"Похоже вы навсегда запретили приложению доступ к геолокации. " +
"Вы можете зайти в настройки, чтобы выдать это разрешение."
} else {
"Приложению необходимо разрешение для определения местоположения " +
- "для работы с Bluetooth на устройствах с Android 11 и ниже."
+ "для работы с Bluetooth на устройствах с Android 11 и ниже."
}
}
}
class BluetoothScanPermissionTextProvider : PermissionTextProvider {
override fun getDescription(isPermanentDeclined: Boolean): String {
- return if (isPermanentDeclined){
- "Похоже вы навсегда запретили приложению доступ к сканированию по Bluetooth. " +
- "Вы можете зайти в настройки, чтобы выдать это разрешение."
+ return if (isPermanentDeclined) {
+ "Похоже вы навсегда запретили приложению доступ к сканированию по Bluetooth. " +
+ "Вы можете зайти в настройки, чтобы выдать это разрешение."
} else {
"Приложению необходимо разрешение для к сканированию по Bluetooth " +
"для работы с Bluetooth на устройствах с Android 11 и ниже"
@@ -107,7 +112,7 @@ class BluetoothScanPermissionTextProvider : PermissionTextProvider {
class BluetoothConnectPermissionTextProvider : PermissionTextProvider {
override fun getDescription(isPermanentDeclined: Boolean): String {
- return if (isPermanentDeclined){
+ return if (isPermanentDeclined) {
"Похоже вы навсегда запретили приложению доступ к подключению по Bluetooth." +
"Вы можете зайти в настройки, чтобы выдать это разрешение."
} else {
@@ -119,7 +124,7 @@ class BluetoothConnectPermissionTextProvider : PermissionTextProvider {
class BluetoothAdminPermissionTextProvider : PermissionTextProvider {
override fun getDescription(isPermanentDeclined: Boolean): String {
- return if (isPermanentDeclined){
+ return if (isPermanentDeclined) {
"Похоже вы навсегда запретили приложению доступ к управлению настройками Bluetooth. " +
"Вы можете зайти в настройки, чтобы выдать это разрешение."
} else {
diff --git a/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt b/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt
index b37bb3e..c9c6f4c 100644
--- a/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt
+++ b/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt
@@ -1,37 +1,41 @@
package com.helible.pilot.components
import android.content.SharedPreferences
-import android.os.Parcelable
-import com.google.gson.Gson
-import kotlinx.parcelize.Parcelize
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+@JsonClass(generateAdapter = true)
+data class AppPreferences(
+ val deviceName: String,
+ val deviceAddress: String,
+)
-@Parcelize
-data class SavedPreferences(
- val deviceAddress: String?
-): Parcelable
-
-interface SavedPreferencesCache {
- fun getPreferences(): SavedPreferences?
- fun savePreferences(preferences: SavedPreferences)
+interface SavedPreferences {
+ fun getPreferences(): AppPreferences?
+ fun savePreferences(preferences: AppPreferences)
fun clearPreferences()
}
-class PreferencesCacheImpl(private val sharedPreferences: SharedPreferences) : SavedPreferencesCache {
- override fun getPreferences(): SavedPreferences? {
+class SavedPreferencesImpl(private val sharedPreferences: SharedPreferences) : SavedPreferences {
+ private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
+ private val preferencesAdapter: JsonAdapter =
+ moshi.adapter(AppPreferences::class.java)
+
+ override fun getPreferences(): AppPreferences? {
val json = sharedPreferences.getString("preferences", null) ?: return null
- return Gson().fromJson(json, SavedPreferences::class.java)
+ return preferencesAdapter.fromJson(json)
}
- override fun savePreferences(preferences: SavedPreferences)
- {
+
+ override fun savePreferences(preferences: AppPreferences) {
sharedPreferences.edit()
- .putString("preferences", Gson().toJson(preferences))
+ .putString("preferences", preferencesAdapter.toJson(preferences))
.apply()
}
- override fun clearPreferences()
- {
+ override fun clearPreferences() {
sharedPreferences.edit().remove("preferences").apply()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/BluetoothController.kt b/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt
similarity index 74%
rename from app/src/main/java/com/helible/pilot/BluetoothController.kt
rename to app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt
index bd0fdae..92bb7a9 100644
--- a/app/src/main/java/com/helible/pilot/BluetoothController.kt
+++ b/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt
@@ -1,4 +1,4 @@
-package com.helible.pilot
+package com.helible.pilot.controllers
import android.Manifest
import android.annotation.SuppressLint
@@ -14,6 +14,11 @@ import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
+import com.helible.pilot.BluetoothDataTransferService
+import com.helible.pilot.dataclasses.BluetoothDeviceDomain
+import com.helible.pilot.KMessage
+import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
+import com.helible.pilot.receivers.BluetoothStateReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -34,8 +39,8 @@ import java.io.IOException
import java.util.UUID
sealed interface ConnectionResult {
- object ConnectionEstablished: ConnectionResult
- data class TransferSucceded(val message: KMessage): ConnectionResult
+ object ConnectionEstablished : ConnectionResult
+ data class TransferSucceded(val message: KMessage) : ConnectionResult
data class Error(val message: String) : ConnectionResult
}
@@ -44,14 +49,14 @@ interface BluetoothController {
val isLocationEnabled: StateFlow
val isConnected: StateFlow
val isScanning: StateFlow
- val scannedDevices: StateFlow>
- val pairedDevices: StateFlow>
+ val scannedDevices: StateFlow>
+ val pairedDevices: StateFlow>
val errors: SharedFlow
fun startDiscovery()
fun cancelDiscovery()
- fun connectToDevice(device: Device?): Flow
- suspend fun trySendMessage(message: KMessage): KMessage?
+ fun connectToDevice(device: String): Flow
+ suspend fun trySendMessage(message: ByteArray): Boolean
fun closeConnection()
fun onDestroy()
}
@@ -92,12 +97,13 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
override val isLocationEnabled: StateFlow
get() = _isLocationEnabled.asStateFlow()
- private val _pairedDevices = MutableStateFlow>(emptyList())
- override val pairedDevices: StateFlow>
+ private val _pairedDevices = MutableStateFlow>(emptyList())
+ override val pairedDevices: StateFlow>
get() = _pairedDevices.asStateFlow()
- private val _scannedDevices: MutableStateFlow> = MutableStateFlow(emptyList())
- override val scannedDevices: StateFlow>
+ private val _scannedDevices: MutableStateFlow> =
+ MutableStateFlow(emptyList())
+ override val scannedDevices: StateFlow>
get() = _scannedDevices.asStateFlow()
private var currentClientSocket: BluetoothSocket? = null
@@ -113,7 +119,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
_isScanning.update { isDiscovering }
},
onLocationEnabledChanged = {
- if(locationManager?.isLocationEnabled == true){
+ if (locationManager?.isLocationEnabled == true) {
_isLocationEnabled.update { true }
} else {
_isLocationEnabled.update { false }
@@ -124,10 +130,11 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
@SuppressLint("MissingPermission")
private val bluetoothStateReceiver = BluetoothStateReceiver(
onDeviceFound = { device, rssi ->
- if(!hasAllPermissions()) return@BluetoothStateReceiver
- val newDevice = Device(device, rssi)
+ if (!hasAllPermissions()) return@BluetoothStateReceiver
+ val newDevice =
+ BluetoothDeviceDomain(device.name ?: "null", device.address, rssi, isScanned = true)
_scannedDevices.update { devices ->
- if(newDevice in devices) devices else devices + newDevice
+ if (newDevice in devices) devices else devices + newDevice
}
Log.i(
"ScanActivity",
@@ -135,7 +142,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
)
},
onConnectedStateChanged = { isConnected, device ->
- if(bluetoothAdapter?.bondedDevices?.contains(device) == true) {
+ if (bluetoothAdapter?.bondedDevices?.contains(device) == true) {
_isConnected.update { isConnected }
} else {
CoroutineScope(Dispatchers.IO).launch {
@@ -160,7 +167,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
- if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
}
}
@@ -178,43 +185,44 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
@SuppressLint("MissingPermission")
override fun startDiscovery() {
- if(!hasAllPermissions()) {
+ if (!hasAllPermissions()) {
Toast.makeText(context, "Ошибка: недостаточно разрешений", Toast.LENGTH_SHORT).show()
return
}
- if(!_isEnabled.value) {
+ if (!_isEnabled.value) {
return
}
- if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
- if(locationManager?.isLocationEnabled != true) return
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
+ if (locationManager?.isLocationEnabled != true) return
}
updatePairedDevices()
_scannedDevices.update { emptyList() }
- if(!bluetoothAdapter.isDiscovering) {
+ if (!bluetoothAdapter.isDiscovering) {
bluetoothAdapter.startDiscovery()
}
}
@SuppressLint("MissingPermission")
override fun cancelDiscovery() {
- if(!hasAllPermissions()) return
- if(bluetoothAdapter.isDiscovering){
+ if (!hasAllPermissions()) return
+ if (bluetoothAdapter.isDiscovering) {
bluetoothAdapter.cancelDiscovery()
}
}
@SuppressLint("MissingPermission")
- override fun connectToDevice(device: Device?): Flow {
- if(!hasAllPermissions()){
+ override fun connectToDevice(device: String): Flow {
+ if (!hasAllPermissions()) {
Toast.makeText(context, "Ошибка: нет разрешений", Toast.LENGTH_SHORT).show()
return flow {}
}
return flow {
- currentClientSocket = device?.bluetoothDevice?.createRfcommSocketToServiceRecord(
- UUID.fromString(SERVICE_UUID)
- )
+ currentClientSocket =
+ bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
+ UUID.fromString(SERVICE_UUID)
+ )
currentClientSocket?.let { socket ->
try {
socket.connect()
@@ -223,7 +231,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
dataTransferService = it
emitAll(
it.listenForIncomingMessages()
- .map {ConnectionResult.TransferSucceded(it)}
+ .map { ConnectionResult.TransferSucceded(it) }
)
}
} catch (e: IOException) {
@@ -234,15 +242,15 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
}
- override suspend fun trySendMessage(message: KMessage): KMessage? {
- if(!hasAllPermissions()){
- return null
+ override suspend fun trySendMessage(message: ByteArray): Boolean {
+ if (!hasAllPermissions()) {
+ return false
}
- if(dataTransferService == null) {
- return null
+ if (dataTransferService == null) {
+ return false
}
- dataTransferService?.sendMessage("R1250\n\r".toByteArray())
- return message
+ dataTransferService?.sendMessage(message)
+ return true
}
override fun closeConnection() {
@@ -258,11 +266,16 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
@SuppressLint("MissingPermission")
private fun updatePairedDevices() {
- if(!hasAllPermissions()) return
+ if (!hasAllPermissions()) return
Log.i("ScanActivity", "${bluetoothAdapter?.bondedDevices}")
bluetoothAdapter?.bondedDevices?.onEach { device ->
_pairedDevices.update {
- val currentDevice = Device(bluetoothDevice = device, rssi=0, isPaired = true)
+ val currentDevice = BluetoothDeviceDomain(
+ name = device.name ?: "null",
+ macAddress = device.address,
+ rssi = 0,
+ isScanned = false
+ )
if (currentDevice in pairedDevices.value) {
pairedDevices.value
} else {
@@ -284,7 +297,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
)
}
perms.forEach { perm ->
- if(context.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED){
+ if (context.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
return false
}
}
diff --git a/app/src/main/java/com/helible/pilot/dataclasses/BluetoothDevice.kt b/app/src/main/java/com/helible/pilot/dataclasses/BluetoothDevice.kt
new file mode 100644
index 0000000..e0c4b3a
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/dataclasses/BluetoothDevice.kt
@@ -0,0 +1,10 @@
+package com.helible.pilot.dataclasses
+
+typealias BluetoothDeviceDomain = BluetoothDevice
+
+data class BluetoothDevice(
+ val name: String,
+ val macAddress: String,
+ val rssi: Short,
+ val isScanned: Boolean = false,
+)
diff --git a/app/src/main/java/com/helible/pilot/BluetoothUiState.kt b/app/src/main/java/com/helible/pilot/dataclasses/BluetoothUiState.kt
similarity index 60%
rename from app/src/main/java/com/helible/pilot/BluetoothUiState.kt
rename to app/src/main/java/com/helible/pilot/dataclasses/BluetoothUiState.kt
index e254fc7..42d949c 100644
--- a/app/src/main/java/com/helible/pilot/BluetoothUiState.kt
+++ b/app/src/main/java/com/helible/pilot/dataclasses/BluetoothUiState.kt
@@ -1,6 +1,5 @@
-package com.helible.pilot
+package com.helible.pilot.dataclasses
-import android.bluetooth.BluetoothDevice
data class BluetoothUiState(
val isEnabled: Boolean = false,
val isLocationEnabled: Boolean = false,
@@ -8,6 +7,6 @@ data class BluetoothUiState(
val isConnected: Boolean = false,
val isConnecting: Boolean = false,
val errorMessage: String? = null,
- val scannedDevices: List = emptyList(),
- val pairedDevices: List = emptyList(),
+ val scannedBluetoothDevices: List = emptyList(),
+ val pairedBluetoothDevices: List = emptyList(),
)
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/dataclasses/MessageTypes.kt b/app/src/main/java/com/helible/pilot/dataclasses/MessageTypes.kt
new file mode 100644
index 0000000..58df9d3
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/dataclasses/MessageTypes.kt
@@ -0,0 +1,12 @@
+package com.helible.pilot.dataclasses
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class RotorsSpeedMessage(val r1: Short, val r2: Short, val r3: Short)
+
+@JsonClass(generateAdapter = true)
+data class EmergStopMessage(val emergStop: Boolean)
+
+@JsonClass(generateAdapter = true)
+data class AlarmStateMessage(val isAlarmOn: Boolean)
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/permissions/PermissionsLauncher.kt b/app/src/main/java/com/helible/pilot/permissions/PermissionsLauncher.kt
new file mode 100644
index 0000000..c07c233
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/permissions/PermissionsLauncher.kt
@@ -0,0 +1,50 @@
+package com.helible.pilot.permissions
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+
+
+class PermissionsLauncher {
+ private val permissionsToRequest: Array by lazy {
+ if (Build.VERSION.SDK_INT <= 30) {
+ arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+ } else {
+ arrayOf(
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT
+ )
+ }
+ }
+
+ private lateinit var launcher: ManagedActivityResultLauncher, Map>
+
+ @SuppressLint("ComposableNaming")
+ @Composable
+ fun setup(
+ onPermissionResult: (permission: String, isGranted: Boolean) -> Unit,
+ onGranted: () -> Unit,
+ ) {
+ launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions(),
+ onResult = { perms ->
+ permissionsToRequest.forEach { permission ->
+ onPermissionResult(permission, perms[permission] == true)
+ }
+ if (perms.values.all { it }) {
+ onGranted()
+ }
+ }
+ )
+ }
+
+ fun launch(permissions: Array = permissionsToRequest) {
+ launcher.launch(permissions)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/PermissionsRequest.kt b/app/src/main/java/com/helible/pilot/permissions/PermissionsRequest.kt
similarity index 91%
rename from app/src/main/java/com/helible/pilot/PermissionsRequest.kt
rename to app/src/main/java/com/helible/pilot/permissions/PermissionsRequest.kt
index ad22353..c746f5a 100644
--- a/app/src/main/java/com/helible/pilot/PermissionsRequest.kt
+++ b/app/src/main/java/com/helible/pilot/permissions/PermissionsRequest.kt
@@ -1,4 +1,4 @@
-package com.helible.pilot
+package com.helible.pilot.permissions
import android.Manifest
import android.app.Activity
@@ -6,7 +6,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
-import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import com.helible.pilot.components.BluetoothAdminPermissionTextProvider
@@ -20,7 +19,7 @@ fun PermissionsRequest(
visiblePermissionDialogQueue: SnapshotStateList,
dismissCurrentDialog: () -> Unit,
activity: Activity,
- permissionLauncher: ManagedActivityResultLauncher, Map>
+ permissionLaunch: (permissions: Array) -> Unit,
) {
/* Create Dialog windows, which requests all permissions */
visiblePermissionDialogQueue.reversed()
@@ -52,7 +51,7 @@ fun PermissionsRequest(
},
onOkClick = {
dismissCurrentDialog()
- permissionLauncher.launch(arrayOf(permission))
+ permissionLaunch(arrayOf(permission))
},
onContinueClick = {
if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
diff --git a/app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt b/app/src/main/java/com/helible/pilot/permissions/RequestHardwareFeatures.kt
similarity index 92%
rename from app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt
rename to app/src/main/java/com/helible/pilot/permissions/RequestHardwareFeatures.kt
index e7436f0..834e38f 100644
--- a/app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt
+++ b/app/src/main/java/com/helible/pilot/permissions/RequestHardwareFeatures.kt
@@ -1,4 +1,4 @@
-package com.helible.pilot
+package com.helible.pilot.permissions
import android.annotation.SuppressLint
import android.app.Activity
@@ -8,14 +8,14 @@ import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.Composable
import com.helible.pilot.components.RequiredHardwareFeatures
+import com.helible.pilot.dataclasses.BluetoothUiState
@SuppressLint("MissingPermission")
@Composable
fun RequestHardwareFeatures(
activity: Activity,
- bluetoothUiState: BluetoothUiState
-)
-{
+ bluetoothUiState: BluetoothUiState,
+) {
RequiredHardwareFeatures(
title = "Включите Bluetooth",
description = "Для работы приложения требуется Bluetooth",
diff --git a/app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt b/app/src/main/java/com/helible/pilot/receivers/BluetoothAdapterStateReceiver.kt
similarity index 51%
rename from app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt
rename to app/src/main/java/com/helible/pilot/receivers/BluetoothAdapterStateReceiver.kt
index 27f589d..56e56e4 100644
--- a/app/src/main/java/com/helible/pilot/BluetoothAdapterStateReceiver.kt
+++ b/app/src/main/java/com/helible/pilot/receivers/BluetoothAdapterStateReceiver.kt
@@ -1,4 +1,4 @@
-package com.helible.pilot
+package com.helible.pilot.receivers
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
@@ -12,7 +12,7 @@ import android.os.Build
class BluetoothAdapterStateReceiver(
private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit,
private val onDiscoveryRunningChanged: (isDiscoveryRunning: Boolean) -> Unit,
- private val onLocationEnabledChanged: () -> Unit
+ private val onLocationEnabledChanged: () -> Unit,
) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
@@ -27,6 +27,7 @@ class BluetoothAdapterStateReceiver(
}
}
}
+
LocationManager.PROVIDERS_CHANGED_ACTION -> {
onLocationEnabledChanged()
}
@@ -40,33 +41,4 @@ class BluetoothAdapterStateReceiver(
}
}
}
-}
-
-class BluetoothStateReceiver(
- private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit,
- private val onConnectedStateChanged: (isConnected: Boolean, BluetoothDevice) -> Unit
-) : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent?.getParcelableExtra(
- BluetoothDevice.EXTRA_DEVICE,
- BluetoothDevice::class.java
- )
- } else {
- @Suppress("DEPRECATION") intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
- }
- when(intent?.action) {
- BluetoothDevice.ACTION_FOUND -> {
- val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
- @SuppressLint("MissingPermission") if (device?.name != null)
- onDeviceFound(device, rssi)
- }
- BluetoothDevice.ACTION_ACL_CONNECTED -> {
- onConnectedStateChanged(true, device ?: return)
- }
- BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
- onConnectedStateChanged(false, device ?: return)
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/receivers/BluuetothStateReceiver.kt b/app/src/main/java/com/helible/pilot/receivers/BluuetothStateReceiver.kt
new file mode 100644
index 0000000..c371605
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/receivers/BluuetothStateReceiver.kt
@@ -0,0 +1,39 @@
+package com.helible.pilot.receivers
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+
+class BluetoothStateReceiver(
+ private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit,
+ private val onConnectedStateChanged: (isConnected: Boolean, BluetoothDevice) -> Unit,
+) : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent?.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE,
+ BluetoothDevice::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION") intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ }
+ when (intent?.action) {
+ BluetoothDevice.ACTION_FOUND -> {
+ val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
+ @SuppressLint("MissingPermission") if (device?.name != null)
+ onDeviceFound(device, rssi)
+ }
+
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
+ onConnectedStateChanged(true, device ?: return)
+ }
+
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
+ onConnectedStateChanged(false, device ?: return)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt b/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt
index 6ad91a4..a8ed995 100644
--- a/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt
+++ b/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt
@@ -42,7 +42,7 @@ fun TestblueTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt
new file mode 100644
index 0000000..12c3355
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt
@@ -0,0 +1,194 @@
+package com.helible.pilot.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.helible.pilot.dataclasses.AlarmStateMessage
+import com.helible.pilot.dataclasses.BluetoothUiState
+import com.helible.pilot.dataclasses.EmergStopMessage
+import com.helible.pilot.dataclasses.RotorsSpeedMessage
+import com.helible.pilot.controllers.BluetoothController
+import com.helible.pilot.controllers.ConnectionResult
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import com.helible.pilot.dataclasses.BluetoothDevice
+
+class BluetoothViewModel(
+ private val bluetoothController: BluetoothController,
+) : ViewModel() {
+
+ private val _selectedDevice: MutableStateFlow = MutableStateFlow(null)
+ val selectedDevice: StateFlow
+ get() = _selectedDevice.asStateFlow()
+
+ private val _state: MutableStateFlow = MutableStateFlow(BluetoothUiState())
+ val state: StateFlow =
+ combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state)
+ { scannedDevices, pairedDevices, state ->
+ state.copy(
+ scannedBluetoothDevices = scannedDevices.toList(),
+ pairedBluetoothDevices = pairedDevices
+ )
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
+ private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
+ private val rotorsStateMessegeAdapter = moshi.adapter(RotorsSpeedMessage::class.java)
+ private val alarmStateMessageAdapter = moshi.adapter(AlarmStateMessage::class.java)
+ private val emergStopMessageAdapter = moshi.adapter(EmergStopMessage::class.java)
+
+ init {
+ bluetoothController.isConnected.onEach { isConnected ->
+ _state.update { it.copy(isConnected = isConnected) }
+ }.launchIn(viewModelScope)
+ bluetoothController.errors.onEach { error ->
+ _state.update {
+ it.copy(errorMessage = error)
+ }
+ }.launchIn(viewModelScope)
+ bluetoothController.isScanning.onEach { result ->
+ _state.update {
+ it.copy(
+ isDiscovering = result,
+ )
+ }
+ }.launchIn(viewModelScope)
+ bluetoothController.isEnabled.onEach { result ->
+ _state.update {
+ it.copy(
+ isEnabled = result,
+ )
+ }
+ }.launchIn(viewModelScope)
+ bluetoothController.isLocationEnabled.onEach { result ->
+ _state.update {
+ it.copy(
+ isLocationEnabled = result
+ )
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ private fun Flow.listen(): Job {
+ return onEach { result ->
+ when (result) {
+ ConnectionResult.ConnectionEstablished -> {
+ _state.update {
+ it.copy(
+ isConnected = true,
+ isConnecting = false,
+ errorMessage = null
+ )
+ }
+ }
+
+ is ConnectionResult.TransferSucceded -> {
+ TODO("Telemetry not implemented")
+ }
+
+ is ConnectionResult.Error -> {
+ _state.update {
+ it.copy(
+ isConnected = false,
+ isConnecting = false,
+ errorMessage = result.message
+ )
+ }
+ }
+ }
+ }
+ .catch { _ ->
+ bluetoothController.closeConnection()
+ _state.update {
+ it.copy(
+ isConnected = false,
+ isConnecting = false
+ )
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+
+ private var deviceConnectionJob: Job? = null
+
+ fun connectToDevice(device: String) {
+ _state.update { it.copy(isConnecting = true) }
+ deviceConnectionJob = bluetoothController
+ .connectToDevice(device)
+ .listen()
+ }
+
+ fun disconnectFromDevice() {
+ deviceConnectionJob?.cancel()
+ bluetoothController.closeConnection()
+ _state.update {
+ it.copy(
+ isConnecting = false,
+ isConnected = false
+ )
+ }
+ }
+
+ fun selectDevice(selectedDevice: BluetoothDevice?) {
+ _selectedDevice.update { selectedDevice }
+ }
+
+ fun startScan() {
+ selectDevice(null)
+ bluetoothController.startDiscovery()
+ }
+
+ fun cancelScan() {
+ bluetoothController.cancelDiscovery()
+ }
+
+ override fun onCleared() {
+ cancelScan()
+ bluetoothController.onDestroy()
+ super.onCleared()
+ }
+
+ fun sendRotorsDutySpeed(rotorsState: RotorsSpeedMessage) {
+ viewModelScope.launch {
+ bluetoothController.trySendMessage(
+ rotorsStateMessegeAdapter.toJson(rotorsState).plus("\r").toByteArray()
+ )
+ }
+ }
+
+ fun sendAlarmState(alarmStateMessage: AlarmStateMessage) {
+ viewModelScope.launch {
+ bluetoothController.trySendMessage(
+ alarmStateMessageAdapter.toJson(alarmStateMessage).plus("\r").toByteArray()
+ )
+ }
+ }
+
+ fun sendEmergStop() {
+ viewModelScope.launch {
+ bluetoothController.trySendMessage(
+ emergStopMessageAdapter.toJson(EmergStopMessage(true)).plus("\r").toByteArray()
+ )
+ }
+ }
+
+ fun sendR3Duty(r3: Int) {
+ viewModelScope.launch {
+ bluetoothController.trySendMessage(
+ "R3$r3\n\r".toByteArray()
+ )
+ delay(30)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModelFactory.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModelFactory.kt
new file mode 100644
index 0000000..215845f
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModelFactory.kt
@@ -0,0 +1,15 @@
+package com.helible.pilot.viewmodels
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.helible.pilot.controllers.AndroidBluetoothController
+
+@Suppress("UNCHECKED_CAST")
+class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return BluetoothViewModel(
+ bluetoothController = AndroidBluetoothController(context)
+ ) as T
+ }
+}
diff --git a/app/src/main/java/com/helible/pilot/viewmodels/PermissionsViewModel.kt b/app/src/main/java/com/helible/pilot/viewmodels/PermissionsViewModel.kt
new file mode 100644
index 0000000..9f36fda
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/viewmodels/PermissionsViewModel.kt
@@ -0,0 +1,35 @@
+package com.helible.pilot.viewmodels
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.ViewModel
+import com.helible.pilot.components.AppPreferences
+import com.helible.pilot.components.SavedPreferences
+
+class PermissionDialogViewModel : ViewModel() {
+ val visiblePermissionDialogQueue = mutableStateListOf()
+
+ fun dismissDialog() {
+ visiblePermissionDialogQueue.removeFirst()
+ }
+
+ fun onPermissionResult(permission: String, isGranted: Boolean) {
+ if (!isGranted && !visiblePermissionDialogQueue.contains(permission)) {
+ visiblePermissionDialogQueue.add(permission)
+ }
+ }
+}
+
+class PreferencesViewModel(
+ private val preferencesStorage: SavedPreferences,
+) : ViewModel() {
+ val preferences: AppPreferences? get() = preferencesStorage.getPreferences()
+ fun savePreferences(savedPreferences: AppPreferences) {
+ preferencesStorage.savePreferences(
+ preferences = savedPreferences
+ )
+ }
+
+ fun clearPreferences() {
+ preferencesStorage.clearPreferences()
+ }
+}
\ No newline at end of file