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 c02d37f3..d5196000 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 @@ -75,7 +75,12 @@ class Ic705Controller( outputStream = btSocket.outputStream inputStream = btSocket.inputStream isConnected = true - Log.i(tag, "Connected to $deviceAddress") + // Enter VFO mode — frequency/mode commands return FA if the radio + // is in memory-channel mode. Safe to send regardless of current state. + Log.i(tag, "Connected to $deviceAddress — entering VFO mode") + val vfoCmd = IcomCivProtocol.buildEnterVfoModeCommand() + Log.d(tag, "CMD enterVfoMode → ${IcomCivProtocol.toHex(vfoCmd)}") + ioMutex.withLock { sendAndWaitAck(vfoCmd) } true } catch (e: Exception) { Log.e(tag, "Connect error: ${e.message}") @@ -162,6 +167,16 @@ class Ic705Controller( // ── IRadioController – IC-705 extended operations ─────────────────────── + /** Select the band for [frequencyHz] via CMD 0x1A sub 0x00 (band stacking register). */ + override suspend fun setBand(frequencyHz: Long): Boolean = withContext(Dispatchers.IO) { + val cmd = IcomCivProtocol.buildBandSelectCommand(frequencyHz) ?: run { + Log.w(tag, "setBand: no band code for ${frequencyHz}Hz — skipping") + return@withContext false + } + Log.d(tag, "CMD setBand (${frequencyHz}Hz) → ${IcomCivProtocol.toHex(cmd)}") + ioMutex.withLock { sendAndWaitAck(cmd) } + } + /** Select VFO-A (main/RX) or VFO-B (sub/TX). */ override suspend fun setVfo(vfoA: Boolean): Boolean = withContext(Dispatchers.IO) { val cmd = if (vfoA) IcomCivProtocol.buildSelectVfoACommand() 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 1b8b807e..215236c3 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 @@ -51,8 +51,16 @@ object IcomCivProtocol { const val CMD_SET_MODE: Byte = 0x06 /** Select VFO / memory. */ const val CMD_SELECT_VFO: Byte = 0x07 + /** + * Select operating mode (VFO vs memory-channel). + * Sub 0x00 = VFO mode. Must be sent after connect if the radio is in + * memory-channel mode — frequency/mode commands return FA until it is. + */ + const val CMD_SELECT_OP_MODE: Byte = 0x08 /** Set repeater duplex / SPLIT. */ const val CMD_DUPLEX_SPLIT: Byte = 0x0F + /** Band stacking register / band select (sub 0x00 = select, data = BCD band number). */ + const val CMD_BAND_SELECT: Byte = 0x1A /** Read/write CTCSS tone frequency. */ const val CMD_CTCSS_TONE: Byte = 0x1B /** Read/write misc settings (used for enabling CTCSS encode). */ @@ -178,10 +186,41 @@ object IcomCivProtocol { /** Read operating frequency (CMD 0x03). */ fun buildReadFreqCommand(): ByteArray = frame(CMD_READ_FREQ) - /** Set operating mode (CMD 0x06). Filter byte 0x00 = keep current filter. */ + /** + * 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. + * Returns null if [frequencyHz] doesn't fall in a known amateur band. + */ + fun buildBandSelectCommand(frequencyHz: Long): ByteArray? { + val code = bandCodeForFrequency(frequencyHz) ?: return null + return frame(CMD_BAND_SELECT, 0x00, code) + } + + /** + * Map a frequency in Hz to the IC-705 band stacking register code. + * Codes are BCD (band number in decimal expressed as hex nibbles). + */ + fun bandCodeForFrequency(frequencyHz: Long): Byte? = when { + frequencyHz in 1_800_000L ..1_999_999L -> 0x01 // 160 m + frequencyHz in 3_500_000L ..3_999_999L -> 0x02 // 80 m + frequencyHz in 7_000_000L ..7_299_999L -> 0x03 // 40 m + frequencyHz in 10_100_000L ..10_149_999L -> 0x04 // 30 m + frequencyHz in 14_000_000L ..14_349_999L -> 0x05 // 20 m + frequencyHz in 18_068_000L ..18_167_999L -> 0x06 // 17 m + frequencyHz in 21_000_000L ..21_449_999L -> 0x07 // 15 m + frequencyHz in 24_890_000L ..24_989_999L -> 0x08 // 12 m + frequencyHz in 28_000_000L ..29_699_999L -> 0x09 // 10 m + frequencyHz in 50_000_000L ..53_999_999L -> 0x10 // 6 m (BCD 10) + frequencyHz in 144_000_000L ..147_999_999L -> 0x11 // 2 m (BCD 11) + frequencyHz in 420_000_000L ..449_999_999L -> 0x12 // 70 cm (BCD 12) + frequencyHz in 1_240_000_000L ..1_299_999_999L -> 0x13 // 23 cm (BCD 13) + else -> null + } + + /** Set operating mode (CMD 0x06). Filter byte is omitted — radio uses its default filter for the mode. */ fun buildSetModeCommand(mode: String): ByteArray? { val modeByte = MODE_TO_BYTE[mode.uppercase(Locale.US)] ?: return null - return frame(CMD_SET_MODE, modeByte, 0x00) + return frame(CMD_SET_MODE, modeByte) } /** Select VFO-A (CMD 0x07 sub 0x00). */ @@ -190,6 +229,13 @@ object IcomCivProtocol { /** Select VFO-B (CMD 0x07 sub 0x01). */ fun buildSelectVfoBCommand(): ByteArray = frame(CMD_SELECT_VFO, SUB_VFO_B) + /** + * Enter VFO operating mode (CMD 0x08 sub 0x00). + * Sent after connect — if the radio is in memory-channel mode frequency + * and mode commands return FA until this is issued. + */ + fun buildEnterVfoModeCommand(): ByteArray = frame(CMD_SELECT_OP_MODE, 0x00) + /** Enable or disable SPLIT mode (CMD 0x0F). */ fun buildSplitModeCommand(enable: Boolean): ByteArray { val sub = if (enable) SUB_SPLIT_ON else SUB_SPLIT_OFF 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 fa43c7dd..fffc4507 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 @@ -47,6 +47,8 @@ class RadioTrackingService( ) : IRadioTrackingService { private val tag = "RadioTracking" + /** Delay between each step of the split-mode init sequence (ms). */ + private val INIT_STEP_DELAY_MS = 200L private val _state = MutableStateFlow(RadioTrackingState()) override val state: StateFlow = _state @@ -316,10 +318,15 @@ class RadioTrackingService( Log.i(tag, "IC-705 split setup: txBase=${txBase}Hz rxNominal=${rxNominal}Hz txMode=$txMode rxMode=$rxMode") // ── Initial setup sequence ────────────────────────────────────────── - // 1. Select VFO-A (will be RX VFO), set downlink frequency and mode + // Sequence per IC-705: explicitly select VFO, then band → freq → mode. + // ACK from each command gates the next — no fixed delays needed. + + // VFO-A = RX (downlink) Log.d(tag, "Split init: selecting VFO-A for RX (downlink)") radio.setVfo(vfoA = true) if (rxNominal != null) { + Log.d(tag, "Split init: VFO-A band for ${rxNominal}Hz") + radio.setBand(rxNominal) Log.d(tag, "Split init: VFO-A freq=${rxNominal}Hz") radio.setFrequency(rxNominal) } @@ -328,10 +335,12 @@ class RadioTrackingService( radio.setMode(rxMode) } - // 2. Select VFO-B (will be TX VFO), set uplink frequency, mode, and CTCSS + // VFO-B = TX (uplink) Log.d(tag, "Split init: selecting VFO-B for TX (uplink)") radio.setVfo(vfoA = false) if (txBase != null) { + Log.d(tag, "Split init: VFO-B band for ${txBase}Hz") + radio.setBand(txBase) Log.d(tag, "Split init: VFO-B freq=${txBase}Hz") radio.setFrequency(txBase) } @@ -340,19 +349,20 @@ class RadioTrackingService( radio.setMode(txMode) } if (txMode?.uppercase() == "FM") { - _state.value.ctcssTone?.let { tone -> + val tone = _state.value.ctcssTone + if (tone != null) { Log.d(tag, "Split init: CTCSS=${tone}Hz") radio.setCtcssTone(tone) radio.setCtcssMode(true) + } else { + radio.setCtcssMode(false) } } - // 3. Enable SPLIT — radio will now TX on VFO-B while listening on VFO-A - Log.d(tag, "Split init: enabling SPLIT mode") - radio.setSplitMode(enabled = true) - - // Return to VFO-A so the display shows RX frequency + // Enable SPLIT on VFO-A (return display to RX VFO first) + Log.d(tag, "Split init: returning to VFO-A, then enabling SPLIT mode") radio.setVfo(vfoA = true) + radio.setSplitMode(enabled = true) _state.update { it.copy(txMode = txMode, rxMode = rxMode, txBaseFrequencyHz = txBase) } Log.i(tag, "IC-705 split init done — entering tracking loop") 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 cd198f4b..80a00b49 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 @@ -41,10 +41,16 @@ interface IRadioController { // ── Extended operations (IC-705 / CI-V) ────────────────────────────── + /** + * Select the band matching [frequencyHz] via the band stacking register. + * Must be called before [setFrequency] and [setMode] when first tracking. + * Default: no-op (Yaesu radios auto-switch band via frequency). + */ + suspend fun setBand(frequencyHz: Long): Boolean = false + /** * Select the active VFO. * @param vfoA true → VFO-A (main/RX), false → VFO-B (sub/TX in split). - * Default: no-op. */ suspend fun setVfo(vfoA: Boolean): Boolean = false