Improvised solution to transmit frequencies to IC705

develop
Rico Beier-Grunwald 2025-02-14 18:48:11 +01:00 zatwierdzone przez Arty Bishop
rodzic 20ba2be2ac
commit e23ebb03ae
7 zmienionych plików z 318 dodań i 6 usunięć

Wyświetl plik

@ -13,8 +13,8 @@ android {
namespace = "com.rtbishop.look4sat" namespace = "com.rtbishop.look4sat"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.rtbishop.look4sat" applicationId = "com.rtbishop.look4satic705"
minSdk = 24 minSdk = 32
targetSdk = 35 targetSdk = 35
versionCode = 400 versionCode = 400
versionName = "4.0.0" versionName = "4.0.0"

Wyświetl plik

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />

Wyświetl plik

@ -28,7 +28,6 @@ import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.MainScreen import com.rtbishop.look4sat.presentation.MainScreen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
enableEdgeToEdge() enableEdgeToEdge()
@ -40,3 +39,5 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
// Check and Request BT Permission?

Wyświetl plik

@ -1,5 +1,6 @@
package com.rtbishop.look4sat.presentation.radar package com.rtbishop.look4sat.presentation.radar
import android.util.Log
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
@ -10,6 +11,7 @@ import androidx.compose.foundation.MarqueeSpacing
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -31,11 +33,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -51,6 +55,7 @@ import androidx.navigation.navArgument
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.framework.BluetoothCIV
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon import com.rtbishop.look4sat.presentation.components.CardIcon
@ -74,6 +79,8 @@ fun NavGraphBuilder.radarDestination(navigateBack: () -> Unit) {
@Composable @Composable
private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) { private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
BluetoothCIV.init(LocalContext.current)
val addToCalendar: () -> Unit = { val addToCalendar: () -> Unit = {
uiState.currentPass?.let { pass -> uiState.currentPass?.let { pass ->
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime)) uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
@ -250,7 +257,20 @@ private fun EclipsedIndicator() {
@Composable @Composable
private fun TransmitterItem(radio: SatRadio) { private fun TransmitterItem(radio: SatRadio) {
Surface(color = MaterialTheme.colorScheme.background) { LaunchedEffect(radio) {
BluetoothCIV.updateOnce(radio)
}
Surface(color = MaterialTheme.colorScheme.background,
modifier = Modifier.clickable(onClick = {
Log.d("BluetoothCivManager", radio.toString())
if(radio.uuid == BluetoothCIV.selected) BluetoothCIV.selected = "NONE" else
{
BluetoothCIV.connect(radio)
BluetoothCIV.updateOnce(radio)
}
})
) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) { Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),

Wyświetl plik

@ -105,7 +105,7 @@ class RadarViewModel(
sendPassData(satPass, pos, satPass.orbitalObject) sendPassData(satPass, pos, satPass.orbitalObject)
sendPassDataBT(pos) sendPassDataBT(pos)
processRadios(transmitters, satPass.orbitalObject, timeNow) processRadios(transmitters, satPass.orbitalObject, timeNow)
delay(1000) delay(1000) //TODO: Change me maybe if smaller freq steps are better?? maybe dynamically?
} }
} }
} }

Wyświetl plik

@ -0,0 +1,285 @@
package com.rtbishop.look4sat.framework
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothSocket
import android.util.Log
import java.io.OutputStream
import java.util.UUID
import java.io.IOException
import com.rtbishop.look4sat.domain.model.SatRadio
import android.content.Context
import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
import java.io.InputStream
enum class Mode(val value: Int) {
LSB(0),
USB(1),
AM(2),
CW(3),
FMN(5),
FM(5),
DSTAR(17);
companion object {
fun fromString(name: String): Int {
return values().find { it.name.equals(name, ignoreCase = true) }?.value ?: 1
}
}
}
fun getBluetoothAdapter(context: Context): BluetoothAdapter? {
val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
return bluetoothManager?.adapter
}
public object BluetoothCIV {
// UUID for Serial Port Service
private val SERIAL_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
// Bluetooth adapter
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothSocket: BluetoothSocket? = null
// CI-V Address of the ICOM IC-705 (verify for your radio)
private val CIV_ADDRESS = 0xA4
private var outputStream: OutputStream? = null
private var inputStream: InputStream? = null
public fun init(context: Context) {
bluetoothAdapter= getBluetoothAdapter(context)
}
private fun ensureConnected():Boolean {
if( bluetoothSocket?.isConnected != true) {
if (bluetoothSocket != null) {
try {
bluetoothSocket?.close()
} catch (e: IOException) {
Log.e("BluetoothCivManager", "Fehler beim Schließen des alten Sockets", e)
}
bluetoothSocket = null
}
val dev = bluetoothAdapter?.bondedDevices?.firstOrNull { device -> device.name == "ICOM BT(IC-705)"}
if(dev==null) {
Log.e("BluetoothCivManager", "Gerät nicht gefunden")
return false
} else {
try{
val localSocket = dev.createRfcommSocketToServiceRecord(SERIAL_UUID)
localSocket.connect()
outputStream = localSocket.outputStream
inputStream = localSocket.inputStream
bluetoothSocket = localSocket
// delay(100);
Thread.sleep(100);
Log.d("BluetoothCivManager", "Verbindung aufgebaut")
} catch (e: IOException) {
Log.e("BluetoothCivManager", "Verbindung nicht aufgebaut")
bluetoothSocket = null // Falls fehlgeschlagen, Socket auf null setzen
return false
}
}
}
return true
}
private fun sendCommand(cmd: Int, sub: Int? = null, data: ByteArray? = null) {
outputStream!!.write(0xfe);
outputStream!!.write(0xfe);
outputStream!!.write(CIV_ADDRESS);
outputStream!!.write(0xe0);
outputStream!!.write(cmd);
if (sub != null) {
outputStream!!.write(sub)
}
if (data != null) {
outputStream!!.write(data)
}
outputStream!!.write(0xfd)
outputStream!!.flush();
}
data class RMsg(val broadcast: Boolean, val cmd: Int, val payload: ByteArray)
private fun receiveCommand():RMsg? {
if (inputStream!!.read()!=0xFE) {
return null;
}
if (inputStream!!.read()!=0xFE) {
return null;
}
val to = inputStream!!.read();
if (inputStream!!.read()!= CIV_ADDRESS) {
return null;
}
val cmd = inputStream!!.read();
val buffer = mutableListOf<Byte>()
var byte: Int
while (inputStream!!.read().also { byte = it } != -1) {
if (byte == 0xFD) break
buffer.add(byte.toByte())
}
return RMsg(to==0x00, cmd, buffer.toByteArray())
}
private fun receiveSpecificCommand():RMsg? {
while (true) {
val msg = receiveCommand() ?: return null
if (!msg.broadcast) {
return msg
}
}
}
private fun callProcedure(cmd: Int, sub: Int? = null, data: ByteArray? = null):Boolean {
if(ensureConnected()){
sendCommand(cmd, sub, data)
val res = receiveSpecificCommand();
return res?.cmd == 0xFB;
} else {
return false;
}
}
data class Result(val subcmd: Int?, val data: ByteArray)
private fun callFunction(cmd: Int, sub: Int? = null, retsub: Boolean): Result{
if(ensureConnected()){
sendCommand(cmd, sub)
val res = receiveSpecificCommand();
if (res!=null){
if (cmd!=res.cmd) {
Log.d("BluetoothCivManager", "Wrong command $cmd != $res.cmd")
return Result(null, byteArrayOf());
}
val subcmd = if (retsub) res.payload[0].toInt() else null;
val data = res.payload.copyOfRange((if (retsub) 1 else 0), res.payload.size);
return Result(subcmd, data)
}else {
Log.d("BluetoothCivManager", "No Reply")
return Result(null, byteArrayOf());
}
} else {
Log.d("BluetoothCivManager", "NotSure")
return Result(null, byteArrayOf());
}
}
public var selected: String = "NONE";
public var isTransponder: Boolean = false;
public var lastDownlinkLow: Long? = null;
public var lastDownlinkHigh: Long? = null;
private fun frequencyToBCD(frequency: Long): ByteArray {
var n = frequency
val bcd = ByteArray(5)
// Process each two-digit group, starting from the rightmost group.
for (i in 4 downTo 0) {
// Extract the last two digits (a value between 0 and 99)
val twoDigits = (n % 100).toInt()
// Pack the tens digit into the high nibble and the ones digit into the low nibble
bcd[i] = (((twoDigits / 10) shl 4) or (twoDigits % 10)).toByte()
// Remove the two digits we just processed
n /= 100
}
return bcd
}
// Function to convert BCD format back to a frequency (Long)
private fun bcdToFrequency(bcd: ByteArray): Long {
var frequency = 0L
for (byte in bcd) {
// Extract the high nibble (first decimal digit)
val high = (byte.toInt() shr 4) and 0x0F
// Extract the low nibble (second decimal digit)
val low = byte.toInt() and 0x0F
// Combine them into the frequency
frequency = frequency * 100 + high * 10 + low
}
return frequency
}
private fun setVFO(A: Boolean): Boolean {
return callProcedure(0x07, if (A) 0x00 else 0x01);
}
private fun setSplit(On: Boolean): Boolean {
return callProcedure(0x0F, if (On) 0x01 else 0x00);
}
private fun setFrequency(Main: Boolean, Frequency: Long): Boolean {
return callProcedure(0x25, if (Main) 0x00 else 0x01, frequencyToBCD(Frequency).reversedArray());
}
private fun getFrequency(Main: Boolean): Long {
val res = callFunction(0x25, if (Main) 0x00 else 0x01, true)
return if (res.data.isNotEmpty()){
bcdToFrequency(res.data.reversedArray());
} else {
-1
}
}
private fun setMode(Main: Boolean, mode: String): Boolean {
return callProcedure(0x26, if (Main) 0x00 else 0x01, byteArrayOf(Mode.fromString(mode).toByte(), 0x00.toByte(), 0x01.toByte()));
}
@SuppressLint("MissingPermission")
public fun connect(radio: SatRadio) {
setVFO(true)
setSplit(true)
setMode(true, radio.downlinkMode?:"USB")
setMode(false, radio.uplinkMode?:"USB")
selected = radio.uuid;
isTransponder = radio.downlinkHigh != null;
lastDownlinkLow = null;
lastDownlinkHigh = null;
// Info on tone?
}
fun Double.clamp(min: Double, max: Double): Double {
return when {
this < min -> min
this > max -> max
else -> this
}
}
// Function to send the CI-V command to the ICOM radio over Bluetooth
public fun updateOnce(radio: SatRadio) {
if(radio.uuid != selected) return;
if(isTransponder) {
if (lastDownlinkLow != null && lastDownlinkHigh != null) {
val actual = getFrequency(true)
val pos = ((actual - lastDownlinkLow!!).toDouble() / (lastDownlinkHigh!! - lastDownlinkLow!!)).clamp(0.0,1.0);
Log.d("BluetoothCivManager", "Pos: $pos");
radio.downlinkLow?.let{
setFrequency(true, (radio.downlinkLow!! + pos * (radio.downlinkHigh!!-radio.downlinkLow!!)).toLong());
}
radio.uplinkLow?.let {
setFrequency(false, (radio.uplinkLow!! + pos * (radio.uplinkHigh!!-radio.uplinkLow!!)).toLong())
}
} else {
radio.downlinkLow?.let{
setFrequency(true, it);
}
radio.uplinkLow?.let {
setFrequency(false, it)
}
}
lastDownlinkLow = radio.downlinkLow;
lastDownlinkHigh = radio.downlinkHigh;
}
else {
radio.downlinkLow?.let{
setFrequency(true, it);
}
radio.uplinkLow?.let {
setFrequency(false, it)
}
}
}
}

Wyświetl plik

@ -82,7 +82,9 @@ class SatelliteRepo(
val copiedList = radios.map { it.copy() } val copiedList = radios.map { it.copy() }
copiedList.forEach { transmitter -> copiedList.forEach { transmitter ->
transmitter.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) } transmitter.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) }
transmitter.downlinkHigh?.let { transmitter.downlinkHigh = satPos.getDownlinkFreq(it) }
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) } transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
transmitter.uplinkHigh?.let { transmitter.uplinkHigh = satPos.getUplinkFreq(it) }
} }
copiedList.map { it.copy() } copiedList.map { it.copy() }
} }