| 2203 | // routing, and the RC-28 deferred press/hold logic (#3323). |
| 2204 | |
| 2205 | void MainWindow::wireExternalControllers() |
| 2206 | { |
| 2207 | // ── External controllers run on a dedicated worker thread (#502) ──── |
| 2208 | // FlexControl, SerialPort, and MIDI controllers are created on the |
| 2209 | // worker thread so their I/O (serial port, RtMidi callbacks, poll timers) |
| 2210 | // never competes with paintEvent. Signals auto-queue to main thread. |
| 2211 | m_extCtrlThread = new QThread(this); |
| 2212 | m_extCtrlThread->setObjectName("ExtControllers"); |
| 2213 | |
| 2214 | // Shared FlexControl coalescing for the USB device and the virtual |
| 2215 | // FlexControl dialog. The timer stays on the main thread because it |
| 2216 | // reads the active slice and updates UI state. |
| 2217 | m_flexCoalesceTimer.setSingleShot(true); |
| 2218 | m_flexCoalesceTimer.setInterval(20); |
| 2219 | connect(&m_flexCoalesceTimer, &QTimer::timeout, this, [this]() { |
| 2220 | if (m_flexTargetMhz < 0.0) return; |
| 2221 | auto* s = activeSlice(); |
| 2222 | if (!s) { m_flexTargetMhz = -1.0; return; } |
| 2223 | if (s->isLocked()) { |
| 2224 | s->notifyTuneBlockedByLock(); |
| 2225 | // Drop queued tuning so unlock does not replay stale wheel input. |
| 2226 | m_flexTargetMhz = -1.0; |
| 2227 | return; |
| 2228 | } |
| 2229 | const double target = m_flexTargetMhz; |
| 2230 | applyTuneRequest(s, target, TuneIntent::IncrementalTune, "flexcontrol"); |
| 2231 | }); |
| 2232 | |
| 2233 | #ifdef HAVE_SERIALPORT |
| 2234 | m_serialPort = new SerialPortController; // no parent — moved to thread |
| 2235 | m_serialPort->moveToThread(m_extCtrlThread); |
| 2236 | m_flexControl = new FlexControlManager; |
| 2237 | m_flexControl->moveToThread(m_extCtrlThread); |
| 2238 | |
| 2239 | // Serial port signals (auto-queued from worker → main) |
| 2240 | connect(m_serialPort, &SerialPortController::externalPttChanged, |
| 2241 | this, [this](bool active) { |
| 2242 | m_radioModel.setTransmit(active); |
| 2243 | }); |
| 2244 | connect(m_serialPort, &SerialPortController::cwKeyChanged, |
| 2245 | this, [this](bool down) { |
| 2246 | m_radioModel.sendCwKey(down); |
| 2247 | }); |
| 2248 | connect(m_serialPort, &SerialPortController::cwPaddleChanged, |
| 2249 | this, [this](bool dit, bool dah) { |
| 2250 | m_lastCwPaddleTraceId.store(0, std::memory_order_relaxed); |
| 2251 | m_lastCwPaddleSourceMs.store(0, std::memory_order_relaxed); |
| 2252 | // When the local iambic keyer is running, feed it the raw paddle |
| 2253 | // state — it forwards to the radio AND drives the sidetone gate |
| 2254 | // directly. Otherwise pass straight through to the radio (radio's |
| 2255 | // RF iambic is still authoritative for the on-air signal). |
| 2256 | if (m_iambicKeyer && m_iambicKeyer->isRunning()) { |
| 2257 | m_iambicKeyer->setPaddleState(dit, dah); |
| 2258 | } else { |
| 2259 | m_radioModel.sendCwPaddle(dit, dah); |
| 2260 | } |
| 2261 | }); |
| 2262 |
nothing calls this directly
no test coverage detected