From 85ffe9426657d40842bebea777fa3f961f1b2da2 Mon Sep 17 00:00:00 2001 From: londre7 <45090531+londre7@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:42:55 +0500 Subject: [PATCH] Implemented extended data output (#195) --- .../presentation/radar/RadarScreen.kt | 112 +++++-- .../look4sat/presentation/radar/RadarState.kt | 3 + .../presentation/radar/RadarViewModel.kt | 80 ++++- .../presentation/settings/SettingsDialog.kt | 281 ++++++++++++++++++ .../presentation/settings/SettingsScreen.kt | 224 ++++++-------- .../presentation/settings/SettingsState.kt | 16 +- .../settings/SettingsViewModel.kt | 16 +- app/src/main/res/values-es/strings.xml | 24 +- app/src/main/res/values-ru/strings.xml | 24 +- app/src/main/res/values-si/strings.xml | 24 +- app/src/main/res/values-uk/strings.xml | 24 +- app/src/main/res/values-zh/strings.xml | 24 +- app/src/main/res/values/strings.xml | 24 +- .../data/framework/BluetoothReporter.kt | 138 ++++++--- .../data/framework/NetworkReporter.kt | 119 +++++--- .../look4sat/data/repository/SettingsRepo.kt | 96 ++++-- .../look4sat/domain/model/Settings.kt | 16 +- .../domain/repository/IReporterRepo.kt | 25 ++ .../domain/repository/ISettingsRepo.kt | 17 +- 19 files changed, 965 insertions(+), 322 deletions(-) create mode 100644 domain/src/main/java/com/rtbishop/look4sat/domain/repository/IReporterRepo.kt diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarScreen.kt index cdc0a892..76bf55a1 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,8 +34,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -55,6 +58,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.graphics.Brush import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavGraphBuilder @@ -194,7 +198,15 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) { } } else { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - TransmittersList(transmitters = uiState.transmitters) + TransmittersList( + transmitters = uiState.transmitters, + selectedUuid = uiState.selectedTransmitterUuid, + onSelect = { uuid -> + if (uiState.selectedTransmitterUuid != null) { + uiState.sendAction(RadarAction.SelectTransmitter(uuid)) + } + } + ) if (uiState.orbitalPos?.eclipsed == true) { EclipsedIndicator() } @@ -282,7 +294,15 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) { } } else { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - TransmittersList(transmitters = uiState.transmitters) + TransmittersList( + transmitters = uiState.transmitters, + selectedUuid = uiState.selectedTransmitterUuid, + onSelect = { uuid -> + if (uiState.selectedTransmitterUuid != null) { + uiState.sendAction(RadarAction.SelectTransmitter(uuid)) + } + } + ) if (uiState.orbitalPos?.eclipsed == true) { EclipsedIndicator() } @@ -315,10 +335,19 @@ private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) { } @Composable -private fun TransmittersList(transmitters: List) { +private fun TransmittersList( + transmitters: List, + selectedUuid: String?, + onSelect: (String) -> Unit +) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(items = transmitters, key = { item -> item.uuid }) { radio -> - TransmitterItem(radio) + TransmitterItem( + radio, + (selectedUuid != null), + (radio.uuid == selectedUuid), + { onSelect(radio.uuid) } + ) } } } @@ -330,7 +359,7 @@ private fun TransmitterItemPreview() { "", "Extremely powerful transmitter", true, 10000000000L, 10000000000L, null, 10000000000L, 10000000000L, "FSK AX.100 Mode 5", true, 0 ) - MainTheme { TransmitterItem(transmitter) } + MainTheme { TransmitterItem(transmitter, true, true,{}) } } @Composable @@ -370,13 +399,24 @@ private fun EclipsedIndicator() { } @Composable -private fun TransmitterItem(radio: SatRadio) { +private fun TransmitterItem( + radio: SatRadio, + isClickable: Boolean, + isSelected: Boolean, + onClick: () -> Unit +) { // LaunchedEffect(radio) { // BluetoothCIV.updateOnce(radio) // } val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})" - Surface(color = MaterialTheme.colorScheme.background, + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier + .fillMaxWidth() + .then( + if (isClickable) Modifier.clickable { onClick() } else Modifier + ) // modifier = Modifier.clickable(onClick = { // Log.d("BluetoothCivManager", radio.toString()) // if(radio.uuid == BluetoothCIV.selected) BluetoothCIV.selected = "NONE" else @@ -387,21 +427,51 @@ private fun TransmitterItem(radio: SatRadio) { // }) ) { Surface(modifier = Modifier.padding(bottom = 2.dp)) { - Column( - verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 8.dp, vertical = 6.dp) - ) { - Text( - text = fullTitle, - textAlign = TextAlign.Center, + Box { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier - .fillMaxWidth() - .infiniteMarquee() - ) - FrequencyRow(radio = radio, isDownlink = true) - FrequencyRow(radio = radio, isDownlink = false) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 8.dp, vertical = 6.dp) + ) { + Text( + text = fullTitle, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .infiniteMarquee() + ) + FrequencyRow(radio = radio, isDownlink = true) + FrequencyRow(radio = radio, isDownlink = false) + } + if (isSelected) { + Box( + modifier = Modifier + .align(Alignment.TopStart) + .padding(start = 4.dp, top = 5.dp) + .background( + brush = Brush.radialGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surface.copy(alpha = 0f) + ), + radius = 80f + ), + shape = CircleShape + ) + .width(30.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_radios), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .align(Alignment.TopStart) + .padding(start = 4.dp) + .size(24.dp) + ) + } + } } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarState.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarState.kt index ff9de3dd..1fe61944 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarState.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarState.kt @@ -31,9 +31,12 @@ data class RadarState( val shouldShowSweep: Boolean, val shouldUseCompass: Boolean, val transmitters: List, + val selectedTransmitterUuid: String?, + val selectedFrequency: Long?, val sendAction: (RadarAction) -> Unit ) sealed class RadarAction { data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction() + data class SelectTransmitter(val uuid: String) : RadarAction() } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarViewModel.kt index 99b643cc..1591b879 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radar/RadarViewModel.kt @@ -25,7 +25,10 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.rtbishop.look4sat.MainApplication +import com.rtbishop.look4sat.data.framework.WithoutExtParams import com.rtbishop.look4sat.data.framework.BluetoothReporter +import com.rtbishop.look4sat.data.framework.BtService +import com.rtbishop.look4sat.data.framework.ExtendedParams import com.rtbishop.look4sat.data.framework.NetworkReporter import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.predict.OrbitalObject @@ -65,6 +68,8 @@ class RadarViewModel( satTrack = emptyList(), shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors, + selectedTransmitterUuid = null, + selectedFrequency = null, transmitters = emptyList(), sendAction = ::handleAction, ) @@ -109,9 +114,9 @@ class RadarViewModel( _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) } } } + processRadios(transmitters, satPass.orbitalObject, timeNow) sendPassData(satPass, pos, satPass.orbitalObject) sendPassDataBT(pos) - processRadios(transmitters, satPass.orbitalObject, timeNow) delay(1000) } } @@ -126,6 +131,7 @@ class RadarViewModel( private fun handleAction(action: RadarAction) { when (action) { is RadarAction.AddToCalendar -> addToCalendar(action.name, action.aosTime, action.losTime) + is RadarAction.SelectTransmitter -> { _uiState.update { it.copy(selectedTransmitterUuid = action.uuid) } } } } @@ -141,25 +147,47 @@ class RadarViewModel( if (settingsRepo.rcSettings.value.rotatorState) { val server = settingsRepo.rcSettings.value.rotatorAddress val port = settingsRepo.rcSettings.value.rotatorPort.toInt() + val format = settingsRepo.rcSettings.value.rotatorFormat val azimuth = orbitalPos.azimuth.toDegrees().round(2) val elevation = orbitalPos.elevation.toDegrees().round(2) - networkReporter.reportRotation(server, port, azimuth, elevation) + networkReporter.reportRotation(format, azimuth, elevation, ExtendedParams(server, port)) + } + if (settingsRepo.rcSettings.value.frequencyState) { + _uiState.value.selectedFrequency?.let { freq -> + val server = settingsRepo.rcSettings.value.frequencyAddress + val port = settingsRepo.rcSettings.value.frequencyPort.toInt() + val format = settingsRepo.rcSettings.value.frequencyFormat + networkReporter.reportFrequency(format,freq, ExtendedParams(server, port)) + } } } } private fun sendPassDataBT(orbitalPos: OrbitalPos) { viewModelScope.launch { - if (settingsRepo.rcSettings.value.bluetoothState) { - val btDevice = settingsRepo.rcSettings.value.bluetoothAddress - if (bluetoothReporter.isConnected()) { - val format = settingsRepo.rcSettings.value.bluetoothFormat - val azimuth = orbitalPos.azimuth.toDegrees().round(0).toInt() - val elevation = orbitalPos.elevation.toDegrees().round(0).toInt() - bluetoothReporter.reportRotation(format, azimuth, elevation) - } else if (!bluetoothReporter.isConnecting()) { -// Log.i("BTReporter", "BTReporter: Attempting to connect...") - bluetoothReporter.connectBTDevice(btDevice) + if (settingsRepo.rcSettings.value.bluetoothRotatorState) { + val btRotatorDevice = settingsRepo.rcSettings.value.bluetoothRotatorAddress + if (bluetoothReporter.isConnected(BtService.ROTATOR)) { + val format = settingsRepo.rcSettings.value.bluetoothRotatorFormat + val azimuth = orbitalPos.azimuth.toDegrees().round(0) + val elevation = orbitalPos.elevation.toDegrees().round(0) + bluetoothReporter.reportRotation(format, azimuth, elevation, WithoutExtParams(0)) + } else if (!bluetoothReporter.isConnecting(BtService.ROTATOR)) { +// Log.i("BTReporter", "BTReporter (rotator): Attempting to connect...") + bluetoothReporter.connect(BtService.ROTATOR,btRotatorDevice) + } + } + if (settingsRepo.rcSettings.value.bluetoothFrequencyState) { + val btFrequencyDevice = settingsRepo.rcSettings.value.bluetoothFrequencyAddress + if (bluetoothReporter.isConnected(BtService.FREQUENCY)) { + val format = settingsRepo.rcSettings.value.bluetoothFrequencyFormat + val frequency = _uiState.value.selectedFrequency + frequency?.let { freq -> + bluetoothReporter.reportFrequency(format, freq, WithoutExtParams(0)) + } + } else if (!bluetoothReporter.isConnecting(BtService.FREQUENCY)) { +// Log.i("BTReporter", "BTReporter (frequency): Attempting to connect...") + bluetoothReporter.connect(BtService.FREQUENCY, btFrequencyDevice) } } } @@ -167,7 +195,33 @@ class RadarViewModel( private suspend fun processRadios(radios: List, orbitalObject: OrbitalObject, time: Long) { val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time) - _uiState.update { it.copy(transmitters = transmitters) } + _uiState.update { state -> + if (!settingsRepo.rcSettings.value.frequencyState && !settingsRepo.rcSettings.value.bluetoothFrequencyState) { + return@update state.copy( + transmitters = transmitters, + selectedTransmitterUuid = null, + selectedFrequency = null + ) + } + val selectedUuid = state.selectedTransmitterUuid ?: transmitters.firstOrNull()?.uuid + val selectedRadio = transmitters.firstOrNull { it.uuid == selectedUuid } + val freq = selectedRadio?.let { radio -> + val low = radio.downlinkLow + val high = radio.downlinkHigh + when { + low != null && high != null -> + (low + high) / 2 + low != null -> + low + else -> null + } + } + state.copy( + transmitters = transmitters, + selectedTransmitterUuid = selectedUuid, + selectedFrequency = freq + ) + } } companion object { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsDialog.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsDialog.kt index b589252d..d0f9653f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsDialog.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsDialog.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.rtbishop.look4sat.presentation.LocalSpacing +import com.rtbishop.look4sat.domain.model.RCSettings @Preview(showBackground = true) @Composable @@ -199,3 +200,283 @@ fun DataSourcesDialog( } } } + +@Preview(showBackground = true) +@Composable +fun PreviewNetworkOutputDialog() { + MainTheme { + NetworkOutputDialog( + initialSettings = RCSettings( + rotatorState = false, + rotatorAddress = "127.0.0.1", + rotatorPort = "4533", + rotatorFormat = $$"P $AZ $EL", + frequencyState = false, + frequencyAddress = "127.0.0.1", + frequencyPort = "4532", + frequencyFormat = $$"set_freq $FREQ", + bluetoothRotatorState = false, + bluetoothRotatorFormat = $$"W$AZ $EL", + bluetoothRotatorName = "Default", + bluetoothRotatorAddress = "00:0C:BF:13:80:5D", + bluetoothFrequencyState = false, + bluetoothFrequencyAddress = "00:0C:BF:13:80:5D", + bluetoothFrequencyFormat = $$"FA$FREQ" + ), + onDismiss = {}, + onSave = { _, _, _, _, _, _, _, _ -> } + ) + } +} + +@Composable +fun NetworkOutputDialog( + initialSettings: RCSettings, + onDismiss: () -> Unit, + onSave: ( + Boolean, String, String, String, + Boolean, String, String, String + ) -> Unit +) { + val rotatorState = rememberSaveable { mutableStateOf(initialSettings.rotatorState) } + val rotatorAddress = rememberSaveable { mutableStateOf(initialSettings.rotatorAddress) } + val rotatorPort = rememberSaveable { mutableStateOf(initialSettings.rotatorPort) } + val rotatorFormat = rememberSaveable { mutableStateOf(initialSettings.rotatorFormat) } + val frequencyState = rememberSaveable { mutableStateOf(initialSettings.frequencyState) } + val frequencyAddress = rememberSaveable { mutableStateOf(initialSettings.frequencyAddress) } + val frequencyPort = rememberSaveable { mutableStateOf(initialSettings.frequencyPort) } + val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.frequencyFormat) } + val onAccept = { + onSave( + rotatorState.value, + rotatorAddress.value, + rotatorPort.value, + rotatorFormat.value, + frequencyState.value, + frequencyAddress.value, + frequencyPort.value, + frequencyFormat.value + ) + onDismiss() + } + SharedDialog( + title = stringResource(R.string.prefs_net_title), + onCancel = onDismiss, + onAccept = onAccept + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.prefs_net_rotator_switch)) + Switch( + checked = rotatorState.value, + onCheckedChange = { rotatorState.value = it } + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = rotatorAddress.value, + onValueChange = { rotatorAddress.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_net_rotator_ip_hint)) }, + modifier = Modifier.weight(1.5f), + enabled = rotatorState.value + ) + OutlinedTextField( + value = rotatorPort.value, + onValueChange = { rotatorPort.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_net_rotator_port_hint)) }, + modifier = Modifier.weight(1f), + enabled = rotatorState.value + ) + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = rotatorFormat.value, + onValueChange = { rotatorFormat.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_net_rotator_format_hint)) }, + modifier = Modifier.fillMaxWidth(), + enabled = rotatorState.value + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.prefs_net_frequency_switch)) + Switch( + checked = frequencyState.value, + onCheckedChange = { frequencyState.value = it } + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = frequencyAddress.value, + onValueChange = { frequencyAddress.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_net_frequency_ip_hint)) }, + modifier = Modifier.weight(1.5f), + enabled = frequencyState.value + ) + OutlinedTextField( + value = frequencyPort.value, + onValueChange = { frequencyPort.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_net_frequency_port_hint)) }, + modifier = Modifier.weight(1f), + enabled = frequencyState.value + ) + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = frequencyFormat.value, + onValueChange = { frequencyFormat.value = it }, + maxLines = 2, + label = { Text(stringResource(R.string.prefs_net_frequency_format_hint)) }, + modifier = Modifier.fillMaxWidth(), + enabled = frequencyState.value + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewBluetoothOutputDialog() { + MainTheme { + BluetoothOutputDialog( + initialSettings = RCSettings( + rotatorState = false, + rotatorAddress = "127.0.0.1", + rotatorPort = "4533", + rotatorFormat = $$"P $AZ $EL", + frequencyState = false, + frequencyAddress = "127.0.0.1", + frequencyPort = "4532", + frequencyFormat = $$"set_freq $FREQ", + bluetoothRotatorState = false, + bluetoothRotatorFormat = $$"W$AZ $EL", + bluetoothRotatorName = "Default", + bluetoothRotatorAddress = "00:0C:BF:13:80:5D", + bluetoothFrequencyState = false, + bluetoothFrequencyAddress = "00:0C:BF:13:80:5D", + bluetoothFrequencyFormat = $$"FA$FREQ" + ), + onDismiss = {}, + onSave = { _, _, _, _, _, _ -> } + ) + } +} + +@Composable +fun BluetoothOutputDialog( + initialSettings: RCSettings, + onDismiss: () -> Unit, + onSave: ( + Boolean, String, String, + Boolean, String, String + ) -> Unit +) { + val rotatorState = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorState) } + val rotatorAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorAddress) } + val rotatorFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorFormat) } + val frequencyState = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyState) } + val frequencyAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyAddress) } + val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) } + val onAccept = { + onSave( + rotatorState.value, + rotatorAddress.value, + rotatorFormat.value, + frequencyState.value, + frequencyAddress.value, + frequencyFormat.value + ) + onDismiss() + } + SharedDialog( + title = stringResource(R.string.prefs_bt_title), + onCancel = onDismiss, + onAccept = onAccept + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.prefs_bt_rotator_switch)) + Switch( + checked = rotatorState.value, + onCheckedChange = { rotatorState.value = it } + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = rotatorAddress.value, + onValueChange = { rotatorAddress.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_bt_rotator_device_hint)) }, + modifier = Modifier.weight(1.5f), + enabled = rotatorState.value + ) + OutlinedTextField( + value = rotatorFormat.value, + onValueChange = { rotatorFormat.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_bt_rotator_output_hint)) }, + modifier = Modifier.weight(1f), + enabled = rotatorState.value + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.prefs_bt_frequency_switch)) + Switch( + checked = frequencyState.value, + onCheckedChange = { frequencyState.value = it } + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = frequencyAddress.value, + onValueChange = { frequencyAddress.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_bt_frequency_device_hint)) }, + modifier = Modifier.weight(1.5f), + enabled = frequencyState.value + ) + OutlinedTextField( + value = frequencyFormat.value, + onValueChange = { frequencyFormat.value = it }, + singleLine = true, + label = { Text(stringResource(R.string.prefs_bt_frequency_output_hint)) }, + modifier = Modifier.weight(1f), + enabled = frequencyState.value + ) + } + } + } +} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt index 82e27091..5dd21fb0 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -60,7 +59,6 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.rtbishop.look4sat.R import com.rtbishop.look4sat.domain.model.OtherSettings -import com.rtbishop.look4sat.domain.model.RCSettings import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.Screen @@ -87,19 +85,6 @@ fun NavGraphBuilder.settingsDestination() { @Composable private fun SettingsScreen(uiState: SettingsState) { - // Permissions setup - val bluetoothContract = ActivityResultContracts.RequestPermission() - val bluetoothError = stringResource(R.string.prefs_bt_perm_error) - val bluetoothPerm = when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH - else -> Manifest.permission.BLUETOOTH_CONNECT - } - val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> - if (!isGranted) { - uiState.sendRCAction(RCAction.SetBluetoothState(false)) - uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError)) - } - } val locationContract = ActivityResultContracts.RequestMultiplePermissions() val locationError = stringResource(R.string.prefs_loc_gps_error) val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION @@ -180,12 +165,84 @@ private fun SettingsScreen(uiState: SettingsState) { val setRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetRotatorState(value)) } val setRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetRotatorAddress(value)) } val setRotatorPort = { value: String -> uiState.sendRCAction(RCAction.SetRotatorPort(value)) } - val setBluetoothState = { value: Boolean -> - bluetoothRequest.launch(bluetoothPerm) - uiState.sendRCAction(RCAction.SetBluetoothState(value)) + val setRotatorFormat = { value: String -> uiState.sendRCAction(RCAction.SetRotatorFormat(value)) } + val setFrequencyState = { value: Boolean -> uiState.sendRCAction(RCAction.SetFrequencyState(value)) } + val setFrequencyAddress = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyAddress(value)) } + val setFrequencyPort = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyPort(value)) } + val setFrequencyFormat = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyFormat(value)) } + val setBluetoothRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetBluetoothRotatorState(value)) } + val setBluetoothRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothRotatorAddress(value)) } + val setBluetoothRotatorFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothRotatorFormat(value)) } + val setBluetoothFrequencyState = { value: Boolean -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyState(value)) } + val setBluetoothFrequencyAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyAddress(value)) } + val setBluetoothFrequencyFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyFormat(value)) } + + // Network data output + val networkDialogState = rememberSaveable { mutableStateOf(false) } + val showNetworkDialog = { networkDialogState.value = true } + val dismissNetworkDialog = { networkDialogState.value = false } + if (networkDialogState.value) { + NetworkOutputDialog( + initialSettings = rcSettings, + onDismiss = dismissNetworkDialog, + onSave = { rotatorState, + rotatorAddress, + rotatorPort, + rotatorFormat, + frequencyState, + frequencyAddress, + frequencyPort, + frequencyFormat -> + setRotatorState(rotatorState) + setRotatorAddress(rotatorAddress) + setRotatorPort(rotatorPort) + setRotatorFormat(rotatorFormat) + setFrequencyState(frequencyState) + setFrequencyAddress(frequencyAddress) + setFrequencyPort(frequencyPort) + setFrequencyFormat(frequencyFormat) + } + ) + } + + // Bluetooth data output + val bluetoothDialogState = rememberSaveable { mutableStateOf(false) } + val bluetoothContract = ActivityResultContracts.RequestPermission() + val bluetoothError = stringResource(R.string.prefs_bt_perm_error) + val bluetoothPerm = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH + else -> Manifest.permission.BLUETOOTH_CONNECT + } + val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> + if (!isGranted) + { + uiState.sendRCAction(RCAction.SetBluetoothRotatorState(false)) + uiState.sendRCAction(RCAction.SetBluetoothFrequencyState(false)) + uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError)) + } + else { bluetoothDialogState.value = true } + } + val showBluetoothDialog = { bluetoothRequest.launch(bluetoothPerm) } + val dismissBluetoothDialog = { bluetoothDialogState.value = false } + if (bluetoothDialogState.value) { + BluetoothOutputDialog( + initialSettings = rcSettings, + onDismiss = dismissBluetoothDialog, + onSave = { rotatorState, + rotatorAddress, + rotatorFormat, + frequencyState, + frequencyAddress, + frequencyFormat -> + setBluetoothRotatorState(rotatorState) + setBluetoothRotatorAddress(rotatorAddress) + setBluetoothRotatorFormat(rotatorFormat) + setBluetoothFrequencyState(frequencyState) + setBluetoothFrequencyAddress(frequencyAddress) + setBluetoothFrequencyFormat(frequencyFormat) + } + ) } - val setBluetoothAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothAddress(value)) } - val setBluetoothFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFormat(value)) } // Other settings val otherSettings = uiState.otherSettings @@ -242,8 +299,7 @@ private fun SettingsScreen(uiState: SettingsState) { LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction) } item { DataCard(dataSettings, updateFromWeb, clearAllData, showDataSourcesDialog) } - item { NetworkOutputCard(rcSettings, setRotatorState, setRotatorAddress, setRotatorPort) } - item { BluetoothOutputCard(rcSettings, setBluetoothState, setBluetoothAddress, setBluetoothFormat) } + item { OutputCard({ showNetworkDialog() }, { showBluetoothDialog() }) } item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) } item { CardCredits() } } @@ -375,126 +431,32 @@ private fun DataCard( @Preview(showBackground = true) @Composable -private fun NetworkOutputCardPreview() = MainTheme { - val settings = RCSettings( - rotatorState = false, - rotatorAddress = "127.0.0.1", - rotatorPort = "4533", - bluetoothState = false, - bluetoothFormat = $$"W$AZ $EL", - bluetoothName = "Name", - bluetoothAddress = "00:0C:BF:13:80:5D" - ) - NetworkOutputCard(settings) -} +private fun OutputCardPreview() = MainTheme { OutputCard({}, {}) } @Composable -private fun NetworkOutputCard( - settings: RCSettings, - setRotatorState: (Boolean) -> Unit = {}, - setRotatorAddress: (String) -> Unit = {}, - setRotatorPort: (String) -> Unit = {} +private fun OutputCard( + onNetworkClick: () -> Unit, + onBluetoothClick: () -> Unit ) { ElevatedCard(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Text( - text = stringResource(id = R.string.prefs_net_title), + text = stringResource(id = R.string.prefs_data_output_title), color = MaterialTheme.colorScheme.primary ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(R.string.prefs_net_switch)) - Switch(checked = settings.rotatorState, onCheckedChange = { setRotatorState(it) }) - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = settings.rotatorAddress, - singleLine = true, - label = { Text(text = stringResource(R.string.prefs_net_ip_hint)) }, - onValueChange = { setRotatorAddress(it) }, - modifier = Modifier.weight(1.5f), - enabled = settings.rotatorState + Spacer(modifier = Modifier.height(6.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + CardButton( + onClick = onNetworkClick, + text = stringResource(id = R.string.prefs_net_output), + modifier = Modifier.weight(1f) ) - OutlinedTextField( - value = settings.rotatorPort, - onValueChange = { setRotatorPort(it) }, - label = { Text(text = stringResource(R.string.prefs_net_port_hint)) }, - singleLine = true, - modifier = Modifier.weight(1f), - enabled = settings.rotatorState + CardButton( + onClick = onBluetoothClick, + text = stringResource(id = R.string.prefs_bt_output), + modifier = Modifier.weight(1f) ) } - Spacer(modifier = Modifier.height(4.dp)) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun BluetoothOutputCardPreview() = MainTheme { - val settings = RCSettings( - rotatorState = false, - rotatorAddress = "127.0.0.1", - rotatorPort = "4533", - bluetoothState = false, - bluetoothFormat = $$"W$AZ $EL", - bluetoothName = "Name", - bluetoothAddress = "00:0C:BF:13:80:5D" - ) - BluetoothOutputCard(settings) -} - -@Composable -private fun BluetoothOutputCard( - settings: RCSettings, - setBluetoothState: (Boolean) -> Unit = {}, - setBluetoothAddress: (String) -> Unit = {}, - setBluetoothFormat: (String) -> Unit = {} -) { - ElevatedCard(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { - Text( - text = stringResource(id = R.string.prefs_bt_title), - color = MaterialTheme.colorScheme.primary - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(R.string.prefs_bt_switch)) - Switch(checked = settings.bluetoothState, onCheckedChange = { setBluetoothState(it) }) - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = settings.bluetoothAddress, - singleLine = true, - label = { Text(text = stringResource(R.string.prefs_bt_device_hint)) }, - onValueChange = { setBluetoothAddress(it) }, - modifier = Modifier.weight(1.5f), - enabled = settings.bluetoothState - ) - OutlinedTextField( - value = settings.bluetoothFormat, - onValueChange = { setBluetoothFormat(it) }, - label = { Text(text = stringResource(R.string.prefs_bt_output_hint)) }, - singleLine = true, - modifier = Modifier.weight(1f), - enabled = settings.bluetoothState - ) - } - Spacer(modifier = Modifier.height(4.dp)) } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt index 880d61cf..7a32a6bc 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt @@ -71,10 +71,18 @@ sealed class RCAction { data class SetRotatorState(val value: Boolean) : RCAction() data class SetRotatorAddress(val value: String) : RCAction() data class SetRotatorPort(val value: String) : RCAction() - data class SetBluetoothState(val value: Boolean) : RCAction() - data class SetBluetoothFormat(val value: String) : RCAction() - data class SetBluetoothName(val value: String) : RCAction() - data class SetBluetoothAddress(val value: String) : RCAction() + data class SetRotatorFormat(val value: String) : RCAction() + data class SetFrequencyState(val value: Boolean) : RCAction() + data class SetFrequencyAddress(val value: String) : RCAction() + data class SetFrequencyPort(val value: String) : RCAction() + data class SetFrequencyFormat(val value: String) : RCAction() + data class SetBluetoothRotatorState(val value: Boolean) : RCAction() + data class SetBluetoothRotatorFormat(val value: String) : RCAction() + data class SetBluetoothRotatorName(val value: String) : RCAction() + data class SetBluetoothRotatorAddress(val value: String) : RCAction() + data class SetBluetoothFrequencyState(val value: Boolean) : RCAction() + data class SetBluetoothFrequencyFormat(val value: String) : RCAction() + data class SetBluetoothFrequencyAddress(val value: String) : RCAction() } sealed class DataSourcesAction { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt index 29a1a31d..f538cb52 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt @@ -152,10 +152,18 @@ class SettingsViewModel( is RCAction.SetRotatorState -> settingsRepo.setRotatorState(action.value) is RCAction.SetRotatorAddress -> settingsRepo.setRotatorAddress(action.value) is RCAction.SetRotatorPort -> settingsRepo.setRotatorPort(action.value) - is RCAction.SetBluetoothState -> settingsRepo.setBluetoothState(action.value) - is RCAction.SetBluetoothAddress -> settingsRepo.setBluetoothAddress(action.value) - is RCAction.SetBluetoothFormat -> settingsRepo.setBluetoothFormat(action.value) - is RCAction.SetBluetoothName -> settingsRepo.setBluetoothName(action.value) + is RCAction.SetRotatorFormat -> settingsRepo.setRotatorFormat(action.value) + is RCAction.SetFrequencyState -> settingsRepo.setFrequencyState(action.value) + is RCAction.SetFrequencyAddress -> settingsRepo.setFrequencyAddress(action.value) + is RCAction.SetFrequencyPort -> settingsRepo.setFrequencyPort(action.value) + is RCAction.SetFrequencyFormat -> settingsRepo.setFrequencyFormat(action.value) + is RCAction.SetBluetoothRotatorState -> settingsRepo.setBluetoothRotatorState(action.value) + is RCAction.SetBluetoothRotatorAddress -> settingsRepo.setBluetoothRotatorAddress(action.value) + is RCAction.SetBluetoothRotatorFormat -> settingsRepo.setBluetoothRotatorFormat(action.value) + is RCAction.SetBluetoothRotatorName -> settingsRepo.setBluetoothRotatorName(action.value) + is RCAction.SetBluetoothFrequencyState -> settingsRepo.setBluetoothFrequencyState(action.value) + is RCAction.SetBluetoothFrequencyFormat -> settingsRepo.setBluetoothFrequencyFormat(action.value) + is RCAction.SetBluetoothFrequencyAddress -> settingsRepo.setBluetoothFrequencyAddress(action.value) } } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8e7d8917..a68859e2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -112,15 +112,27 @@ Custom TLE URL Custom transceivers URL + Data output + Network + Bluetooth + Salida de datos de red - Habilitar salida - Dirección IP - Puerto + Enable rotation output + Dirección IP + Puerto + Formato de datos + Enable frequency output + Dirección IP + Puerto + Formato de datos Salida por Bluetooth - Habilitar salida - Id dispositivo - Formato de datos + Enable rotation output + Id dispositivo + Formato de datos + Enable frequency output + Id dispositivo + Formato de datos Revisa los permisos de Bluetooth Otros ajustes diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f02c71c3..83e5d918 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -111,15 +111,27 @@ Custom TLE URL Custom Transceivers URL + Вывод данных + Сеть + Bluetooth + Вывод сетевых данных - Включить вывод - IP адрес - Порт + Включить вывод ротатора + IP адрес + Порт + Формат + Включить вывод частоты + IP адрес + Порт + Формат Вывод данных Bluetooth - Включить вывод - Id устройства - Формат данных + Включить вывод ротатора + Id устройства + Формат + Включить вывод частоты + Id устройства + Формат Нет разрешения использовать bluetooth Другие настройки diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index e687df9a..b8a1c495 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -112,15 +112,27 @@ Custom TLE URL Custom transceivers URL + Data output + Network + Bluetooth + ජාල ප්‍රතිදානය - ප්‍රතිදානය සක්‍රීය කරන්න - IP ලිපිනය - Port + Enable rotation output + IP ලිපිනය + Port + දත්ත අකාරය + Enable frequency output + IP ලිපිනය + Port + දත්ත අකාරය Bluetooth දත්ත ප්‍රතිදානය - ප්‍රතිදානය සක්‍රීය කරන්න - උපාංග id - දත්ත අකාරය + Enable rotation output + උපාංග id + දත්ත අකාරය + Enable frequency output + උපාංග id + දත්ත අකාරය Bluetooth අවසර පරික්‍ෂා ක° වෙනත් සැකසුම් diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 26bddb87..19c0d6d7 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -112,15 +112,27 @@ Custom TLE URL Custom transceivers URL + Data output + Network + Bluetooth + Вивід мережевих даних - Увімкнути вихід - IP адреса - Порт + Enable rotation output + IP адреса + Порт + Формат даних + Enable frequency output + IP адреса + Порт + Формат даних Вивід даних Bluetooth - Увімкнути вихід - Id пристрою - Формат даних + Enable rotation output + Id пристрою + Формат даних + Enable frequency output + Id пристрою + Формат даних Перевірте дозвіл Bluetooth Інші налаштування diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 024a7119..48acc3ca 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -104,15 +104,27 @@ 自定义TLE URL 自定义收发器URL + Data output + Network + Bluetooth + 网络数据输出 - 启用输出 - IP地址 - 端口 + Enable rotation output + IP地址 + 端口 + 数据格式 + Enable frequency output + IP地址 + 端口 + 数据格式 蓝牙数据输出 - 启用输出 - 蓝牙设备 ID - 数据格式 + Enable rotation output + 蓝牙设备 ID + 数据格式 + Enable frequency output + 蓝牙设备 ID + 数据格式 请检查蓝牙权限 其他设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08d4939e..e463ce2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,15 +136,27 @@ Custom transceivers URL URL (HTTPS) + Data output + Network + Bluetooth + Network data output - Enable output - IP address - Port + Enable rotation output + IP address + Port + Format + Enable frequency output + IP address + Port + Format Bluetooth data output - Enable output - Device id - Data format + Enable rotation output + Device id + Data format + Enable frequency output + Device id + Data format Check your bluetooth permission Other settings diff --git a/data/src/main/java/com/rtbishop/look4sat/data/framework/BluetoothReporter.kt b/data/src/main/java/com/rtbishop/look4sat/data/framework/BluetoothReporter.kt index 0ddf311d..c9a40a9e 100644 --- a/data/src/main/java/com/rtbishop/look4sat/data/framework/BluetoothReporter.kt +++ b/data/src/main/java/com/rtbishop/look4sat/data/framework/BluetoothReporter.kt @@ -18,6 +18,7 @@ package com.rtbishop.look4sat.data.framework import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothSocket import android.util.Log import java.io.OutputStream import java.util.* @@ -25,69 +26,110 @@ import kotlin.math.abs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import com.rtbishop.look4sat.domain.repository.IReporterParams +import com.rtbishop.look4sat.domain.repository.IReporterRepo + +data class WithoutExtParams( + val dummy: Int +) : IReporterParams + +enum class BtService { ROTATOR, FREQUENCY } + +data class DeviceConnection( + var socket: BluetoothSocket? = null, + var outputStream: OutputStream? = null, + var connected: Boolean = false, + var connecting: Boolean = false, + var connectionJob: Job? = null +) class BluetoothReporter( private val bluetoothManager: BluetoothManager, private val reporterScope: CoroutineScope -) { +) : IReporterRepo { private val tag = "BTReporter" private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") - private lateinit var outputStream: OutputStream - private var rotationConnectionJob: Job? = null - private var rotationReportingJob: Job? = null - private var connected = false - private var connecting = false - fun isConnected(): Boolean = connected + private val serviceToDevice = mutableMapOf() + private val deviceConnections = mutableMapOf() + private val writeMutex = Mutex() - fun isConnecting(): Boolean = connecting + fun isConnected(service: BtService): Boolean { + val deviceId = serviceToDevice[service] ?: return false + return deviceConnections[deviceId]?.connected == true + } + fun isConnecting(service: BtService): Boolean { + val deviceId = serviceToDevice[service] ?: return false + return deviceConnections[deviceId]?.connecting == true + } - fun connectBTDevice(deviceId: String) { - if (!connected) { - rotationConnectionJob = reporterScope.launch { - try { - bluetoothManager.adapter.getRemoteDevice(deviceId)?.let { device -> - device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket -> - connecting = true - socket.connect() - outputStream = socket.outputStream - connected = true - connecting = false - Log.i(tag, "$tag: Connected!") - } - } - } catch (e: SecurityException) { - Log.e(tag, "$tag: ${e.message}") - } catch (e: Exception) { - Log.e(tag, "$tag: ${e.message}") - } + fun connect(service: BtService, deviceId: String) { + serviceToDevice[service] = deviceId + val connection = deviceConnections.getOrPut(deviceId) { + DeviceConnection() + } + if (connection.connected || connection.connecting) return + connection.connectionJob = reporterScope.launch { + try { + connection.connecting = true + val device = bluetoothManager.adapter.getRemoteDevice(deviceId) + val socket = device.createInsecureRfcommSocketToServiceRecord(sppid) + socket.connect() + connection.socket = socket + connection.outputStream = socket.outputStream + connection.connected = true + Log.i(tag, "$tag: Connected to $deviceId") + } catch (e: Exception) { + Log.e(tag, "$tag: ${e.message}") + connection.connected = false + } finally { + connection.connecting = false } } } - fun reportRotation(format: String, azimuth: Int, elevation: Int) { - if (connected) { - rotationReportingJob = reporterScope.launch { - val newElevation = if (elevation > 0) elevation else 0 - try { - val azimuthString = intToStringWithLeadingZeroes(azimuth) - val elevationString = intToStringWithLeadingZeroes(newElevation) - val crChar = '\r' - val nlChar = '\n' - val tbChar = '\t' - var buffer = format.replace("\$AZ", azimuthString) - buffer = buffer.replace("\$EL", elevationString) - buffer = buffer.replace("\\r", crChar.toString()) - buffer = buffer.replace("\\n", nlChar.toString()) - buffer = buffer.replace("\\t", tbChar.toString()) - Log.i(tag, "$tag: Sending $buffer") - if (connected) outputStream.write(buffer.toByteArray()) - } catch (e: Exception) { - Log.e(tag, "$tag: ${e.message}") - connected = false - } + private suspend fun write(service: BtService, buffer: String) { + val deviceId = serviceToDevice[service] ?: return + val connection = deviceConnections[deviceId] ?: return + if (!connection.connected) return + try { + writeMutex.withLock { + connection.outputStream?.write(buffer.toByteArray()) } + } catch (e: Exception) { + Log.e(tag, "$tag: Write failed ${e.message}") + connection.connected = false + } + } + + override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: WithoutExtParams) { + reporterScope.launch { + if (!isConnected(BtService.ROTATOR)) return@launch + val newElevation = if (elevation > 0.0) elevation else 0.0 + val azimuthString = intToStringWithLeadingZeroes(azimuth.toInt()) + val elevationString = intToStringWithLeadingZeroes(newElevation.toInt()) + var buffer = format + .replace("\$AZ", azimuthString) + .replace("\$EL", elevationString) + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t") + write(BtService.ROTATOR, buffer) + } + } + + override fun reportFrequency(format: String, frequency: Long, params: WithoutExtParams) { + reporterScope.launch { + if (!isConnected(BtService.FREQUENCY)) return@launch + var buffer = format + .replace("\$FREQ", frequency.toString()) + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t") + write(BtService.FREQUENCY, buffer) } } diff --git a/data/src/main/java/com/rtbishop/look4sat/data/framework/NetworkReporter.kt b/data/src/main/java/com/rtbishop/look4sat/data/framework/NetworkReporter.kt index de65a2c4..b9096c1e 100644 --- a/data/src/main/java/com/rtbishop/look4sat/data/framework/NetworkReporter.kt +++ b/data/src/main/java/com/rtbishop/look4sat/data/framework/NetworkReporter.kt @@ -22,49 +22,94 @@ import java.nio.ByteBuffer import java.nio.channels.SocketChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.launch +import com.rtbishop.look4sat.domain.repository.IReporterParams +import com.rtbishop.look4sat.domain.repository.IReporterRepo -class NetworkReporter(private val reporterScope: CoroutineScope) { +data class ExtendedParams( + val server: String, + val port: Int +) : IReporterParams - private var rotationSocketChannel: SocketChannel? = null - private var rotationReporting: Job? = null +enum class NetworkService { ROTATOR, FREQUENCY } -// private var frequencySocketChannel: SocketChannel? = null -// private var frequencyReporting: Job? = null -// -// fun reportFrequency(server: String, port: Int, frequency: Long) { -// frequencyReporting = reporterScope.launch { -// runCatching { -// if (frequencySocketChannel == null) { -// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port)) -// } else { -// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray()) -// frequencySocketChannel?.write(buffer) -// } -// }.onFailure { error -> -// println(error.localizedMessage) -// frequencySocketChannel = null -// frequencyReporting?.cancelAndJoin() -// } -// } -// } +data class SocketConnection( + var socket: SocketChannel? = null, + var connected: Boolean = false, + var connecting: Boolean = false, + var connectionJob: Job? = null +) - fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) { - rotationReporting = reporterScope.launch { - val newElevation = if (elevation > 0.0) elevation else 0.0 - runCatching { - if (rotationSocketChannel == null) { - rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port)) - } else { - val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray()) - rotationSocketChannel?.write(buffer) - } - }.onFailure { error -> - println(error.localizedMessage) - rotationSocketChannel = null - rotationReporting?.cancelAndJoin() +class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo { + private val serviceToAddress = mutableMapOf() + private val connections = mutableMapOf() + private val writeMutex = Mutex() + + fun isConnected(service: NetworkService): Boolean { + val addr = serviceToAddress[service] ?: return false + return connections[addr]?.connected == true + } + + private fun getOrCreateConnection(addr: String): SocketConnection { + return connections.getOrPut(addr) { SocketConnection() } + } + + fun connect(service: NetworkService, server: String, port: Int) { + val addr = "$server:$port" + serviceToAddress[service] = addr + val connection = getOrCreateConnection(addr) + if (connection.connected || connection.connecting) return + connection.connectionJob = reporterScope.launch { + try { + connection.connecting = true + val socket = SocketChannel.open(InetSocketAddress(server, port)) + connection.socket = socket + connection.connected = true + println("NetworkReporter: $service connected to $addr") + } catch (e: Exception) { + println("NetworkReporter connect error: ${e.message}") + connection.connected = false + } finally { + connection.connecting = false } } } + + private suspend fun write(service: NetworkService, command: String) { + val addr = serviceToAddress[service] ?: return + val connection = connections[addr] ?: return + if (!connection.connected) return + try { + writeMutex.withLock { + val buffer = ByteBuffer.wrap("\\$command\n".toByteArray()) + connection.socket?.write(buffer) + } + } catch (e: Exception) { + println("NetworkReporter write error: ${e.message}") + connection.connected = false + } + } + + override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: ExtendedParams) { + reporterScope.launch { + connect(NetworkService.ROTATOR, params.server, params.port) + if (!isConnected(NetworkService.ROTATOR)) return@launch + val el = if (elevation > 0.0) elevation else 0.0 + val command = format + .replace("\$AZ", azimuth.toString()) + .replace("\$EL", el.toString()) + write(NetworkService.ROTATOR, command) + } + } + + override fun reportFrequency(format: String, frequency: Long, params: ExtendedParams) { + reporterScope.launch { + connect(NetworkService.FREQUENCY, params.server, params.port) + if (!isConnected(NetworkService.FREQUENCY)) return@launch + val command = format.replace("\$FREQ", frequency.toString()) + write(NetworkService.FREQUENCY, command) + } + } } diff --git a/data/src/main/java/com/rtbishop/look4sat/data/repository/SettingsRepo.kt b/data/src/main/java/com/rtbishop/look4sat/data/repository/SettingsRepo.kt index c4894af8..0e03e4f6 100644 --- a/data/src/main/java/com/rtbishop/look4sat/data/repository/SettingsRepo.kt +++ b/data/src/main/java/com/rtbishop/look4sat/data/repository/SettingsRepo.kt @@ -42,10 +42,13 @@ class SettingsRepo( private val preferences: SharedPreferences ) : ISettingsRepo, LocationListenerCompat { - private val keyBluetoothAddress = "bluetoothAddress" - private val keyBluetoothName = "bluetoothName" - private val keyBluetoothFormat = "bluetoothFormat" - private val keyBluetoothState = "bluetoothState" + private val keyBluetoothRotatorAddress = "bluetoothAddress" + private val keyBluetoothRotatorName = "bluetoothName" + private val keyBluetoothRotatorFormat = "bluetoothFormat" + private val keyBluetoothRotatorState = "bluetoothState" + private val keyBluetoothFrequencyState = "bluetoothFrequencyState" + private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress" + private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat" private val keyFilterHoursAhead = "filterHoursAhead" private val keyFilterMinElevation = "filterMinElevation" private val keyNumberOfRadios = "numberOfRadios" @@ -53,6 +56,11 @@ class SettingsRepo( private val keyRotatorAddress = "rotatorAddress" private val keyRotatorPort = "rotatorPort" private val keyRotatorState = "rotatorState" + private val keyRotatorFormat = "rotatorFormat" + private val keyFrequencyState = "frequencyState" + private val keyFrequencyAddress = "frequencyAddress" + private val keyFrequencyPort = "frequencyPort" + private val keyFrequencyFormat = "frequencyFormat" private val keySelectedIds = "selectedIds" private val keySelectedTypes = "selectedTypes" private val keySelectedModes = "selectedModes" @@ -240,24 +248,39 @@ class SettingsRepo( private val _rcSettings = MutableStateFlow(getRCSettings()) override val rcSettings: StateFlow = _rcSettings - override fun setBluetoothAddress(value: String) { - preferences.edit { putString(keyBluetoothAddress, value) } - _rcSettings.update { it.copy(bluetoothAddress = value) } + override fun setBluetoothRotatorAddress(value: String) { + preferences.edit { putString(keyBluetoothRotatorAddress, value) } + _rcSettings.update { it.copy(bluetoothRotatorAddress = value) } } - override fun setBluetoothFormat(value: String) { - preferences.edit { putString(keyBluetoothFormat, value) } - _rcSettings.update { it.copy(bluetoothFormat = value) } + override fun setBluetoothRotatorFormat(value: String) { + preferences.edit { putString(keyBluetoothRotatorFormat, value) } + _rcSettings.update { it.copy(bluetoothRotatorFormat = value) } } - override fun setBluetoothName(value: String) { - preferences.edit { putString(keyBluetoothName, value) } - _rcSettings.update { it.copy(bluetoothName = value) } + override fun setBluetoothRotatorName(value: String) { + preferences.edit { putString(keyBluetoothRotatorName, value) } + _rcSettings.update { it.copy(bluetoothRotatorName = value) } } - override fun setBluetoothState(value: Boolean) { - preferences.edit { putBoolean(keyBluetoothState, value) } - _rcSettings.update { it.copy(bluetoothState = value) } + override fun setBluetoothRotatorState(value: Boolean) { + preferences.edit { putBoolean(keyBluetoothRotatorState, value) } + _rcSettings.update { it.copy(bluetoothRotatorState = value) } + } + + override fun setBluetoothFrequencyState(value: Boolean) { + preferences.edit { putBoolean(keyBluetoothFrequencyState, value) } + _rcSettings.update { it.copy(bluetoothFrequencyState = value) } + } + + override fun setBluetoothFrequencyAddress(value: String) { + preferences.edit { putString(keyBluetoothFrequencyAddress, value) } + _rcSettings.update { it.copy(bluetoothFrequencyAddress = value) } + } + + override fun setBluetoothFrequencyFormat(value: String) { + preferences.edit { putString(keyBluetoothFrequencyFormat, value) } + _rcSettings.update { it.copy(bluetoothFrequencyFormat = value) } } override fun setRotatorAddress(value: String) { @@ -275,14 +298,47 @@ class SettingsRepo( _rcSettings.update { it.copy(rotatorState = value) } } + override fun setRotatorFormat(value: String) { + preferences.edit { putString(keyRotatorFormat, value) } + _rcSettings.update { it.copy(rotatorFormat = value) } + } + + override fun setFrequencyState(value: Boolean) { + preferences.edit { putBoolean(keyFrequencyState, value) } + _rcSettings.update { it.copy(frequencyState = value) } + } + + override fun setFrequencyAddress(value: String) { + preferences.edit { putString(keyFrequencyAddress, value) } + _rcSettings.update { it.copy(frequencyAddress = value) } + } + + override fun setFrequencyPort(value: String) { + preferences.edit { putString(keyFrequencyPort, value) } + _rcSettings.update { it.copy(frequencyPort = value) } + } + + override fun setFrequencyFormat(value: String) { + preferences.edit { putString(keyFrequencyFormat, value) } + _rcSettings.update { it.copy(frequencyFormat = value) } + } + private fun getRCSettings(): RCSettings = RCSettings( rotatorState = preferences.getBoolean(keyRotatorState, false), rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1", rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533", - bluetoothState = preferences.getBoolean(keyBluetoothState, false), - bluetoothFormat = preferences.getString(keyBluetoothFormat, null) ?: $$"W$AZ $EL", - bluetoothName = preferences.getString(keyBluetoothName, null) ?: "Default", - bluetoothAddress = preferences.getString(keyBluetoothAddress, null) ?: "00:0C:BF:13:80:5D" + rotatorFormat = preferences.getString(keyRotatorFormat, null) ?: $$"P $AZ $EL", + frequencyState = preferences.getBoolean(keyFrequencyState, false), + frequencyAddress = preferences.getString(keyFrequencyAddress, null) ?: "127.0.0.1", + frequencyPort = preferences.getString(keyFrequencyPort, null) ?: "4532", + frequencyFormat = preferences.getString(keyFrequencyFormat, null) ?: $$"set_freq $FREQ", + bluetoothRotatorState = preferences.getBoolean(keyBluetoothRotatorState, false), + bluetoothRotatorFormat = preferences.getString(keyBluetoothRotatorFormat, null) ?: $$"W$AZ $EL", + bluetoothRotatorName = preferences.getString(keyBluetoothRotatorName, null) ?: "Default", + bluetoothRotatorAddress = preferences.getString(keyBluetoothRotatorAddress, null) ?: "00:0C:BF:13:80:5D", + bluetoothFrequencyState = preferences.getBoolean(keyBluetoothFrequencyState, false), + bluetoothFrequencyAddress = preferences.getString(keyBluetoothFrequencyAddress, null) ?: "00:0C:BF:13:80:5D", + bluetoothFrequencyFormat = preferences.getString(keyBluetoothFrequencyFormat, null) ?: $$"FA$FREQ" ) //endregion diff --git a/domain/src/main/java/com/rtbishop/look4sat/domain/model/Settings.kt b/domain/src/main/java/com/rtbishop/look4sat/domain/model/Settings.kt index dbfef841..4642e23a 100644 --- a/domain/src/main/java/com/rtbishop/look4sat/domain/model/Settings.kt +++ b/domain/src/main/java/com/rtbishop/look4sat/domain/model/Settings.kt @@ -33,10 +33,18 @@ data class RCSettings( val rotatorState: Boolean, val rotatorAddress: String, val rotatorPort: String, - val bluetoothState: Boolean, - val bluetoothFormat: String, - val bluetoothName: String, - val bluetoothAddress: String, + val rotatorFormat: String, + val frequencyState: Boolean, + val frequencyAddress: String, + val frequencyPort: String, + val frequencyFormat: String, + val bluetoothRotatorState: Boolean, + val bluetoothRotatorFormat: String, + val bluetoothRotatorName: String, + val bluetoothRotatorAddress: String, + val bluetoothFrequencyState: Boolean, + val bluetoothFrequencyFormat: String, + val bluetoothFrequencyAddress: String ) data class OtherSettings( diff --git a/domain/src/main/java/com/rtbishop/look4sat/domain/repository/IReporterRepo.kt b/domain/src/main/java/com/rtbishop/look4sat/domain/repository/IReporterRepo.kt new file mode 100644 index 00000000..1053fd19 --- /dev/null +++ b/domain/src/main/java/com/rtbishop/look4sat/domain/repository/IReporterRepo.kt @@ -0,0 +1,25 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.domain.repository + +interface IReporterParams + +interface IReporterRepo { + fun reportRotation(format: String, azimuth: Double, elevation: Double, params: T) + fun reportFrequency(format: String, frequency: Long, params: T) +} diff --git a/domain/src/main/java/com/rtbishop/look4sat/domain/repository/ISettingsRepo.kt b/domain/src/main/java/com/rtbishop/look4sat/domain/repository/ISettingsRepo.kt index 387939c1..a0ebc95c 100644 --- a/domain/src/main/java/com/rtbishop/look4sat/domain/repository/ISettingsRepo.kt +++ b/domain/src/main/java/com/rtbishop/look4sat/domain/repository/ISettingsRepo.kt @@ -55,13 +55,22 @@ interface ISettingsRepo { //region # RC settings val rcSettings: StateFlow - fun setBluetoothAddress(value: String) - fun setBluetoothFormat(value: String) - fun setBluetoothName(value: String) - fun setBluetoothState(value: Boolean) + fun setBluetoothRotatorAddress(value: String) + fun setBluetoothRotatorFormat(value: String) + fun setBluetoothRotatorName(value: String) + fun setBluetoothRotatorState(value: Boolean) + fun setBluetoothFrequencyAddress(value: String) + fun setBluetoothFrequencyFormat(value: String) + fun setBluetoothFrequencyState(value: Boolean) fun setRotatorAddress(value: String) fun setRotatorPort(value: String) fun setRotatorState(value: Boolean) + fun setRotatorFormat(value: String) + fun setFrequencyAddress(value: String) + fun setFrequencyPort(value: String) + fun setFrequencyState(value: Boolean) + fun setFrequencyFormat(value: String) + //endregion //region # Other settings