#include "JS8_UI/mainwindow.h" void MainWindow::processDecodeEvent(JS8::Event::Variant const &event) { static QList driftQueue; static qint32 syncStart = -1; std::visit( [this](auto &&e) { using T = std::decay_t; if constexpr (std::is_same_v) { if (m_wideGraph->shouldDisplayDecodeAttempts()) { m_wideGraph->drawHorizontalLine(QColor(Qt::yellow), 0, 5); } } else if constexpr (std::is_same_v) { syncStart = e.position; } else if constexpr (std::is_same_v) { if (m_wideGraph->shouldDisplayDecodeAttempts()) { auto const drawDecodeLine = [this, freq = static_cast(e.frequency), mode = e.mode](QColor const &color) { m_wideGraph->drawDecodeLine( color, freq, freq + JS8::Submode::bandwidth(mode)); }; if (e.type == JS8::Event::SyncState::Type::DECODED) { drawDecodeLine(Qt::red); } else if (auto const xdtMs = static_cast(e.dt * 1000); std::abs(xdtMs) <= 2000) { if (e.sync.candidate < 10) drawDecodeLine(Qt::darkCyan); else if (e.sync.candidate <= 15) drawDecodeLine(Qt::cyan); else if (e.sync.candidate <= 21) drawDecodeLine(Qt::white); } } } else if constexpr (std::is_same_v) { qCDebug(decoder_js8) << "decode duration" << m_decoderBusyStartTime.msecsTo( QDateTime::currentDateTimeUtc()) << "ms"; // TODO: move this into a function if (!driftQueue.isEmpty()) { if (m_driftMsMMA_N == 0) { m_driftMsMMA_N = 1; m_driftMsMMA = DriftingDateTime::drift(); } // let the widegraph know for timing control m_wideGraph->notifyDriftedSignalsDecoded( driftQueue.count()); while (!driftQueue.isEmpty()) { qint32 newDrift = driftQueue.first(); driftQueue.removeFirst(); m_driftMsMMA = (((m_driftMsMMA_N - 1) * m_driftMsMMA) + newDrift) / m_driftMsMMA_N; if (m_driftMsMMA_N < 60) m_driftMsMMA_N++; // cap it to 60 observations } // XXX The following lines do nothing; it's a completely dead // store. For // now, just #ifdefing them out, but they were in // the 2.2.1-devel code, and presumably they were important; // need to see what the intent was here. #if 0 qint32 driftLimitMs = JS8::Submode::period(Varicode::JS8CallNormal) * 1000; qint32 newDriftMs = m_driftMsMMA; if(newDriftMs < 0){ newDriftMs = -((-newDriftMs) % driftLimitMs); } else { newDriftMs = ((newDriftMs) % driftLimitMs); } #endif setDrift(m_driftMsMMA); // writeNoticeTextToUI(QDateTime::currentDateTimeUtc(), // QString("Automatic Drift: %1").arg(driftAvg)); } m_bDecoded = e.decoded > 0; decodeDone(); } else if constexpr (std::is_same_v) { // A frame is valid if we haven't seen the same frame in the // past 1/2 decode period. // // Note: Success here depends on decodes ordered such that // frequencies // near `dec_data.params.nfqso` arrive here first, so it's // key to process the decode candidates in an ordered // manner, likely by sorting the raw take from the initial // selection pass. DecodedText decodedtext(e); FrameCacheKey dedupeKey(decodedtext.submode(), decodedtext.frame()); if (auto const it = m_messageDupeCache.find(dedupeKey); it != m_messageDupeCache.end()) { if (it->second.secsTo(QDateTime::currentDateTimeUtc()) < 0.5 * JS8::Submode::period(decodedtext.submode())) { qCDebug(mainwindow_js8) << "duplicate frame at" << it->second << "using key" << QString("%1:%2") .arg(dedupeKey.submode) .arg(dedupeKey.frame); return; } } #if 0 // frames are valid if they meet our minimum rx threshold for the submode bool bValidFrame = decodedtext.snr() >= JS8::Submode::rxSNRThreshold(decodedtext.submode()); qCDebug(mainwindow_js8) << "valid" << bValidFrame << JS8::Submode::name(decodedtext.submode()) << "decoded text" << decodedtext.message(); // skip if invalid if(!bValidFrame) { return; } #else qCDebug(mainwindow_js8) << JS8::Submode::name(decodedtext.submode()) << "decoded text" << decodedtext.message(); #endif // TODO: move this into a function // compute time drift for non-dupe messages if (m_wideGraph->shouldAutoSyncSubmode(decodedtext.submode())) { int m = decodedtext.submode(); float xdt = decodedtext.dt(); // if we're here at this point, we _should_ be operating a // decode every second // // so we need to figure out where: // // 1) this current decode started // 2) when that cycle _should_ have started // 3) compute the delta // 4) apply the drift qint32 periodMs = 1000 * JS8::Submode::period(m); // writeNoticeTextToUI(now, QString("Decode at %1 (kin: %2, // lastDecoded: // %3)").arg(syncStart).arg(dec_data.params.kin).arg(m_lastDecodeStartMap.value(m))); float expectedStartDelay = JS8::Submode::startDelayMS(m) / 1000.0; float decodedSignalTime = (float)syncStart / (float)JS8_RX_SAMPLE_RATE; // writeNoticeTextToUI(now, QString("--> started at %1 // seconds into the start of my drifted // minute").arg(decodedSignalTime)); // writeNoticeTextToUI(now, QString("--> we add a time delta // of %1 seconds into the start of the cycle").arg(xdt)); // adjust for expected start delay decodedSignalTime -= expectedStartDelay; // adjust for time delta decodedSignalTime += xdt; // ensure that we are within a 60 second minute if (decodedSignalTime < 0) { decodedSignalTime += 60.0f; } else if (decodedSignalTime > 60) { decodedSignalTime -= 60.0f; } // writeNoticeTextToUI(now, QString("--> so signal adjusted // started at %1 seconds into the start of my drifted // minute").arg(decodedSignalTime)); qint32 decodedSignalTimeMs = 1000 * decodedSignalTime; qint32 cycleStartTimeMs = (decodedSignalTimeMs / periodMs) * periodMs; qint32 driftMs = cycleStartTimeMs - decodedSignalTimeMs; // writeNoticeTextToUI(now, QString("--> which is a drift // adjustment of %1 milliseconds").arg(driftMs)); // if we have a large negative offset (say -14000), use the // positive inverse of +1000 if (driftMs + periodMs < qAbs(driftMs)) { driftMs += periodMs; } // if we have a large positive offset (say 14000, use the // negative inverse of -1000) else if (qAbs(driftMs - periodMs) < driftMs) { driftMs -= periodMs; } // writeNoticeTextToUI(now, QString("--> which is a // corrected drift adjustment of %1 // milliseconds").arg(driftMs)); qint32 newDrift = DriftingDateTime::drift() + driftMs; if (newDrift < 0) { newDrift %= -periodMs; } else { newDrift %= periodMs; } // writeNoticeTextToUI(now, QString("--> which is rounded to // a total drift of %1 milliseconds for this // period").arg(newDrift)); driftQueue.append(newDrift); } // if the frame is valid, cache it! m_messageDupeCache.insert_or_assign( dedupeKey, QDateTime::currentDateTimeUtc()); // log valid frames to ALL.txt (and correct their timestamp // format) auto freq = dialFrequency(); // if we changed frequencies, use the old frequency that we // started the decode with if (m_decoderBusyFreq != freq) { freq = m_decoderBusyFreq; } auto date = DriftingDateTime::currentDateTimeUtc().toString( "yyyy-MM-dd"); writeAllTxt(date + " " + decodedtext.string() + " " + decodedtext.message()); /** * @brief Send decode to WSJT-X protocol * * Converts JS8Call decode events to WSJT-X Decode messages and * sends them to WSJT-X protocol clients. For HeartBeat * messages, ensures the message text includes callsign and grid * so clients can properly associate them with grid plots. */ if (m_wsjtxMessageMapper && m_config.wsjtx_protocol_enabled()) { // Convert decode time from JS8Call format to QTime auto const hms = decode_time(decodedtext.time()); QTime decode_time = QTime(hms.hour, hms.minute, hms.second); // Send decode message // Use "JS8" as the mode string (WSJT-X expects mode names // like "FT8", "FT4", "JT9", etc.) m_wsjtxMessageMapper->sendDecode( true, // is_new - always true for new decodes decode_time, decodedtext.snr(), decodedtext.dt(), static_cast(decodedtext.frequencyOffset()), "JS8", // mode string decodedtext.message(), decodedtext.isLowConfidence()); } ActivityDetail d = {}; CallDetail cd = {}; CommandDetail cmd = {}; CallDetail td = {}; // Parse General Activity #if 1 bool shouldParseGeneralActivity = true; if (shouldParseGeneralActivity && !decodedtext.messageWords().isEmpty()) { int offset = decodedtext.frequencyOffset(); if (!m_bandActivity.contains(offset)) { int const range = JS8::Submode::rxThreshold(decodedtext.submode()); QList offsets = generateOffsets(offset - range, offset + range); foreach (int prevOffset, offsets) { if (!m_bandActivity.contains(prevOffset)) { continue; } m_bandActivity[offset] = m_bandActivity[prevOffset]; m_bandActivity.remove(prevOffset); break; } } // ActivityDetail d = {}; d.isLowConfidence = decodedtext.isLowConfidence(); d.isCompound = decodedtext.isCompound(); d.isDirected = decodedtext.isDirectedMessage(); d.bits = decodedtext.bits(); d.dial = freq; d.offset = offset; d.text = decodedtext.message(); d.utcTimestamp = DriftingDateTime::currentDateTimeUtc(); d.snr = decodedtext.snr(); d.isBuffered = false; d.submode = decodedtext.submode(); d.tdrift = m_wideGraph->shouldAutoSyncSubmode(d.submode) ? DriftingDateTime::drift() / 1000.0 : decodedtext.dt(); // if we have any "first" frame, and a buffer is already // established, clear it... int prevBufferOffset = -1; if (((d.bits & Varicode::JS8CallFirst) == Varicode::JS8CallFirst) && hasExistingMessageBuffer(decodedtext.submode(), d.offset, true, &prevBufferOffset)) { qCDebug(mainwindow_js8) << "first message encountered, " "clearing existing buffer" << prevBufferOffset; m_messageBuffer.remove(d.offset); } // if we have a data frame, and a message buffer has been // established, buffer it... if (hasExistingMessageBuffer(decodedtext.submode(), d.offset, true, &prevBufferOffset) && !decodedtext.isCompound() && !decodedtext.isDirectedMessage()) { qCDebug(mainwindow_js8) << "buffering data" << d.dial << d.offset << d.text; d.isBuffered = true; m_messageBuffer[d.offset].msgs.append(d); // TODO: incremental display if it's "to" me. } m_rxActivityQueue.append(d); m_bandActivity[offset].append(d); while (m_bandActivity[offset].count() > 10) { m_bandActivity[offset].removeFirst(); } } #endif // Process compound callsign commands (put them in cache)" #if 1 qCDebug(mainwindow_js8) << "decoded" << decodedtext.frameType() << decodedtext.isCompound() << decodedtext.isDirectedMessage() << decodedtext.isHeartbeat(); bool shouldProcessCompound = true; if (shouldProcessCompound && decodedtext.isCompound() && !decodedtext.isDirectedMessage()) { cd.call = decodedtext.compoundCall(); cd.grid = decodedtext.extra(); // compound calls via pings // may contain grid... cd.snr = decodedtext.snr(); cd.dial = freq; cd.offset = decodedtext.frequencyOffset(); cd.utcTimestamp = DriftingDateTime::currentDateTimeUtc(); cd.bits = decodedtext.bits(); cd.submode = decodedtext.submode(); cd.tdrift = m_wideGraph->shouldAutoSyncSubmode(d.submode) ? DriftingDateTime::drift() / 1000.0 : decodedtext.dt(); // Only respond to HEARTBEATS...remember that CQ messages // are "Alt" pings if (decodedtext.isHeartbeat()) { if (decodedtext.isAlt()) { // this is a cq with a standard or compound call, // ala "KN4CRD/P: @ALLCALL CQ CQ CQ" cd.cqTimestamp = DriftingDateTime::currentDateTimeUtc(); // convert CQ to a directed command and process... cmd.from = cd.call; cmd.to = "@ALLCALL"; cmd.cmd = " CQ"; cmd.snr = cd.snr; cmd.bits = cd.bits; cmd.grid = cd.grid; cmd.dial = cd.dial; cmd.offset = cd.offset; cmd.utcTimestamp = cd.utcTimestamp; cmd.tdrift = cd.tdrift; cmd.submode = cd.submode; cmd.text = decodedtext.message(); // TODO: check bits so we only auto respond to // "finished" cqs m_rxCommandQueue.append(cmd); // since this is no longer processed here we omit // logging it here. if we change this behavior, we'd // change this back to logging here. // logCallActivity(cd, true); // notification for cq tryNotify("cq"); } else { // convert HEARTBEAT to a directed command and // process... cmd.from = cd.call; cmd.to = "@HB"; cmd.cmd = " HEARTBEAT"; cmd.snr = cd.snr; cmd.bits = cd.bits; cmd.grid = cd.grid; cmd.dial = cd.dial; cmd.offset = cd.offset; cmd.utcTimestamp = cd.utcTimestamp; cmd.tdrift = cd.tdrift; cmd.submode = cd.submode; // TODO: check bits so we only auto respond to // "finished" heartbeats m_rxCommandQueue.append(cmd); // notification for hb tryNotify("hb"); } } else { qCDebug(mainwindow_js8) << "buffering compound call" << cd.offset << cd.call << cd.bits; hasExistingMessageBuffer(cd.submode, cd.offset, true, nullptr); m_messageBuffer[cd.offset].compound.append(cd); } } #endif // Parse commands // KN4CRD K1JT ? #if 1 bool shouldProcessDirected = true; if (shouldProcessDirected && decodedtext.isDirectedMessage()) { auto parts = decodedtext.directedMessage(); cmd.from = parts.at(0); cmd.to = parts.at(1); cmd.cmd = parts.at(2); cmd.dial = freq; cmd.offset = decodedtext.frequencyOffset(); cmd.snr = decodedtext.snr(); cmd.utcTimestamp = DriftingDateTime::currentDateTimeUtc(); cmd.bits = decodedtext.bits(); cmd.extra = parts.length() > 2 ? parts.mid(3).join(" ") : ""; cmd.submode = decodedtext.submode(); cmd.tdrift = m_wideGraph->shouldAutoSyncSubmode(cmd.submode) ? DriftingDateTime::drift() / 1000.0 : decodedtext.dt(); // if the command is a buffered command and its not the last // frame OR we have from or to in a separate message // (compound call) if ((Varicode::isCommandBuffered(cmd.cmd) && (cmd.bits & Varicode::JS8CallLast) != Varicode::JS8CallLast) || cmd.from == "<....>" || cmd.to == "<....>") { qCDebug(mainwindow_js8) << "buffering cmd" << cmd.dial << cmd.offset << cmd.cmd << cmd.from << cmd.to; // log complete buffered callsigns immediately if (cmd.from != "<....>" && cmd.to != "<....>") { CallDetail cmdcd = {}; cmdcd.call = cmd.from; cmdcd.bits = cmd.bits; cmdcd.snr = cmd.snr; cmdcd.dial = cmd.dial; cmdcd.offset = cmd.offset; cmdcd.utcTimestamp = cmd.utcTimestamp; cmdcd.ackTimestamp = cmd.to == m_config.my_callsign() ? cmd.utcTimestamp : QDateTime{}; cmdcd.tdrift = cmd.tdrift; cmdcd.submode = cmd.submode; logCallActivity(cmdcd, false); logHeardGraph(cmd.from, cmd.to); } // merge any existing buffer to this frequency hasExistingMessageBuffer(cmd.submode, cmd.offset, true, nullptr); if (cmd.to == m_config.my_callsign()) { d.shouldDisplay = true; } m_messageBuffer[cmd.offset].cmd = cmd; m_messageBuffer[cmd.offset].msgs.clear(); } else { m_rxCommandQueue.append(cmd); } // check to see if this is a station we've heard 3rd party bool shouldCaptureThirdPartyCallsigns = false; if (shouldCaptureThirdPartyCallsigns && Radio::base_callsign(cmd.to) != Radio::base_callsign(m_config.my_callsign())) { QString relayCall = QString("%1|%2") .arg(Radio::base_callsign(cmd.from)) .arg(Radio::base_callsign(cmd.to)); int snr = -100; if (parts.length() == 4) { snr = QString(parts.at(3)).toInt(); } // CallDetail td = {}; td.through = cmd.from; td.call = cmd.to; td.grid = ""; td.snr = snr; td.dial = cmd.dial; td.offset = cmd.offset; td.utcTimestamp = cmd.utcTimestamp; td.tdrift = cmd.tdrift; td.submode = cmd.submode; logCallActivity(td, true); logHeardGraph(cmd.from, cmd.to); } } #endif } }, event); }