| 741 | // ═════════════════════════════════════════════════════════════════════════════ |
| 742 | |
| 743 | void section5(RigctlClient& c, Runner& r, qint64 origFreq) |
| 744 | { |
| 745 | r.section(QStringLiteral("Section 5 — Split VFO")); |
| 746 | const qint64 splitTxFreq = origFreq + 1000; |
| 747 | |
| 748 | // ── DAX-assign settle guard ────────────────────────────────────────────── |
| 749 | // Closing a TX slice we created on demand frees its SliceModel. If a deferred |
| 750 | // DAX-channel-assign timer is still pending — it's armed on sliceAdded when a |
| 751 | // DAX/TCI audio client is active, e.g. after WSJT-X has been used on this radio |
| 752 | // — it fires on the freed slice and crashes ASDR. A real client never creates |
| 753 | // and removes a split slice this fast; only this test does. So settle before |
| 754 | // every on-demand split teardown to let that timer fire on the LIVE slice |
| 755 | // first. (The underlying DAX-timer UAF is a separate, app-side issue.) |
| 756 | constexpr int kDaxSettleMs = 700; |
| 757 | const QString kSplitOff = QStringLiteral("\\set_split_vfo 0 VFOA"); |
| 758 | auto disableSplitSettled = [&]() { |
| 759 | QThread::msleep(kDaxSettleMs); |
| 760 | return c.send(kSplitOff); |
| 761 | }; |
| 762 | |
| 763 | // Poll get_split_vfo until Split == want (or timeout); return the final value. |
| 764 | // Replaces the do/while poll loop that was hand-rolled at every split check. |
| 765 | auto waitForSplit = [&](QLatin1String want, int timeoutMs) -> QString { |
| 766 | QString s; |
| 767 | QElapsedTimer t; t.start(); |
| 768 | do { |
| 769 | s = c.field(c.send(QStringLiteral("\\get_split_vfo")), QStringLiteral("Split")); |
| 770 | if (s == want || t.elapsed() >= timeoutMs) break; |
| 771 | QThread::msleep(150); |
| 772 | } while (true); |
| 773 | return s; |
| 774 | }; |
| 775 | // Settle-disable split and confirm it reached the off state — the "known-off |
| 776 | // baseline" precondition several subtests need. |
| 777 | auto resetSplitOff = [&]() { |
| 778 | disableSplitSettled(); |
| 779 | return waitForSplit(QLatin1String("0"), 2000) == QLatin1String("0"); |
| 780 | }; |
| 781 | |
| 782 | // 5.1 baseline split state |
| 783 | QStringList lines = c.send(QStringLiteral("\\get_split_vfo")); |
| 784 | const QString splitVal = c.field(lines, QStringLiteral("Split")); |
| 785 | const QString txVfo = c.field(lines, QStringLiteral("TX VFO")); |
| 786 | r.check(QStringLiteral("5.1 get_split_vfo returns Split (0/1) and TX VFO fields"), |
| 787 | c.ok(lines) |
| 788 | && (splitVal == QLatin1String("0") || splitVal == QLatin1String("1")) |
| 789 | && (txVfo == QLatin1String("VFOA") || txVfo == QLatin1String("VFOB")), |
| 790 | QStringLiteral("Split=%1 TX VFO=%2").arg(splitVal, txVfo)); |
| 791 | |
| 792 | // 5.2 enable split |
| 793 | lines = c.send(QStringLiteral("\\set_split_vfo 1 VFOB")); |
| 794 | r.check(QStringLiteral("5.2 set_split_vfo 1 VFOB returns RPRT 0"), c.ok(lines)); |
| 795 | |
| 796 | // 5.3 poll until split is confirmed active (creating a new slice takes variable time) |
| 797 | const QString splitOn = waitForSplit(QLatin1String("1"), 5000); |
| 798 | r.check(QStringLiteral("5.3 get_split_vfo reports Split: 1 [needs 2 slices in GUI]"), |
| 799 | splitOn == QLatin1String("1"), |
| 800 | QStringLiteral("Split=%1 — open a second slice in AetherSDR if this fails") |