/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2019, 2023 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include "channel/channelapi.h" #include "dsp/wavfilerecord.h" #include "util/messagequeue.h" #include "util/ft8message.h" #include "util/maidenhead.h" #include "maincore.h" #include "SWGMapItem.h" #include "ft8demodsettings.h" #include "ft8demodworker.h" FT8DemodWorker::FT8Callback::FT8Callback( const QDateTime& periodTS, qint64 baseFrequency, FT8::Packing& packing, const QString& name ) : m_packing(packing), m_periodTS(periodTS), m_name(name), m_validCallsigns(nullptr) { m_msgReportFT8Messages = MsgReportFT8Messages::create(); m_msgReportFT8Messages->setBaseFrequency(baseFrequency); } int FT8DemodWorker::FT8Callback::hcb( int *a91, float hz0, float off, const char *comment, float snr, int pass, int correct_bits ) { std::string call1; std::string call2; std::string loc; std::string type; std::string msg = m_packing.unpack(a91, call1, call2, loc, type); cycle_mu.lock(); if (cycle_already.count(msg) > 0) { // already decoded this message on this cycle cycle_mu.unlock(); return 1; // 1 => already seen, don't subtract. } cycle_already[msg] = true; QString call2Str(call2.c_str()); QString info(comment); if (m_validCallsigns && info.startsWith("OSD") && !m_validCallsigns->contains(call2Str)) { cycle_mu.unlock(); return 2; // leave without reporting. Set as new decode. } QList& ft8Messages = m_msgReportFT8Messages->getFT8Messages(); FT8Message baseMessage{ m_periodTS, QString(type.c_str()), pass, (int) snr, correct_bits, off - 0.5f, hz0, QString(call1.c_str()).simplified(), call2Str, QString(loc.c_str()).simplified(), info }; // DXpedition packs two messages in one with the two callees in the first call area separated by a semicolon if (type == "0.1") { QStringList callees = QString(call1.c_str()).simplified().split(";"); for (int i = 0; i < 2; i++) { baseMessage.call1 = callees[i]; if (i == 0) { // first is with RR73 greetings in locator area baseMessage.loc = "RR73"; } ft8Messages.push_back(baseMessage); } } else { ft8Messages.push_back(baseMessage); } cycle_mu.unlock(); // qDebug("FT8DemodWorker::FT8Callback::hcb: %6.3f %d %3d %3d %5.2f %6.1f %s (%s)", // m_baseFrequency / 1000000.0, // pass, // (int)snr, // correct_bits, // off - 0.5, // hz0, // msg.c_str(), // comment // ); return 2; // 2 => new decode, do subtract. } QString FT8DemodWorker::FT8Callback::get_name() { return m_name; } FT8DemodWorker::FT8DemodWorker() : m_recordSamples(false), m_nbDecoderThreads(6), m_decoderTimeBudget(0.5), m_useOSD(false), m_osdDepth(0), m_osdLDPCThreshold(70), m_verifyOSD(false), m_lowFreq(200), m_highFreq(3000), m_invalidSequence(true), m_baseFrequency(0), m_reportingMessageQueue(nullptr), m_channel(nullptr) { QString relPath = "ft8/save"; QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.mkpath(relPath); m_samplesPath = dir.absolutePath() + "/" + relPath; qDebug("FT8DemodWorker::FT8DemodWorker: samples path: %s", qPrintable(m_samplesPath)); relPath = "ft8/logs"; m_logsPath = dir.absolutePath() + "/" + relPath; qDebug("FT8DemodWorker::FT8DemodWorker: logs path: %s", qPrintable(m_logsPath)); } FT8DemodWorker::~FT8DemodWorker() {} void FT8DemodWorker::processBuffer(int16_t *buffer, QDateTime periodTS) { qDebug("FT8DemodWorker::processBuffer: %6.3f %s %d:%f [%d:%d]", m_baseFrequency / 1000000.0, qPrintable(periodTS.toString("yyyy-MM-dd HH:mm:ss")), m_nbDecoderThreads, m_decoderTimeBudget, m_lowFreq, m_highFreq ); if (m_invalidSequence) { qDebug("FT8DemodWorker::processBuffer: invalid sequence"); m_invalidSequence = false; return; } QString channelReference = "d0c0"; // default if (m_channel) { channelReference = tr("d%1c%2").arg(m_channel->getDeviceSetIndex()).arg(m_channel->getIndexInDeviceSet()); } int hints[2] = { 2, 0 }; // CQ FT8Callback ft8Callback(periodTS, m_baseFrequency, m_packing, channelReference); ft8Callback.setValidCallsigns((m_useOSD && m_verifyOSD) ? &m_validCallsigns : nullptr); m_ft8Decoder.getParams().nthreads = m_nbDecoderThreads; m_ft8Decoder.getParams().use_osd = m_useOSD ? 1 : 0; m_ft8Decoder.getParams().osd_depth = m_osdDepth; m_ft8Decoder.getParams().osd_ldpc_thresh = m_osdLDPCThreshold; std::vector samples(15*FT8DemodSettings::m_ft8SampleRate); std::transform( buffer, buffer + (15*FT8DemodSettings::m_ft8SampleRate), samples.begin(), [](const int16_t& s) -> float { return s / 32768.0f; } ); m_ft8Decoder.entry( samples.data(), samples.size(), 0.5 * FT8DemodSettings::m_ft8SampleRate, FT8DemodSettings::m_ft8SampleRate, m_lowFreq, m_highFreq, hints, hints, m_decoderTimeBudget, m_decoderTimeBudget, &ft8Callback, 0, (struct FT8::cdecode *) nullptr ); m_ft8Decoder.wait(m_decoderTimeBudget + 1.0); // add one second to budget to force quit threads qDebug("FT8DemodWorker::processBuffer: done: at %6.3f %d messages", m_baseFrequency / 1000000.0, (int)ft8Callback.getReportMessage()->getFT8Messages().size()); if (m_reportingMessageQueue) { m_reportingMessageQueue->push(new MsgReportFT8Messages(*ft8Callback.getReportMessage())); } QList mapPipes; MainCore::instance()->getMessagePipes().getMessagePipes((const QObject*) m_channel, "mapitems", mapPipes); const QList& ft8Messages = ft8Callback.getReportMessage()->getFT8Messages(); std::ofstream logFile; double baseFrequencyMHz = m_baseFrequency/1000000.0; for (const auto& ft8Message : ft8Messages) { if (m_logMessages) { if (!logFile.is_open()) { QString logFileName(tr("%1_%2.txt").arg(periodTS.toString("yyyyMMdd")).arg(channelReference)); QFileInfo lfi(QDir(m_logsPath), logFileName); QString logFilePath = lfi.absoluteFilePath(); if (lfi.exists()) { logFile.open(logFilePath.toStdString(), std::ios::app); } else { logFile.open(logFilePath.toStdString()); } } if (ft8Message.call1 == "UNK") { continue; } QString logMessage = QString("%1 %2 Rx FT8 %3 %4 %5 %6 %7 %8") .arg(periodTS.toString("yyyyMMdd_HHmmss")) .arg(baseFrequencyMHz, 9, 'f', 3) .arg(ft8Message.snr, 6) .arg(ft8Message.dt, 4, 'f', 1) .arg(ft8Message.df, 4, 'f', 0) .arg(ft8Message.call1) .arg(ft8Message.call2) .arg(ft8Message.loc); logMessage.remove(0, 2); logFile << logMessage.toStdString() << std::endl; } if (mapPipes.size() > 0) { // If message contains a Maidenhead locator, display caller on Map feature float latitude, longitude; if ((ft8Message.loc.size() == 4) && (ft8Message.loc != "RR73") && Maidenhead::fromMaidenhead(ft8Message.loc, latitude, longitude)) { QString text = QString("%1\nMode: FT8\nFrequency: %2 Hz\nLocator: %3\nSNR: %4\nLast heard: %5") .arg(ft8Message.call2) .arg(baseFrequencyMHz*1000000 + ft8Message.df) .arg(ft8Message.loc) .arg(ft8Message.snr) .arg(periodTS.toString("dd MMM yyyy HH:mm:ss")); for (const auto& pipe : mapPipes) { MessageQueue *messageQueue = qobject_cast(pipe->m_element); SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); swgMapItem->setName(new QString(ft8Message.call2)); swgMapItem->setLatitude(latitude); swgMapItem->setLongitude(longitude); swgMapItem->setAltitude(0); swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND swgMapItem->setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))); swgMapItem->setImageRotation(0); swgMapItem->setText(new QString(text)); swgMapItem->setImage(new QString("antenna.png")); swgMapItem->setModel(new QString("antenna.glb")); swgMapItem->setModelAltitudeOffset(0.0); swgMapItem->setLabel(new QString(ft8Message.call2)); swgMapItem->setLabelAltitudeOffset(4.5); swgMapItem->setFixedPosition(false); swgMapItem->setOrientation(0); swgMapItem->setHeading(0); MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create((const QObject*) m_channel, swgMapItem); messageQueue->push(msg); } } } if (m_verifyOSD && !ft8Message.decoderInfo.startsWith("OSD")) { if ((ft8Message.type == "1") || (ft8Message.type == "2")) { if (!ft8Message.call2.startsWith("<")) { m_validCallsigns.insert(ft8Message.call2); } if (!ft8Message.call1.startsWith("CQ") && !ft8Message.call1.startsWith("<")) { m_validCallsigns.insert(ft8Message.call1); } } } } delete ft8Callback.getReportMessage(); if (m_recordSamples) { WavFileRecord *wavFileRecord = new WavFileRecord(FT8DemodSettings::m_ft8SampleRate); QFileInfo wfi(QDir(m_samplesPath), periodTS.toString("yyyyMMdd_HHmmss")); QString wpath = wfi.absoluteFilePath(); qDebug("FT8DemodWorker::processBuffer: WAV file: %s.wav", qPrintable(wpath)); wavFileRecord->setFileName(wpath); wavFileRecord->setFileBaseIsFileName(true); wavFileRecord->setMono(true); wavFileRecord->startRecording(); wavFileRecord->writeMono(buffer, 15*FT8DemodSettings::m_ft8SampleRate); wavFileRecord->stopRecording(); delete wavFileRecord; } }