/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2023 Jon Beniston, M7RCE // // Copyright (C) 2023 Daniele Forsi // // // // 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 "dsp/scopevis.h" #include "util/db.h" #include "navtexdemod.h" #include "navtexdemodsink.h" NavtexDemodSink::NavtexDemodSink(NavtexDemod *packetDemod) : m_navtexDemod(packetDemod), m_channelSampleRate(NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE), m_channelFrequencyOffset(0), m_magsqSum(0.0f), m_magsqPeak(0.0f), m_magsqCount(0), m_messageQueueToChannel(nullptr), m_exp(nullptr), m_sampleBufferIndex(0) { m_magsq = 0.0; m_sampleBuffer.resize(m_sampleBufferSize); applySettings(m_settings, true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); m_lowpassComplex1.create(301, NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE, NavtexDemodSettings::NAVTEXDEMOD_BAUD_RATE * 1.1); m_lowpassComplex2.create(301, NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE, NavtexDemodSettings::NAVTEXDEMOD_BAUD_RATE * 1.1); } NavtexDemodSink::~NavtexDemodSink() { delete[] m_exp; } void NavtexDemodSink::sampleToScope(Complex sample) { if (m_scopeSink) { Real r = std::real(sample) * SDR_RX_SCALEF; Real i = std::imag(sample) * SDR_RX_SCALEF; m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); if (m_sampleBufferIndex == m_sampleBufferSize) { std::vector vbegin; vbegin.push_back(m_sampleBuffer.begin()); m_scopeSink->feed(vbegin, m_sampleBufferSize); m_sampleBufferIndex = 0; } } } void NavtexDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) { Complex ci; for (SampleVector::const_iterator it = begin; it != end; ++it) { Complex c(it->real(), it->imag()); c *= m_nco.nextIQ(); if (m_interpolatorDistance < 1.0f) // interpolate { while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) { processOneSample(ci); m_interpolatorDistanceRemain += m_interpolatorDistance; } } else // decimate { if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) { processOneSample(ci); m_interpolatorDistanceRemain += m_interpolatorDistance; } } } } void NavtexDemodSink::eraseChars(int n) { if (getMessageQueueToChannel()) { QString msg = QString("%1").arg(QChar(0x8)); // Backspace for (int i = 0; i < n; i++) { NavtexDemod::MsgCharacter *msg = NavtexDemod::MsgCharacter::create(QChar(0x8)); getMessageQueueToChannel()->push(msg); } } } void NavtexDemodSink::processOneSample(Complex &ci) { // Calculate average and peak levels for level meter double magsqRaw = ci.real()*ci.real() + ci.imag()*ci.imag();; Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); m_movingAverage(magsq); m_magsq = m_movingAverage.asDouble(); m_magsqSum += magsq; if (magsq > m_magsqPeak) { m_magsqPeak = magsq; } m_magsqCount++; // Sum power while data is being received if (m_gotSOP) { m_rssiMagSqSum += magsq; m_rssiMagSqCount++; } ci /= SDR_RX_SCALEF; // Correlate with expected frequencies Complex exp = m_exp[m_expIdx]; m_expIdx = (m_expIdx + 1) % m_expLength; Complex corr1 = ci * exp; Complex corr2 = ci * std::conj(exp); // Low pass filter Real abs1Filt = std::abs(m_lowpassComplex1.filter(corr1)); Real abs2Filt = std::abs(m_lowpassComplex2.filter(corr2)); // Envelope calculation m_movMax1(abs1Filt); m_movMax2(abs2Filt); Real env1 = m_movMax1.getMaximum(); Real env2 = m_movMax2.getMaximum(); // Automatic threshold correction to compensate for frequency selective fading // http://www.w7ay.net/site/Technical/ATC/index.html Real bias1 = abs1Filt - 0.5 * env1; Real bias2 = abs2Filt - 0.5 * env2; Real unbiasedData = abs1Filt - abs2Filt; Real biasedData = bias1 - bias2; // Save current data for edge detection m_dataPrev = m_data; // Set data according to stongest correlation m_data = biasedData < 0; // Generate sampling clock by aligning to correlator zero-crossing if (m_data && !m_dataPrev) { if ((m_clockCount > 2) && (m_clockCount < m_samplesPerBit*3/4) && m_gotSOP) { //qDebug() << "Clock toggle ignored at " << m_clockCount; } else { m_clockCount = 0; m_clock = false; } } else { // Sample in middle of symbol if (m_clockCount == m_samplesPerBit/2) { receiveBit(m_data); m_clock = true; } m_clockCount = (m_clockCount + 1) % m_samplesPerBit; if (m_clockCount == 0) { m_clock = false; } } // Select signals to feed to scope Complex scopeSample; switch (m_settings.m_scopeCh1) { case 0: scopeSample.real(ci.real()); break; case 1: scopeSample.real(ci.imag()); break; case 2: scopeSample.real(real(exp)); break; case 3: scopeSample.real(imag(exp)); break; case 4: scopeSample.real(real(corr1)); break; case 5: scopeSample.real(imag(corr1)); break; case 6: scopeSample.real(real(corr2)); break; case 7: scopeSample.real(imag(corr2)); break; case 8: scopeSample.real(abs1Filt); break; case 9: scopeSample.real(abs2Filt); break; case 10: scopeSample.real(env1); break; case 11: scopeSample.real(env2); break; case 12: scopeSample.real(unbiasedData); break; case 13: scopeSample.real(biasedData); break; case 14: scopeSample.real(m_data); break; case 15: scopeSample.real(m_clock); break; case 16: scopeSample.real(m_bit); break; case 17: scopeSample.real(m_gotSOP); break; } switch (m_settings.m_scopeCh2) { case 0: scopeSample.imag(ci.real()); break; case 1: scopeSample.imag(ci.imag()); break; case 2: scopeSample.imag(real(exp)); break; case 3: scopeSample.imag(imag(exp)); break; case 4: scopeSample.imag(real(corr1)); break; case 5: scopeSample.imag(imag(corr1)); break; case 6: scopeSample.imag(real(corr2)); break; case 7: scopeSample.imag(imag(corr2)); break; case 8: scopeSample.imag(abs1Filt); break; case 9: scopeSample.imag(abs2Filt); break; case 10: scopeSample.imag(env1); break; case 11: scopeSample.imag(env2); break; case 12: scopeSample.imag(unbiasedData); break; case 13: scopeSample.imag(biasedData); break; case 14: scopeSample.imag(m_data); break; case 15: scopeSample.imag(m_clock); break; case 16: scopeSample.imag(m_bit); break; case 17: scopeSample.imag(m_gotSOP); break; } sampleToScope(scopeSample); } void NavtexDemodSink::receiveBit(bool bit) { m_bit = bit; // Store in shift reg m_bits = (m_bits << 1) | m_bit; m_bitCount++; if (!m_gotSOP) { if (m_bitCount == 14) { if ((m_bits & 0x3fff) == 0x19f8) // phase 2 followed by phase 1 { m_gotSOP = true; m_bitCount = 0; m_sitorBDecoder.init(); m_rssiMagSqSum = 0.0; m_rssiMagSqCount = 0; } else { m_bitCount--; } } } else { if (m_bitCount == 7) { signed char c = m_sitorBDecoder.decode(m_bits & 0x7f); if (c != -1) { //qDebug() << "Out: " << SitorBDecoder::printable(c); m_consecutiveErrors = 0; if ((c != '<') && (c != '>') && (c != 0x2)) { // 7 bytes per second, so may as well send individually to be displayed if (getMessageQueueToChannel()) { NavtexDemod::MsgCharacter *msg = NavtexDemod::MsgCharacter::create(SitorBDecoder::printable(c)); getMessageQueueToChannel()->push(msg); } // Add character to message buffer m_messageBuffer.append(QChar(c)); } else { if (m_messageBuffer.size() > 0) { QRegularExpression re("[Z*][C*][Z*][C*](.|\n|\r)*[N*][N*][N*][N*]"); QRegularExpressionMatch match = re.match(m_messageBuffer); if (match.hasMatch()) { if (getMessageQueueToChannel()) { NavtexMessage navtexMsg = NavtexMessage(match.captured(0)); float rssi = CalcDb::dbPower(m_rssiMagSqSum / m_rssiMagSqCount); NavtexDemod::MsgMessage *msg = NavtexDemod::MsgMessage::create(navtexMsg, m_sitorBDecoder.getErrors(), rssi); getMessageQueueToChannel()->push(msg); } // Navtex messages can span multiple blocks? m_messageBuffer = ""; } } if (c == 0x2) // End of text { // Reset demod init(); } } } if (c == '*') { m_errorCount++; m_consecutiveErrors++; // ITU 476-5 just says return to standby after the percentage of // mutilated signals received has reached a predetermined value // without saying what that value is if (m_messageBuffer.size() >= 12) { float errorPC = m_errorCount / (float)(m_messageBuffer.size() + m_errorCount); if (errorPC >= 0.2f) { //qDebug() << "Too many errors" << m_errorCount << m_messageBuffer.size(); init(); } } else if (m_errorCount >= 3) { //qDebug() << "Too many errors" << m_errorCount << m_messageBuffer.size(); eraseChars(m_messageBuffer.size()); init(); } if (m_consecutiveErrors >= 5) { //qDebug() << "Too many consecutive errors"; init(); } } m_bitCount = 0; } } } void NavtexDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) { qDebug() << "NavtexDemodSink::applyChannelSettings:" << " channelSampleRate: " << channelSampleRate << " channelFrequencyOffset: " << channelFrequencyOffset; if ((m_channelFrequencyOffset != channelFrequencyOffset) || (m_channelSampleRate != channelSampleRate) || force) { m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); } if ((m_channelSampleRate != channelSampleRate) || force) { m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2); m_interpolatorDistance = (Real) channelSampleRate / (Real) NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; m_interpolatorDistanceRemain = m_interpolatorDistance; } m_channelSampleRate = channelSampleRate; m_channelFrequencyOffset = channelFrequencyOffset; } void NavtexDemodSink::init() { m_expIdx = 0; m_bit = 0; m_bits = 0; m_bitCount = 0; m_gotSOP = false; m_errorCount = 0; m_clockCount = 0; m_clock = 0; m_rssiMagSqSum = 0.0; m_rssiMagSqCount = 0; m_consecutiveErrors = 0; m_sitorBDecoder.init(); m_messageBuffer = ""; } void NavtexDemodSink::applySettings(const NavtexDemodSettings& settings, bool force) { qDebug() << "NavtexDemodSink::applySettings:" << " m_rfBandwidth: " << settings.m_rfBandwidth << " force: " << force; if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); m_interpolatorDistance = (Real) m_channelSampleRate / (Real) NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; m_interpolatorDistanceRemain = m_interpolatorDistance; } if (force) { delete[] m_exp; m_exp = new Complex[m_expLength]; Real f0 = 0.0f; for (int i = 0; i < m_expLength; i++) { m_exp[i] = Complex(cos(f0), sin(f0)); f0 += 2.0f * (Real)M_PI * (NavtexDemodSettings::NAVTEXDEMOD_FREQUENCY_SHIFT/2.0f) / NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; } init(); // Due to start and stop bits, we should get mark and space at least every 8 bits // while something is being transmitted m_movMax1.setSize(m_samplesPerBit * 8); m_movMax2.setSize(m_samplesPerBit * 8); } m_settings = settings; }