diff --git a/docs/user/docs/before-you-transmit.md b/docs/user/docs/before-you-transmit.md index 55d7caff5e..a1a66de58f 100644 --- a/docs/user/docs/before-you-transmit.md +++ b/docs/user/docs/before-you-transmit.md @@ -63,6 +63,7 @@ The checks are written so you can do them without putting a signal on the air. W - Controller buttons can be bound to PTT, MOX or TUNE: [USB Control Surfaces](./usb-control-surfaces.md) (including serial PTT and foot switches), [MIDI Controller Mapping](./midi-controller-mapping.md), [Ulanzi Dial](./ulanzi-dial.md), [StreamDeck](./streamdeck.md) and the [FlexControl Tuning Knob](./flexcontrol-tuning-knob.md). - CAT and TCI clients can key the radio. See [CAT Control](./cat-control.md) and [TCI Server](./tci-server.md). - The automation bridge cannot key the transmitter unless **Allow TX via MCP** is ticked, which is off by default. Leave it off unless you need it. See [Automation Bridge and MCP](./automation-bridge-and-mcp.md). +- [ ] **(FlexRadio, firmware v4.3 and later)** **You know what your footswitch transmits.** A hardware PTT now sends the selected TX source, DAX or PC audio included, unless **PTT Mic Override** is on. See [Hardware PTT and the TX source](./tx-controls.md#hardware-ptt-and-the-tx-source). - [ ] **(FlexRadio)** **The radio's transmit time-out and interlocks are set:** **Timeout (sec):** and the **Interlocks - TX REQ** group on the **Transmit** page of Radio Setup. See [Radio Setup](./radio-setup.md). - [ ] **You can see when you are transmitting.** A green `M:SS` transmit timer appears in the title bar, just left of **PC Audio**, for each of your own transmissions, and the VFO marker turns orange/red. If an interlock blocks transmit, a notification says why. See [TX Controls](./tx-controls.md). diff --git a/docs/user/docs/radio-setup.md b/docs/user/docs/radio-setup.md index 361ee51f62..ed93aee0c5 100644 --- a/docs/user/docs/radio-setup.md +++ b/docs/user/docs/radio-setup.md @@ -70,7 +70,7 @@ their nicknames on this computer. | Page | What it covers | |------|----------------| | **Audio** | The radio's Line Out, Headphone and Front Speaker outputs; audio compression for SmartLink and tailnet connections; packet-loss smoothing; **Prevent system sleep while connected**; PC audio devices, Audio Boost and Audio Buffer; recording. See [Audio Settings](./audio-settings.md). | -| **Transmit** | TX timings (ACC TX, TX delay, timeout), TX REQ interlock polarity (RCA, Accessory), Max Power, Show TX in Waterfall, Slice/TX Follow, and **TX Band Settings** (per-band RF/tune power, PTT inhibit and interlocks). The Show TX in Waterfall choice is remembered per [Hermes-Lite 2](./hermes-lite-2.md); a FlexRadio owns and reports its setting. See [TX Controls](./tx-controls.md). | +| **Transmit** | TX timings (ACC TX, TX delay, timeout), TX REQ interlock polarity (RCA, Accessory), **PTT Mic Override** (firmware v4.3 and later; see [TX Controls](./tx-controls.md#hardware-ptt-and-the-tx-source)), Max Power, Show TX in Waterfall, Slice/TX Follow, and **TX Band Settings** (per-band RF/tune power, PTT inhibit and interlocks). The Show TX in Waterfall choice is remembered per [Hermes-Lite 2](./hermes-lite-2.md); a FlexRadio owns and reports its setting. See [TX Controls](./tx-controls.md). | | **Phone & CW** | Microphone bias and +20 dB boost; CW iambic mode, paddle swap, sideband and CWX sync, and CW decoder RX/TX decode; Digital (RTTY mark default). See [CW Decoder](./cw-decoder.md) and [CWX Panel](./cwx-panel.md). | | **Receive** | Frequency offset (GPSDO detection, manual calibration) and the 10 MHz reference source | | **Filters** | Voice, CW and Digital filter sharpness (Low Latency ↔ Sharp Filters) with Auto buttons, and **Use Low Latency Filters for Digital Modes** | diff --git a/docs/user/docs/tx-controls.md b/docs/user/docs/tx-controls.md index c05e1732e8..1431092064 100644 --- a/docs/user/docs/tx-controls.md +++ b/docs/user/docs/tx-controls.md @@ -148,6 +148,25 @@ CW is excluded, and the card is not available on the Hermes-Lite 2. not allowed) appear as notifications, only when transmit is actually blocked +### Hardware PTT and the TX source + +**(FlexRadio)** A hardware PTT is a footswitch or keying line on the RCA PTT +jack, the ACC connector or a supported mic-jack PTT input. On firmware before +SmartSDR v4.3, a hardware PTT always transmits the analog microphone. **From +v4.3, a hardware PTT transmits whatever TX source is selected**, just as MOX +does. If DAX is on for a digital-mode program, or the mic source is PC, a +footswitch press puts that audio on the air, not your microphone. + +To restore the old behavior, turn on **PTT Mic Override** in the +**Interlocks - TX REQ** group of **Settings → Radio Setup... → Transmit**. +With it on, a hardware PTT transmits the analog mic input you last used, even +while DAX or PC audio is selected. It changes nothing when an analog input +(MIC, BAL, LINE or ACC) is already selected. The radio stores the setting and +shares it with every client connected to it. With several clients connected +(Multi-Flex), FlexRadio says the override applies when the client whose +hardware PTT is pressed is also the transmitting client. On firmware before +v4.3 the control is grayed out. + ### Transmit timer While you transmit, a green `M:SS` timer (`H:MM:SS` after an hour) appears in diff --git a/docs/user/docs/wsjt-x-integration.md b/docs/user/docs/wsjt-x-integration.md index 48689d32ed..9588b50299 100644 --- a/docs/user/docs/wsjt-x-integration.md +++ b/docs/user/docs/wsjt-x-integration.md @@ -82,6 +82,7 @@ AetherSDR can listen to WSJT-X's UDP decode messages and put the stations it dec - FT8 transmitted on slice B can show heavy ALC and be decoded far less often than the same signal on slice A ([#5340](https://github.com/aethersdr/AetherSDR/issues/5340)). - Over TCI, the mic profile, **MIC** level and **PROC** in the Phone/CW applet change the transmitted digital signal ([#5844](https://github.com/aethersdr/AetherSDR/issues/5844)). - Over TCI, the transmitted FT8 signal can show random jumps ([#5133](https://github.com/aethersdr/AetherSDR/issues/5133)). +- **(FlexRadio, firmware v4.3 and later)** A footswitch or other hardware PTT transmits the selected TX source, so with DAX on it sends WSJT-X's audio, not your microphone. Turn on **PTT Mic Override** to make a hardware PTT use the microphone instead. See [Hardware PTT and the TX source](./tx-controls.md#hardware-ptt-and-the-tx-source). - On macOS, DAX transmit can stop sending audio to the radio until AetherSDR is restarted ([#5870](https://github.com/aethersdr/AetherSDR/issues/5870)). ## Troubleshooting diff --git a/src/core/AutomationServer.cpp b/src/core/AutomationServer.cpp index f5846426de..56b9e401d0 100644 --- a/src/core/AutomationServer.cpp +++ b/src/core/AutomationServer.cpp @@ -1922,6 +1922,10 @@ QJsonObject transmitSnapshot(const TransmitModel* t, snapshot.insert(QStringLiteral("txFilterLow"), t->txFilterLow()); snapshot.insert(QStringLiteral("txFilterHigh"), t->txFilterHigh()); } + // Absent until the radio reports it: firmware before v4.3 has no such key. + if (t->havePttMicOverride()) { + snapshot.insert(QStringLiteral("pttMicOverride"), t->pttMicOverride()); + } return snapshot; } diff --git a/src/core/backends/ModelControlRequest.h b/src/core/backends/ModelControlRequest.h index f30062940f..bd189d9bdf 100644 --- a/src/core/backends/ModelControlRequest.h +++ b/src/core/backends/ModelControlRequest.h @@ -48,7 +48,7 @@ struct TransmitControlRequest { ExpanderEnabled, ExpanderLevel, Filter, CwSpeed, CwPitch, CwBreakIn, CwDelay, CwSidetone, CwIambic, CwIambicMode, CwSwap, CwlEnabled, CwMonitorGain, CwMonitorPan, TxProfile, MicProfile, ApdEnabled, - ApdSampler, ApdReset, AtuMemories, AtuClear, Count + ApdSampler, ApdReset, AtuMemories, AtuClear, PttMicOverride, Count }; using Value = std::variant; @@ -158,6 +158,7 @@ inline bool TransmitControlRequest::valid() const case Field::ExpanderEnabled: case Field::CwBreakIn: case Field::CwSidetone: case Field::CwIambic: case Field::CwSwap: case Field::CwlEnabled: case Field::ApdEnabled: case Field::ApdReset: case Field::AtuMemories: case Field::AtuClear: + case Field::PttMicOverride: return std::holds_alternative(value); case Field::Count: return false; } @@ -238,6 +239,7 @@ inline QString TransmitControlRequest::label() const case Field::ApdSampler: return QStringLiteral("apd sampler"); case Field::ApdReset: return QStringLiteral("apd reset"); case Field::AtuMemories: return QStringLiteral("atu memories"); + case Field::PttMicOverride: return QStringLiteral("ptt mic override"); case Field::AtuClear: return QStringLiteral("atu clear"); case Field::Count: break; } diff --git a/src/core/backends/TransmitDelta.h b/src/core/backends/TransmitDelta.h index 411a2ede29..0b32b0c9eb 100644 --- a/src/core/backends/TransmitDelta.h +++ b/src/core/backends/TransmitDelta.h @@ -25,6 +25,7 @@ struct TransmitDelta { std::optional txSliceMode; std::optional showTxInWaterfall; std::optional metInRx; + std::optional pttMicOverride; // v4.3+ firmware only // ── Mic / monitor / processor ── std::optional micSelection; // uppercased diff --git a/src/core/backends/flex/FlexBackend.cpp b/src/core/backends/flex/FlexBackend.cpp index 949f562f85..558605db85 100644 --- a/src/core/backends/flex/FlexBackend.cpp +++ b/src/core/backends/flex/FlexBackend.cpp @@ -1389,6 +1389,11 @@ void FlexBackend::decodeTransmitStatus(const QMap& kvs) carry(kvs, "mic_boost", d.micBoost); carry(kvs, "mic_bias", d.micBias); carry(kvs, "met_in_rx", d.metInRx); + // FlexLib 4.3.16.42034 rejects anything but 0/1 (Radio.cs:11151-11163): + // a malformed value must not confirm "off" or mark the key as reported. + if (const std::optional v = parseInt(kvs, "ptt_mic_override"); v && (*v == 0 || *v == 1)) { + d.pttMicOverride = (*v == 1); + } carry(kvs, "synccwx", d.syncCwx); carryClamp(kvs, "am_carrier_level", d.amCarrierLevel, 0, 100); // dexp / noise_gate_level alias compander / compander_level, but only when diff --git a/src/core/backends/flex/FlexModelControls.cpp b/src/core/backends/flex/FlexModelControls.cpp index a7e87c8d6a..f111b30963 100644 --- a/src/core/backends/flex/FlexModelControls.cpp +++ b/src/core/backends/flex/FlexModelControls.cpp @@ -138,6 +138,8 @@ ReceiveDispatch FlexBackend::requestTransmitControl(const TransmitControlRequest command = QStringLiteral("apd enable=") + QString::number(int(std::get(request.value))); break; case Field::AtuMemories: command = QStringLiteral("atu set memories_enabled=") + QString::number(int(std::get(request.value))); break; + case Field::PttMicOverride: + command = QStringLiteral("transmit set ptt_mic_override=") + QString::number(int(std::get(request.value))); break; case Field::ProcessorEnabled: case Field::ProcessorLevel: { const ProcessorSetting setting = std::get(request.value); if (request.field == Field::ProcessorLevel && setting.level > 2) { diff --git a/src/core/backends/hl2/Hl2Backend.cpp b/src/core/backends/hl2/Hl2Backend.cpp index 9c7e7a76c7..97ced4c742 100644 --- a/src/core/backends/hl2/Hl2Backend.cpp +++ b/src/core/backends/hl2/Hl2Backend.cpp @@ -3338,7 +3338,8 @@ ReceiveDispatch Hl2Backend::requestTransmitControl(const TransmitControlRequest& case Field::MicBias: case Field::AmCarrier: case Field::ExpanderEnabled: case Field::ExpanderLevel: case Field::CwlEnabled: case Field::TxProfile: case Field::MicProfile: case Field::ApdEnabled: case Field::ApdSampler: - case Field::ApdReset: case Field::AtuMemories: case Field::AtuClear: case Field::Count: + case Field::ApdReset: case Field::AtuMemories: case Field::AtuClear: + case Field::PttMicOverride: case Field::Count: return ReceiveDispatch::Unsupported; } return ReceiveDispatch::Dispatched; diff --git a/src/core/backends/icom/IcomCivBackend.cpp b/src/core/backends/icom/IcomCivBackend.cpp index fc99537522..2c5053e8ba 100644 --- a/src/core/backends/icom/IcomCivBackend.cpp +++ b/src/core/backends/icom/IcomCivBackend.cpp @@ -4521,7 +4521,8 @@ ReceiveDispatch IcomCivBackend::requestTransmitControl(const TransmitControlRequ case Field::CwIambicMode: case Field::CwSwap: case Field::CwlEnabled: case Field::CwMonitorGain: case Field::CwMonitorPan: case Field::TxProfile: case Field::MicProfile: case Field::ApdEnabled: case Field::ApdSampler: - case Field::ApdReset: case Field::AtuMemories: case Field::AtuClear: case Field::Count: + case Field::ApdReset: case Field::AtuMemories: case Field::AtuClear: + case Field::PttMicOverride: case Field::Count: return ReceiveDispatch::Unsupported; } return ReceiveDispatch::Dispatched; diff --git a/src/gui/RadioSetupDialog.cpp b/src/gui/RadioSetupDialog.cpp index 5e6105d40b..2a6fcd6aba 100644 --- a/src/gui/RadioSetupDialog.cpp +++ b/src/gui/RadioSetupDialog.cpp @@ -1373,7 +1373,7 @@ RadioSetupDialog::RadioSetupDialog(RadioModel* model, AudioEngine* audio, addPage(signalCategory, QStringLiteral("Audio"), QStringLiteral("speaker microphone device sample rate latency sound dax"), [this] { return buildAudioTab(); }); addPage(signalCategory, QStringLiteral("Transmit"), - QStringLiteral("tx transmit rf power tune ptt band settings"), [this] { return buildTxTab(); }); + QStringLiteral("tx transmit rf power tune ptt mic override band settings"), [this] { return buildTxTab(); }); addPage(signalCategory, QStringLiteral("Phone & CW"), QStringLiteral("phone cw keyer break-in sidetone microphone voice"), [this] { return buildPhoneCwTab(); }); addPage(signalCategory, QStringLiteral("Receive"), @@ -3242,6 +3242,50 @@ QWidget* RadioSetupDialog::buildTxTab() AetherSDR::applyComboStyle(accCmb); grid->addWidget(accCmb, 0, 3); + // PTT Mic Override (SmartSDR v4.3, SMART-9340): from 4.3 a hardware + // PTT transmits the selected TX source, DAX and remote audio included; + // on, it forces the analog mic as firmware before 4.3 always did. + // Radio-wide, and only a 4.3 radio reports it (#6442). + auto* pttOvrLbl = new QLabel("PTT Mic Override:"); + applyLabelStyle(pttOvrLbl); + grid->addWidget(pttOvrLbl, 1, 0); + auto* pttOvrBtn = new QPushButton; + pttOvrBtn->setCheckable(true); + setButtonRole(pttOvrBtn, ButtonRole::Select); + pttOvrBtn->setAccessibleName(tr("PTT Mic Override")); + grid->addWidget(pttOvrBtn, 1, 1, Qt::AlignLeft); + const auto syncPttOverride = [this, pttOvrBtn]() { + const auto& t = m_model->transmitModel(); + const bool reported = t.havePttMicOverride(); + QSignalBlocker block(pttOvrBtn); + pttOvrBtn->setChecked(t.pttMicOverride()); + pttOvrBtn->setText(t.pttMicOverride() ? "Enabled" : "Disabled"); + pttOvrBtn->setEnabled(reported); + const QString tip = reported + ? tr("Enabled: a hardware PTT (RCA PTT jack, ACC or mic-jack PTT) " + "transmits the analog mic, even when DAX or PC audio is the TX source.\n" + "Disabled (firmware 4.3 default): a hardware PTT transmits the " + "selected TX source, as MOX does.\n" + "This setting is shared by every client on the radio.") + : tr("Requires SmartSDR v4.3 firmware or later. Earlier firmware " + "always transmits the analog mic on a hardware PTT."); + pttOvrBtn->setToolTip(tip); + pttOvrBtn->setAccessibleDescription(tip); + }; + syncPttOverride(); + // The button shows the radio's value: a click only requests the change + // and snaps back, and the transmit-status echo moves it. + connect(pttOvrBtn, &QPushButton::toggled, this, [this, syncPttOverride](bool on) { + m_model->transmitModel().setPttMicOverride(on); + syncPttOverride(); + }); + connect(&tx, &TransmitModel::stateChanged, pttOvrBtn, syncPttOverride); + // resetState() on disconnect announces through micStateChanged, not + // stateChanged; without this the page keeps the last session's value. + connect(&tx, &TransmitModel::micStateChanged, pttOvrBtn, syncPttOverride); + // A family switch clears the report without either signal. + connect(m_model, &RadioModel::backendRebuilt, pttOvrBtn, syncPttOverride); + vbox->addWidget(group); } diff --git a/src/gui/SliceTroubleshootingDialog.cpp b/src/gui/SliceTroubleshootingDialog.cpp index d3dea366dc..b50516936e 100644 --- a/src/gui/SliceTroubleshootingDialog.cpp +++ b/src/gui/SliceTroubleshootingDialog.cpp @@ -1033,6 +1033,10 @@ QString SliceTroubleshootingDialog::buildSummary(const QJsonObject& snapshot) .arg(mic["am_carrier_level"].toInt()) .arg(mic["tx_filter_low"].toInt()) .arg(mic["tx_filter_high"].toInt()); + if (mic.contains("ptt_mic_override")) { + lines << QString("- PTT mic override: `%1`") + .arg(yesNo(mic["ptt_mic_override"].toBool())); + } lines << QString("- VOX: enabled `%1`, level `%2`, delay `%3`") .arg(yesNo(vox["enabled"].toBool())) .arg(vox["level"].toInt()) diff --git a/src/models/RadioModel.cpp b/src/models/RadioModel.cpp index 788c08cd64..ce35a1dcd2 100644 --- a/src/models/RadioModel.cpp +++ b/src/models/RadioModel.cpp @@ -1736,6 +1736,7 @@ void RadioModel::teardownBackend() // radio's confirmed drive. resetPowerProvenance(), not resetState(): one caller // is ~RadioModel(), and resetState() emits TX signals. m_transmitModel.resetPowerProvenance(); + m_transmitModel.resetPttMicOverrideReport(); m_backend.reset(); m_cwPitchHandedToBackend = -1; // the next backend has been handed nothing acknowledgeTxTransportTeardown(m_txOperation); @@ -13028,6 +13029,9 @@ QJsonObject RadioModel::troubleshootingSnapshot() const mic["mic_boost"] = m_transmitModel.micBoost(); mic["mic_bias"] = m_transmitModel.micBias(); mic["met_in_rx"] = m_transmitModel.metInRx(); + if (m_transmitModel.havePttMicOverride()) { + mic["ptt_mic_override"] = m_transmitModel.pttMicOverride(); + } mic["sync_cwx"] = m_transmitModel.syncCwx(); mic["am_carrier_level"] = m_transmitModel.amCarrierLevel(); mic["dexp_on"] = m_transmitModel.dexpOn(); diff --git a/src/models/TransmitModel.cpp b/src/models/TransmitModel.cpp index 65b1034e31..79f6218dc7 100644 --- a/src/models/TransmitModel.cpp +++ b/src/models/TransmitModel.cpp @@ -49,6 +49,7 @@ void TransmitModel::resetState() m_memoriesEnabled = false; m_usingMemory = false; m_showTxInWaterfall = false; + resetPttMicOverrideReport(); // Radio-reported; a new session re-reports it. A radio with no Flex command // plane never echoes dax=, so a Flex session's DAX=on must not survive into // it and hold the client TX chain's MIC-ready indicator off (#5871). @@ -206,6 +207,13 @@ void TransmitModel::applyChanges(const TransmitDelta& d) if (provenanceMoved) emit powerProvenanceChanged(); changed |= assign(d.tuneMode, m_tuneMode); changed |= assign(d.showTxInWaterfall, m_showTxInWaterfall); + // Latch on presence: pre-4.3 firmware never sends the key, and a 4.3 radio + // reporting the 0 default leaves the value unchanged. + if (d.pttMicOverride && !m_havePttMicOverride) { + m_havePttMicOverride = true; + changed = true; + } + changed |= assign(d.pttMicOverride, m_pttMicOverride); if (assign(d.txSliceMode, m_txSliceMode)) { changed = true; emit txSliceModeChanged(m_txSliceMode); } // ── Interlock (no emit — plain state, matching applyInterlockStatus) ── @@ -909,6 +917,16 @@ void TransmitModel::setMicBias(bool on) emit controlRequested({field, on}); } +void TransmitModel::setPttMicOverride(bool on) +{ + if (refuseControlOffThread()) { return; } + const auto field = TransmitControlRequest::Field::PttMicOverride; + captureControlIntent(field); + // Not optimistic: the setting is radio-wide and the radio echoes it in + // transmit status, so the model changes only on that echo. + emit controlRequested({field, on}); +} + void TransmitModel::setAmCarrierLevel(int level) { if (refuseControlOffThread()) { return; } diff --git a/src/models/TransmitModel.h b/src/models/TransmitModel.h index fd2b3f27aa..61aca12705 100644 --- a/src/models/TransmitModel.h +++ b/src/models/TransmitModel.h @@ -91,6 +91,14 @@ class TransmitModel : public QObject { m_haveMaxPowerLevel = false; m_rfPowerFromRadio = false; } + // The radio-reported PTT mic override belongs to the radio that reported it, + // so a family switch clears it too (#6442). Non-emitting, like + // resetPowerProvenance() and for the same caller (teardownBackend(), from + // ~RadioModel()); RadioModel::backendRebuilt tells an open view. + void resetPttMicOverrideReport() { + m_pttMicOverride = false; + m_havePttMicOverride = false; + } int tunePower() const { return m_tunePower; } bool isTuning() const { return m_tune; } // CW admission while TUNE is active (#5422; FLEX-8400 fw 4.2.20): `cw key 1` @@ -175,6 +183,11 @@ class TransmitModel : public QObject { int interlockTimeout() const { return m_interlockTimeout; } int accTxReqPolarity() const { return m_accTxReqPolarity; } int rcaTxReqPolarity() const { return m_rcaTxReqPolarity; } + // Hardware PTT (RCA/ACC/mic jack) forces the analog mic even while DAX or + // remote audio is the TX source. Radio-wide; firmware v4.3+ only, so + // havePttMicOverride() says whether this radio has reported it (#6442). + bool pttMicOverride() const { return m_pttMicOverride; } + bool havePttMicOverride() const { return m_havePttMicOverride; } int maxPowerLevel() const { return m_maxPowerLevel; } // Latches haveMaxPowerLevel() on PRESENCE, not on change: a radio reporting // 100 into a model already at the 100 default must still count as reported, @@ -393,6 +406,7 @@ class TransmitModel : public QObject { void setVoxDelay(int delay); void setMicBoost(bool on); void setMicBias(bool on); + void setPttMicOverride(bool on); void setAmCarrierLevel(int level); void setDexp(bool on); void setDexpLevel(int level); @@ -646,6 +660,8 @@ class TransmitModel : public QObject { int m_interlockTimeout{0}; int m_accTxReqPolarity{0}; int m_rcaTxReqPolarity{0}; + bool m_pttMicOverride{false}; // v4.3 firmware default + bool m_havePttMicOverride{false}; int m_maxPowerLevel{100}; QString m_tuneMode{"single_tone"}; bool m_tuneAvailable = true; diff --git a/tests/aetherd_transmit_decode_test.cpp b/tests/aetherd_transmit_decode_test.cpp index 2ecbfcce88..17b075d1fe 100644 --- a/tests/aetherd_transmit_decode_test.cpp +++ b/tests/aetherd_transmit_decode_test.cpp @@ -53,6 +53,29 @@ int main(int argc, char** argv) CHECK(d.cwIambicMode.has_value() && *d.cwIambicMode == 1); } + // ---- ptt_mic_override (v4.3 firmware, #6442) ---- + { + const TransmitDelta d = decode(b, [](FlexBackend& fb){ + fb.decodeTransmitStatus({{QStringLiteral("ptt_mic_override"), QStringLiteral("1")}}); + }); + CHECK(d.pttMicOverride.has_value() && *d.pttMicOverride == true); + const TransmitDelta absent = decode(b, [](FlexBackend& fb){ + fb.decodeTransmitStatus({{QStringLiteral("rfpower"), QStringLiteral("50")}}); + }); + CHECK(!absent.pttMicOverride.has_value()); // pre-4.3 firmware: never reported + const TransmitDelta off = decode(b, [](FlexBackend& fb){ + fb.decodeTransmitStatus({{QStringLiteral("ptt_mic_override"), QStringLiteral("0")}}); + }); + CHECK(off.pttMicOverride.has_value() && *off.pttMicOverride == false); + // FlexLib rejects anything but 0/1: malformed input is dropped, never "off". + for (const char* bad : {"garbage", "-1", "2", ""}) { + const TransmitDelta invalid = decode(b, [bad](FlexBackend& fb){ + fb.decodeTransmitStatus({{QStringLiteral("ptt_mic_override"), QLatin1String(bad)}}); + }); + CHECK(!invalid.pttMicOverride.has_value()); + } + } + // ---- ok-guard: malformed present numeric is dropped ---- { const TransmitDelta d = decode(b, [](FlexBackend& fb){ diff --git a/tests/model_control_routing_test.cpp b/tests/model_control_routing_test.cpp index f9f6210431..f9f2cad15f 100644 --- a/tests/model_control_routing_test.cpp +++ b/tests/model_control_routing_test.cpp @@ -46,6 +46,7 @@ void transmitEncoding() {"setVoxDelay", [](TransmitModel& model) { model.setVoxDelay(27); }, {QStringLiteral("transmit set vox_delay=27")}}, {"setMicBoost", [](TransmitModel& model) { model.setMicBoost(true); }, {QStringLiteral("mic boost 1")}}, {"setMicBias", [](TransmitModel& model) { model.setMicBias(true); }, {QStringLiteral("mic bias 1")}}, + {"setPttMicOverride", [](TransmitModel& model) { model.setPttMicOverride(true); }, {QStringLiteral("transmit set ptt_mic_override=1")}}, {"setAmCarrierLevel", [](TransmitModel& model) { model.setAmCarrierLevel(28); }, {QStringLiteral("transmit set am_carrier=28")}}, {"setDexp", [](TransmitModel& model) { model.setDexp(true); }, {QStringLiteral("transmit set compander=1")}}, {"setDexpLevel", [](TransmitModel& model) { model.setDexpLevel(29); }, {QStringLiteral("transmit set compander_level=29")}}, diff --git a/tests/radio_setup_tx_timing_fields_test.cpp b/tests/radio_setup_tx_timing_fields_test.cpp index 12e5aaaf08..7ca0c64808 100644 --- a/tests/radio_setup_tx_timing_fields_test.cpp +++ b/tests/radio_setup_tx_timing_fields_test.cpp @@ -19,13 +19,16 @@ #include "core/backends/IRadioBackend.h" #include "core/backends/sim/SimBackend.h" #include "gui/RadioSetupDialog.h" +#include "core/backends/TransmitDelta.h" #include "models/RadioModel.h" +#include "models/TransmitModel.h" #include #include #include #include #include +#include #include #include #include @@ -301,6 +304,63 @@ private slots: } QCOMPARE(dropped.count(), 0); } + + // PTT Mic Override (#6442) shows the RADIO's value: unreported (firmware + // before 4.3) it is dimmed with a reason; a click sends the request but + // snaps back until the echo; and a session reset clears an open page. + void pttMicOverrideFollowsTheRadioAndResets() + { + RadioModel model; // a bare model is on the Flex backend + RadioSetupDialog dialog(&model); + dialog.show(); + dialog.selectTab(QStringLiteral("Transmit")); + + QPushButton* button = nullptr; + for (QPushButton* b : dialog.findChildren()) { + if (b->accessibleName() == QStringLiteral("PTT Mic Override")) { + button = b; + } + } + QVERIFY2(button, "the PTT Mic Override control exists"); + QVERIFY2(!button->isEnabled(), "unreported: dimmed"); + QVERIFY(button->accessibleDescription().startsWith(QStringLiteral("Requires SmartSDR v4.3"))); + + TransmitModel& tx = model.transmitModel(); + TransmitDelta reportedOff; + reportedOff.pttMicOverride = false; + tx.applyChanges(reportedOff); + QVERIFY2(button->isEnabled(), "a reported 0 enables the control"); + QVERIFY(!button->isChecked()); + + // The typed request; model_control_routing_test pins its wire text. + QSignalSpy requests(&tx, &TransmitModel::controlRequested); + button->click(); + QCOMPARE(requests.count(), 1); + const auto request = requests.at(0).at(0).value(); + QVERIFY(request.field == TransmitControlRequest::Field::PttMicOverride); + QVERIFY(std::get(request.value)); + QVERIFY2(!button->isChecked(), "no echo yet: the button still shows the radio's 0"); + QVERIFY(!tx.pttMicOverride()); + + TransmitDelta echoOn; + echoOn.pttMicOverride = true; + tx.applyChanges(echoOn); + QVERIFY(button->isChecked()); + QCOMPARE(button->text(), QStringLiteral("Enabled")); + + // A family switch starts a new radio's session without a disconnect: + // the old radio's report must not carry over to it. + QVERIFY(model.rebuildBackendForTest(QStringLiteral("sim"))); + QVERIFY2(!tx.havePttMicOverride() && !tx.pttMicOverride(), "family switch: unreported"); + QVERIFY2(!button->isChecked() && !button->isEnabled(), "family switch: the open control resets"); + + tx.applyChanges(echoOn); + QVERIFY(button->isChecked() && button->isEnabled()); + tx.resetState(); + QVERIFY2(!button->isChecked(), "reset: the last session's value is gone"); + QVERIFY2(!button->isEnabled(), "reset: unreported again"); + QCOMPARE(button->text(), QStringLiteral("Disabled")); + } }; int main(int argc, char** argv) diff --git a/tests/transmit_model_test.cpp b/tests/transmit_model_test.cpp index 1ac9d156ae..c6b7c9dbdb 100644 --- a/tests/transmit_model_test.cpp +++ b/tests/transmit_model_test.cpp @@ -750,5 +750,28 @@ int main(int argc, char** argv) "Initial status FailBypass without InProgress does not emit atuTuneFailed"); } + // PTT Mic Override (#6442): v4.3 firmware reports it, earlier firmware never + // does. The presence latch is what tells the two apart, so it must fire on + // the 0 default even though the value does not change. + { + TransmitModel pttTx; + int stateEdges = 0; + QObject::connect(&pttTx, &TransmitModel::stateChanged, [&stateEdges] { ++stateEdges; }); + ok &= expect(!pttTx.havePttMicOverride() && !pttTx.pttMicOverride(), + "PTT mic override is unreported before any transmit status"); + pttTx.applyChanges(td([](TransmitDelta& d) { d.rfPower = 50; })); + ok &= expect(!pttTx.havePttMicOverride(), + "pre-4.3 transmit status without the key leaves it unreported"); + stateEdges = 0; + pttTx.applyChanges(td([](TransmitDelta& d) { d.pttMicOverride = false; })); + ok &= expect(pttTx.havePttMicOverride() && !pttTx.pttMicOverride() && stateEdges == 1, + "a reported 0 default latches the key and announces it"); + pttTx.applyChanges(td([](TransmitDelta& d) { d.pttMicOverride = true; })); + ok &= expect(pttTx.pttMicOverride(), "the radio's echo is adopted"); + pttTx.resetState(); + ok &= expect(!pttTx.havePttMicOverride() && !pttTx.pttMicOverride(), + "a session reset forgets the radio's PTT mic override"); + } + return ok ? 0 : 1; }