| 471 | // ═════════════════════════════════════════════════════════════════════════════ |
| 472 | |
| 473 | void section3(RigctlClient& c, Runner& r, const QString& origMode, int origPb) |
| 474 | { |
| 475 | r.section(QStringLiteral("Section 3 — Mode")); |
| 476 | |
| 477 | // 3.1 get_mode |
| 478 | QStringList lines = c.send(QStringLiteral("\\get_mode")); |
| 479 | const QString mode = c.field(lines, QStringLiteral("Mode")); |
| 480 | const QString pb = c.field(lines, QStringLiteral("Passband")); |
| 481 | r.check(QStringLiteral("3.1 get_mode returns known mode string and numeric passband"), |
| 482 | c.ok(lines) && kKnownModes.contains(mode) && isInt(pb), |
| 483 | QStringLiteral("mode=%1 passband=%2").arg(mode, pb)); |
| 484 | |
| 485 | struct ModeCase { const char* mode; int pb; const char* label; }; |
| 486 | static constexpr ModeCase kModes[] = { |
| 487 | {"USB", 2700, "3.2"}, |
| 488 | {"LSB", 2700, "3.3"}, |
| 489 | {"CW", 500, "3.4"}, |
| 490 | {"PKTUSB", 3000, "3.5"}, |
| 491 | {"AM", 6000, "3.6"}, |
| 492 | {"FM", 10000, "3.7"}, |
| 493 | }; |
| 494 | for (const auto& t : kModes) { |
| 495 | lines = c.send(QStringLiteral("\\set_mode %1 %2") |
| 496 | .arg(QLatin1String(t.mode)).arg(t.pb)); |
| 497 | r.check(QStringLiteral("%1 set_mode %2 %3 returns RPRT 0") |
| 498 | .arg(QLatin1String(t.label)).arg(QLatin1String(t.mode)).arg(t.pb), |
| 499 | c.ok(lines)); |
| 500 | } |
| 501 | |
| 502 | // 3.8 passband 0 → radio picks default |
| 503 | lines = c.send(QStringLiteral("\\set_mode USB 0")); |
| 504 | r.check(QStringLiteral("3.8 set_mode USB 0 (radio default passband) returns RPRT 0"), |
| 505 | c.ok(lines)); |
| 506 | |
| 507 | // 3.9 / 3.10 set_mode ? capability probe — must return mode list, NOT change current mode |
| 508 | // (MacLoggerDX regression: missing guard caused set_mode ? to set mode to USB) |
| 509 | { |
| 510 | // Drain the earlier async mode sets (3.2–3.8) deterministically: set a |
| 511 | // known mode and poll until it actually lands. The previous "two equal |
| 512 | // reads = stable" heuristic was fooled when queued sets arrived slower |
| 513 | // than the poll interval — it latched a transient (e.g. PKTUSB) as the |
| 514 | // baseline, then a late USB from 3.8 fired during the guard and looked |
| 515 | // like set_mode ? changed the mode. USB is the last mode 3.8 sets, so |
| 516 | // observing USB confirms the FIFO queue has fully drained. |
| 517 | c.send(QStringLiteral("\\set_mode USB 2700")); |
| 518 | QString modeBeforeProbe; |
| 519 | { |
| 520 | QElapsedTimer t; t.start(); |
| 521 | do { |
| 522 | modeBeforeProbe = c.field(c.send(QStringLiteral("\\get_mode")), |
| 523 | QStringLiteral("Mode")); |
| 524 | if (modeBeforeProbe == QLatin1String("USB") || t.elapsed() >= 1500) break; |
| 525 | QThread::msleep(100); |
| 526 | } while (true); |
| 527 | } |
| 528 | QStringList probe = c.send(QStringLiteral("\\set_mode ?")); |
| 529 | const bool hasModes = std::any_of(probe.cbegin(), probe.cend(), |
| 530 | [](const QString& l){ |