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"
compileSdk = 35
defaultConfig {
applicationId = "com.rtbishop.look4sat"
minSdk = 24
applicationId = "com.rtbishop.look4satic705"
minSdk = 32
targetSdk = 35
versionCode = 400
versionName = "4.0.0"

Wyświetl plik

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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.INTERNET" />
<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
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
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
import android.util.Log
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@ -10,6 +11,7 @@ import androidx.compose.foundation.MarqueeSpacing
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
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
@ -31,11 +33,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -51,6 +55,7 @@ import androidx.navigation.navArgument
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.framework.BluetoothCIV
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
@ -74,6 +79,8 @@ fun NavGraphBuilder.radarDestination(navigateBack: () -> Unit) {
@Composable
private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
BluetoothCIV.init(LocalContext.current)
val addToCalendar: () -> Unit = {
uiState.currentPass?.let { pass ->
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
@ -250,7 +257,20 @@ private fun EclipsedIndicator() {
@Composable
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)) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),

Wyświetl plik

@ -105,7 +105,7 @@ class RadarViewModel(
sendPassData(satPass, pos, satPass.orbitalObject)
sendPassDataBT(pos)
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() }
copiedList.forEach { transmitter ->
transmitter.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) }
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
transmitter.downlinkHigh?.let { transmitter.downlinkHigh = satPos.getDownlinkFreq(it) }
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
transmitter.uplinkHigh?.let { transmitter.uplinkHigh = satPos.getUplinkFreq(it) }
}
copiedList.map { it.copy() }
}