diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/Ic705Controller.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/Ic705Controller.kt index d5196000..3a890583 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/Ic705Controller.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/Ic705Controller.kt @@ -217,6 +217,50 @@ class Ic705Controller( ioMutex.withLock { sendAndWaitAck(cmd) } } + /** + * Read the frequency of the currently active VFO (CMD 0x25 sub 0x00). + * Used for tuning detection in split mode. + */ + override suspend fun readWorkingFrequency(): Long? = withContext(Dispatchers.IO) { + ioMutex.withLock { + val cmd = IcomCivProtocol.buildReadWorkingFreqCommand() + Log.d(tag, "CMD readWorkingFreq → ${IcomCivProtocol.toHex(cmd)}") + val payload = sendAndReadResponse(cmd, IcomCivProtocol.CMD_SELECTED_VFO_FREQ) ?: return@withContext null + // Response payload: [sub] [5 freq bytes] — CMD byte already stripped by parseResponse + Log.d(tag, "readWorkingFreq: got ${payload.size} bytes: ${IcomCivProtocol.toHex(payload)}") + if (payload.size < 6) { + Log.w(tag, "readWorkingFreq: payload too short (${payload.size} bytes)") + return@withContext null + } + val freqBcd = payload.sliceArray(1..5) + val freq = IcomCivProtocol.decodeFrequencyBcd(freqBcd) + Log.d(tag, "readWorkingFreq: ${freq}Hz") + freq + } + } + + /** + * Read the frequency of the inactive/TX VFO (CMD 0x25 sub 0x01). + * Used for tuning detection in split mode. + */ + override suspend fun readTxVfoFrequency(): Long? = withContext(Dispatchers.IO) { + ioMutex.withLock { + val cmd = IcomCivProtocol.buildReadTxVfoFreqCommand() + Log.d(tag, "CMD readTxVfoFreq → ${IcomCivProtocol.toHex(cmd)}") + val payload = sendAndReadResponse(cmd, IcomCivProtocol.CMD_SELECTED_VFO_FREQ) ?: return@withContext null + // Response payload: [sub] [5 freq bytes] — CMD byte already stripped by parseResponse + Log.d(tag, "readTxVfoFreq: got ${payload.size} bytes: ${IcomCivProtocol.toHex(payload)}") + if (payload.size < 6) { + Log.w(tag, "readTxVfoFreq: payload too short (${payload.size} bytes)") + return@withContext null + } + val freqBcd = payload.sliceArray(1..5) + val freq = IcomCivProtocol.decodeFrequencyBcd(freqBcd) + Log.d(tag, "readTxVfoFreq: ${freq}Hz") + freq + } + } + // ── Internal I/O helpers ──────────────────────────────────────────────── /** diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/IcomCivProtocol.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/IcomCivProtocol.kt index e20c0ed7..cc281ed5 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/IcomCivProtocol.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/IcomCivProtocol.kt @@ -187,6 +187,12 @@ object IcomCivProtocol { /** Read operating frequency (CMD 0x03). */ fun buildReadFreqCommand(): ByteArray = frame(CMD_READ_FREQ) + /** Read selected (active) VFO frequency (CMD 0x25 sub 0x00). */ + fun buildReadWorkingFreqCommand(): ByteArray = frame(CMD_SELECTED_VFO_FREQ, SUB_SELECTED_VFO) + + /** Read unselected (inactive/TX in split) VFO frequency (CMD 0x25 sub 0x01). */ + fun buildReadTxVfoFreqCommand(): ByteArray = frame(CMD_SELECTED_VFO_FREQ, SUB_UNSELECTED_VFO) + /** * Select band via CMD 0x1A sub 0x00. * Band codes are BCD-numbered: 1=160m, 2=80m, …, 9=10m, 0x10=6m, 0x11=2m, 0x12=70cm, 0x13=23cm. diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/RadioTrackingService.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/RadioTrackingService.kt index fffc4507..e5a83f7a 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/RadioTrackingService.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/RadioTrackingService.kt @@ -367,21 +367,77 @@ class RadioTrackingService( _state.update { it.copy(txMode = txMode, rxMode = rxMode, txBaseFrequencyHz = txBase) } Log.i(tag, "IC-705 split init done — entering tracking loop") - // ── Tracking loop ─────────────────────────────────────────────────── - // After initial setup we never change VFO or band. - // We only update the active-VFO frequency using CMD 0x25 sub 0x00. - // The IC-705 automatically makes VFO-B active while PTT is pressed. + // ── Tracking loop with tuning detection ───────────────────────────── + var lastSetTxFreq = 0.0 + var lastSetRxFreq = 0.0 + var tuningRadio = "" // "tx" or "rx" when manual tuning detected + var lastReadFreq = 0L + var stableCount = 0 + while (currentCoroutineContext().isActive) { val currentState = _state.value if (!currentState.isActive) break val satPass = currentState.currentPass ?: break val xpdr = currentState.selectedTransponder ?: break - val txBaseFreq = currentState.txBaseFrequencyHz + var txBaseFreq = currentState.txBaseFrequencyHz val stationPos = settingsRepo.stationPosition.value val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, System.currentTimeMillis()) val v = pos.distanceRate * 1000.0 + if (tuningRadio.isNotEmpty()) { + // User is tuning — wait for frequency to stabilize + val readFreq = if (tuningRadio == "tx") radio.readTxVfoFrequency() else radio.readWorkingFrequency() + if (readFreq != null) { + if (kotlin.math.abs(readFreq - lastReadFreq) <= 20) stableCount++ + else { stableCount = 0; lastReadFreq = readFreq } + + if (stableCount >= 2) { + // Frequency stable — reverse-calculate base frequency + if (tuningRadio == "tx" && txBaseFreq != null) { + val newBase = (readFreq.toDouble() * SPEED_OF_LIGHT / (SPEED_OF_LIGHT + v)).toLong() + if (newBase > 0) { + txBaseFreq = newBase + _state.update { it.copy(txBaseFrequencyHz = newBase) } + Log.i(tag, "Split TX tuning done → base=$newBase") + } + } else if (tuningRadio == "rx") { + val rxNominal = (readFreq.toDouble() * SPEED_OF_LIGHT / (SPEED_OF_LIGHT - v)).toLong() + val newTxBase = TransponderMapper.mapDownlinkToUplink(rxNominal, xpdr) + if (newTxBase != null && newTxBase > 0) { + txBaseFreq = newTxBase + _state.update { it.copy(txBaseFrequencyHz = newTxBase) } + Log.i(tag, "Split RX tuning done → txBase=$newTxBase") + } + } + tuningRadio = "" + stableCount = 0 + lastSetTxFreq = 0.0 + lastSetRxFreq = 0.0 + } + } + } else { + // Detect manual dial changes + if (txBaseFreq != null && lastSetTxFreq > 0.0) { + val readTx = radio.readTxVfoFrequency() + if (readTx != null && kotlin.math.abs(readTx - lastSetTxFreq) >= 20.0) { + tuningRadio = "tx" + lastReadFreq = readTx + stableCount = 0 + Log.i(tag, "Split TX tuning detected (read=${readTx}, lastSet=$lastSetTxFreq)") + } + } + if (tuningRadio.isEmpty() && lastSetRxFreq > 0.0) { + val readRx = radio.readWorkingFrequency() + if (readRx != null && kotlin.math.abs(readRx - lastSetRxFreq) >= 20.0) { + tuningRadio = "rx" + lastReadFreq = readRx + stableCount = 0 + Log.i(tag, "Split RX tuning detected (read=${readRx}, lastSet=$lastSetRxFreq)") + } + } + } + // Determine Doppler-corrected frequencies val txRadioFreq = txBaseFreq?.let { pos.getUplinkFreq(it) } val rxBaseCalc = if (txBaseFreq != null) { @@ -389,18 +445,20 @@ class RadioTrackingService( } else xpdr.downlinkLow val rxRadioFreq = rxBaseCalc?.let { pos.getDownlinkFreq(it) } - if (radio.isConnected) { - // Update both VFOs every cycle — no PTT polling needed. - // 0x25/00 = active (RX) VFO, 0x25/01 = inactive (TX) VFO. - if (rxRadioFreq != null) { - Log.d(tag, "Split loop RX (0x25/00): ${rxRadioFreq}Hz") - radio.setWorkingFrequency(rxRadioFreq) - } - if (txRadioFreq != null) { - Log.d(tag, "Split loop TX (0x25/01): ${txRadioFreq}Hz") - radio.setTxVfoFrequency(txRadioFreq) - } + if (radio.isConnected && tuningRadio.isEmpty()) { + // Update both VFOs every cycle — no PTT polling needed. + // 0x25/00 = active (RX) VFO, 0x25/01 = inactive (TX) VFO. + if (rxRadioFreq != null) { + Log.d(tag, "Split loop RX (0x25/00): ${rxRadioFreq}Hz") + radio.setWorkingFrequency(rxRadioFreq) + lastSetRxFreq = rxRadioFreq.toDouble() } + if (txRadioFreq != null) { + Log.d(tag, "Split loop TX (0x25/01): ${txRadioFreq}Hz") + radio.setTxVfoFrequency(txRadioFreq) + lastSetTxFreq = txRadioFreq.toDouble() + } + } _state.update { it.copy( diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IRadioController.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IRadioController.kt index 80a00b49..74bb08bd 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IRadioController.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IRadioController.kt @@ -72,4 +72,17 @@ interface IRadioController { * Default: delegates to [setWorkingFrequency]. */ suspend fun setTxVfoFrequency(frequencyHz: Long): Boolean = setWorkingFrequency(frequencyHz) + + /** + * Read the frequency of the currently active VFO (IC-705: CMD 0x25 sub 0x00). + * Default: delegates to [readFrequencyAndMode]. + */ + suspend fun readWorkingFrequency(): Long? = readFrequencyAndMode()?.first + + /** + * Read the frequency of the inactive/TX VFO (IC-705: CMD 0x25 sub 0x01). + * Used for tuning detection in split mode. + * Default: delegates to [readWorkingFrequency]. + */ + suspend fun readTxVfoFrequency(): Long? = readWorkingFrequency() }