├── SOURCE_CODE.txt ├── CWSL_DIGI.exe ├── Qt5Core.dll ├── source ├── TimeUtils.hpp ├── HamUtils.hpp ├── LowPass.hpp ├── CWSL_Utils.hpp ├── Decoder.hpp ├── StringUtils.hpp ├── Decoder.cpp ├── SafeQueue.h ├── WSPRNet.hpp ├── Stats.hpp ├── Instance.hpp ├── decode_audio_buffer.h ├── ring_buffer.h ├── CWSL_DIGI_Types.hpp ├── OutputHandler.hpp ├── WaveFile.hpp ├── PSKReporter.hpp ├── SharedMemory.h ├── CWSL_DIGI.hpp ├── ring_buffer_spmc.h ├── ScreenPrinter.hpp ├── SSBD.hpp ├── SharedMemory.cpp ├── CWSL_DIGI.vcxproj ├── Receiver.hpp ├── RBNHandler.hpp ├── Instance.cpp ├── WSPRNet.cpp └── PSKReporter.cpp ├── CHANGELOG.txt ├── CALIBRATION.txt ├── README.md └── config.ini /SOURCE_CODE.txt: -------------------------------------------------------------------------------- 1 | https://github.com/alexranaldi/CWSL_DIGI 2 | -------------------------------------------------------------------------------- /CWSL_DIGI.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexranaldi/CWSL_DIGI/HEAD/CWSL_DIGI.exe -------------------------------------------------------------------------------- /Qt5Core.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexranaldi/CWSL_DIGI/HEAD/Qt5Core.dll -------------------------------------------------------------------------------- /source/TimeUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | static std::uint64_t inline getEpochTimeMs() { 8 | return std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); 9 | } 10 | 11 | 12 | static std::uint64_t inline getEpochTime() { 13 | return getEpochTimeMs() / 1000; 14 | } 15 | 16 | static int inline getTimeSecondsPart() { 17 | std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); 18 | time_t tt = std::chrono::system_clock::to_time_t(now); 19 | tm utc = *gmtime(&tt); 20 | return utc.tm_sec; 21 | } 22 | -------------------------------------------------------------------------------- /source/HamUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | 26 | static inline bool isValidLocator(const std::string& loc) { 27 | if (loc.size() != 4) { 28 | return false; 29 | } 30 | if (!std::isalpha(loc.at(0))) { 31 | return false; 32 | } 33 | if (!std::isalpha(loc.at(1))) { 34 | return false; 35 | } 36 | if (!std::isdigit(loc.at(2))) { 37 | return false; 38 | } 39 | if (!std::isdigit(loc.at(3))) { 40 | return false; 41 | } 42 | return true; 43 | } 44 | -------------------------------------------------------------------------------- /source/LowPass.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef LOWPASS_HPP 3 | #define LOWPASS_HPP 4 | 5 | // David Mittiga 6 | // Alex Ranaldi W2AXR alexranaldi@gmail.com 7 | 8 | // LICENSE: GNU General Public License v3.0 9 | // THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. 10 | 11 | 12 | #include 13 | #define PI 3.14159265358979323846 14 | 15 | // Builds a lowpass filter with the given order and fractional bandwidth. 16 | template 17 | static PRECISION* BuildLowPass(const size_t order, const double bandwidth) 18 | { 19 | // Uses a weighted sinc for the taps 20 | // Note that the number of taps in the filter is order+1 but since 21 | // the last coeff would be 0 so we don't need the +1. 22 | // The first tap is 0, the middle tap is 1, and the taps are symmetrical. 23 | PRECISION *filter = new PRECISION[order]; 24 | filter[0] = static_cast(0.0); 25 | filter[order/2] = static_cast(1.0); 26 | const double x0 = -1.0*order/2; 27 | for (size_t n = 1; n < order/2; ++n) { // filter[0] = 0 so skip 28 | const double xPi = (x0+n)*PI*bandwidth; 29 | const double y = sin(xPi)/xPi * (0.54-0.46*cos(2.0*PI*n/(double)order)); 30 | filter[n] = static_cast(y); 31 | filter[order-n] = static_cast(y); 32 | } 33 | return filter; 34 | 35 | } 36 | 37 | #endif // LOWPASS_HPP 38 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.88 2 | Enhancement: FT8 - support Fox/Hound messages. This will increase the number of FT8 spots significantly if you skim the DX frequencies when an expedition is going on! 3 | Enhancement: JS8 - support for some JS8 message formats. Requires JS8Call to be installed. 4 | Enhancement: Supports SOTAmat FT8 messages (Resolves https://github.com/alexranaldi/CWSL_DIGI/issues/6) 5 | Fix: use hostname instead of IP Address when connecting to WSPRNet and PSK Reporter. This resolves the ongoing issue with 0.86 and earlier being unable to contact WSPRNet. 6 | Fix: FST4 and FST4W decoding bugs that prevented successful decoding in many cases. 7 | Fix: several bugs in callsign handling and bad callsign detection. 8 | Fix: Improve PSK Reporter connection reliability. 9 | 10 | 0.87 11 | Support for JS8 mode - experimental! 12 | Fixes for FST4 and FST4W 13 | 14 | 0.86 15 | Support for FST4 and FST4W modes 16 | Change PSK Reporter handling to allow re-spotting with band changes 17 | 18 | 0.85 19 | Add UDP message to provide active band information to RBN Aggregator 20 | Add ability to process contest FT4 and FT8 messages 21 | 22 | 0.84 23 | Allow bands to be inactive (no shared memory) to support band rotation 24 | 25 | 0.83 26 | A separate spotter callsign can now be specified per WSPR decoder 27 | A list of callsigns to ignore can be specified in the config file 28 | Fixed bug that caused WSPR spots to be reported late to WSPRNet, sometimes by many hours 29 | Reworked statistics reporting code to be more efficient 30 | 31 | 0.82 32 | Add support for JT65. 33 | Fix crash in Decoder Instance caused by missing lock in SafeQueue. Found by NA3M. 34 | Handle FT4 and FT8 messages of six characters. E.g., "CQ W6L". Previously was discarded. 35 | 36 | 0.81 37 | First public release. 38 | -------------------------------------------------------------------------------- /source/CWSL_Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "SharedMemory.h" 7 | 8 | // Maximum of CWSL bands 9 | #define MAX_CWSL 32 10 | 11 | 12 | // Prefix and suffix for the shared memories names 13 | static std::string gPreSM = "CWSL"; 14 | static std::string gPostSM = "Band"; 15 | 16 | static inline std::string createSharedMemName(const int bandIndex, const int SMNumber) { 17 | // create name of shared memory 18 | std::string Name = gPreSM + std::to_string(bandIndex) + gPostSM; 19 | if (SMNumber != -1) { 20 | Name += std::to_string(SMNumber); 21 | } 22 | return Name; 23 | } 24 | 25 | ////////////////////////////////////////////////////////////////////////////// 26 | // Find the right band 27 | static inline int findBand(const int64_t f, const int SMNumber) { 28 | CSharedMemory SM; 29 | SM_HDR h; 30 | 31 | // try to find right band - for all possible bands ... 32 | for (int bandIndex = 0; bandIndex < MAX_CWSL; ++bandIndex) { 33 | 34 | // create name of shared memory 35 | const std::string Name = createSharedMemName(bandIndex, SMNumber); 36 | 37 | // try to open shared memory 38 | if (SM.Open(Name.c_str())) { 39 | // save data from header of this band 40 | memcpy(&h, SM.GetHeader(), sizeof(SM_HDR)); 41 | 42 | // close shared memory 43 | SM.Close(); 44 | 45 | // is frequency in this band ? 46 | if ((h.SampleRate > 0) && (f >= h.L0 - h.SampleRate / 2) && (f <= h.L0 + h.SampleRate / 2)) { 47 | // yes -> assign it and break the finding loop 48 | return bandIndex; 49 | } 50 | } 51 | } 52 | return -1; 53 | } 54 | -------------------------------------------------------------------------------- /source/Decoder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2022 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | 27 | #include "CWSL_DIGI_Types.hpp" 28 | 29 | class Instance; 30 | 31 | class Decoder { 32 | private: 33 | FrequencyHz freq; 34 | FrequencyHz freqCalibrated; 35 | std::string mode; 36 | int smNum; 37 | double freqCalFactor; 38 | std::string reporterCallsign; 39 | std::unique_ptr instance; 40 | public: 41 | Decoder( 42 | FrequencyHz freq_In, 43 | FrequencyHz freqCalibrated_In, 44 | std::string mode_In, 45 | int smNum_In, 46 | double freqCalFactor_In, 47 | std::string reporterCallsign_In); 48 | 49 | float getTRPeriod(); 50 | 51 | void setInstance(std::unique_ptr inst); 52 | 53 | InstanceStatus getStatus(); 54 | 55 | void terminate(); 56 | 57 | int getsmNum() const; 58 | 59 | FrequencyHz getFreq() const; 60 | 61 | FrequencyHz getFreqCalibrated() const; 62 | 63 | std::string getMode() const; 64 | 65 | std::string getReporterCallsign() const; 66 | 67 | std::unique_ptr& getInstance(); 68 | 69 | }; 70 | 71 | using DecoderVec = std::vector; 72 | -------------------------------------------------------------------------------- /CALIBRATION.txt: -------------------------------------------------------------------------------- 1 | CWSL_DIGI uses the CWSL interface via CW Skimmer to receive I/Q data from the SDR. CW Skimmer's frequency calibration configuration setting will effect CWSL_DIGI and other applications using CWSL. CWSL_DIGI provides a global frequency calibration value in the configuration within the [radio] section, which applies to all decoders in CWSL_DIGI. In addition, a frequency calibration factor may be applied to each decoder individually, on the decoders line. The final calibration value for a given decoder is the product of the global frequency calibration factor and the decoder's frequency calibration factor. CW Skimmer's frequency calibration configuration setting may have its own impact. In testing, CW Skimmer's frequency calibration setting was found to impact downstream CWSL applications in a non-linear fashion. 2 | 3 | To obtain the calibration factors for CWSL_DIGI for own station, I used a combination of WWV and WSPRNet data. In the absence of CW Skimmer, my QS1R was found to have a linear frequency offset that could be corrected at 5MHz, 10MHz and 15MHz with a single factor. With CW Skimmer running, that offset became non-linear, and I needed to apply frequency factors per band. The reason is not entirely understood. WSPRNet provides reports to the nearest 1 Hz, and a simple average may be adequate to derive a suitable calibration factor for each band. 4 | 5 | To find a calibration factor for 20m using WSPR: 6 | 1. Turn off all reporting in CWSL_DIGI to prevent posting bad spots. 7 | 2. Enable one WSPR decoder for the band of interest. Make sure no calibration factor is specified. e.g., 8 | decoder=14095600 WSPR 9 | 3. Wait for a few WSPR spots to appear in the CWSL_DIGI window. Note the callsign and frequency. 10 | 4. Visit http://www.wsprnet.org/drupal/wsprnet/spotquery and enter the callsign of one of the spots. 11 | 5. Compute the calibration factor by dividing. The CWSL_DIGI frequency is the divisor. 12 | 13 | As an example: 14 | WSPRNet frequency for a given spot: 14097050 (eye-ball average from what is reported on page) 15 | CWSL_DIGI reported frequency for same spot: 14097117 16 | 14097050/14097117 = 0.999995247255166 17 | The frequency calibration factor for 20m is thus 0.9999952 18 | 19 | 6. Update the decoder lines for the band, as needed. 20 | 21 | W2AXR 22 | -------------------------------------------------------------------------------- /source/StringUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // trim from start (in place) 11 | static inline void ltrim(std::string& s) { 12 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { 13 | return !std::isspace(ch); 14 | })); 15 | } 16 | 17 | // trim from end (in place) 18 | static inline void rtrim(std::string& s) { 19 | s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { 20 | return !std::isspace(ch); 21 | }).base(), s.end()); 22 | } 23 | 24 | // trim from both ends (in place) 25 | static inline void trim(std::string& s) { 26 | ltrim(s); 27 | rtrim(s); 28 | } 29 | 30 | // trim from start (copying) 31 | static inline std::string ltrim_copy(std::string s) { 32 | ltrim(s); 33 | return s; 34 | } 35 | 36 | // trim from end (copying) 37 | static inline std::string rtrim_copy(std::string s) { 38 | rtrim(s); 39 | return s; 40 | } 41 | 42 | // trim from both ends (copying) 43 | static inline std::string trim_copy(std::string s) { 44 | trim(s); 45 | return s; 46 | } 47 | 48 | static inline std::vector splitStringByDelim(const std::string& input, const char delim, bool noEmpty=false) { 49 | std::istringstream iss(input); 50 | std::vector v; 51 | std::string s; 52 | while (std::getline(iss, s, delim)) { 53 | if (noEmpty) { 54 | bool all = true; 55 | for (size_t c = 0; c < s.size(); ++c) { 56 | if (s.at(c) != delim) { 57 | all = false; 58 | break; 59 | } 60 | } 61 | if (all) { 62 | continue; 63 | } 64 | } 65 | v.push_back(s); 66 | } 67 | return v; 68 | } 69 | 70 | static inline char asciitolower(char in) { 71 | if (in <= 'Z' && in >= 'A') 72 | return in - ('Z' - 'z'); 73 | return in; 74 | } 75 | 76 | static inline std::string ws2s(const std::wstring& wstr) 77 | { 78 | using convert_typeX = std::codecvt_utf8; 79 | std::wstring_convert converterX; 80 | 81 | return converterX.to_bytes(wstr); 82 | } 83 | -------------------------------------------------------------------------------- /source/Decoder.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2022 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include "Decoder.hpp" 25 | #include "Instance.hpp" 26 | #include "CWSL_DIGI.hpp" 27 | 28 | Decoder::Decoder( 29 | FrequencyHz freq_In, 30 | FrequencyHz freqCalibrated_In, 31 | std::string mode_In, 32 | int smNum_In, 33 | double freqCalFactor_In, 34 | std::string reporterCallsign_In) : 35 | freq(freq_In), 36 | freqCalibrated(freqCalibrated_In), 37 | mode(mode_In), 38 | smNum(smNum_In), 39 | freqCalFactor(freqCalFactor_In), 40 | reporterCallsign(reporterCallsign_In), 41 | instance(nullptr) 42 | {} 43 | 44 | void Decoder::setInstance(std::unique_ptr inst) { 45 | instance = std::move(inst); 46 | } 47 | 48 | float Decoder::getTRPeriod() { 49 | return getRXPeriod(mode); 50 | } 51 | 52 | InstanceStatus Decoder::getStatus() { 53 | if (!instance) { 54 | return InstanceStatus::FINISHED; 55 | } 56 | return instance->getStatus(); 57 | } 58 | 59 | void Decoder::terminate() { 60 | if (instance) { 61 | instance->terminate(); 62 | } 63 | } 64 | 65 | int Decoder::getsmNum() const { 66 | return smNum; 67 | } 68 | 69 | std::unique_ptr& Decoder::getInstance() { 70 | return instance; 71 | } 72 | 73 | FrequencyHz Decoder::getFreq() const { 74 | return freq; 75 | } 76 | 77 | FrequencyHz Decoder::getFreqCalibrated() const { 78 | return freqCalibrated; 79 | } 80 | 81 | std::string Decoder::getMode() const { 82 | return mode; 83 | } 84 | 85 | std::string Decoder::getReporterCallsign() const { 86 | return reporterCallsign; 87 | } -------------------------------------------------------------------------------- /source/SafeQueue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef SAFE_QUEUE 3 | #define SAFE_QUEUE 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace std::chrono_literals; 11 | 12 | 13 | template 14 | class SafeQueue 15 | { 16 | public: 17 | SafeQueue(void) 18 | : q() 19 | , m() 20 | , c() 21 | {} 22 | 23 | ~SafeQueue(void) 24 | {} 25 | 26 | // Add an element to the queue. 27 | void enqueue(T t) 28 | { 29 | std::lock_guard lock(m); 30 | q.push(t); 31 | c.notify_one(); 32 | } 33 | 34 | // Add an element to the queue. 35 | void enqueue_move(T& t) 36 | { 37 | std::lock_guard lock(m); 38 | q.push(std::move(t)); 39 | c.notify_one(); 40 | } 41 | 42 | bool dequeue(T& item) { 43 | std::unique_lock lock(m); 44 | if (q.empty()) 45 | { 46 | return false; 47 | } 48 | item = q.front(); 49 | q.pop(); 50 | return true; 51 | } 52 | 53 | T dequeue_move() { 54 | std::unique_lock lock(m); 55 | auto item = std::move(q.front()); 56 | q.pop(); 57 | return std::move(item); 58 | } 59 | 60 | T dequeue(void) 61 | { 62 | std::unique_lock lock(m); 63 | while (q.empty()) 64 | { 65 | // release lock as long as the wait and reaquire it afterwards. 66 | c.wait(lock); 67 | } 68 | T val = q.front(); 69 | q.pop(); 70 | return val; 71 | } 72 | 73 | bool dequeue_timeout(T& item) 74 | { 75 | std::unique_lock lock(m); 76 | while (q.empty()) 77 | { 78 | // release lock as long as the wait and reaquire it afterwards. 79 | if (c.wait_for(lock, 250ms) == std::cv_status::timeout) { 80 | return false; 81 | } 82 | } 83 | item = q.front(); 84 | q.pop(); 85 | return true; 86 | } 87 | 88 | bool empty(void) 89 | { 90 | std::unique_lock lock(m); 91 | return q.empty(); 92 | } 93 | 94 | size_t size(void) 95 | { 96 | std::unique_lock lock(m); 97 | return q.size(); 98 | } 99 | 100 | private: 101 | std::queue q; 102 | mutable std::mutex m; 103 | std::condition_variable c; 104 | }; 105 | #endif -------------------------------------------------------------------------------- /source/WSPRNet.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2023 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | 31 | #include "SafeQueue.h" 32 | #include "CWSL_DIGI.hpp" 33 | 34 | class ScreenPrinter; 35 | 36 | using namespace std::literals; 37 | using namespace std; 38 | 39 | namespace wspr { 40 | struct Report 41 | { 42 | std::string callsign; 43 | std::int32_t snr; 44 | std::uint32_t freq; 45 | std::string locator; 46 | std::uint64_t epochTime; 47 | std::int32_t mode = -1; 48 | float dt; 49 | std::int16_t drift; 50 | std::uint32_t recvfreq; 51 | std::int16_t dbm; 52 | std::string reporterCallsign; 53 | }; 54 | } 55 | 56 | class WSPRNet { 57 | 58 | public: 59 | WSPRNet(const std::string& grid, std::shared_ptr sp ); 60 | 61 | virtual ~WSPRNet(); 62 | 63 | bool init(); 64 | 65 | void terminate(); 66 | 67 | void handle(const std::string& callsign, const std::string& mode, const int32_t snr, const float dt, const std::int16_t drift, const std::int16_t dbm, const uint32_t freq, const uint32_t rf, const uint64_t epochTime, const std::string& grid, const std::string& reporterCallsign); 68 | 69 | bool isConnected(); 70 | 71 | bool closeSocket(); 72 | 73 | bool connectSocket(); 74 | 75 | void sendReportWrapper(const wspr::Report& report); 76 | 77 | bool sendReport(const wspr::Report& report); 78 | 79 | bool sendMessageWithRetry(const std::string& message); 80 | 81 | int sendMessage(const std::string& message); 82 | 83 | std::string readMessage(); 84 | 85 | void processingLoop(); 86 | 87 | void reportStats(); 88 | 89 | std::shared_ptr screenPrinter; 90 | SafeQueue< wspr::Report > mReports; 91 | const std::string SERVER_HOSTNAME = "wsprnet.org"; 92 | const std::string portStr = "80"; 93 | bool terminateFlag{0}; 94 | std::thread mSendThread; 95 | SOCKET mSocket; 96 | sockaddr_storage target; 97 | std::string operatorGrid; 98 | 99 | std::atomic_int mCountSendsOK; 100 | std::atomic_int mCountSendsErrored; 101 | }; 102 | -------------------------------------------------------------------------------- /source/Stats.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "SafeQueue.h" 32 | #include "ScreenPrinter.hpp" 33 | #include "TimeUtils.hpp" 34 | #include "Decoder.hpp" 35 | 36 | struct StatsItem { 37 | StatsItem(const std::size_t id, const std::uint64_t ms) : 38 | instanceId(id), 39 | tsMs(ms) 40 | {} 41 | std::size_t instanceId; 42 | std::uint64_t tsMs; 43 | }; 44 | 45 | using DecoderStats = std::vector< std::uint64_t >; 46 | 47 | class Stats { 48 | public: 49 | Stats( 50 | const std::uint32_t maxIntervalSec_In, 51 | const std::uint32_t numDecoders_In): 52 | maxIntervalSec(maxIntervalSec_In), 53 | maxIntervalMs(static_cast(maxIntervalSec_In) * 1000), 54 | numDecoders(numDecoders_In), 55 | decoderStatsVec(numDecoders_In) { 56 | 57 | for (auto& v : decoderStatsVec) { 58 | v.reserve(10000); 59 | } 60 | 61 | } 62 | 63 | virtual ~Stats() {} 64 | 65 | std::uint64_t getCounts(const size_t index, const std::uint32_t intervalSec) { 66 | const std::uint64_t intervalMs = static_cast( intervalSec ) * 1000; 67 | const std::int64_t diff = getEpochTimeMs() - intervalMs; 68 | std::uint64_t out = 0; 69 | DecoderStats& stats = decoderStatsVec[index]; 70 | for (std::size_t k = 0; k < stats.size(); ++k) { 71 | if (static_cast(stats[k]) > diff) { 72 | out++; 73 | } 74 | } 75 | return out; 76 | } 77 | 78 | void handleReport(const std::size_t instanceId, const std::uint64_t tsMs) { 79 | StatsItem item(instanceId, tsMs); 80 | reports.enqueue(item); 81 | } 82 | 83 | void process() { 84 | while (!reports.empty()) { 85 | StatsItem item = reports.dequeue(); 86 | decoderStatsVec[item.instanceId].push_back(item.tsMs); 87 | } 88 | prune(); 89 | } 90 | 91 | void prune() { 92 | const std::int64_t dt = getEpochTimeMs() - maxIntervalMs; 93 | for (DecoderStats& dec : decoderStatsVec) { 94 | std::sort(dec.begin(), dec.end()); 95 | while (!dec.empty()) { 96 | const std::int64_t dv = static_cast(dec.front()); 97 | if (dv <= dt) { 98 | dec.erase(dec.begin()); 99 | } 100 | else { 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | 107 | private: 108 | std::uint32_t maxIntervalSec; 109 | std::uint64_t maxIntervalMs; 110 | std::uint32_t numDecoders; 111 | std::vector< DecoderStats > decoderStatsVec; 112 | 113 | SafeQueue reports; 114 | }; 115 | 116 | 117 | -------------------------------------------------------------------------------- /source/Instance.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef INSTANCE_HPP 4 | #define INSTANCE_HPP 5 | 6 | /* 7 | Copyright 2022 Alexander Ranaldi 8 | W2AXR 9 | alexranaldi@gmail.com 10 | 11 | This file is part of CWSL_DIGI. 12 | 13 | CWSL_DIGI is free software : you can redistribute it and /or modify 14 | it under the terms of the GNU General Public License as published by 15 | the Free Software Foundation, either version 3 of the License, or 16 | (at your option) any later version. 17 | 18 | CWSL_DIGI is distributed in the hope that it will be useful, 19 | but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 21 | GNU General Public License for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 25 | */ 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | class Receiver; 37 | class ScreenPrinter; 38 | class DecoderPool; 39 | 40 | #include "SSBD.hpp" 41 | 42 | #include "CWSL_DIGI_Types.hpp" 43 | #include "ring_buffer.h" 44 | #include "decode_audio_buffer.h" 45 | #include "ring_buffer_spmc.h" 46 | 47 | class Instance { 48 | public: 49 | Instance( 50 | std::shared_ptr receiverIn, 51 | const size_t idIn, 52 | std::shared_ptr predIn, 53 | const FrequencyHz ssbFreqIn, 54 | const FrequencyHz calibratedFreqIn, 55 | const std::string& modeIn, 56 | const std::string& callsignIn, 57 | const uint32_t waveSampleRateIn, 58 | const float audioScaleFactor_ftIn, 59 | const float audioScaleFactor_wsprIn, 60 | std::shared_ptr sp, 61 | std::shared_ptr dp, 62 | const float trperiodIn 63 | ); 64 | 65 | virtual ~Instance(); 66 | 67 | std::string getMode() const; 68 | 69 | FrequencyHz getFrequency() const; 70 | 71 | std::string getCallsign() const; 72 | 73 | void terminate(); 74 | 75 | bool init(); 76 | 77 | void sampleManager(); 78 | 79 | inline bool terminateNow(); 80 | 81 | void prepareAudio(sample_buffer_t& audioBuffer, const std::string& mode); 82 | 83 | std::string instanceLog() const; 84 | 85 | InstanceStatus getStatus(); 86 | 87 | void setReceiver(std::shared_ptr r); 88 | 89 | 90 | 91 | private: 92 | 93 | ring_buffer_spmc_t*>* iq_buffer; 94 | 95 | ring_buffer_t< sample_buffer_t > af_buffer; 96 | FrequencyHz ssbFreq; 97 | FrequencyHz calibratedSSBFreq; 98 | 99 | std::string digitalMode; 100 | std::uint32_t decRatio; 101 | std::uint32_t SSB_SR; 102 | std::uint32_t waveSampleRate; 103 | std::unique_ptr> ssbd; 104 | std::shared_ptr screenPrinter; 105 | 106 | std::size_t iq_reader_id; 107 | 108 | std::atomic_bool terminateFlag; 109 | 110 | std::thread sampleManagerThread; 111 | 112 | 113 | float audioScaleFactor_ft; 114 | float audioScaleFactor_wspr; 115 | 116 | std::shared_ptr decoderPool; 117 | 118 | std::shared_ptr pred; 119 | 120 | std::size_t id; 121 | 122 | std::string smname; 123 | 124 | std::shared_ptr receiver; 125 | 126 | std::string cwd; 127 | 128 | std::string callsign; 129 | 130 | std::atomic status; 131 | 132 | float trperiod; 133 | 134 | }; 135 | 136 | #endif 137 | -------------------------------------------------------------------------------- /source/decode_audio_buffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | template 29 | struct sample_buffer_t { 30 | public: 31 | std::uint64_t write_index; 32 | T* buf; 33 | std::uint64_t startEpochTime; 34 | std::size_t size; 35 | 36 | sample_buffer_t() : 37 | write_index(0), 38 | size(0), 39 | buf(nullptr), 40 | startEpochTime(0) 41 | { 42 | } 43 | 44 | std::size_t byte_size() const { 45 | return sizeof(T) * size; 46 | } 47 | 48 | sample_buffer_t(const sample_buffer_t& obj) { 49 | startEpochTime = obj.startEpochTime; 50 | write_index = 0; 51 | size = obj.size; 52 | buf = reinterpret_cast(malloc(byte_size())); 53 | memcpy(buf, obj.buf, byte_size()); 54 | } 55 | 56 | virtual ~sample_buffer_t() { 57 | deallocate(); 58 | } 59 | 60 | void deallocate() { 61 | if (buf) { 62 | free(buf); 63 | buf = nullptr; 64 | } 65 | } 66 | 67 | void initialize(const std::size_t new_size) { 68 | init(new_size); 69 | } 70 | 71 | void init(const std::size_t new_size) 72 | { 73 | size = new_size; 74 | buf = reinterpret_cast(malloc(new_size * sizeof(T))); 75 | write_index = 0; 76 | startEpochTime = 0; 77 | } 78 | 79 | void initByteSize(const std::size_t new_size) 80 | { 81 | size = new_size / sizeof(T); 82 | buf = reinterpret_cast(malloc(new_size)); 83 | write_index = 0; 84 | startEpochTime = 0; 85 | } 86 | 87 | bool full() const { 88 | return size == write_index - 1; 89 | } 90 | 91 | bool write(const std::vector& samples) 92 | { 93 | if ((samples.size() + write_index) > size) 94 | { 95 | size_t k = 0; 96 | while (write_index < size) { 97 | buf[write_index] = samples[k]; 98 | write_index++; 99 | k++; 100 | } 101 | return false; 102 | } 103 | else { 104 | for (const auto& sample : samples) { 105 | buf[write_index] = sample; 106 | write_index++; 107 | } 108 | return true; 109 | } 110 | } 111 | 112 | void resetIndices() { 113 | write_index = 0; 114 | } 115 | 116 | void reset() { 117 | resetIndices(); 118 | } 119 | 120 | void scale(const T factor) { 121 | for (size_t k = 0; k < size; ++k) { 122 | buf[k] *= factor; 123 | } 124 | } 125 | 126 | void clear() 127 | { 128 | for (size_t k = 0; k < size; ++k) { 129 | buf[k] = 0; 130 | } 131 | resetIndices(); 132 | } 133 | 134 | 135 | }; 136 | 137 | template 138 | struct decode_audio_buffer_t : public sample_buffer_t { 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /source/ring_buffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | // Used internally as a single producer single consumer queue (ring buffer) 30 | template 31 | struct ring_buffer_t { 32 | T* recs; 33 | // Use atomics for indices to prevent instruction reordering 34 | std::atomic read_index; 35 | std::atomic write_index; 36 | // Number of items the ring buffer can hold. 37 | std::size_t size; 38 | std::atomic_bool terminateFlag = false; 39 | bool initialized = false; 40 | 41 | ring_buffer_t() : 42 | terminateFlag(false), 43 | recs(nullptr), 44 | initialized(false), 45 | size(0) 46 | { 47 | } 48 | 49 | bool initialize(const size_t sizeIn) 50 | { 51 | size = sizeIn; 52 | recs = reinterpret_cast(malloc(sizeof(T) * size)); 53 | initialized = recs != nullptr; 54 | reset(); 55 | return initialized; 56 | } 57 | 58 | void reset() { 59 | read_index = 0; 60 | write_index = 0; 61 | terminateFlag = false; 62 | } 63 | 64 | void terminate() 65 | { 66 | terminateFlag = true; 67 | } 68 | 69 | ~ring_buffer_t() 70 | { 71 | if (nullptr != recs) 72 | { 73 | free(recs); 74 | } 75 | } 76 | 77 | bool full() const { 78 | return ((read_index == write_index + 1) || (read_index == 0 && static_cast(write_index) == static_cast(size) - 1)); 79 | } 80 | 81 | bool wait_for_empty_slot() const 82 | { 83 | while (full()) { 84 | std::this_thread::yield(); 85 | if (terminateFlag) { 86 | return false; 87 | } 88 | } 89 | return true; 90 | } 91 | 92 | void inc_write_index() 93 | { 94 | // cast to signed so we don't break subtraction 95 | if (static_cast(write_index) == static_cast(size) - 1) { 96 | write_index = 0; 97 | } 98 | else { 99 | write_index++; 100 | } 101 | } 102 | 103 | std::uint64_t get_next_write_index() 104 | { 105 | // cast to signed so we don't break subtraction 106 | if (static_cast(write_index) == static_cast(size) - 1) { 107 | return 0; 108 | } 109 | else { 110 | return write_index + 1; 111 | } 112 | } 113 | 114 | T& current() { 115 | return recs[read_index]; 116 | } 117 | 118 | T& pop_ref() 119 | { 120 | T& curr = recs[read_index]; 121 | if (read_index == size - 1) { 122 | read_index = 0; 123 | } 124 | else { 125 | read_index++; 126 | } 127 | return curr; 128 | } 129 | 130 | T pop() 131 | { 132 | T curr = recs[read_index]; 133 | if (read_index == size - 1) { 134 | read_index = 0; 135 | } 136 | else { 137 | read_index++; 138 | } 139 | return curr; 140 | } 141 | 142 | bool wait_for_data() const 143 | { 144 | while (empty()) { 145 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 146 | if (terminateFlag) { 147 | return false; 148 | } 149 | } 150 | return true; 151 | } 152 | 153 | bool empty() const { 154 | return read_index == write_index; 155 | } 156 | 157 | }; 158 | -------------------------------------------------------------------------------- /source/CWSL_DIGI_Types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2022 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | class Instance; 32 | 33 | using FrequencyHz = std::uint32_t; 34 | 35 | struct JT9Output 36 | { 37 | std::string output = ""; 38 | std::string mode = ""; 39 | uint64_t epochTime = 0; 40 | FrequencyHz baseFreq = 0; 41 | std::size_t instanceId = 0; 42 | 43 | JT9Output( 44 | const std::string& rawOutputIn, 45 | const std::string& modeIn, 46 | const uint64_t epochTimeIn, 47 | const FrequencyHz baseFreqIn, 48 | const std::size_t instanceIdIn) : 49 | output(rawOutputIn), 50 | mode(modeIn), 51 | epochTime(epochTimeIn), 52 | baseFreq(baseFreqIn), 53 | instanceId(instanceIdIn) 54 | {} 55 | }; 56 | 57 | enum class InstanceStatus : int { 58 | NOT_INITIALIZED, 59 | RUNNING, 60 | STOPPED, 61 | FINISHED, 62 | }; 63 | 64 | 65 | class SyncPredicate { 66 | public: 67 | SyncPredicate() { 68 | pred.store(false); 69 | } 70 | bool load() { 71 | return pred.load(); 72 | } 73 | void store(bool a) { 74 | pred.store(a); 75 | } 76 | private: 77 | std::atomic_bool pred; 78 | }; 79 | 80 | class SyncPredicates { 81 | public: 82 | SyncPredicates() {} 83 | std::shared_ptr createPredicate(std::string mode) { 84 | std::shared_ptr pred = std::make_shared(); 85 | if (mode == "FT8") { 86 | ft8Preds.push_back(pred); 87 | } 88 | else if (mode == "JS8") { 89 | ft8Preds.push_back(pred); 90 | } 91 | else if (mode == "FT4") { 92 | ft4Preds.push_back(pred); 93 | } 94 | else if (mode == "JT65") { 95 | s60sPreds.push_back(pred); 96 | } 97 | else if (mode == "Q65-30") { 98 | q65_30Preds.push_back(pred); 99 | } 100 | else if (mode == "FST4-60") { 101 | s60sPreds.push_back(pred); 102 | } 103 | else if (mode == "WSPR") { 104 | s120sPreds.push_back(pred); 105 | } 106 | else if (mode == "FST4-120") { 107 | s120sPreds.push_back(pred); 108 | } 109 | else if (mode == "FST4-300") { 110 | s300sPreds.push_back(pred); 111 | } 112 | else if (mode == "FST4-900") { 113 | s900sPreds.push_back(pred); 114 | } 115 | else if (mode == "FST4-1800") { 116 | s1800sPreds.push_back(pred); 117 | } 118 | else if (mode == "FST4W-120") { 119 | s120sPreds.push_back(pred); 120 | } 121 | else if (mode == "FST4W-300") { 122 | s300sPreds.push_back(pred); 123 | } 124 | else if (mode == "FST4W-900") { 125 | s900sPreds.push_back(pred); 126 | } 127 | else if (mode == "FST4W-1800") { 128 | s1800sPreds.push_back(pred); 129 | } 130 | else { 131 | throw std::runtime_error("Unhandled mode: " + mode); 132 | } 133 | return pred; 134 | } 135 | 136 | std::vector> ft8Preds; 137 | std::vector> ft4Preds; 138 | std::vector> q65_30Preds; 139 | std::vector> s60sPreds; 140 | std::vector> s120sPreds; 141 | std::vector> s300sPreds; 142 | std::vector> s900sPreds; 143 | std::vector> s1800sPreds; 144 | 145 | }; 146 | -------------------------------------------------------------------------------- /source/OutputHandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef OUTPUT_HANDLER_HPP 3 | #define OUTPUT_HANDLER_HPP 4 | 5 | /* 6 | Copyright 2022 Alexander Ranaldi 7 | W2AXR 8 | alexranaldi@gmail.com 9 | 10 | This file is part of CWSL_DIGI. 11 | 12 | CWSL_DIGI is free software : you can redistribute it and /or modify 13 | it under the terms of the GNU General Public License as published by 14 | the Free Software Foundation, either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | CWSL_DIGI is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 24 | */ 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | 33 | class ScreenPrinter; 34 | class Stats; 35 | class RBNHandler; 36 | class WSPRNet; 37 | class Decoder; 38 | 39 | namespace pskreporter { 40 | class PSKReporter; 41 | } 42 | 43 | #include "SafeQueue.h" 44 | #include "CWSL_DIGI.hpp" 45 | 46 | class OutputHandler { 47 | 48 | public: 49 | 50 | OutputHandler( 51 | const bool printHandledReportsIn, 52 | const std::string& badMessageLogFileIn, 53 | const std::string& decodesFileNameIn, 54 | std::shared_ptr sp, 55 | std::shared_ptr statsHandler_In, 56 | std::vector& instances_In); 57 | 58 | virtual ~OutputHandler(); 59 | 60 | void setRBNHandler(std::shared_ptr rep); 61 | 62 | void setPSKReporter(std::shared_ptr rep); 63 | 64 | void setWSPRNet(std::shared_ptr wn); 65 | 66 | void processingLoop(); 67 | 68 | // TODO: make this a separate thread some day? 69 | void handle(const JT9Output& output); 70 | 71 | void parseOutputWSPR(const std::string& out, const uint64_t epochTime, const FrequencyHz baseFreq, const std::size_t instanceId); 72 | 73 | void parseOutputFST4(const std::string& out, const uint64_t epochTime, const FrequencyHz baseFreq, const std::size_t instanceId); 74 | 75 | void parseOutputFST4W(const std::string& out, const uint64_t epochTime, const FrequencyHz baseFreq, const std::size_t instanceId); 76 | 77 | void parseOutputFT4FT8(const std::string out, const uint64_t epochTime, const std::string mode, const FrequencyHz baseFreq, const std::size_t instanceId); 78 | 79 | void parseOutputJS8(const std::string out, const uint64_t epochTime, const std::string mode, const FrequencyHz baseFreq, const std::size_t instanceId); 80 | 81 | void parseOutputJT65(const JT9Output& output); 82 | 83 | void parseOutputQ65(const JT9Output& output); 84 | 85 | void logBadMessage(const std::string badMsg); 86 | 87 | bool checkCall(const std::string& call); 88 | 89 | void parseCall(std::string& call); 90 | 91 | bool isCallPacked(const std::string& call); 92 | 93 | void ignoreCallsign(const std::string& call); 94 | 95 | bool isCallsignIgnored(const std::string& test); 96 | 97 | bool isSOTAMATMessage(const std::string& s1, const std::string& s2); 98 | 99 | bool handleMessageUniversal( 100 | std::string& o_call, 101 | std::string& o_loc, 102 | const int32_t snr, 103 | const uint32_t freq, 104 | std::string msg, 105 | const uint64_t epochTime, 106 | const std::string& mode, 107 | const FrequencyHz baseFreq, 108 | const float dt); 109 | 110 | 111 | private: 112 | int rbnPort; 113 | 114 | std::shared_ptr screenPrinter; 115 | std::shared_ptr rbn; 116 | 117 | std::shared_ptr reporter; 118 | std::shared_ptr wsprNet; 119 | 120 | SafeQueue< JT9Output > items; 121 | 122 | bool useDecodesFile; 123 | std::string decodesFileName; 124 | 125 | std::string badMessageLogFile; 126 | bool printHandledReports; 127 | std::ofstream ofs; 128 | 129 | std::thread processingThread; 130 | 131 | std::shared_ptr statsHandler; 132 | 133 | std::vector ignoredCallsigns; 134 | 135 | std::vector& instances; 136 | }; 137 | 138 | #endif 139 | -------------------------------------------------------------------------------- /source/WaveFile.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include 5 | #include 6 | 7 | #include "windows.h" 8 | 9 | 10 | #ifdef __GNUC__ 11 | #define PACK( __Declaration__ ) __Declaration__ __attribute__((__packed__)) 12 | #endif 13 | 14 | #ifdef _MSC_VER 15 | #define PACK( __Declaration__ ) __pragma( pack(push, 1) ) __Declaration__ __pragma( pack(pop)) 16 | #endif 17 | 18 | // Complete WAV file header 19 | PACK(struct WavHdr 20 | { 21 | char _RIFF[4]; // "RIFF" 22 | std::uint32_t FileLen; // length of all data after this (FileLength - 8) 23 | 24 | char _WAVE[4]; // "WAVE" 25 | 26 | char _fmt[4]; // "fmt " 27 | DWORD FmtLen; // length of the next item (sizeof(WAVEFORMATEX)) 28 | // WAVEFORMATEXTENSIBLE Format; // wave format description 29 | 30 | WAVEFORMATEX Format; // wave format description 31 | 32 | char _data[4]; // "data" 33 | DWORD DataLen; // length of the next data (FileLength - sizeof(struct WavHdr)) 34 | 35 | }); 36 | 37 | 38 | ////////////////////////////////////////////////////////////////////////////////////////////// 39 | // Open wav file 40 | inline HANDLE wavOpen(const std::string& filename, WavHdr& Hdr, const bool isTemp) 41 | { 42 | LPCVOID h; 43 | DWORD hl, l; 44 | 45 | HANDLE File = INVALID_HANDLE_VALUE; 46 | 47 | if (isTemp) { 48 | File = ::CreateFile(filename.c_str(), GENERIC_WRITE | GENERIC_READ, 49 | FILE_SHARE_READ , NULL, CREATE_ALWAYS, 50 | FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_SEQUENTIAL_SCAN, 51 | NULL 52 | ); 53 | } 54 | else { 55 | File = ::CreateFile(filename.c_str(), GENERIC_WRITE | GENERIC_READ, 56 | FILE_SHARE_READ, NULL, CREATE_ALWAYS, 57 | FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, 58 | NULL 59 | ); 60 | } 61 | 62 | if (File == INVALID_HANDLE_VALUE) { 63 | return File; 64 | } 65 | 66 | // use Hdr 67 | h = &Hdr; 68 | hl = sizeof(Hdr); 69 | 70 | // std::cout << "Wave header FILE SIZE FIELD IS set to " << Hdr.FileLen << " Bytes" << std::endl; 71 | 72 | // std::cout << "Writing wave header, " << hl << " Bytes" << std::endl; 73 | 74 | // write header to file 75 | if ((!::WriteFile(File, h, hl, &l, NULL)) || (l != hl)) 76 | { 77 | ::CloseHandle(File); 78 | File = INVALID_HANDLE_VALUE; 79 | std::cerr << "Header write failure" << std::endl; 80 | return(FALSE); 81 | } 82 | 83 | // success 84 | return File; 85 | } 86 | 87 | inline void waveWrite(const std::vector& audioBuffer, const std::string& fileName) { 88 | // screenPrinter->print("Beginning wave file generation...", LOG_LEVEL::DEBUG); 89 | 90 | const uint64_t startTime = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); 91 | 92 | std::size_t DataLen = audioBuffer.size() * sizeof(std::int16_t); 93 | // screenPrinter->print("Audio Data Length (bytes): " + std::to_string(DataLen), LOG_LEVEL::DEBUG); 94 | // screenPrinter->print("Audio Data Length (samples): " + std::to_string(audioBuffer.size()), LOG_LEVEL::DEBUG); 95 | 96 | WavHdr Hdr; 97 | 98 | Hdr._RIFF[0] = 'R'; Hdr._RIFF[1] = 'I'; Hdr._RIFF[2] = 'F'; Hdr._RIFF[3] = 'F'; 99 | Hdr.FileLen = static_cast((sizeof(Hdr) + DataLen) - 8); 100 | Hdr._WAVE[0] = 'W'; Hdr._WAVE[1] = 'A'; Hdr._WAVE[2] = 'V'; Hdr._WAVE[3] = 'E'; 101 | Hdr._fmt[0] = 'f'; Hdr._fmt[1] = 'm'; Hdr._fmt[2] = 't'; Hdr._fmt[3] = ' '; 102 | 103 | Hdr.FmtLen = sizeof(WAVEFORMATEX); 104 | Hdr.Format.wFormatTag = WAVE_FORMAT_PCM; 105 | Hdr.Format.nChannels = 1; 106 | Hdr.Format.nSamplesPerSec = 12000; 107 | Hdr.Format.nBlockAlign = 2; 108 | Hdr.Format.nAvgBytesPerSec = Hdr.Format.nSamplesPerSec * Hdr.Format.nBlockAlign; 109 | Hdr.Format.wBitsPerSample = 16; 110 | Hdr.Format.cbSize = 0; 111 | 112 | Hdr._data[0] = 'd'; Hdr._data[1] = 'a'; Hdr._data[2] = 't'; Hdr._data[3] = 'a'; 113 | Hdr.DataLen = static_cast(DataLen); 114 | 115 | std::uint32_t bytesWritten = 0; 116 | 117 | HANDLE File = wavOpen(fileName, Hdr, false); 118 | 119 | const void* Data = audioBuffer.data(); 120 | 121 | try { 122 | const bool writeStatus = WriteFile(File, Data, (DWORD)DataLen, (LPDWORD)&bytesWritten, NULL); 123 | if (!writeStatus) { 124 | // screenPrinter->err("Error writing wave file data"); 125 | } 126 | } 127 | catch (const std::exception&) { 128 | } 129 | 130 | CloseHandle(File); 131 | 132 | const uint64_t stopTime = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); 133 | // screenPrinter->debug("Wave writing completed in " + std::to_string(static_cast(stopTime - startTime) / 1000) + " sec"); 134 | 135 | } -------------------------------------------------------------------------------- /source/PSKReporter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2023 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "CWSL_DIGI.hpp" 31 | 32 | #include "SafeQueue.h" 33 | 34 | class ScreenPrinter; 35 | 36 | using namespace std::literals; 37 | 38 | namespace pskreporter { 39 | 40 | using Byte = std::uint8_t; // byte 41 | using Packet = std::vector; 42 | 43 | using FrequencyHz = std::uint32_t; 44 | 45 | struct Report 46 | { 47 | std::string callsign = ""; 48 | std::int32_t snr = 0; 49 | FrequencyHz freq = 0; 50 | std::string locator = ""; 51 | uint64_t epochTime = 0; 52 | std::string mode = ""; 53 | Report( 54 | const std::string callsign_in, 55 | const int32_t snr_in, 56 | const FrequencyHz freq_in, 57 | const std::string locator_in, 58 | const uint64_t epochTime_in, 59 | const std::string mode_in 60 | ) : 61 | callsign(callsign_in), 62 | snr(snr_in), 63 | freq(freq_in), 64 | locator(locator_in), 65 | epochTime(epochTime_in), 66 | mode(mode_in) 67 | { 68 | // fix modes. wsjt-x does not report the tx/rx periods for FST4 and FST4W, so we won't either. 69 | // FST4W-XXX -> FST4W 70 | // FST4-XXX -> FST4 71 | 72 | if (isModeFST4W(mode)) { 73 | mode = "FST4W"; 74 | } 75 | else if (isModeFST4(mode)) { 76 | mode = "FST4"; 77 | } 78 | } 79 | }; 80 | 81 | enum class REPORT_TYPE { 82 | TYPE_HAS_LOCATOR = 1, 83 | TYPE_NO_LOCATOR = 2, 84 | }; 85 | 86 | class PSKReporter { 87 | 88 | public: 89 | PSKReporter(); 90 | ~PSKReporter(); 91 | 92 | void handle(const std::string& callsign, int32_t snr, FrequencyHz freq, uint64_t epochTime, const std::string& mode); 93 | 94 | void handle(const std::string& callsign, int32_t snr, uint32_t freq, const std::string& loc, uint64_t epochTime, const std::string& mode); 95 | 96 | bool init(const std::string& operatorCallsign, const std::string& operatorLocator, const std::string& programName, std::shared_ptr printer); 97 | 98 | Packet getHeader(); 99 | 100 | Packet getReceiverInformation(); 101 | 102 | template 103 | void packetAppend(Packet& packet, T data) { 104 | packet.push_back(static_cast(data)); 105 | } 106 | 107 | void processingLoop(); 108 | 109 | Packet getSenderRecord(Report& report); 110 | 111 | void sendPacket(Packet& packet); 112 | 113 | std::size_t makePackets(); 114 | 115 | void terminate(); 116 | 117 | private: 118 | 119 | bool isSameBand(const FrequencyHz f1, const FrequencyHz f2); 120 | 121 | void addHeaderToPacket(Packet& packet); 122 | 123 | void addDescriptorsToPacket(Packet& packet); 124 | 125 | std::vector getDescriptorReceiver(); 126 | 127 | std::vector getDescriptorSender(REPORT_TYPE reportType); 128 | 129 | std::uint32_t mID; 130 | std::uint32_t mSeq; 131 | 132 | bool terminateFlag; 133 | 134 | const std::string PORT = "4739"; 135 | const std::string PORT_TEST = "14739"; 136 | 137 | SOCKET mSocket; 138 | 139 | SafeQueue< Report > mReports; 140 | std::list< Report > mSentReports; 141 | 142 | std::thread mSendThread; 143 | 144 | const std::int64_t MIN_SECONDS_BETWEEN_SAME_CALLSIGN_REPORTS = 181; 145 | 146 | // max number of "safe" bytes to put in a UDP payload (one report fewer) 147 | static const size_t MAX_UDP_PAYLOAD_SIZE = 1342; 148 | 149 | std::chrono::time_point mTimeDescriptorsSent; 150 | 151 | size_t mPacketsSentWithDescriptors{0}; 152 | 153 | std::string mReceiverCallsign; 154 | std::string mReceiverLocator; 155 | 156 | std::string mProgramName; 157 | 158 | std::queue< Packet > mPackets; 159 | 160 | std::shared_ptr screenPrinter; 161 | 162 | const std::string SERVER_HOSTNAME = "report.pskreporter.info"; 163 | }; 164 | 165 | }; // namespace 166 | -------------------------------------------------------------------------------- /source/SharedMemory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // 4 | // SharedMemory.h - class for working with shared memory on Win32 5 | // (adapted from my old works - comments in czech) 6 | // 7 | 8 | /////////////////////////////////////////////////////////////////////////////////////////// 9 | // Definition of header struct 10 | typedef struct { 11 | 12 | // sample rate 13 | int SampleRate; 14 | 15 | // block length in samples 16 | int BlockInSamples; 17 | 18 | // center frequency in Hz 19 | int L0; 20 | 21 | } SM_HDR; 22 | 23 | /////////////////////////////////////////////////////////////////////////////////////////// 24 | // Deklarace tridy CSharedMemory 25 | class CSharedMemory { 26 | 27 | // Atributy 28 | private: 29 | 30 | HANDLE m_MapFile; // handle na file-mapping objekt 31 | 32 | LPVOID m_MapAddress; // adresa zacatku sdilene pameti 33 | DWORD m_Length; // delka sdilene pameti 34 | 35 | LPVOID m_Data; // adresa zacatku dat ve sdilene pameti 36 | DWORD m_DataLength; // delka dat ve sdilene pameti 37 | 38 | DWORD m_PageSize; // velikost pametove stranky 39 | 40 | HANDLE m_Event; // event pro udalosti na bufferu (typ. nova data) 41 | 42 | BOOL m_Write; // muzeme zapisovat ? 43 | 44 | PBYTE m_Top; // ukazatel na zacatek bufferu 45 | PBYTE m_Bot; // ukazatel na konec bufferu 46 | PBYTE m_Current; // ukazatel na aktualni pozici v bufferu 47 | 48 | DWORD *m_pLength; // ukazatel na delku dat v hlavicce 49 | DWORD *m_pCurrent; // ukazatel na aktualni pozici (offset) v bufferu v hlavicce 50 | DWORD *m_pLastWrite; // ukazatel na cas posledniho zapisu v hlavicce 51 | 52 | 53 | // Metody 54 | public: 55 | 56 | // vytvor usek sdilene pameti 57 | BOOL Create(LPCTSTR Name, DWORD Length, BOOL WriteAccess = FALSE); 58 | 59 | // pripoj se na usek sdilene pameti 60 | BOOL Open(LPCTSTR Name, BOOL WriteAccess = FALSE, char *ErrorInfo = NULL); 61 | 62 | // jsme pripojeni na usek sdilene pameti ? 63 | BOOL IsOpen(void) {return (m_MapAddress != NULL);} 64 | 65 | // vraci ukazatel na hlavicku bufferu 66 | SM_HDR *GetHeader(void) {return((SM_HDR *)m_MapAddress);} 67 | 68 | // vraci handle na event udalosti na bufferu 69 | HANDLE GetEvent(void) {return(m_Event);} 70 | 71 | // ziskej pravo zapisu do bufferu 72 | BOOL GrantWriteAccess(void) 73 | {DWORD Dummy; 74 | if (VirtualProtect(m_MapAddress, m_Length, PAGE_READWRITE, &Dummy)) 75 | {m_Write = TRUE; 76 | return(TRUE); 77 | } 78 | else return(FALSE); 79 | } 80 | 81 | // uvolni pravo zapisu do bufferu 82 | BOOL DenyWriteAccess(void) 83 | {DWORD Dummy; 84 | if (VirtualProtect(m_MapAddress, m_Length, PAGE_READONLY, &Dummy)) 85 | {m_Write = FALSE; 86 | return(TRUE); 87 | } 88 | else return(FALSE); 89 | } 90 | 91 | // uvolni vsechna prava na buffer 92 | BOOL DenyAllAccess(void) 93 | {DWORD Dummy; 94 | if (VirtualProtect(m_MapAddress, m_Length, PAGE_NOACCESS, &Dummy)) 95 | {m_Write = FALSE; 96 | return(TRUE); 97 | } 98 | else return(FALSE); 99 | } 100 | 101 | // zjisti vlastnosti sdilene pameti 102 | BOOL GetMemoryProperties(PMEMORY_BASIC_INFORMATION pMI) 103 | { 104 | if (pMI == NULL) return(FALSE); 105 | if (VirtualQuery(m_MapAddress, pMI, sizeof(MEMORY_BASIC_INFORMATION)) 106 | != sizeof(MEMORY_BASIC_INFORMATION) 107 | ) return(FALSE); 108 | return(TRUE); 109 | } 110 | 111 | // pockej na nova data 112 | // (vraci TRUE pokud nekdo v prubehu cekani nastavil event) 113 | BOOL WaitForNewData(DWORD TimeOut = INFINITE) 114 | { 115 | if (m_Write || (m_Event == NULL)) return(FALSE); 116 | ResetEvent(m_Event); 117 | return (WaitForSingleObject(m_Event, TimeOut) != WAIT_TIMEOUT); 118 | } 119 | 120 | // prerus vsechna cekani na data 121 | void BreakWaitForNewData(void) 122 | { 123 | if (m_Event != NULL) SetEvent(m_Event); 124 | } 125 | 126 | // zapis blok dat 127 | // (pokud neni pravo k zapisu nebo je blok prilis velky vraci FALSE) 128 | BOOL Write(PBYTE Ptr, DWORD Len); 129 | 130 | // zjisti cas posledniho zapisu 131 | DWORD GetLastWrite(void) 132 | {// pokud neni inicializovano vrat 0 133 | if (m_Event == NULL) return(0l); 134 | return(*m_pLastWrite); 135 | } 136 | 137 | // zjisti pocet bytu k nacteni 138 | DWORD BytesToRead(void) 139 | {long len = m_Top + (*m_pCurrent) - m_Current; 140 | if (len < 0) len += m_DataLength; 141 | return((DWORD)len); 142 | } 143 | 144 | // zjisti pocet bytu k nacteni a posun ukazatel za ne 145 | DWORD ClearBytesToRead(void) 146 | {PBYTE Current = m_Top + (*m_pCurrent); 147 | long len = Current - m_Current; 148 | if (len < 0) len += m_DataLength; 149 | m_Current = Current; 150 | return((DWORD)len); 151 | } 152 | 153 | // precti blok dat 154 | // (pokud neni k dispozici dostatecny pocet dat vraci FALSE) 155 | BOOL Read(PBYTE Ptr, DWORD Len); 156 | 157 | // odpoj se od useku sdilene pameti 158 | void Close(void); 159 | 160 | // konstruktor 161 | CSharedMemory(); 162 | 163 | // destruktor 164 | ~CSharedMemory() {Close();} 165 | }; 166 | 167 | -------------------------------------------------------------------------------- /source/CWSL_DIGI.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2022 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include "CWSL_DIGI_Types.hpp" 35 | 36 | #include 37 | #include 38 | #include "windows.h" 39 | # pragma comment(lib, "ws2_32") 40 | 41 | static const std::string PROGRAM_NAME = "CWSL_DIGI"; 42 | static const std::string PROGRAM_VERSION = "0.88"; 43 | 44 | constexpr float Q65_30_PERIOD = 30.0f; 45 | constexpr float FT8_PERIOD = 15.0f; 46 | constexpr float FT4_PERIOD = 7.5f; 47 | constexpr float WSPR_PERIOD = 120.0f; 48 | constexpr float JT65_PERIOD = 60.0f; 49 | constexpr float JS8_PERIOD = 15.0f; 50 | 51 | constexpr size_t Wave_SR = 12000; 52 | constexpr size_t SSB_BW = 6000; 53 | constexpr bool USB = 1; 54 | 55 | const float AUDIO_CLIP_VAL = std::pow(2.0f, 15.0f) - 1.0f; 56 | 57 | using FrequencyHz = std::uint32_t; 58 | 59 | const int MAX_SLEEP_MS = 250; 60 | const int MIN_SLEEP_MS = 25; 61 | 62 | constexpr int MAIN_LOOP_SLEEP_MS = 1000; 63 | 64 | static inline float getRXPeriod(const std::string& mode) { 65 | if (mode == "FT8") { 66 | return FT8_PERIOD; 67 | } 68 | else if (mode == "JS8") { 69 | return JS8_PERIOD; 70 | } 71 | else if (mode == "FT4") { 72 | return FT4_PERIOD; 73 | } 74 | else if (mode == "WSPR") { 75 | return WSPR_PERIOD; 76 | } 77 | else if (mode == "Q65-30") { 78 | return Q65_30_PERIOD; 79 | } 80 | else if (mode == "JT65") { 81 | return JT65_PERIOD; 82 | } 83 | else if (mode == "FST4-60") { 84 | return 60.0; 85 | } 86 | else if (mode == "FST4-120") { 87 | return 120.0; 88 | } 89 | else if (mode == "FST4-300") { 90 | return 300.0; 91 | } 92 | else if (mode == "FST4-900") { 93 | return 900.0; 94 | } 95 | else if (mode == "FST4-1800") { 96 | return 1800.0; 97 | } 98 | else if (mode == "FST4W-120") { 99 | return 120.0; 100 | } 101 | else if (mode == "FST4W-300") { 102 | return 300.0; 103 | } 104 | else if (mode == "FST4W-900") { 105 | return 900.0; 106 | } 107 | else if (mode == "FST4W-1800") { 108 | return 1800.0; 109 | } 110 | else { 111 | throw std::runtime_error("Unhandled mode: " + mode); 112 | } 113 | } 114 | 115 | 116 | #include 117 | using boost::lexical_cast; 118 | 119 | #include 120 | using boost::uuids::uuid; 121 | 122 | #include 123 | using boost::uuids::random_generator; 124 | 125 | #include 126 | 127 | static inline std::string make_uuid() 128 | { 129 | return lexical_cast((random_generator())()); 130 | } 131 | 132 | static inline bool doesFileExist(const std::string& fname) { 133 | std::ifstream file(fname, std::ifstream::in); 134 | if (!file.is_open()) { 135 | return false; 136 | } 137 | file.close(); 138 | return true; 139 | } 140 | 141 | static inline std::string createTemporaryDirectory() 142 | { 143 | char pathBuf[MAX_PATH] = { 0 }; 144 | GetTempPathA(MAX_PATH, pathBuf); 145 | std::string pathstr(pathBuf); 146 | std::string fullPath = pathBuf + std::string("\\") + make_uuid(); 147 | CreateDirectoryA((LPCSTR)fullPath.c_str(),NULL); 148 | return fullPath; 149 | } 150 | 151 | static inline bool isModeFST4(const std::string& in) { 152 | return in.compare(0, 5, "FST4-") == 0; 153 | } 154 | 155 | static inline bool isModeFST4W(const std::string& in) { 156 | return in.compare(0, 6, "FST4W-") == 0; 157 | } 158 | 159 | // chatgpt generated code, for the win 160 | static inline std::string GetSocketError() { 161 | int error = WSAGetLastError(); 162 | if (error == 0) { 163 | return "No error"; 164 | } 165 | 166 | LPVOID errorMsg = NULL; 167 | FormatMessage( 168 | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, 169 | NULL, 170 | error, 171 | 0, 172 | (LPSTR)&errorMsg, 173 | 0, 174 | NULL 175 | ); 176 | 177 | std::string ret(static_cast(errorMsg)); 178 | LocalFree(errorMsg); 179 | 180 | return ret; 181 | } 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview # 2 | 3 | CWSL_DIGI uses the [CWSL](https://github.com/HrochL/CWSL) utility to decode WSJT-X digital modes including FT8, FT4, JT65, WSPR, FST4, FST4W, and JS8 and sends spots to [PSK Reporter](https://www.pskreporter.info/pskmap.html), [Reverse Beacon Network](https://www.reversebeacon.net/), and [WSPRNet](https://www.wsprnet.org/drupal/wsprnet/map). CWSL_DIGI was developed by W2AXR, with testing and feedback from WZ7I, W3OA, 9V1RM, N4ZR, K9IMM and MM3NDH. 4 | 5 | With CWSL_DIGI, [CW Skimmer Server](http://www.dxatlas.com/SkimServer/), and an appropriate SDR (such as a [Red Pitaya](https://redpitaya.com/red-pitaya-for-radio-amateurs-sdr/) or [QS1R](https://www.ab9il.net/software-defined-radio/sdr2.html)), is possible to decode CW and multiple digital modes across multiple bands, all simultaneously. 6 | 7 | Problem reports and feature requests can be sent to W2AXR, alexranaldi@gmail.com, or filed as issues on Github. 8 | 9 | CWSL_DIGI is released under the GNU GENERAL PUBLIC LICENSE, Version 3. 10 | 11 | 12 | # Latest Release # 13 | 14 | The latest version is 0.88 - [Download Windows 64-bit zip](https://github.com/alexranaldi/CWSL_DIGI/archive/refs/tags/0.88-Release.zip) 15 | 16 | Notable changes in 0.88 include: 17 | 1. Enhancement: FT8 - support Fox/Hound messages. This will increase the number of FT8 spots significantly if you skim the DX frequencies when an expedition is going on! 18 | 2. Enhancement: JS8 - support for some JS8 message formats. Requires JS8Call to be installed. 19 | 3. Enhancement: Supports SOTAmat FT8 messages (Resolves https://github.com/alexranaldi/CWSL_DIGI/issues/6) 20 | 4. Fix: use hostname instead of IP Address when connecting to WSPRNet and PSK Reporter. This resolves the ongoing issue with 0.86 and earlier being unable to contact WSPRNet. 21 | 5. Fix: FST4 and FST4W decoding bugs that prevented successful decoding in many cases. 22 | 6. Fix: several bugs in callsign handling and bad callsign detection. 23 | 7. Fix: Improve PSK Reporter connection reliability. 24 | ## Installation ## 25 | 26 | 1. Install and configure [CW Skimmer Server](http://www.dxatlas.com/SkimServer/). The detailed steps can be found on the CW Skimmer Server website. 27 | 2. Install [CWSL](https://github.com/HrochL/CWSL). Configure it with CW Skimmer Server. The detailed steps can be found on the CWSL page. 28 | 3. Install the latest [Microsoft Visual Studio 2022 redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe). This may not be required if your computer is up to date. 29 | 4. Install the [Intel Fortran redistributable](https://drive.google.com/drive/folders/1pk99ruANXTd_87oLce2L32H2V-P6dAFn?usp=drive_link). This may not be required if your computer is up to date. 30 | 5. Download the latest release of CWSL_DIGI. See download link above. Unzip into a folder of your choice. e.g., C:\CWSL_DIGI 31 | 6. Configure CWSL_DIGI by editing the config.ini file. This step is required. See the detailed Configuration section below. 32 | 7. Start CWSL_DIGI by running CWSL_DIGI.exe. 33 | 34 | ## Configuration ## 35 | 36 | CWSL_DIGI is configured by editing the config.ini file that comes with CWSL_DIGI. 37 | 38 | 1. Open config.ini in your favorite text editor, such as notepad or Notepad++. 39 | 2. Any line beginning with a # character is a comment and will be ignored by CWSL_DIGI. The file contains comments to guide the user in editing. 40 | 3. Set the operator callsign and grid square. 41 | 1. Replace *MYCALL* with your callsign. 42 | 2. Replace *AB01xy* with your 6 character grid square. 43 | 4. Enable at least one decoder. It is suggested to start with a single decoder, and once working, enable additional decoders. 44 | 1. Find the [decoders] section in the file. 45 | 2. To enable a decoder, remove the # character at the beginning of the line. For example, to enable 10m FT8: 46 | 47 | ``` 48 | # 10m 49 | #decoder=28180000 FT4 50 | decoder=28074000 FT8 51 | #decoder=28076000 JT65 52 | ``` 53 | 54 | NOTE: The bandwidth selected in CW Skimmer Server and the center frequencies for each band may limit which frequencies CWSL_DIGI can access. CWSL_DIGI has access to only the bands and frequencies setup in CW Skimmer Server, which must be kept in mind when enabling decoders. 55 | 56 | 5. Enable reporting to spotting networks, if desired. 57 | 1. Find the [reporting] section. 58 | 2. Set *pskreporter*, *rbn* and *wsprnet* to true to enable spotting, as desired. Note that Reverse Beacon Network additionally requires Aggregator - additional steps are detailed below. 59 | 60 | ## Running CWSL_DIGI ## 61 | 62 | CWSL_DIGI is best run from a Command prompt. While not required, this allows console output, including error messages, to be viewed if the program crashes or terminates unexpectedly. 63 | 64 | 1. Open a Command prompt. 65 | 2. Change to the directory where CWSL_DIGI was installed (Extracted from zip) 66 | 3. Run CWSL_DIGI.exe 67 | 68 | 69 | ### Reverse Beacon Network ### 70 | 71 | For spotting to [Reverse Beacon Network (RBN)](https://www.reversebeacon.net/index.php), additional software called [Aggregator](https://www.reversebeacon.net/pages/Aggregator+34) is required. 72 | 73 | 1. In the CWSL_DIGI config.ini file, set rbn=true 74 | 2. Install [Aggregator](https://www.reversebeacon.net/pages/Aggregator+34). 75 | 3. In Aggregator, select the *FT#* tab. 76 | 1. Put a check next to source number 40 77 | 2. Make sure it is set to port 2215 78 | 3. When running properly, the white box at the upper right will begin to display FT4/8 spots. Only CQ spots are submitted to RBN, and duplicates are culled. 79 | 5. Confirm that FT4/FT8 spots are present on the Status page of Aggregator. 80 | 81 | 82 | ### Calibration ### 83 | 84 | 1. If desired, read the file [CALIBRATION.txt](./CALIBRATION.txt) for instructions on how to configure receiver calibration in CWSL_DIGI. 85 | 86 | 87 | ## Troubleshooting ## 88 | 89 | If the program opens and then closes immediately, run CWSL_DIGI from a Command Window. See the "Running" section above. 90 | 91 | -------------------------------------------------------------------------------- /source/ring_buffer_spmc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | // Used internally as a single producer single consumer queue (ring buffer) 30 | template 31 | struct ring_buffer_spmc_t { 32 | T* recs; 33 | // Use atomics for indices to prevent instruction reordering 34 | std::vector>> read_index; 35 | std::atomic write_index; 36 | // Number of items the ring buffer can hold. 37 | std::size_t size; 38 | std::atomic_bool terminateFlag = false; 39 | bool initialized = false; 40 | 41 | ring_buffer_spmc_t() : 42 | terminateFlag(false), 43 | recs(nullptr), 44 | initialized(false), 45 | size(0) 46 | { 47 | } 48 | 49 | std::uint64_t inline getReadIndex(const size_t readerIndex) const { 50 | return read_index[readerIndex]->load(); 51 | } 52 | 53 | void inline setReadIndex(const size_t readerIndex, const std::uint64_t newVal) { 54 | read_index[readerIndex]->store(newVal); 55 | } 56 | 57 | bool initialize(const size_t sizeIn) { 58 | size = sizeIn; 59 | recs = reinterpret_cast(malloc(sizeof(T) * size)); 60 | initialized = recs != nullptr; 61 | reset(); 62 | return initialized; 63 | } 64 | 65 | size_t addReader() { 66 | read_index.emplace_back( std::make_unique< std::atomic >() ); 67 | return read_index.size() - 1; 68 | } 69 | 70 | void reset() { 71 | for (size_t k = 0; k < read_index.size(); ++k) { 72 | setReadIndex(k, 0); 73 | } 74 | 75 | write_index = 0; 76 | terminateFlag = false; 77 | } 78 | 79 | void terminate() 80 | { 81 | terminateFlag = true; 82 | } 83 | 84 | ~ring_buffer_spmc_t() 85 | { 86 | if (nullptr != recs) 87 | { 88 | free(recs); 89 | } 90 | } 91 | 92 | bool full() const { 93 | for (std::size_t k = 0; k < read_index.size(); ++k) { 94 | if ((getReadIndex(k) == write_index + 1) || (getReadIndex(k) == 0 && static_cast(write_index) == static_cast(size) - 1)) 95 | { 96 | return true; 97 | } 98 | } 99 | return false; 100 | } 101 | 102 | bool wait_for_empty_slot() const 103 | { 104 | while (full()) { 105 | std::this_thread::yield(); 106 | if (terminateFlag) { 107 | return false; 108 | } 109 | } 110 | return true; 111 | } 112 | 113 | std::uint64_t get_next_write_index() 114 | { 115 | // cast to signed so we don't break subtraction 116 | if (static_cast(write_index) == static_cast(size) - 1) { 117 | return 0; 118 | } 119 | else { 120 | return write_index + 1; 121 | } 122 | } 123 | 124 | void inc_write_index() 125 | { 126 | // cast to signed so we don't break subtraction 127 | if (static_cast(write_index) == static_cast(size) - 1) { 128 | write_index = 0; 129 | } 130 | else { 131 | write_index++; 132 | } 133 | } 134 | 135 | T& pop_ref(const std::size_t readerIndex) 136 | { 137 | wait_for_data(); 138 | T& curr = recs[getReadIndex(readerIndex)]; 139 | if (getReadIndex(readerIndex) == size - 1) { 140 | setReadIndex(readerIndex, 0); 141 | } 142 | else { 143 | setReadIndex(readerIndex, getReadIndex(readerIndex) + 1); 144 | } 145 | return curr; 146 | } 147 | 148 | T pop_no_wait(const std::size_t readerIndex) 149 | { 150 | T curr = recs[getReadIndex(readerIndex)]; 151 | if (getReadIndex(readerIndex) == size - 1) { 152 | setReadIndex(readerIndex, 0); 153 | } 154 | else { 155 | setReadIndex(readerIndex, getReadIndex(readerIndex) + 1); 156 | } 157 | return curr; 158 | } 159 | 160 | T pop(const std::size_t readerIndex) 161 | { 162 | wait_for_data(readerIndex); 163 | T curr = recs[getReadIndex(readerIndex)]; 164 | if (getReadIndex(readerIndex) == size - 1) { 165 | setReadIndex(readerIndex, 0); 166 | } 167 | else { 168 | setReadIndex(readerIndex, getReadIndex(readerIndex) + 1); 169 | } 170 | return curr; 171 | } 172 | 173 | bool wait_for_data(const std::size_t readerIndex) const 174 | { 175 | while (empty(readerIndex)) { 176 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 177 | if (terminateFlag) { 178 | return false; 179 | } 180 | } 181 | return true; 182 | } 183 | 184 | bool empty(const std::size_t readerIndex) const { 185 | if (getReadIndex(readerIndex) != write_index) { 186 | return false; 187 | } 188 | return true; 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /source/ScreenPrinter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2023 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "SafeQueue.h" 34 | #include "CWSL_DIGI.hpp" 35 | 36 | 37 | enum LOG_LEVEL : int { 38 | MAX_VERBOSE = 8, 39 | TRACE = 5, 40 | DEBUG = 4, 41 | INFO = 3, 42 | WARN = 2, 43 | ERR = 1, 44 | NONE = 0, 45 | }; 46 | 47 | struct LogMessage { 48 | LogMessage(const std::string& s, const LOG_LEVEL l) : 49 | str(s), 50 | lvl(l) { 51 | GetSystemTime(&time); 52 | } 53 | std::string str; 54 | LOG_LEVEL lvl{LOG_LEVEL::INFO}; 55 | SYSTEMTIME time; 56 | }; 57 | 58 | class ScreenPrinter { 59 | public: 60 | ScreenPrinter(const bool logImmediately) : 61 | terminateFlag(false), 62 | logLevel(LOG_LEVEL::INFO), 63 | logFileEnabled(false), 64 | logFileName(""), 65 | immediate(logImmediately) 66 | { 67 | if (!immediate) { 68 | printThread = std::thread(&ScreenPrinter::printLoop, this); 69 | SetThreadPriority(printThread.native_handle(), THREAD_PRIORITY_IDLE); 70 | printThread.detach(); 71 | } 72 | } 73 | 74 | virtual ~ScreenPrinter() 75 | { 76 | flush(); 77 | if (logFileEnabled) { 78 | ofs.close(); 79 | } 80 | } 81 | 82 | void enableLogFile(const std::string& fn) { 83 | logFileEnabled = true; 84 | logFileName = fn; 85 | ofs.open(logFileName, std::ios_base::app | std::ofstream::out); 86 | } 87 | 88 | void setLogLevel(LOG_LEVEL lvl) { 89 | logLevel = lvl; 90 | } 91 | 92 | void setLogLevel(int lvl) { 93 | setLogLevel(static_cast(lvl)); 94 | } 95 | 96 | void info(const std::string& message) { 97 | log(message, LOG_LEVEL::INFO); 98 | } 99 | 100 | void print(const std::string& message) { 101 | info(message); 102 | } 103 | 104 | void print(const std::string& message, LOG_LEVEL lvl) { 105 | log(message, lvl); 106 | } 107 | 108 | void warning(const std::string& message) { 109 | log(message, LOG_LEVEL::WARN); 110 | } 111 | 112 | void print(const std::exception& e) { 113 | err(std::string("Caught Exception: ") + e.what()); 114 | } 115 | 116 | void print(const std::string& msg, const std::exception& e) { 117 | err(msg); 118 | err(std::string("Caught Exception: ") + e.what()); 119 | } 120 | 121 | void debug(const std::string& message) { 122 | log(message, LOG_LEVEL::DEBUG); 123 | } 124 | 125 | void trace(const std::string& message) { 126 | log(message, LOG_LEVEL::TRACE); 127 | } 128 | 129 | void err(const std::string& message) { 130 | log(message, LOG_LEVEL::ERR); 131 | } 132 | 133 | void log(const LOG_LEVEL lvl, const std::string& message) { 134 | log(message, lvl); 135 | } 136 | 137 | void log(const std::string& message, const LOG_LEVEL lvl) { 138 | if (lvl <= logLevel) { 139 | if (immediate) { 140 | LogMessage msg = LogMessage(message, lvl); 141 | processMessage(msg); 142 | } 143 | else { 144 | strs.enqueue(LogMessage(message, lvl)); 145 | } 146 | } 147 | } 148 | 149 | void printLoop() { 150 | while (1) { 151 | flush(); 152 | if (terminateFlag) { return; } 153 | std::this_thread::sleep_for(std::chrono::milliseconds(250)); 154 | if (terminateFlag) { return; } 155 | } 156 | } 157 | 158 | void processMessage(LogMessage& message) { 159 | std::string str = ""; 160 | if (LOG_LEVEL::ERR == message.lvl) { 161 | str = "### ERROR :: " + message.str; 162 | } 163 | else if (LOG_LEVEL::WARN == message.lvl) { 164 | str = "@@@ WARNING ::" + message.str; 165 | } 166 | else if (LOG_LEVEL::TRACE == message.lvl) { 167 | str = "%%% TRACE :: " + message.str; 168 | } 169 | else { 170 | str = message.str; 171 | } 172 | std::stringstream ss; 173 | ss << std::setw(2) << std::setfill('0') << message.time.wYear 174 | << "-" 175 | << std::setw(2) << std::setfill('0') << message.time.wMonth 176 | << "-" 177 | << std::setw(2) << std::setfill('0') << message.time.wDay 178 | << " " 179 | << std::setw(2) << std::setfill('0') << message.time.wHour 180 | << ":" 181 | << std::setw(2) << std::setfill('0') << message.time.wMinute 182 | << ":" 183 | << std::setw(2) << std::setfill('0') << message.time.wSecond 184 | << "." 185 | << std::setw(3) << std::setfill('0') << message.time.wMilliseconds 186 | ; 187 | 188 | str = ss.str() + " " + str; 189 | 190 | // Write to console 191 | std::cout << str << std::endl; 192 | 193 | // Write to log file 194 | if (logFileEnabled) { 195 | ofs << str << std::endl; 196 | } 197 | } 198 | 199 | void flush() { 200 | do { 201 | auto message = strs.dequeue(); 202 | processMessage(message); 203 | } while (!strs.empty()); 204 | } 205 | 206 | void terminate() { 207 | debug("printer received terminate flag"); 208 | terminateFlag = true; 209 | } 210 | 211 | private: 212 | 213 | SafeQueue strs; 214 | std::thread printThread; 215 | LOG_LEVEL logLevel; 216 | bool terminateFlag; 217 | 218 | bool logFileEnabled; 219 | std::string logFileName; 220 | std::ofstream ofs; 221 | bool immediate; 222 | }; 223 | -------------------------------------------------------------------------------- /source/SSBD.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Alexander Ranaldi 3 | W2AXR 4 | alexranaldi@gmail.com 5 | 6 | This file is part of CWSL_DIGI. 7 | 8 | CWSL_DIGI is free software : you can redistribute it and /or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | CWSL_DIGI is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 20 | */ 21 | 22 | #pragma once 23 | #ifndef SSBD_HPP 24 | # define SSBD_HPP 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "LowPass.hpp" 33 | 34 | // An efficient single sideband demodulator. 35 | // - Operates on blocks based on the demication rate: Fs/B 36 | // - Consumes 2*Fs/B complex samples at a time, outputting 4 real samples. 37 | // - The output sample rate is 2*B. 38 | // - The output samples are delayed by several cycles (GetDelay) 39 | // - This latency is a consequence of better filtering. 40 | // - Lowering this value will result lower quality output. 41 | // All frequencies are in Hz 42 | template 43 | class SSBD { 44 | 45 | public: 46 | 47 | // Constructor: default tunes to Upper Sideband @ 0 Hz 48 | SSBD(const size_t Fs, const size_t B, const double F = 0.0, 49 | const bool isUSB = true, const size_t latency_log2 = 3) : 50 | Fs(Fs), B(B), latency(1 << latency_log2) 51 | { 52 | 53 | // Check inputs 54 | if (0 == B || (Fs/B/2)*2*B != Fs || Fs < 4*B) 55 | throw std::invalid_argument("Fs/B must be an even integer >= 4"); 56 | if (latency_log2 < 1) 57 | throw std::invalid_argument("log2(Latency) must >= 1"); 58 | if (16 < latency_log2) 59 | throw std::invalid_argument("log2(Latency) must be <= 16"); 60 | 61 | // Create the lowpass filter 62 | FiltOrder = latency*2*Fs/B; 63 | filter = BuildLowPass(FiltOrder, B/(double)Fs); 64 | 65 | // Normalize the filter 66 | PRECISION sum = 0.0; 67 | for (size_t n = 0; n < FiltOrder; sum += filter[n++]); 68 | for (size_t n = 0; n < FiltOrder; filter[n++] /= sum); 69 | 70 | // Create the down-conversion tone 71 | BlockSize = Fs/B/2; 72 | tone = new std::complex[BlockSize]; 73 | 74 | // Create the workspace 75 | NumWS = FiltOrder/BlockSize; 76 | workspace = new std::complex[NumWS]; 77 | index_max = NumWS-1; 78 | index = 0; 79 | 80 | // Perform the initial tune 81 | Tune(F, isUSB); 82 | 83 | } 84 | 85 | // Destructor 86 | ~SSBD(void) 87 | { 88 | 89 | // Free allocated memory 90 | delete[] filter; 91 | delete[] tone; 92 | delete[] workspace; 93 | 94 | } 95 | 96 | // Retunes the demodulator 97 | void Tune(const double F, const bool isUSB, const bool reset = true) 98 | { 99 | // Check the parameters 100 | if (fabs(F) > Fs/2) 101 | throw std::invalid_argument("Signal outside of band (low)"); 102 | if (fabs(F+B*(isUSB ? 1.0 : -1.0)) > Fs/2) 103 | throw std::invalid_argument("Signal outside of band (high)"); 104 | 105 | // Store demodulation parameters 106 | Fc = F; 107 | USB = isUSB; 108 | 109 | // Update the down-conversion tone 110 | sign = static_cast(isUSB ? 1.0 : -1.0); 111 | PRECISION phase_delta = static_cast(-2.0*PI*(F+sign*B/2.0)/static_cast(Fs)); 112 | for (size_t n = 0; n < BlockSize; ++n) 113 | tone[n] = std::exp(std::complex(0.0, phase_delta*n)); 114 | phase_inc = std::exp(std::complex(0.0, phase_delta*BlockSize)); 115 | 116 | // Reset the workspace 117 | if (reset) { 118 | std::fill(workspace, workspace+NumWS, std::complex(0.0, 0.0)); 119 | index = 0; 120 | phase = std::complex(1.0, 0.0); 121 | } 122 | 123 | } 124 | 125 | // Consumes the next 2*Fs/B complex input samples from the provided pointer 126 | // and generates 4 real output samples at the provided pointer. 127 | template 128 | void Iterate(const std::complex *in, TYPE *out) 129 | { 130 | 131 | // Process the four blocks 132 | out[0] = static_cast(+ProcessBlock(in+0*BlockSize).real()); 133 | out[1] = static_cast(-ProcessBlock(in+1*BlockSize).imag()*sign); 134 | out[2] = static_cast(-ProcessBlock(in+2*BlockSize).real()); 135 | out[3] = static_cast(+ProcessBlock(in+3*BlockSize).imag()*sign); 136 | 137 | } 138 | 139 | // Returns the input sample rate (complex samples per second) 140 | inline size_t GetInRate(void) {return Fs;} 141 | // Returns the output sample rate (real samples per second) 142 | inline size_t GetOutRate(void) {return 2*B;} 143 | // Returns the number of input complex samples consumed each iteration. 144 | inline size_t GetInSize(void) {return 2*Fs/B;} 145 | // Returns the number of output real samples generated each iteration. 146 | inline size_t GetOutSize(void) {return 4;} 147 | // Returns the demodulator bandwidth 148 | inline size_t GetBandwidth(void) { return B;} 149 | // Returns the currently tuned frequency 150 | inline double GetCarrier(void) { return Fc;} 151 | // Returns true if the demodulator is tuned for upper sideband 152 | inline bool IsUSB(void) { return USB;} 153 | // Returns the output delay of this filter (at the output rate) 154 | inline size_t GetDelay(void) const {return latency;} 155 | 156 | private: 157 | 158 | // Processes a single block of samples 159 | template 160 | std::complex ProcessBlock(const std::complex *in) 161 | { 162 | 163 | // Update the workspace 164 | for (size_t n = 0; n < NumWS; ++n) { 165 | std::complex sum(0.0, 0.0); 166 | for (size_t m = 0; m < BlockSize; ++m) { 167 | sum += static_cast>(in[m]) * 168 | tone[m] * filter[m+n*BlockSize]; 169 | } 170 | workspace[(NumWS-n-1+index)&index_max] += sum*phase; 171 | } 172 | 173 | // Update the initial phase for the next block 174 | phase *= phase_inc; 175 | 176 | // Retrieve the output sample and reset it in the workspace 177 | std::complex out = workspace[index]; 178 | workspace[index] = std::complex(0.0, 0.0); 179 | index = (index+1) & index_max; 180 | 181 | return out; 182 | 183 | } 184 | 185 | // Input complex sample rate 186 | const size_t Fs; 187 | // Demodulator bandwidth 188 | const size_t B; 189 | // Currently tuned frequency 190 | double Fc; 191 | // True when upper sideband 192 | bool USB; 193 | // +/- 1 for USB/LSB 194 | PRECISION sign; 195 | 196 | // Weighted low-pass filter 197 | PRECISION *filter; 198 | // Low-pass filter order 199 | size_t FiltOrder; 200 | // Output sample latency of filter/demodulator 201 | const size_t latency; 202 | 203 | // Down-conversion mixing tone for a block 204 | std::complex *tone; 205 | // Current initial phase for next block 206 | std::complex phase; 207 | // Phase increment for a full block of samples 208 | std::complex phase_inc; 209 | // Input samples per block 210 | size_t BlockSize; 211 | 212 | // Convolution workspace 213 | std::complex *workspace; 214 | // Index of next output sample 215 | size_t index; 216 | // Used to bit-mask instead of modular arithmetic: CPU savings 217 | size_t index_max; 218 | // Size of the workspace 219 | size_t NumWS; 220 | 221 | }; // class SSBD 222 | 223 | 224 | #endif // SSBD_HPP 225 | -------------------------------------------------------------------------------- /source/SharedMemory.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // SharedMemory.h - class for working with shared memory on Win32 3 | // (adapted from my old works - comments in czech) 4 | // 5 | #include 6 | #include 7 | #include "SharedMemory.h" 8 | 9 | /////////////////////////////////////////////////////////////////////////////////////////// 10 | // vytvor usek sdilene pameti 11 | BOOL CSharedMemory::Create(LPCTSTR Name, DWORD Length, BOOL WriteAccess) 12 | {char eName[MAX_PATH + 1]; 13 | SECURITY_DESCRIPTOR *pSD; 14 | SECURITY_ATTRIBUTES *pSA; 15 | 16 | // pokud je jiz vytvoreno/namapovano vrat FALSE 17 | if (m_MapAddress != NULL) return(FALSE); 18 | 19 | // spocti delku hlavicky 20 | m_DataLength = Length; 21 | m_Length = Length + m_PageSize; 22 | 23 | // pokus se vytvorit file-mapping objekt 24 | m_MapFile = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE, 0, m_Length, Name); 25 | if ((m_MapFile == NULL) || (GetLastError() == ERROR_ALREADY_EXISTS)) 26 | {// nepovedlo se (nebo objekt tohoto jmena uz existuje) 27 | if (m_MapFile != NULL) CloseHandle(m_MapFile); 28 | m_MapFile = NULL; 29 | m_Length = m_DataLength = 0; 30 | return(FALSE); 31 | } 32 | 33 | // namapuj ho do pameti 34 | m_MapAddress = MapViewOfFile(m_MapFile, FILE_MAP_WRITE, 0, 0, 0); 35 | if (m_MapAddress == NULL) 36 | {// nepovedlo se 37 | CloseHandle(m_MapFile); 38 | m_MapFile = NULL; 39 | m_Length = m_DataLength = 0; 40 | return(FALSE); 41 | } 42 | 43 | // inicializuj ukazatele na promenne v hlavicce 44 | m_pLength = (DWORD *)((PBYTE)m_MapAddress + sizeof(SM_HDR)); 45 | m_pCurrent = (DWORD *)((PBYTE)m_pLength + sizeof(DWORD)); 46 | m_pLastWrite = (DWORD *)((PBYTE)m_pCurrent + sizeof(DWORD)); 47 | pSA = (SECURITY_ATTRIBUTES *)((PBYTE)m_pLastWrite + sizeof(DWORD)); 48 | pSD = (SECURITY_DESCRIPTOR *)((PBYTE)pSA + sizeof(SECURITY_ATTRIBUTES)); 49 | 50 | // pro objekt udalosti na bufferu vytvor bezpecnostni deskriptor 51 | // povolujici vsechno vsem 52 | ZeroMemory(pSA, sizeof(SECURITY_ATTRIBUTES)); 53 | ZeroMemory(pSD, SECURITY_DESCRIPTOR_MIN_LENGTH); 54 | pSA->nLength = sizeof(SECURITY_ATTRIBUTES); 55 | pSA->lpSecurityDescriptor = pSD; 56 | pSA->bInheritHandle = TRUE; 57 | if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION)) 58 | { 59 | UnmapViewOfFile(m_MapAddress); m_MapAddress = NULL; 60 | CloseHandle(m_MapFile); m_MapFile = NULL; 61 | m_Length = m_DataLength = 0; 62 | return(FALSE); 63 | } 64 | if (!SetSecurityDescriptorDacl(pSD, TRUE, (PACL) NULL, FALSE)) 65 | { 66 | UnmapViewOfFile(m_MapAddress); m_MapAddress = NULL; 67 | CloseHandle(m_MapFile); m_MapFile = NULL; 68 | m_Length = m_DataLength = 0; 69 | return(FALSE); 70 | } 71 | 72 | // vytvor event pro udalosti na bufferu 73 | sprintf(eName,"e%s",Name); 74 | m_Event = CreateEvent(pSA, TRUE, FALSE, eName); 75 | if ((m_Event == NULL) || (GetLastError() == ERROR_ALREADY_EXISTS)) 76 | {// nepovedlo se (nebo objekt tohoto jmena uz existuje) 77 | UnmapViewOfFile(m_MapAddress); m_MapAddress = NULL; 78 | CloseHandle(m_MapFile); m_MapFile = NULL; 79 | m_Length = m_DataLength = 0; 80 | return(FALSE); 81 | } 82 | 83 | // inicializuj buffer pro zapis 84 | m_Data = (LPVOID)((BYTE *)m_MapAddress + m_PageSize); 85 | (*m_pLength) = m_DataLength; 86 | (*m_pCurrent) = 0L; 87 | m_Top = m_Current = (PBYTE)m_Data; 88 | m_Bot = m_Top + m_DataLength; 89 | (*m_pLastWrite) = 0L; 90 | 91 | // nastav pozadovany pristup 92 | if (!WriteAccess) DenyWriteAccess(); 93 | m_Write = WriteAccess; 94 | 95 | // uspesny navrat 96 | return(TRUE); 97 | } 98 | 99 | /////////////////////////////////////////////////////////////////////////////////////////// 100 | // pripoj se na usek sdilene pameti 101 | BOOL CSharedMemory::Open(LPCTSTR Name, BOOL WriteAccess, char *ErrorInfo) 102 | {char eName[MAX_PATH + 1]; 103 | 104 | // pro jistotu ... 105 | if (ErrorInfo != NULL) *ErrorInfo = '\0'; 106 | 107 | // pokud je jiz vytvoreno/namapovano ... 108 | if (m_MapAddress != NULL) 109 | {// ... vrat FALSE 110 | if (ErrorInfo != NULL) sprintf(ErrorInfo, "m_MapAddress != NULL"); 111 | return(FALSE); 112 | } 113 | 114 | // pokus se pripojit na file-mapping objekt 115 | m_MapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, Name); 116 | if (m_MapFile == NULL) 117 | {// nepovedlo se 118 | if (ErrorInfo != NULL) sprintf(ErrorInfo, "OpenFileMapping %d", GetLastError()); 119 | return(FALSE); 120 | } 121 | 122 | // namapuj ho do pameti 123 | m_MapAddress = MapViewOfFile(m_MapFile, WriteAccess ? FILE_MAP_WRITE : FILE_MAP_READ, 0, 0, 0); 124 | if (m_MapAddress == NULL) 125 | {// nepovedlo se 126 | if (ErrorInfo != NULL) sprintf(ErrorInfo, "MapViewOfFile %d", GetLastError()); 127 | CloseHandle(m_MapFile); 128 | m_MapFile = NULL; 129 | return(FALSE); 130 | } 131 | 132 | // pripoj se na event pro udalosti na bufferu 133 | sprintf(eName,"e%s",Name); 134 | m_Event = OpenEvent(EVENT_ALL_ACCESS,FALSE,eName); 135 | if (m_Event == NULL) 136 | {// nepovedlo se 137 | if (ErrorInfo != NULL) sprintf(ErrorInfo, "OpenEvent %d", GetLastError()); 138 | UnmapViewOfFile(m_MapAddress); m_MapAddress = NULL; 139 | CloseHandle(m_MapFile); m_MapFile = NULL; 140 | return(FALSE); 141 | } 142 | 143 | // inicializuj ukazatele na promenne v hlavicce 144 | m_pLength = (DWORD *)((PBYTE)m_MapAddress + sizeof(SM_HDR)); 145 | m_pCurrent = (DWORD *)((PBYTE)m_pLength + sizeof(DWORD)); 146 | m_pLastWrite = (DWORD *)((PBYTE)m_pCurrent + sizeof(DWORD)); 147 | 148 | // inicializuj lokalni ridici promenne bufferu 149 | m_DataLength = *m_pLength; 150 | m_Length = m_DataLength + m_PageSize; 151 | m_Data = (LPVOID)((BYTE *)m_MapAddress + m_PageSize); 152 | m_Top = (PBYTE)m_Data; 153 | m_Bot = m_Top + m_DataLength; 154 | m_Current = m_Top + (*m_pCurrent); 155 | 156 | // nastav nejvyssi povoleny pristup 157 | m_Write = WriteAccess; 158 | 159 | // uspesny navrat 160 | return(TRUE); 161 | } 162 | 163 | /////////////////////////////////////////////////////////////////////////////////////////// 164 | // zapis blok dat 165 | BOOL CSharedMemory::Write(PBYTE Ptr, DWORD Len) 166 | {DWORD bLen; 167 | 168 | // pokud neni pravo k zapisu, nebo je blok prilis velky, nebo neni inicializovano vraci FALSE 169 | if ((m_Event == NULL) || (!m_Write) || (Len > m_DataLength)) return(FALSE); 170 | 171 | // spocti kolik muzeme ulozit dat do konce bufferu 172 | bLen = m_Bot - m_Current; 173 | 174 | // pokud se vejde zapis cele 175 | if (Len < bLen) 176 | {CopyMemory((PVOID)m_Current, (CONST VOID *)Ptr, Len); 177 | m_Current += Len; 178 | } 179 | else 180 | // pokud se nevejde musis to rozdelit na dva bloky 181 | {// prvni blok 182 | CopyMemory((PVOID)m_Current, (CONST VOID *)Ptr, bLen); 183 | Ptr += bLen; 184 | Len -= bLen; 185 | m_Current = m_Top; 186 | 187 | // druhy blok 188 | if (Len > 0) 189 | {CopyMemory((PVOID)m_Current, (CONST VOID *)Ptr, Len); 190 | m_Current += Len; 191 | } 192 | } 193 | 194 | // aktualizuj ridici promenne ve sdilene pameti 195 | (*m_pLastWrite) = GetTickCount(); 196 | (*m_pCurrent) = m_Current - m_Top; 197 | 198 | // oznam, ze prisla nova data 199 | SetEvent(m_Event); 200 | 201 | // uspesny navrat 202 | return(TRUE); 203 | } 204 | 205 | /////////////////////////////////////////////////////////////////////////////////////////// 206 | // precti blok dat 207 | BOOL CSharedMemory::Read(PBYTE Ptr, DWORD Len) 208 | {long dLen; 209 | DWORD bLen; 210 | 211 | // pokud nejsme inicializovani vrat FALSE 212 | if (m_Event == NULL) return(FALSE); 213 | 214 | // zjisti kolik je k dispozici dat 215 | dLen = m_Top + (*m_pCurrent) - m_Current; 216 | if (dLen < 0) dLen += m_DataLength; 217 | 218 | // pokud je pozadovano vice dat vrat FALSE 219 | if ((long)Len > dLen) return(FALSE); 220 | 221 | // spocti kolik muzeme precist dat do konce bufferu 222 | bLen = m_Bot - m_Current; 223 | 224 | // pokud lze precist jednofazove, precti 225 | if (Len < bLen) 226 | {CopyMemory((PVOID)Ptr, (CONST VOID *)m_Current, Len); 227 | m_Current += Len; 228 | } 229 | else 230 | // jinak po blocich 231 | {// prvni blok 232 | CopyMemory((PVOID)Ptr, (CONST VOID *)m_Current, bLen); 233 | Ptr += bLen; 234 | Len -= bLen; 235 | m_Current = m_Top; 236 | 237 | // druhy blok 238 | if (Len > 0) 239 | {CopyMemory((PVOID)Ptr, (CONST VOID *)m_Current, Len); 240 | m_Current += Len; 241 | } 242 | } 243 | 244 | // uspesny navrat 245 | return(TRUE); 246 | } 247 | 248 | /////////////////////////////////////////////////////////////////////////////////////////// 249 | // odpoj se od useku sdilene pameti 250 | void CSharedMemory::Close(void) 251 | { 252 | // pokud je namapovane view zrus ho 253 | if (m_MapAddress != NULL) 254 | {UnmapViewOfFile(m_MapAddress); 255 | m_MapAddress = NULL; 256 | } 257 | 258 | // pokud je otevreny handle na file-mapping objekt zavri ho 259 | if (m_MapFile != NULL) 260 | {CloseHandle(m_MapFile); 261 | m_MapFile = NULL; 262 | } 263 | 264 | // pokud je otevreny handle na event zavri ho 265 | if (m_Event != NULL) 266 | {CloseHandle(m_Event); 267 | m_Event = NULL; 268 | } 269 | 270 | // vynuluj adresu a delku dat 271 | m_Data = NULL; 272 | m_Length = m_DataLength = 0; 273 | } 274 | 275 | 276 | /////////////////////////////////////////////////////////////////////////////////////////// 277 | // konstruktor 278 | CSharedMemory::CSharedMemory() 279 | {SYSTEM_INFO SI; 280 | 281 | // inicializuj promenne 282 | m_MapFile = NULL; 283 | m_MapAddress = m_Data = NULL; 284 | m_Length = m_DataLength = 0; 285 | m_Event = NULL; 286 | 287 | // zjisti velikost pametove stranky 288 | GetSystemInfo(&SI); 289 | m_PageSize = SI.dwPageSize; 290 | } 291 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | # For use with CWSL_DIGI v0.88 2 | 3 | ### INSTRUCTIONS 4 | # Lines beginning with a # are a comment and are ignored by CWSL_DIGI. 5 | # For missing or commented-out lines, CWSL_DIGI will use the default value. 6 | 7 | 8 | [radio] 9 | # Frequency Calibration. A global value that is applied to all decoders. 10 | # A frequency calibration can also be applied to each decoder under [decoders] 11 | # Note that CW Skimmer's Frequency Calibration setting will impact CWSL_DIGI. 12 | # For more information see CALIBRATION.txt. 13 | # The default is 1.0. 14 | #freqcalibration=1.000000000 15 | 16 | # For multiple receivers. Use -1 for one receiver. Default is -1. 17 | #sharedmem=2 18 | 19 | 20 | [operator] 21 | # Operator amateur radio callsign. Required. 22 | callsign=MYCALL 23 | 24 | # 6 character grid square of the receiver. Required. 25 | gridsquare=AB01xy 26 | 27 | 28 | [decoders] 29 | # The set of decoders. Each line is one decoder of at least "frequency mode" 30 | # frequency is in Hz 31 | # mode is FT4, FT8, WSPR, Q65-30, JT65 or JS8 32 | # frequency, mode 33 | # e.g., 14074000 FT8 34 | # frequency, mode, sharedmem 35 | # e.g., 14074000 FT8 2 36 | # frequency, mode, sharedmem, frequency calibration 37 | # e.g., 14074000 FT8 2 1.00005738745 38 | # 39 | # WSPR decoders optionally allow a spotter callsign per decoder. 40 | # e.g., 14095600 WSPR 2 1.0 W2AXR-2 41 | # 42 | # If you add additional lines here, please send them to W2AXR, 43 | # alexranaldi@gmail.com for release with future versions! 44 | 45 | # 6m 46 | #decoder=50323000 FT8 47 | #decoder=50318000 FT4 48 | #decoder=50313000 FT8 49 | #decoder=50310000 FT8 50 | #decoder=50275000 Q65-30 51 | 52 | # 10m 53 | #decoder=28180000 FT4 54 | #decoder=28074000 FT8 55 | #decoder=28076000 JT65 56 | #decoder=28078000 JS8 57 | 58 | # 12m 59 | #decoder=24919000 FT4 60 | #decoder=24915000 FT8 61 | #decoder=24917000 JT65 62 | 63 | # 15m 64 | #decoder=21140000 FT4 65 | #decoder=21094600 WSPR 66 | #decoder=21074000 FT8 67 | #decoder=21076000 JT65 68 | #decoder=21078000 JS8 69 | 70 | # 17m 71 | #decoder=18104600 WSPR 72 | #decoder=18104000 FT4 73 | #decoder=18100000 FT8 74 | #decoder=18102000 JT65 75 | 76 | # 20m 77 | #decoder=14095600 WSPR 78 | #decoder=14090000 FT8 79 | #decoder=14080000 FT4 80 | #decoder=14074000 FT8 81 | #decoder=14076000 JT65 82 | #decoder=14078000 JS8 83 | 84 | # 30m 85 | #decoder=10140000 FT4 86 | #decoder=10138700 WSPR 87 | #decoder=10136000 FT8 88 | #decoder=10131000 FT8 89 | #decoder=10138000 JT65 90 | #decoder=10130000 JS8 91 | 92 | # 40m 93 | #decoder=7074000 FT8 94 | #decoder=7047500 FT4 95 | #decoder=7038600 WSPR 96 | #decoder=7076000 JT65 97 | #decoder=7078000 JS8 98 | 99 | # 60m 100 | #decoder=5357000 FT8 101 | #decoder=5287200 WSPR 102 | #decoder=5364700 WSPR 103 | 104 | # 80m 105 | #decoder=3575000 FT4 106 | #decoder=3573000 FT8 107 | #decoder=3568600 WSPR 108 | #decoder=3570000 JT65 109 | #decoder=3578000 JS8 110 | 111 | # 80m Korea 112 | #decoder=3547000 FT8 113 | 114 | # 160m 115 | #decoder=1840000 FT8 116 | #decoder=1840000 FT4 117 | #decoder=1838000 JT65 118 | #decoder=1836600 WSPR 119 | #decoder=1836800 FST4W-120 120 | #decoder=1839000 FST4-60 121 | #decoder=1839000 FST4-120 122 | 123 | # 160m Korea 124 | #decoder=1810000 FT8 125 | 126 | # 600m 127 | #decoder=0474200 FT8 128 | #decoder=474200 FST4-60 129 | #decoder=474200 FST4-120 130 | #decoder=474200 FST4-300 131 | #decoder=474200 FST4W-120 132 | #decoder=474200 FST4W-300 133 | #decoder=474200 WSPR 134 | 135 | # 2200m 136 | #decoder=136000 FST4-60 137 | #decoder=136000 FST4-120 138 | #decoder=136000 FST4-300 139 | #decoder=136000 FST4-900 140 | #decoder=136000 FST4-1800 141 | #decoder=136000 FST4W-120 142 | #decoder=136000 FST4W-300 143 | #decoder=136000 FST4W-900 144 | #decoder=136000 FST4W-1800 145 | #decoder=136000 WSPR 146 | 147 | [wsjtx] 148 | # A temporary folder for writing *.wav files. Must already exist. Required if 149 | # the "transfermethod" option is set to "wavefile", OR if any WSPR decoders 150 | # are enabled 151 | temppath=C:\CWSL_DIGI\wave 152 | 153 | # The WSJT-X bin path. jt9.exe and wsprd.exe must be in this folder. 154 | binpath=C:\WSJT\wsjtx\bin 155 | 156 | # The mechanism to use for data transfers from CWSL_DIGI to the WSJT-X decoders 157 | # Valid values are "shmem" and "wavefile". The default is shmem. 158 | # "shmem" relies on shared memory. shmem decodes slower and uses more 159 | # memory. "wavefile" writes temporary files to disk, which may decrease 160 | # the lifespan of solid state drives. 161 | # WSPR decoders always use wave files regardless of this setting, as the 162 | # WSJT-X WSPR decoder does not provide a shared memory interface. 163 | # JS8 decoders always use wave files. 164 | #transfermethod=shmem 165 | 166 | # Highest decode frequency per decoder in Hz 167 | # Default is 3000 Hz. 168 | # The maximum is 6000. 169 | #highestdecodefreq=3000 170 | 171 | # FT4 and FT8 audio scale factor, recommend 0.5 to 1.0. Default 0.90 172 | #ftaudioscalefactor=0.90 173 | 174 | # WSPR audio scale factor, recommend 0.1 to 0.25. Default 0.20 175 | #wspraudioscalefactor=0.20 176 | 177 | # Maximum age of data to report, in multiples of the mode's time frame 178 | # e.g., for FT8, 15s * 10 = 150s. Data older than 150 sec is dropped 179 | # Data is dropped at 260 sec always, regardless of this value. 180 | # The default value is 10. 181 | # maxdataage=10 182 | 183 | # Max number of simultaneous jt9.exe/wsprd.exe instances. Default is computed. 184 | # A safe maximum is the number of cores on the computer, minus one or two. 185 | # If performance issues are encountered, decrease this at the risk of getting 186 | # behind and dropping old data, OR decrease number of bands/modes 187 | # being decoded. 188 | # If this is specified, the decoderburden setting has no impact. 189 | #numjt9instances=4 190 | 191 | # Informs the calculated value for numjt9instances of the computational burden 192 | # of each decoder. Lower values result in less CPU usage overall, but spread 193 | # out more over time, at the risk of getting behind while decoding and 194 | # eventually dropping data. Higher values will increase instantaneous CPU 195 | # usage and the simultaneous number of processes on the computer. This may 196 | # manifest as "lag spikes" if the computer is being used for other purposes. 197 | # The default is 1.0. To halve the computed number of jt9/wsprd instances, 198 | # specify 0.5. To double the number, specify 2.0. 199 | # If numjt9instances is specified, this has no impact, as the value for 200 | # numjt9instances is no longer computed but specified manually. 201 | # For fast computers dedicated to skimming, a value of 2.0 is suggested. For 202 | # computers used for other purposes while skimming, the default is suggested. 203 | #decoderburden=1.0 204 | 205 | # Number of threads used by each jt9.exe. The default is the same as the WSJT-X 206 | # default of 3 and should be suitable for most use cases. 207 | #numjt9threads=3 208 | 209 | # Keep recorded wav files, if true. The default is false. Enabling this will 210 | # cause significant hard disk usage! 211 | #keepwav=true 212 | 213 | # FT4 and FT8 decode depth. Default is 3, which is "deep" in WSJT-X. 214 | # For less aggressive and faster decoding, use 2 or 1. 215 | #decodedepth=3 216 | 217 | # Number of WSPR decoder cycles per bit. The default is 3000. The WSJT-X 218 | # default is 500. Higher numbers will impact performance and may require 219 | # that the decoderburden be increased, or numjt9instances be specified 220 | # manually. For faster WSPR decoding, smaller numbers such as 1000 or 500 221 | # may be best. For deeper but slower decoding, try 6000. The maximum is 10000 222 | #wsprcycles=3000 223 | 224 | 225 | [js8call] 226 | 227 | # To use JS8 decoders, the program JS8CALL must be installed and this option must 228 | # be set to the location of js8.exe 229 | #binpath=C:\Program Files (x86)\js8call\bin 230 | 231 | 232 | [reporting] 233 | # This section contains settings for controlling reporting to spotting networks 234 | # including PSK Reporter, Reverse Beacon Network (RBN) and WSPRNet. 235 | 236 | # If true spots are sent to PSK Reporter for all modes. 237 | pskreporter=false 238 | 239 | # If true, FT4 and FT8 spots are sent to RBN. Must be running RBN Aggregator. 240 | rbn=false 241 | # IP Address of RBN Aggregator. Default is 127.0.0.1 242 | # aggregatorip=127.0.0.1 243 | # UDP Port for RBN Aggregator. This must match the port being used in 244 | # Aggregator. 245 | aggregatorport=2215 246 | 247 | # If true, WSPR spots are sent to WSPRNet. Default is false. 248 | wsprnet=false 249 | 250 | # A comma separate list of callsigns to ignore 251 | #ignoredcalls= 252 | 253 | 254 | [logging] 255 | 256 | # How often to report decoder statistics to the display, in seconds. 257 | # Default is 300s, or 5 minutes 258 | #statsreportinginterval = 300 259 | 260 | # Set to 8 for debugging, 0 for no output, 3 for default output 261 | # This value applies to console output and the log file. 262 | #loglevel=8 263 | 264 | # Log file for recording all output that is logged to the console. 265 | # Uncomment to enable. 266 | # If problems are encountered, set "loglevel" to 8, enable the "logfile" 267 | # option and send the resulting log to W2AXR, alexranaldi@gmail.com 268 | #logfile=C:\CWSL_DIGI\console_log.txt 269 | 270 | # Spots can be logged in the file below. 271 | # Uncomment to enable. Path must exist. File will be created if needed. 272 | #decodesfile=C:\CWSL_DIGI\decode_log.txt 273 | 274 | # Messages that are not understood can be logged to the "bad message log". 275 | # Uncomment to enable. Path must exist. File will be created if needed. 276 | #badmsglog=C:\CWSL_DIGI\bad_message_log.txt 277 | 278 | # If true, logs all spots to the console. Default is true. Uncomment and set to 279 | # false to disable the logging of spots to the console and log file. 280 | #logreports=true 281 | -------------------------------------------------------------------------------- /source/CWSL_DIGI.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 16.0 64 | {64169AA7-3999-40F0-9FDA-2E0896499C1B} 65 | CWSLFT8 66 | 10.0 67 | 68 | 69 | 70 | Application 71 | true 72 | v143 73 | MultiByte 74 | 75 | 76 | Application 77 | false 78 | v143 79 | true 80 | MultiByte 81 | 82 | 83 | Application 84 | true 85 | v143 86 | MultiByte 87 | 88 | 89 | Application 90 | false 91 | v143 92 | true 93 | MultiByte 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Level3 117 | MaxSpeed 118 | true 119 | true 120 | true 121 | true 122 | _CRT_SECURE_NO_WARNINGS;_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) 123 | C:\Qt\5.14.2\msvc2017_64\include;C:\js8call_source;C:\boost_1_70_0;C:\CWSL\CWSL_FT8;C:\ft8_lib\common;C:\ft8_lib\fft;C:\ft8_lib\ft8;%(AdditionalIncludeDirectories) 124 | stdcpp17 125 | Fast 126 | Speed 127 | true 128 | true 129 | false 130 | AdvancedVectorExtensions 131 | 132 | 133 | Console 134 | true 135 | true 136 | C:\Program Files (x86)\Intel\oneAPI\compiler\latest\windows\compiler\lib\intel64_win;C:\wsjt_fort\wsjtx\Release;C:\boost_1_70_0\stage\lib;C:\Qt\5.14.2\msvc2017_64\lib;%(AdditionalLibraryDirectories) 137 | Qt5Core.lib;wsjt_fort.lib;%(AdditionalDependencies) 138 | 139 | 140 | 141 | 142 | Level3 143 | Disabled 144 | true 145 | true 146 | 147 | 148 | Console 149 | 150 | 151 | 152 | 153 | Level3 154 | Disabled 155 | true 156 | true 157 | C:\js8call_source;C:\CWSL\CWSL_FT8;C:\ft8_lib\utils;C:\ft8_lib\ft8;C:\ft8_lib\fft;C:\ft8_lib\common;%(AdditionalIncludeDirectories) 158 | _CRT_SECURE_NO_WARNINGS;QT_NO_DEPRECATED;%(PreprocessorDefinitions) 159 | 160 | 161 | Console 162 | 163 | 164 | 165 | 166 | Level3 167 | MaxSpeed 168 | true 169 | true 170 | true 171 | true 172 | _CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) 173 | C:\ft8_lib\ft8;C:\ft8_lib\fft;C:\ft8_lib\common;%(AdditionalIncludeDirectories) 174 | 175 | 176 | Console 177 | true 178 | true 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /source/Receiver.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2022 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "ring_buffer_spmc.h" 34 | 35 | #include "CWSL_DIGI.hpp" 36 | #include "CWSL_DIGI_Types.hpp" 37 | #include "ring_buffer.h" 38 | #include "decode_audio_buffer.h" 39 | #include "StringUtils.hpp" 40 | #include "HamUtils.hpp" 41 | #include "ScreenPrinter.hpp" 42 | #include "Instance.hpp" 43 | #include "CWSL_Utils.hpp" // shared memory utils 44 | 45 | enum class ReceiverStatus : int { 46 | NOT_INITIALIZED, 47 | RUNNING, 48 | STOPPED, 49 | FINISHED, 50 | }; 51 | 52 | class Receiver { 53 | public: 54 | Receiver( 55 | const std::string& smnameIn, 56 | std::shared_ptr sp) : 57 | instances(), 58 | iq_buffer_base_mem(nullptr), 59 | iq_len(0), 60 | smname(smnameIn), 61 | screenPrinter(sp), 62 | status(ReceiverStatus::NOT_INITIALIZED), 63 | terminateFlag(false) { 64 | } 65 | 66 | virtual ~Receiver(){ 67 | screenPrinter->debug(receiverLog() + "Destructor"); 68 | if (status != ReceiverStatus::FINISHED) { 69 | terminate(); 70 | } 71 | if (iq_buffer_base_mem) { 72 | free(iq_buffer_base_mem); 73 | } 74 | } 75 | 76 | bool openSharedMemory() { 77 | if (!SM.Open(smname.c_str())) { 78 | screenPrinter->err(receiverLog()+"Can't open shared memory : " + smname); 79 | return false; 80 | } 81 | else { 82 | screenPrinter->debug(receiverLog()+"Opened shared memory OK: " + smname); 83 | } 84 | 85 | // get info about channel 86 | SHDR = SM.GetHeader(); 87 | radioSR = static_cast(SHDR->SampleRate); 88 | iq_len = SHDR->BlockInSamples; 89 | screenPrinter->print(receiverLog() 90 | + "\tSample Rate: " + std::to_string(radioSR) 91 | + "\tBlock In Samples: " + std::to_string(iq_len) 92 | + "\tLO: " + std::to_string(SHDR->L0) 93 | + "\tShared Memory: " + smname, 94 | LOG_LEVEL::INFO); 95 | 96 | return true; 97 | } 98 | 99 | std::size_t getIQLength() const { 100 | return iq_len; 101 | } 102 | 103 | std::uint32_t getSampleRate() const { 104 | return radioSR; 105 | } 106 | 107 | FrequencyHz getLO() const { 108 | return SHDR->L0; 109 | } 110 | 111 | void addInstance(Instance* inst) { 112 | instances.push_back(inst); 113 | } 114 | 115 | bool init() 116 | { 117 | terminateFlag = false; 118 | 119 | if (!openSharedMemory()) { 120 | return false; 121 | } 122 | 123 | // 124 | // Prepare circular buffers 125 | // 126 | 127 | try { 128 | if (iq_buffer.initialized) { 129 | iq_buffer.reset(); 130 | } 131 | else { 132 | const size_t iqBufSz = ((static_cast(radioSR) / iq_len) + 1) * 3; 133 | screenPrinter->debug(receiverLog() + "Initializing IQ ring buffer, length = " + std::to_string(iqBufSz) + " blocks"); 134 | const bool initStatus = iq_buffer.initialize(iqBufSz); 135 | if (!initStatus) { 136 | screenPrinter->print(receiverLog() + "Failed to initialize memory for iq buffer"); 137 | return false; 138 | } 139 | 140 | const size_t iqBufferByLen = sizeof(std::complex) * iq_len * iq_buffer.size; 141 | screenPrinter->debug(receiverLog() + "Allocating IQ buffer, size = " + std::to_string(iqBufferByLen) + " bytes"); 142 | iq_buffer_base_mem = malloc(iqBufferByLen); 143 | if (!iq_buffer_base_mem) { 144 | return false; 145 | } 146 | memset(iq_buffer_base_mem, 0, iqBufferByLen); 147 | 148 | std::complex* mem = reinterpret_cast*>(iq_buffer_base_mem); 149 | for (size_t k = 0; k < iq_buffer.size; ++k) { 150 | iq_buffer.recs[k] = mem; 151 | mem += iq_len; 152 | } 153 | } 154 | } 155 | catch (const std::exception& e) { 156 | screenPrinter->err(receiverLog() + std::string("Caught exception allocating IQ buffers: ") + e.what()); 157 | return false; 158 | } 159 | 160 | startThreads(); 161 | 162 | return true; 163 | } 164 | 165 | void startThreads() { 166 | screenPrinter->print(receiverLog() + "Creating receiver thread...", LOG_LEVEL::DEBUG); 167 | iqThread = std::thread(&Receiver::readIQ, this); 168 | SetThreadPriority(iqThread.native_handle(), THREAD_PRIORITY_ABOVE_NORMAL); 169 | } 170 | 171 | ring_buffer_spmc_t*>* getIQBuffer() { 172 | return &iq_buffer; 173 | } 174 | 175 | void terminate() { 176 | screenPrinter->debug(receiverLog() + "Receiver terminating..."); 177 | iq_buffer.terminate(); 178 | terminateFlag = true; 179 | finish(); 180 | } 181 | 182 | void finish() { 183 | screenPrinter->debug(receiverLog() + "finishing..."); 184 | 185 | screenPrinter->debug(receiverLog() + "joining threads..."); 186 | 187 | if (iqThread.joinable()) 188 | { 189 | iqThread.join(); 190 | } 191 | SM.Close(); 192 | screenPrinter->debug(receiverLog() + "removing " + std::to_string(instances.size()) + " instances..."); 193 | 194 | while (!instances.empty()) { 195 | instances[0]->terminate(); 196 | instances.erase(std::begin(instances)); 197 | screenPrinter->debug(receiverLog() + "instance removed"); 198 | 199 | } 200 | status = ReceiverStatus::FINISHED; 201 | screenPrinter->debug(receiverLog() + "finished"); 202 | 203 | } 204 | 205 | ReceiverStatus getStatus() { 206 | return status.load(); 207 | } 208 | 209 | void readIQ() { 210 | screenPrinter->debug(receiverLog() + "readIQ thread started"); 211 | 212 | const size_t readSize = (DWORD)iq_len * sizeof(std::complex); 213 | 214 | status = ReceiverStatus::RUNNING; 215 | 216 | while (!terminateFlag) { 217 | 218 | try { 219 | 220 | // wait for new data from receiver. Blocks until data received 221 | 222 | if (iq_buffer.full()) { 223 | screenPrinter->err(receiverLog() + "I/Q buffer is full!"); 224 | 225 | // wait for a slot to be ready in the buffer. Blocks until slot available. 226 | if (!iq_buffer.wait_for_empty_slot()) { 227 | break; 228 | } 229 | } 230 | 231 | // read block of data from receiver 232 | 233 | std::vector> rawiq(readSize); 234 | 235 | if (!SM.WaitForNewData(1000)) { 236 | break; 237 | } 238 | if (terminateFlag) { 239 | break; 240 | } 241 | 242 | const bool readSuccess = SM.Read((PBYTE)rawiq.data(), (DWORD)readSize); 243 | if (!readSuccess) { 244 | screenPrinter->warning(receiverLog() + "Did not read any I/Q data from shared memory. CPU Overload?"); 245 | } 246 | 247 | memcpy(iq_buffer.recs[iq_buffer.write_index],rawiq.data(),readSize); 248 | 249 | iq_buffer.inc_write_index(); 250 | 251 | /* 252 | float maxVal = std::numeric_limits::lowest(); 253 | for (size_t k = 0; k < readSize; ++k) { 254 | if (rawiq[k].real() > maxVal) { 255 | maxVal = rawiq[k].real(); 256 | } 257 | } 258 | 259 | ofs<warning(receiverLog() + "Did not read any I/Q data from shared memory. CPU Overload?"); 265 | } 266 | iq_buffer.inc_write_index(); 267 | 268 | */ 269 | } 270 | catch (const std::exception& e) { 271 | screenPrinter->err(receiverLog() + std::string("Caught exception in readIQ(): ") + e.what()); 272 | } 273 | } 274 | status = ReceiverStatus::STOPPED; 275 | screenPrinter->debug(receiverLog() + "readIQ thread finished"); 276 | } 277 | 278 | std::string receiverLog() const { 279 | const std::string s = "Receiver " + smname + " "; 280 | return s; 281 | } 282 | 283 | ring_buffer_spmc_t*> iq_buffer; 284 | CSharedMemory SM; 285 | std::uint32_t radioSR; 286 | SM_HDR* SHDR; 287 | std::size_t iq_len; 288 | std::shared_ptr screenPrinter; 289 | 290 | std::atomic_bool terminateFlag; 291 | 292 | std::thread iqThread; 293 | 294 | void* iq_buffer_base_mem; 295 | 296 | std::string smname; 297 | 298 | std::atomic< ReceiverStatus > status; 299 | 300 | std::vector instances; 301 | 302 | }; 303 | -------------------------------------------------------------------------------- /source/RBNHandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2021 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "SafeQueue.h" 32 | 33 | using namespace std::literals; 34 | 35 | struct RBNDecoder { 36 | std::string mode; 37 | uint64_t freq; 38 | }; 39 | 40 | struct RBNStatus 41 | { 42 | uint32_t highestDecodeFreq; 43 | uint8_t numActiveDecoders; 44 | std::vector decoders; 45 | }; 46 | 47 | struct RBNReport 48 | { 49 | std::string callsign; 50 | int32_t snr; 51 | uint32_t freq; 52 | uint32_t baseFreq; 53 | std::string locator; 54 | uint64_t epochTime; 55 | std::string message; 56 | std::string mode; 57 | RBNReport( 58 | const std::string& callsign_in, 59 | const int32_t snr_in, 60 | const uint32_t freq_in, 61 | const uint32_t baseFreq_in, 62 | const std::string locator_in, 63 | const uint64_t epochTime_in, 64 | const std::string message_in, 65 | const std::string mode_in 66 | ) : 67 | callsign(callsign_in), 68 | snr(snr_in), 69 | freq(freq_in), 70 | baseFreq(baseFreq_in), 71 | locator(locator_in), 72 | epochTime(epochTime_in), 73 | message(message_in), 74 | mode(mode_in) 75 | { } 76 | }; 77 | 78 | class RBNHandler { 79 | 80 | using Packet = std::vector; 81 | 82 | public: 83 | RBNHandler() : 84 | mSocket(INVALID_SOCKET), 85 | lastBaseFreq(0), 86 | lastMode(""), 87 | terminateFlag(false) 88 | { 89 | 90 | } 91 | 92 | virtual ~RBNHandler() { 93 | 94 | } 95 | 96 | void handle(const std::uint32_t freq, const uint32_t baseFreq, const std::int32_t snr, const std::string& message, const std::string& mode) { 97 | RBNReport rep("", snr, freq, baseFreq, "", 0, message, mode); 98 | mReports.enqueue(rep); 99 | } 100 | 101 | void handleStatus(const RBNStatus& status) { 102 | mStatus.enqueue(status); 103 | } 104 | 105 | bool init(const std::string& operatorCallsign, const std::string& operatorLocator, const std::string& programName, const std::string& ipAddr, int port) { 106 | 107 | mReceiverCallsign = operatorCallsign; 108 | mReceiverLocator = operatorLocator; 109 | mProgramName = programName; 110 | mServerIP = ipAddr; 111 | 112 | WSADATA wsaData; 113 | int res = WSAStartup(MAKEWORD(2, 2), &wsaData); 114 | if (res != NO_ERROR) { 115 | std::cerr << "WSAStartup failed" << std::endl; 116 | return false; 117 | } 118 | 119 | mSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 120 | if (mSocket == INVALID_SOCKET) { 121 | printf("socket failed with error %d\n", WSAGetLastError()); 122 | return false; 123 | } 124 | 125 | Recv_addr.sin_family = AF_INET; 126 | Recv_addr.sin_port = htons(port); 127 | int len = sizeof(struct sockaddr_in); 128 | Recv_addr.sin_addr.s_addr = inet_addr(mServerIP.c_str()); 129 | 130 | mSendThread = std::thread(&RBNHandler::processingLoop, this); 131 | SetThreadPriority(mSendThread.native_handle(), THREAD_PRIORITY_IDLE); 132 | mSendThread.detach(); 133 | 134 | return true; 135 | } 136 | 137 | void processingLoop() { 138 | while (!terminateFlag) { 139 | std::this_thread::sleep_for(std::chrono::seconds(2)); 140 | const auto reportCount = makePackets(); 141 | while (!mPackets.empty() && !terminateFlag) { 142 | auto packet = mPackets.front(); 143 | mPackets.pop(); 144 | send(packet); 145 | } 146 | } 147 | } 148 | 149 | void send(const Packet& packet) { 150 | //std::cout << "Sending packet, num bytes: " << packet.size() << std::endl; 151 | sendto(mSocket, (const char*)packet.data(), (int)packet.size(), 0, (sockaddr*)& Recv_addr, sizeof(Recv_addr)); 152 | } 153 | 154 | std::size_t makePackets() { 155 | 156 | std::size_t count = 0; 157 | 158 | while (!mStatus.empty()) { 159 | auto status = mStatus.dequeue(); 160 | Packet statusPacket; 161 | addStatusHeaderToPacket(statusPacket); 162 | addInt32ToPacket(statusPacket, status.highestDecodeFreq); 163 | statusPacket.push_back( static_cast(status.decoders.size()) ); 164 | 165 | for (size_t k = 0; k < status.decoders.size(); ++k) { 166 | addStringToPacket(statusPacket, status.decoders[k].mode); // Rx Mode 167 | addInt64ToPacket(statusPacket, status.decoders[k].freq); 168 | } 169 | mPackets.push(statusPacket); 170 | } 171 | 172 | while (!mReports.empty()) { 173 | 174 | auto report = mReports.dequeue(); 175 | 176 | // first make status packet 177 | 178 | if (lastBaseFreq != report.baseFreq || lastMode != report.mode) 179 | { 180 | Packet statusPacket; 181 | 182 | addReportHeaderToPacket(statusPacket); 183 | 184 | std::vector msg1 = { 0x00, 0x00, 0x00, 0x01 }; // Message number for status datagram 185 | for (auto b : msg1) { 186 | statusPacket.push_back(b); 187 | } 188 | 189 | addStringToPacket(statusPacket, mProgramName); 190 | 191 | // Base frequency as 8 byte integer 192 | // upper 4 193 | addInt32ToPacket(statusPacket, 0x0); 194 | // lower 4 195 | addInt32ToPacket(statusPacket, report.baseFreq); 196 | 197 | addStringToPacket(statusPacket, report.mode); // Rx Mode 198 | addStringToPacket(statusPacket, report.callsign); // DX call - ignored by RBNA 199 | addStringToPacket(statusPacket, std::to_string(report.snr)); // SNR as string - ignored by RBNA 200 | addStringToPacket(statusPacket, report.mode); // Tx Mode - ignored by RBNA 201 | statusPacket.push_back(0x0); // TX enable = false - ignored by RBNA 202 | statusPacket.push_back(0x0); // Transmitting = false - ignorded by RBNA 203 | statusPacket.push_back(0x0); // Decoding = false - ignored by RBNA 204 | 205 | addInt32ToPacket(statusPacket, report.freq); // rxdf - ignored by RBNA 206 | addInt32ToPacket(statusPacket, report.freq); // txdf - ignored by RBNA 207 | 208 | addStringToPacket(statusPacket, mReceiverCallsign); // DE call - ignored by RBNA 209 | addStringToPacket(statusPacket, mReceiverLocator); // DE grid - ignored by RBNA 210 | addStringToPacket(statusPacket, "AB12"); // DX grid - ignored by RBNA 211 | 212 | statusPacket.push_back(0x0); // TX watchdog = false - ignored by RBNA 213 | addStringToPacket(statusPacket, ""); // Submode - ignored by RBNA 214 | statusPacket.push_back(0x0); // Fast mode = false - ignored by RBNA 215 | statusPacket.push_back(0x0); // Special operation mode = 0 - ignored by RBNA 216 | 217 | mPackets.push(statusPacket); 218 | count++; 219 | 220 | } 221 | 222 | // second, make decode packet 223 | Packet decodePacket; 224 | 225 | addReportHeaderToPacket(decodePacket); 226 | std::vector msg2 = { 0x00, 0x00, 0x00, 0x02 }; // Message number for decode datagram 227 | for (auto b : msg2) { 228 | decodePacket.push_back(b); 229 | } 230 | 231 | addStringToPacket(decodePacket, mProgramName); // Software ID - ignored by RBNA 232 | decodePacket.push_back(0x1); // New decode = true 233 | addInt32ToPacket(decodePacket, 0x0); // Time = zero - ignored by RBNA 234 | addInt32ToPacket(decodePacket, report.snr); // Report as 4 byte integer 235 | addDoubleToPacket(decodePacket, 0x0); // Delta time - ignored by RBNA 236 | uint32_t hz = report.freq - report.baseFreq; // Delta frequency for decode datagram 237 | 238 | addInt32ToPacket(decodePacket, hz); // Delta frequency in hertz - ignored by RBNA 239 | addStringToPacket(decodePacket, report.mode); // Receive mode - ignored by RBNA 240 | addStringToPacket(decodePacket, report.message); // message 241 | decodePacket.push_back(0x0); // Low-confidence = false 242 | decodePacket.push_back(0x0); // Off air = false - ignored by RBNA 243 | 244 | mPackets.push(decodePacket); 245 | count++; 246 | 247 | lastBaseFreq = report.baseFreq; 248 | lastMode = report.mode; 249 | } 250 | 251 | return count; 252 | 253 | } 254 | 255 | void terminate() { 256 | terminateFlag = true; 257 | } 258 | 259 | private: 260 | 261 | std::vector getStatusHeader() { 262 | return { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; 263 | } 264 | 265 | std::vector getReportHeader() { 266 | return { 0xAD, 0xBC, 0xCB, 0xDA, 0x00, 0x00, 0x00, 0x02 }; 267 | } 268 | 269 | void addStatusHeaderToPacket(Packet& packet) { 270 | auto hdr = getStatusHeader(); 271 | //std::cout << "HEADER" << std::endl; 272 | for (auto p : hdr) { 273 | packet.push_back(p); 274 | } 275 | } 276 | 277 | 278 | void addReportHeaderToPacket(Packet& packet) { 279 | auto hdr = getReportHeader(); 280 | //std::cout << "HEADER" << std::endl; 281 | for (auto p : hdr) { 282 | packet.push_back(p); 283 | } 284 | } 285 | 286 | void addStringToPacket(Packet& packet, const std::string& s) { 287 | addInt32ToPacket(packet, static_cast(s.size())); 288 | for (char c : s) { 289 | packet.push_back(c); 290 | } 291 | } 292 | 293 | void addInt32ToPacket(Packet& packet, const uint32_t num) { 294 | packet.push_back(static_cast((num & 0xFF000000) >> 24)); 295 | packet.push_back(static_cast((num & 0x00FF0000) >> 16)); 296 | packet.push_back(static_cast((num & 0x0000FF00) >> 8)); 297 | packet.push_back(static_cast((num & 0x000000FF) >> 0)); 298 | } 299 | 300 | void addInt64ToPacket(Packet& packet, const uint64_t num) { 301 | packet.push_back(static_cast((num & 0xFF00000000000000) >> 56)); 302 | packet.push_back(static_cast((num & 0x00FF000000000000) >> 48)); 303 | packet.push_back(static_cast((num & 0x0000FF0000000000) >> 40)); 304 | packet.push_back(static_cast((num & 0x000000FF00000000) >> 32)); 305 | packet.push_back(static_cast((num & 0x00000000FF000000) >> 24)); 306 | packet.push_back(static_cast((num & 0x0000000000FF0000) >> 16)); 307 | packet.push_back(static_cast((num & 0x000000000000FF00) >> 8)); 308 | packet.push_back(static_cast((num & 0x00000000000000FF) >> 0)); 309 | } 310 | 311 | 312 | void addDoubleToPacket(Packet& packet, double d) { 313 | uint64_t *num = reinterpret_cast(&d); 314 | packet.push_back(static_cast((*num & 0xFF00000000000000) >> 56)); 315 | packet.push_back(static_cast((*num & 0x00FF000000000000) >> 48)); 316 | packet.push_back(static_cast((*num & 0x0000FF0000000000) >> 40)); 317 | packet.push_back(static_cast((*num & 0x000000FF00000000) >> 32)); 318 | packet.push_back(static_cast((*num & 0x00000000FF000000) >> 24)); 319 | packet.push_back(static_cast((*num & 0x0000000000FF0000) >> 16)); 320 | packet.push_back(static_cast((*num & 0x000000000000FF00) >> 8)); 321 | packet.push_back(static_cast((*num & 0x00000000000000FF) >> 0)); 322 | } 323 | 324 | struct sockaddr_in Recv_addr; 325 | struct sockaddr_in Sender_addr; 326 | 327 | SOCKET mSocket; 328 | 329 | SafeQueue< RBNReport > mReports; 330 | 331 | SafeQueue< RBNStatus > mStatus; 332 | 333 | std::thread mSendThread; 334 | 335 | std::string mReceiverCallsign; 336 | std::string mReceiverLocator; 337 | 338 | std::string mProgramName; 339 | 340 | std::queue< Packet > mPackets; 341 | 342 | std::string mMode; 343 | 344 | std::string mServerIP; 345 | 346 | uint32_t lastBaseFreq; 347 | std::string lastMode; 348 | 349 | bool terminateFlag; 350 | }; -------------------------------------------------------------------------------- /source/Instance.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright 2022 Alexander Ranaldi 4 | W2AXR 5 | alexranaldi@gmail.com 6 | 7 | This file is part of CWSL_DIGI. 8 | 9 | CWSL_DIGI is free software : you can redistribute it and /or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | CWSL_DIGI is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 21 | */ 22 | 23 | #include "Instance.hpp" 24 | #include "Receiver.hpp" 25 | 26 | #include "DecoderPool.hpp" 27 | #include "ScreenPrinter.hpp" 28 | //#include "WaveFile.hpp" 29 | 30 | Instance::Instance( 31 | std::shared_ptr receiverIn, 32 | const size_t idIn, 33 | std::shared_ptr predIn, 34 | const FrequencyHz ssbFreqIn, 35 | const FrequencyHz calibratedFreqIn, 36 | const std::string& modeIn, 37 | const std::string& callsignIn, 38 | const uint32_t waveSampleRateIn, 39 | const float audioScaleFactor_ftIn, 40 | const float audioScaleFactor_wsprIn, 41 | std::shared_ptr sp, 42 | std::shared_ptr dp, 43 | const float trperiodIn 44 | ) : 45 | audioScaleFactor_ft(audioScaleFactor_ftIn), 46 | audioScaleFactor_wspr(audioScaleFactor_wsprIn), 47 | pred(predIn), 48 | ssbd(nullptr), 49 | ssbFreq(ssbFreqIn), 50 | calibratedSSBFreq(calibratedFreqIn), 51 | digitalMode(modeIn), 52 | callsign(callsignIn), 53 | decRatio(0), 54 | id(idIn), 55 | iq_buffer(nullptr), 56 | iq_reader_id(0), 57 | waveSampleRate(waveSampleRateIn), 58 | screenPrinter(sp), 59 | receiver(receiverIn), 60 | decoderPool(dp), 61 | smname(""), 62 | terminateFlag(false), 63 | status(InstanceStatus::NOT_INITIALIZED), 64 | trperiod(trperiodIn) 65 | { 66 | receiver->addInstance(this); 67 | } 68 | 69 | Instance::~Instance(){ 70 | terminate(); 71 | } 72 | 73 | void Instance::setReceiver(std::shared_ptr r) { 74 | receiver = r; 75 | r->addInstance(this); 76 | } 77 | 78 | std::string Instance::getMode() const { 79 | return digitalMode; 80 | } 81 | 82 | FrequencyHz Instance::getFrequency() const { 83 | return ssbFreq; 84 | } 85 | 86 | std::string Instance::getCallsign() const { 87 | return callsign; 88 | } 89 | 90 | void Instance::terminate() { 91 | screenPrinter->debug(instanceLog() + "Instance terminating..."); 92 | af_buffer.terminate(); 93 | 94 | terminateFlag = true; 95 | 96 | screenPrinter->debug(instanceLog() + "Instance joining threads..."); 97 | 98 | if (sampleManagerThread.joinable()) { 99 | screenPrinter->debug(instanceLog() + "calling join()"); 100 | 101 | sampleManagerThread.join(); 102 | } 103 | 104 | status = InstanceStatus::FINISHED; 105 | 106 | receiver = nullptr; 107 | screenPrinter->debug(instanceLog() + "Instance finished"); 108 | 109 | } 110 | 111 | InstanceStatus Instance::getStatus() { 112 | return status.load(); 113 | } 114 | 115 | /* 116 | ReceiverStatus Instance::getReceiverStatus() { 117 | return receiver->getStatus(); 118 | } 119 | */ 120 | 121 | bool Instance::init() 122 | { 123 | terminateFlag = false; 124 | 125 | SSB_SR = Wave_SR; 126 | decRatio = receiver->getSampleRate() / Wave_SR; 127 | 128 | cwd = createTemporaryDirectory(); 129 | screenPrinter->debug(instanceLog() + " temporary directory: " + cwd); 130 | 131 | // 132 | // Prepare circular buffers 133 | // 134 | 135 | try { 136 | if (af_buffer.initialized) { 137 | af_buffer.reset(); 138 | for (size_t k = 0; k < af_buffer.size; ++k) { 139 | af_buffer.recs[k].reset(); 140 | } 141 | } 142 | else { 143 | const bool initStatus = af_buffer.initialize(2); 144 | if (!initStatus) { 145 | screenPrinter->err(instanceLog() + "Failed to initialize memory for af buffer"); 146 | return false; 147 | } 148 | 149 | const size_t afBufNumSa = static_cast( static_cast(SSB_SR) * static_cast(getRXPeriod(digitalMode)+5) ); 150 | screenPrinter->debug(instanceLog() + "Initializing af ring buffer, length = " + std::to_string(afBufNumSa) + " samples"); 151 | 152 | for (size_t k = 0; k < af_buffer.size; ++k) { 153 | screenPrinter->debug(instanceLog() + "Initializing af ring buffer, buffer " + std::to_string(k) + " of " + std::to_string(af_buffer.size)); 154 | af_buffer.recs[k].init(afBufNumSa); 155 | memset(af_buffer.recs[k].buf, 0.0f, af_buffer.recs[k].byte_size()); 156 | af_buffer.recs[k].resetIndices(); 157 | } 158 | } 159 | } 160 | catch (const std::exception& e) { 161 | screenPrinter->err(instanceLog() + std::string("Caught exception allocating af buffers: ") + e.what()); 162 | return false; 163 | } 164 | 165 | iq_buffer = receiver->getIQBuffer(); 166 | iq_reader_id = iq_buffer->addReader(); 167 | 168 | // 169 | // Start Threads 170 | // 171 | 172 | screenPrinter->print(instanceLog() + "Creating samplemanager thread...", LOG_LEVEL::DEBUG); 173 | sampleManagerThread = std::thread(&Instance::sampleManager, this); 174 | 175 | return true; 176 | } 177 | 178 | void Instance::sampleManager() { 179 | screenPrinter->debug(instanceLog() + "Sample manager thread started!"); 180 | 181 | const size_t iq_len = receiver->getIQLength(); 182 | 183 | const std::int32_t demodFreq = calibratedSSBFreq - receiver->getLO(); 184 | 185 | screenPrinter->print("SSBD Freq: " + std::to_string(demodFreq) + " Hz", LOG_LEVEL::DEBUG); 186 | 187 | ssbd = std::make_unique< SSBD >(receiver->getSampleRate(), SSB_BW, static_cast(demodFreq), USB); 188 | 189 | SSB_SR = static_cast(ssbd->GetOutRate()); 190 | screenPrinter->debug(instanceLog() + "SSBD SR: " + std::to_string(SSB_SR)); 191 | 192 | decRatio = receiver->getSampleRate() / Wave_SR; 193 | screenPrinter->debug(instanceLog() + "SSBD Dec Ratio: " + std::to_string(decRatio)); 194 | const size_t ssbd_in_size = ssbd->GetInSize(); 195 | 196 | status = InstanceStatus::RUNNING; 197 | 198 | while (!terminateNow()) { 199 | 200 | try { 201 | // screenPrinter->debug(instanceLog() + "a"); 202 | 203 | if (pred->load()) { 204 | // screenPrinter->debug(instanceLog() + "b"); 205 | 206 | pred->store(false); 207 | // screenPrinter->debug(instanceLog() + "c"); 208 | 209 | auto idx_next = af_buffer.get_next_write_index(); 210 | 211 | screenPrinter->debug(instanceLog() + "Next af buffer write index: " + std::to_string(idx_next)); 212 | 213 | memset(af_buffer.recs[idx_next].buf, 0.0f, af_buffer.recs[af_buffer.write_index].byte_size()); 214 | af_buffer.recs[idx_next].reset(); 215 | af_buffer.recs[idx_next].startEpochTime = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1); 216 | 217 | af_buffer.inc_write_index(); 218 | 219 | screenPrinter->debug(instanceLog() + "Getting an af buffer, read index=" + std::to_string(af_buffer.read_index)); 220 | 221 | auto& current_af_buf = af_buffer.pop_ref(); 222 | const auto startTime = current_af_buf.startEpochTime; 223 | screenPrinter->debug(instanceLog() + "Got an af buffer, size = " + std::to_string(current_af_buf.size) + " samples. start time=" + std::to_string(startTime)); 224 | if (0 == startTime) { 225 | screenPrinter->debug(instanceLog() + "Discarding af buffer, start time is zero"); 226 | continue; // begin demodulating next iteration 227 | } 228 | 229 | try { 230 | prepareAudio(current_af_buf, digitalMode); 231 | } 232 | catch (const std::exception& e) { 233 | screenPrinter->print(instanceLog() + "Caught exception in prepareAudio", e); 234 | continue; 235 | } 236 | //screenPrinter->debug(instanceLog() + "Audio prepared"); 237 | 238 | std::vector audioBuf_i16(current_af_buf.size); 239 | for (std::size_t k = 0; k < current_af_buf.size; ++k) { 240 | audioBuf_i16[k] = static_cast(current_af_buf.buf[k] + 0.5f); 241 | } 242 | //screenPrinter->debug(instanceLog() + "Audio converted from float to i16"); 243 | 244 | ItemToDecode toDecode(audioBuf_i16, digitalMode, startTime, ssbFreq, static_cast(id), cwd, trperiod); 245 | decoderPool->push(toDecode); 246 | 247 | screenPrinter->debug(instanceLog() + "Item pushed to decode queue"); 248 | 249 | if (terminateNow()) { break; } 250 | 251 | ssbd = std::make_unique< SSBD >(receiver->getSampleRate(), SSB_BW, static_cast(demodFreq), USB); 252 | 253 | } 254 | // screenPrinter->debug(instanceLog() + "d"); 255 | 256 | if (terminateNow()) { break; } 257 | // screenPrinter->debug(instanceLog() + "e"); 258 | 259 | try { 260 | if (!iq_buffer->wait_for_data(iq_reader_id)) { 261 | break; 262 | } 263 | // screenPrinter->debug(instanceLog() + "f"); 264 | 265 | std::complex* xc = iq_buffer->pop_no_wait(iq_reader_id); 266 | // screenPrinter->debug(instanceLog() + "g"); 267 | 268 | if (af_buffer.recs[af_buffer.write_index].write_index + iq_len > af_buffer.recs[af_buffer.write_index].size - 1) { 269 | screenPrinter->err(instanceLog() + "af buffer full. size=" + std::to_string(af_buffer.recs[af_buffer.write_index].size) + " write_index=" + std::to_string(af_buffer.recs[af_buffer.write_index].write_index) + " new data size=" + std::to_string(iq_len)); 270 | continue; 271 | } 272 | float* dest = af_buffer.recs[af_buffer.write_index].buf + af_buffer.recs[af_buffer.write_index].write_index; 273 | for (size_t n = 0; n < iq_len; n += ssbd_in_size) { 274 | ssbd->Iterate(xc + n, dest + n / decRatio); 275 | } 276 | af_buffer.recs[af_buffer.write_index].write_index += (iq_len / decRatio); 277 | } 278 | catch (const std::exception& e) { 279 | screenPrinter->print(instanceLog() + "Caught exception in demodulation", e); 280 | } 281 | } 282 | catch (const std::exception& e) { 283 | screenPrinter->print(instanceLog() + "Caught exception in sampleManager(): ", e); 284 | } // try 285 | } // while 286 | screenPrinter->debug(instanceLog() + "Sample manager thread terminating"); 287 | status = InstanceStatus::STOPPED; 288 | } 289 | 290 | inline bool Instance::terminateNow() { 291 | return terminateFlag.load(); 292 | } 293 | 294 | void Instance::prepareAudio(sample_buffer_t& audioBuffer, const std::string& mode) { 295 | float maxVal = std::numeric_limits::lowest(); 296 | for (size_t k = 0; k < audioBuffer.size; ++k) { 297 | if (audioBuffer.buf[k] > maxVal) { 298 | maxVal = audioBuffer.buf[k]; 299 | } 300 | } 301 | // parens prevent macro expansion, fix windows.h max definition collision 302 | float minVal = (std::numeric_limits::max)(); 303 | 304 | for (size_t k = 0; k < audioBuffer.size; ++k) { 305 | if (audioBuffer.buf[k] < minVal) { 306 | minVal = audioBuffer.buf[k]; 307 | } 308 | } 309 | 310 | if (std::fabs(minVal) > maxVal) { 311 | maxVal = std::fabs(minVal); 312 | } 313 | 314 | screenPrinter->debug(instanceLog() + "Maximum audio value: " + std::to_string(maxVal)); 315 | 316 | float factor = AUDIO_CLIP_VAL / (maxVal + 1.0f); 317 | //screenPrinter->debug(instanceLog() + "AUDIO_CLIP_VAL="+std::to_string(AUDIO_CLIP_VAL)); 318 | //screenPrinter->debug(instanceLog() + "factor=" + std::to_string(factor)); 319 | 320 | if (mode == "WSPR") { 321 | //screenPrinter->debug(instanceLog() + "applying WSPR factor=" + std::to_string(audioScaleFactor_wspr)); 322 | 323 | factor *= audioScaleFactor_wspr; 324 | } 325 | else { 326 | //screenPrinter->debug(instanceLog() + "applying FT factor=" + std::to_string(audioScaleFactor_ft)); 327 | 328 | factor *= audioScaleFactor_ft; 329 | } 330 | //screenPrinter->debug(instanceLog() + "new factor=" + std::to_string(factor)); 331 | 332 | for (size_t k = 0; k < audioBuffer.size; ++k) { 333 | audioBuffer.buf[k] *= factor; 334 | } 335 | 336 | screenPrinter->debug(instanceLog() + "Computed audio scale factor: " + std::to_string(factor)); 337 | screenPrinter->debug(instanceLog() + "New maximum audio value: " + std::to_string(factor * maxVal)); 338 | } 339 | 340 | std::string Instance::instanceLog() const { 341 | const std::string s = "Instance " + std::to_string(id) + " "; 342 | return s; 343 | } 344 | -------------------------------------------------------------------------------- /source/WSPRNet.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2023 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | 27 | #include "WSPRNet.hpp" 28 | #include "ScreenPrinter.hpp" 29 | 30 | 31 | WSPRNet::WSPRNet(const std::string& grid, std::shared_ptr sp ) : 32 | operatorGrid(grid), 33 | screenPrinter(sp), 34 | mCountSendsErrored(0), 35 | mCountSendsOK(0), 36 | terminateFlag(false) { 37 | } 38 | 39 | WSPRNet::~WSPRNet() { 40 | WSACleanup(); 41 | } 42 | 43 | bool WSPRNet::init() { 44 | 45 | WSADATA wsaData; 46 | int res = WSAStartup(MAKEWORD(2, 2), &wsaData); 47 | if (res != NO_ERROR) { 48 | std::cerr << "WSAStartup failed" << std::endl; 49 | return false; 50 | } 51 | 52 | 53 | mSendThread = std::thread(&WSPRNet::processingLoop, this); 54 | SetThreadPriority(mSendThread.native_handle(), THREAD_PRIORITY_IDLE); 55 | mSendThread.detach(); 56 | return true; 57 | } 58 | 59 | void WSPRNet::terminate() { 60 | screenPrinter->debug("WSPRNet interface terminating"); 61 | terminateFlag = true; 62 | } 63 | 64 | void WSPRNet::handle(const std::string& callsign, const std::string& mode, const int32_t snr, const float dt, const std::int16_t drift, const std::int16_t dbm, const uint32_t freq, const uint32_t rf, const uint64_t epochTime, const std::string& grid, const std::string& reporterCallsign) { 65 | 66 | wspr::Report rep; 67 | rep.callsign = callsign; 68 | 69 | // Mapping comes from: 70 | // http://www.wsprnet.org/drupal/node/8983 71 | // and WSJT-X source code, 2.6.0 72 | 73 | // WSPR-15 -> 15 (unsupported here) 74 | // WSPR -> 2 75 | // FST4W-120 -> 3 76 | // FST4W-300 -> 5 77 | // FST4W-900 -> 15 78 | // FST4W-1800 -> 30 79 | 80 | if (mode == "WSPR") { 81 | rep.mode = 2; 82 | } 83 | else if (mode == "FST4W-120") { 84 | rep.mode = 3; 85 | } 86 | else if (mode == "FST4W-300") { 87 | rep.mode = 5; 88 | } 89 | else if (mode == "FST4W-900") { 90 | rep.mode = 16; 91 | } 92 | else if (mode == "FST4W-1800") { 93 | rep.mode = 30; 94 | } 95 | else { 96 | screenPrinter->err("Unsupported mode for WSPRNet: " + mode + " so not sending report to WSPRNet"); 97 | return; 98 | } 99 | 100 | rep.snr = snr; 101 | rep.freq = freq; 102 | rep.locator = grid; 103 | rep.epochTime = epochTime; 104 | rep.dt = dt; 105 | rep.drift = drift; 106 | rep.recvfreq = rf; 107 | rep.dbm = dbm; 108 | rep.reporterCallsign = reporterCallsign; 109 | 110 | mReports.enqueue(rep); 111 | } 112 | 113 | bool WSPRNet::isConnected() { 114 | int error = 0; 115 | int len = sizeof(error); 116 | int retval = getsockopt(mSocket, SOL_SOCKET, SO_ERROR, (char*)&error, &len); 117 | 118 | if (retval != 0) { 119 | return false; 120 | } 121 | 122 | if (error != 0) { 123 | return false; 124 | } 125 | 126 | return true; 127 | } 128 | 129 | bool WSPRNet::closeSocket() { 130 | int closeStatus = closesocket(mSocket); 131 | if (0 != closeStatus) { 132 | screenPrinter->print("Error closing socket", LOG_LEVEL::ERR); 133 | return false; 134 | } 135 | return true; 136 | } 137 | 138 | bool WSPRNet::connectSocket() { 139 | 140 | struct addrinfo* addrs; 141 | struct addrinfo hints; 142 | ZeroMemory(&hints, sizeof(hints)); 143 | hints.ai_family = AF_INET; 144 | hints.ai_socktype = SOCK_STREAM; 145 | hints.ai_protocol = IPPROTO_TCP; 146 | 147 | DWORD dwRetval = getaddrinfo(SERVER_HOSTNAME.c_str(), portStr.c_str(), &hints, &addrs); 148 | if (dwRetval != 0) { 149 | screenPrinter->err("WSPRNet getaddrinfo (DNS Lookup) failed looking up host: " + SERVER_HOSTNAME + " at port: " + portStr); 150 | return false; 151 | } 152 | 153 | for (struct addrinfo* addr = addrs; addr != NULL; addr = addr->ai_next) 154 | { 155 | if (addr->ai_addr->sa_family == hints.ai_family && addr->ai_protocol == hints.ai_protocol && addr->ai_socktype == hints.ai_socktype) { 156 | 157 | mSocket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); 158 | if (mSocket == INVALID_SOCKET) { 159 | screenPrinter->err("WSPRNet socket creation failed with error: " + GetSocketError()); 160 | break; // fall through to return false 161 | } 162 | 163 | if (0 == connect(mSocket, addr->ai_addr, addr->ai_addrlen)) { 164 | screenPrinter->debug("WSPRNet socket connection established"); 165 | return true; 166 | } 167 | } 168 | } 169 | // if reached, connect operation failed 170 | freeaddrinfo(addrs); 171 | screenPrinter->print("Socket connect failed with error: " + GetSocketError(), LOG_LEVEL::ERR); 172 | screenPrinter->print("Error connecting to WSPRNet", LOG_LEVEL::ERR); 173 | 174 | return false; 175 | } 176 | 177 | void WSPRNet::sendReportWrapper(const wspr::Report& report) { 178 | const bool status = sendReport(report); 179 | if (status) { 180 | mCountSendsOK++; 181 | } 182 | else { 183 | mCountSendsErrored++; 184 | screenPrinter->err("Failed to send WSPR report to WSPRNet"); 185 | } 186 | } 187 | 188 | bool WSPRNet::sendReport(const wspr::Report& report) { 189 | 190 | std::ostringstream all; 191 | std::ostringstream content; 192 | std::ostringstream header; 193 | std::ostringstream request; 194 | 195 | content << "function=" << "wspr" << "&"; 196 | content << "rcall=" << report.reporterCallsign << "&"; 197 | content << "rgrid=" << operatorGrid << "&"; 198 | 199 | // frequency in MHz 200 | const float rfreqf = static_cast(report.recvfreq) / static_cast(1000000.0); 201 | content << "rqrg=" << std::fixed << std::setprecision(6) << rfreqf << "&"; 202 | 203 | // date, yymmdd utc 204 | content << "date="; 205 | std::chrono::system_clock::time_point tp{ std::chrono::seconds{report.epochTime} }; 206 | time_t tt = std::chrono::system_clock::to_time_t(tp); 207 | tm utc = *gmtime(&tt); 208 | 209 | content << std::setfill('0') << std::setw(2) << utc.tm_year - 100; 210 | content << std::setfill('0') << std::setw(2) << utc.tm_mon + 1; // tm_mon is 0-based 211 | content << std::setfill('0') << std::setw(2) << utc.tm_mday; 212 | content << "&"; 213 | 214 | // time, hhmm utc 215 | content << "time="; 216 | content << std::setfill('0') << std::setw(2) << utc.tm_hour; 217 | content << std::setfill('0') << std::setw(2) << utc.tm_min; 218 | content << "&"; 219 | 220 | // snr 221 | content << "sig=" << report.snr << "&"; 222 | 223 | // dt 224 | content << "dt=" << std::setprecision(2) << report.dt << "&"; 225 | 226 | // drift 227 | content << "drift=" << report.drift << "&"; 228 | 229 | content << "tcall=" << report.callsign << "&"; 230 | 231 | content << "tgrid=" << report.locator << "&"; 232 | 233 | const float freqf = static_cast(report.freq) / static_cast(1000000.0); 234 | content << "tqrg=" << std::fixed << std::setprecision(6) << freqf << "&"; 235 | 236 | content << "dbm=" << report.dbm << "&"; 237 | 238 | content << "version=" << PROGRAM_NAME << " " << PROGRAM_VERSION << "&"; 239 | 240 | // WSPR-15 -> 15 (unsupported here) 241 | // WSPR -> 2 242 | // FST4W-120 -> 3 243 | // FST4W-300 -> 5 244 | // FST4W-900 -> 15 245 | // FST4W-1800 -> 30 246 | 247 | content << "mode=" << report.mode; 248 | 249 | screenPrinter->debug("WSPR Message content: " + content.str()); 250 | 251 | // http request line 252 | request << "POST /post? HTTP/1.1\r\n"; 253 | 254 | // header 255 | header << "Connection: Keep-Alive\r\n"; 256 | header << "Host: wsprnet.org\r\n"; 257 | header << "Content-Type: application/x-www-form-urlencoded\r\n"; 258 | content.seekp(0, ios::end); 259 | const auto contentLength = content.tellp(); 260 | content.seekp(0, ios::beg); 261 | screenPrinter->debug("content length: " + std::to_string(contentLength)); 262 | header << "Content-Length: " << std::to_string(contentLength) << "\r\n"; 263 | header << "Accept-Language: en-US,*\r\n"; 264 | header << "User-Agent: Mozilla/5.0\r\n"; 265 | 266 | all << header.str(); 267 | all << "\r\n"; // blank line between headers and body 268 | all << content.str(); 269 | 270 | bool sendSuccess = sendMessageWithRetry(request.str()); 271 | if (!sendSuccess) { 272 | screenPrinter->debug("Failed to send data to WSPRNet"); 273 | return false; 274 | } 275 | 276 | sendSuccess = sendMessageWithRetry(all.str()); 277 | if (!sendSuccess) { 278 | screenPrinter->debug("Failed to send data to WSPRNet"); 279 | return false; 280 | } 281 | 282 | int numTries= 0; 283 | std::string response = ""; 284 | do { 285 | if (numTries) { 286 | std::this_thread::sleep_for(std::chrono::milliseconds(333)); 287 | } 288 | numTries++; 289 | screenPrinter->debug("WSPRNet attempting read, try: " + std::to_string(numTries)); 290 | response += readMessage(); 291 | screenPrinter->debug("WSPRNet read message of size: " + std::to_string(response.size()) + "message: " + response); 292 | } 293 | while (response.empty() && numTries <= 3); 294 | 295 | if (response.size()) { 296 | screenPrinter->debug("WSPRNet received response: " + response); 297 | } 298 | else { 299 | screenPrinter->debug("WSPRNet No response received, giving up!"); 300 | // closeSocket(); 301 | return false; 302 | } 303 | 304 | // closeSocket(); 305 | return true; 306 | } 307 | 308 | bool WSPRNet::sendMessageWithRetry(const std::string& message) { 309 | 310 | int tries = 0; 311 | bool sendSuccess = false; 312 | while (tries <= 2) { 313 | ++tries; 314 | screenPrinter->debug("Sending message: " + message); 315 | const int bytesSent = sendMessage(message); 316 | screenPrinter->debug("sent bytes: " + std::to_string(bytesSent) +" message size: " + std::to_string(message.size())); 317 | if (bytesSent == message.size()) { 318 | screenPrinter->debug("message send success!"); 319 | sendSuccess = true; 320 | break; // success! 321 | } 322 | else if (bytesSent == SOCKET_ERROR) { 323 | return false; 324 | } 325 | } 326 | return sendSuccess; 327 | } 328 | 329 | int WSPRNet::sendMessage(const std::string& message) { 330 | int total = static_cast(message.size()); 331 | int sent = 0; 332 | do { 333 | int bytes = send(mSocket, message.c_str() + sent, total - sent, NULL); 334 | screenPrinter->debug("send() call resulted in value: " + std::to_string(bytes)); 335 | if (bytes == SOCKET_ERROR) { 336 | return SOCKET_ERROR; 337 | } 338 | sent += bytes; 339 | if (sent < total) { 340 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 341 | } 342 | } while (sent < total); 343 | 344 | return sent; 345 | } 346 | 347 | std::string WSPRNet::readMessage() { 348 | int received = 0; 349 | std::string message = ""; 350 | int bytes = 0; 351 | char buf[8192] = {0}; 352 | bytes = recv(mSocket, buf, 8192, NULL); 353 | screenPrinter->debug("recv() call yielded " + std::to_string(bytes) + " bytes"); 354 | if (bytes > 0) { 355 | message.append(buf); 356 | } 357 | return message; 358 | } 359 | 360 | void WSPRNet::processingLoop() { 361 | while (!terminateFlag) { 362 | screenPrinter->debug("Reports in send queue: " + std::to_string(mReports.size())); 363 | 364 | while (!mReports.empty()) { 365 | const bool connectStatus = connectSocket(); 366 | if (!connectStatus) 367 | { 368 | std::this_thread::sleep_for(std::chrono::milliseconds(2000)); 369 | continue; 370 | } 371 | auto report = mReports.dequeue(); 372 | sendReportWrapper(report); 373 | closeSocket(); 374 | } 375 | 376 | std::this_thread::sleep_for(std::chrono::milliseconds(10000)); 377 | if (terminateFlag) { return; } 378 | 379 | reportStats(); 380 | } 381 | } 382 | 383 | void WSPRNet::reportStats() { 384 | screenPrinter->debug("Count of successful reports to WSPRNet: " + std::to_string(mCountSendsOK)); 385 | screenPrinter->debug("Count of failed reports to WSPRNet: " + std::to_string(mCountSendsErrored)); 386 | } 387 | -------------------------------------------------------------------------------- /source/PSKReporter.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | Copyright 2023 Alexander Ranaldi 5 | W2AXR 6 | alexranaldi@gmail.com 7 | 8 | This file is part of CWSL_DIGI. 9 | 10 | CWSL_DIGI is free software : you can redistribute it and /or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | CWSL_DIGI is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with CWSL_DIGI. If not, see < https://www.gnu.org/licenses/>. 22 | */ 23 | 24 | #include 25 | #include 26 | 27 | #include "PSKReporter.hpp" 28 | #include "TimeUtils.hpp" 29 | #include "ScreenPrinter.hpp" 30 | 31 | using namespace std::literals; 32 | 33 | namespace pskreporter { 34 | 35 | PSKReporter::PSKReporter() : 36 | mSocket(INVALID_SOCKET), 37 | terminateFlag(false) 38 | { 39 | 40 | } 41 | 42 | PSKReporter::~PSKReporter() 43 | { 44 | } 45 | 46 | void PSKReporter::handle(const std::string& callsign, int32_t snr, FrequencyHz freq, uint64_t epochTime, const std::string& mode) { 47 | screenPrinter->debug("PSKReporter handling callsign=" + callsign); 48 | Report rep(callsign, snr, freq, "", epochTime, mode); 49 | mReports.enqueue(rep); 50 | } 51 | 52 | void PSKReporter::handle(const std::string& callsign, int32_t snr, uint32_t freq, const std::string& loc, uint64_t epochTime, const std::string& mode) { 53 | screenPrinter->debug("PSKReporter handling callsign=" + callsign); 54 | Report rep(callsign, snr, freq, loc, epochTime, mode); 55 | mReports.enqueue(rep); 56 | } 57 | 58 | bool PSKReporter::init(const std::string& operatorCallsign, const std::string& operatorLocator, const std::string& programName, std::shared_ptr printer) { 59 | mReceiverCallsign = operatorCallsign; 60 | mReceiverLocator = operatorLocator; 61 | mProgramName = programName; 62 | screenPrinter = printer; 63 | 64 | WSADATA wsaData; 65 | int res = WSAStartup(MAKEWORD(2, 2), &wsaData); 66 | if (res != NO_ERROR) { 67 | std::cerr << "WSAStartup failed" << std::endl; 68 | return false; 69 | } 70 | 71 | struct addrinfo* addrs; 72 | struct addrinfo hints; 73 | ZeroMemory(&hints, sizeof(hints)); 74 | hints.ai_family = AF_INET; 75 | hints.ai_socktype = SOCK_DGRAM; 76 | hints.ai_protocol = IPPROTO_UDP; 77 | 78 | DWORD dwRetval = getaddrinfo(SERVER_HOSTNAME.c_str(), PORT.c_str(), &hints, &addrs); 79 | if (dwRetval != 0) { 80 | screenPrinter->err("PSKReporter getaddrinfo failed looking up host: " + SERVER_HOSTNAME + " at port: " + PORT); 81 | freeaddrinfo(addrs); 82 | return false; 83 | } 84 | 85 | bool ok = false; 86 | for (struct addrinfo* addr = addrs; addr != NULL; addr = addr->ai_next) 87 | { 88 | if (addr->ai_addr->sa_family == hints.ai_family && addr->ai_protocol == hints.ai_protocol && addr->ai_socktype == hints.ai_socktype) { 89 | mSocket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); 90 | if (mSocket == INVALID_SOCKET) { 91 | screenPrinter->err("PSKReporter Socket creation failed with error: " + std::to_string(WSAGetLastError())); 92 | break; // break, so we free() after the loop 93 | } 94 | char ipv4[INET_ADDRSTRLEN]; 95 | struct sockaddr_in* addr4 = (struct sockaddr_in*)addr->ai_addr; 96 | inet_ntop(AF_INET, &addr4->sin_addr, ipv4, INET_ADDRSTRLEN); 97 | screenPrinter->info("PSKReporter IPv4 address resolved to: " + std::string(ipv4)); 98 | screenPrinter->debug("PSKReporter sa_family: " + std::to_string(addr->ai_addr->sa_family)); 99 | screenPrinter->debug("PSKReporter ai_family: " + std::to_string(addr->ai_family)); 100 | screenPrinter->debug("PSKReporter ai_protocol: " + std::to_string(addr->ai_protocol)); 101 | screenPrinter->debug("PSKReporter ai_socktype: " + std::to_string(addr->ai_socktype)); 102 | 103 | // Now, connect. 104 | const int res = connect( 105 | mSocket, //[in] SOCKET s, 106 | addr->ai_addr, //[in] const sockaddr * name, 107 | (int)addr->ai_addrlen //[in] int namelen 108 | ); 109 | 110 | if (res == SOCKET_ERROR) { 111 | screenPrinter->debug("PSKReporter Socket failed to connect: " + std::to_string(WSAGetLastError())); 112 | closesocket(mSocket); 113 | continue; // we may have multiple IP addresses - go to the next one 114 | } 115 | else { 116 | screenPrinter->debug("PSKReporter Socket connected"); 117 | ok = true; 118 | break; 119 | } 120 | } 121 | } 122 | freeaddrinfo(addrs); 123 | if (!ok) { 124 | screenPrinter->err("PSKReporter connection failed!"); 125 | return false; 126 | } 127 | 128 | // init random generator 129 | std::random_device rd; 130 | std::mt19937_64 gen(rd()); 131 | 132 | std::uniform_int_distribution dis; 133 | const std::uint32_t num = dis(gen); 134 | 135 | mID = dis(gen); 136 | 137 | mSeq = 0; 138 | 139 | mTimeDescriptorsSent = std::chrono::steady_clock::now() - 24h; 140 | 141 | mSendThread = std::thread(&PSKReporter::processingLoop, this); 142 | SetThreadPriority(mSendThread.native_handle(), THREAD_PRIORITY_IDLE); 143 | mSendThread.detach(); 144 | 145 | return true; 146 | } 147 | 148 | Packet PSKReporter::getHeader() { 149 | 150 | Packet header; 151 | packetAppend(header, 0x00); 152 | packetAppend(header, 0x0A); 153 | 154 | // next 2 are length - populated later on 155 | header.push_back(0x00); 156 | header.push_back(0x00); 157 | 158 | const std::uint32_t epochTime = static_cast(getEpochTime()); 159 | header.push_back(static_cast((epochTime & 0xFF000000) >> 24)); 160 | header.push_back(static_cast((epochTime & 0x00FF0000) >> 16)); 161 | header.push_back(static_cast((epochTime & 0x0000FF00) >> 8)); 162 | header.push_back(static_cast((epochTime & 0x000000FF) >> 0)); 163 | 164 | // seq # 165 | header.push_back((mSeq & 0xFF000000) >> 24); 166 | header.push_back((mSeq & 0x00FF0000) >> 16); 167 | header.push_back((mSeq & 0x0000FF00) >> 8); 168 | header.push_back(mSeq & 0x000000FF); 169 | 170 | // random id 171 | header.push_back((mID & 0xFF000000) >> 24); 172 | header.push_back((mID & 0x00FF0000) >> 16); 173 | header.push_back((mID & 0x0000FF00) >> 8); 174 | header.push_back((mID & 0x000000FF) >> 0); 175 | 176 | return header; 177 | } 178 | 179 | Packet PSKReporter::getReceiverInformation() { 180 | Packet payload; 181 | 182 | payload.push_back(static_cast(mReceiverCallsign.size())); 183 | for (char c : mReceiverCallsign) { 184 | payload.push_back(c); 185 | } 186 | 187 | payload.push_back(static_cast(mReceiverLocator.size())); 188 | for (char c : mReceiverLocator) { 189 | payload.push_back(c); 190 | } 191 | 192 | payload.push_back(static_cast(mProgramName.size())); 193 | for (char c : mProgramName) { 194 | payload.push_back(c); 195 | } 196 | 197 | while (payload.size() % 4 != 0) { 198 | payload.push_back(0); 199 | } 200 | 201 | Packet out; 202 | packetAppend(out, 0x99); 203 | packetAppend(out, 0x92); 204 | 205 | uint16_t totalSize = static_cast(payload.size() + 4); 206 | 207 | packetAppend(out, (totalSize & 0xFF00) >> 16); 208 | packetAppend(out, totalSize & 0x00FF); 209 | 210 | for (auto v : payload) { 211 | packetAppend(out, v); 212 | } 213 | 214 | return out; 215 | } 216 | 217 | 218 | void PSKReporter::processingLoop() { 219 | std::random_device rd; 220 | std::mt19937 mt(rd()); 221 | std::uniform_real_distribution dist(18, 38); 222 | 223 | while (!terminateFlag) { 224 | const float sleepTime = dist(mt); 225 | std::this_thread::sleep_for(std::chrono::seconds(static_cast(sleepTime))); 226 | if (terminateFlag) { return; } 227 | 228 | // clean up old entries in mSentReports 229 | const auto et = getEpochTime(); 230 | for (auto it = mSentReports.begin(); it != mSentReports.end();) { 231 | std::int64_t agoSec = et - it->epochTime; 232 | if (agoSec > MIN_SECONDS_BETWEEN_SAME_CALLSIGN_REPORTS * 2) { 233 | it = mSentReports.erase(it); 234 | } 235 | else { 236 | ++it; 237 | } 238 | } 239 | 240 | size_t reportCount = 0; 241 | size_t reportTotal = 0; 242 | do { 243 | reportCount = makePackets(); 244 | reportTotal += reportCount; 245 | } 246 | while (!terminateFlag && reportCount); 247 | 248 | screenPrinter->print("PSKReporter - Total reports packetized: " + std::to_string(reportTotal), LOG_LEVEL::DEBUG); 249 | screenPrinter->print("PSKReporter - Packets waiting to be sent:" + std::to_string(mPackets.size()), LOG_LEVEL::DEBUG); 250 | screenPrinter->print("Sent reports size:" + std::to_string(mSentReports.size()), LOG_LEVEL::DEBUG); 251 | 252 | while (!terminateFlag && !mPackets.empty()) { 253 | auto packet = mPackets.front(); 254 | mPackets.pop(); 255 | sendPacket(packet); 256 | std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(180))); 257 | } 258 | } 259 | } 260 | 261 | Packet PSKReporter::getSenderRecord(Report& report) { 262 | 263 | Packet payload; 264 | 265 | const bool hasLocator = !report.locator.empty(); 266 | 267 | if (hasLocator) { 268 | payload.push_back(0x64); 269 | payload.push_back(0xAF); 270 | payload.push_back(0x00); 271 | payload.push_back(0x00); 272 | } 273 | else { 274 | payload.push_back(0x62); 275 | payload.push_back(0xA7); 276 | payload.push_back(0x00); 277 | payload.push_back(0x00); 278 | } 279 | 280 | payload.push_back(static_cast(report.callsign.size())); 281 | for (char byte : report.callsign) { 282 | payload.push_back(byte); 283 | } 284 | payload.push_back(static_cast((report.freq & 0xFF000000) >> 24)); 285 | payload.push_back(static_cast((report.freq & 0x00FF0000) >> 16)); 286 | payload.push_back(static_cast((report.freq & 0x0000FF00) >> 8)); 287 | payload.push_back(static_cast((report.freq & 0x000000FF) >> 0)); 288 | 289 | // snr 290 | payload.push_back(report.snr & 0xFF); 291 | 292 | //std::cout << report.mode << std::endl; 293 | 294 | payload.push_back(static_cast(report.mode.size())); 295 | for (char c : report.mode) { 296 | payload.push_back(c); 297 | } 298 | 299 | if (hasLocator) { 300 | payload.push_back(static_cast(report.locator.size())); 301 | for (char byte : report.locator) { 302 | payload.push_back(byte); 303 | } 304 | } 305 | 306 | // info src - always 1 307 | payload.push_back(0x01); 308 | 309 | const std::uint32_t epochTime = static_cast(report.epochTime); 310 | payload.push_back(static_cast((epochTime & 0xFF000000) >> 24)); 311 | payload.push_back(static_cast((epochTime & 0x00FF0000) >> 16)); 312 | payload.push_back(static_cast((epochTime & 0x0000FF00) >> 8)); 313 | payload.push_back(static_cast((epochTime & 0x000000FF) >> 0)); 314 | 315 | while (payload.size() % 4 != 0) { 316 | payload.push_back(0); 317 | } 318 | 319 | const uint16_t totalSize = static_cast(payload.size()); 320 | payload[2] = static_cast((totalSize & 0xFF00) >> 16); 321 | payload[3] = static_cast(totalSize & 0x00FF); 322 | 323 | return payload; 324 | } 325 | 326 | void PSKReporter::sendPacket(Packet& packet) { 327 | std::int32_t res = send( 328 | mSocket, // [in] SOCKET 329 | (const char*)packet.data(), // [in] const char *buf, 330 | static_cast(packet.size()), // [in] int len, 331 | 0 // [in] int flags 332 | ); 333 | if (res != packet.size()) { 334 | screenPrinter->err("PSK Reporter datagram send error! Sent datagram with size: " + std::to_string(packet.size()) + " bytes, result=" + std::to_string(res)); 335 | screenPrinter->err(GetSocketError()); 336 | } 337 | else { 338 | screenPrinter->debug("PSKr sent datagram with size: " + std::to_string(packet.size()) + " bytes, result=" + std::to_string(res)); 339 | } 340 | } 341 | 342 | std::size_t PSKReporter::makePackets() { 343 | Packet wholePacket; 344 | 345 | addHeaderToPacket(wholePacket); 346 | 347 | bool hasDescriptors = false; 348 | const std::uint32_t desc_ss = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - mTimeDescriptorsSent).count()); 349 | screenPrinter->debug("PSKr descriptors last sent " + std::to_string(desc_ss) + " sec ago"); 350 | screenPrinter->debug("PSKr successive packets with descriptors=" + std::to_string(mPacketsSentWithDescriptors)); 351 | 352 | // if it's been awhile since we've put descriptors on packets 353 | if (desc_ss >= 500) { 354 | screenPrinter->debug("PSKr desc_ss >= 500"); 355 | mPacketsSentWithDescriptors = 0; 356 | } 357 | // if we haven't sent successive packets with descriptors 358 | if (mPacketsSentWithDescriptors <= 3) { 359 | screenPrinter->debug("<=3 PSKr successive packets with descriptors, count=" + std::to_string(mPacketsSentWithDescriptors) + " so adding descriptors"); 360 | addDescriptorsToPacket(wholePacket); 361 | hasDescriptors = true; 362 | } 363 | auto rx = getReceiverInformation(); 364 | for (auto p : rx) { 365 | wholePacket.push_back(p); 366 | } 367 | 368 | size_t count = 0; 369 | while (!terminateFlag && wholePacket.size() <= MAX_UDP_PAYLOAD_SIZE && !mReports.empty()) { 370 | 371 | auto report = mReports.dequeue(); 372 | 373 | bool skip = false; 374 | for (auto it = mSentReports.begin(); it != mSentReports.end(); ++it) { 375 | if (it->callsign == report.callsign && isSameBand(it->freq, report.freq) && it->mode == report.mode) { 376 | const std::int64_t agoSec = report.epochTime - it->epochTime; 377 | if (agoSec <= MIN_SECONDS_BETWEEN_SAME_CALLSIGN_REPORTS) { 378 | skip = true; 379 | break; 380 | } 381 | } 382 | } 383 | if (skip) { 384 | continue; 385 | } 386 | 387 | auto senderRec = getSenderRecord(report); 388 | 389 | for (auto b : senderRec) { 390 | wholePacket.push_back(b); 391 | } 392 | 393 | mSentReports.push_back(report); 394 | count++; 395 | } 396 | 397 | 398 | // set packet size field 399 | uint16_t ps = static_cast(wholePacket.size()); 400 | wholePacket[2] = static_cast((ps & 0xFF00) >> 8); 401 | wholePacket[3] = static_cast(ps & 0x00FF); 402 | 403 | if (count) { 404 | if (hasDescriptors) { 405 | screenPrinter->debug("PSKr created packet with descriptors"); 406 | mTimeDescriptorsSent = std::chrono::steady_clock::now(); 407 | mPacketsSentWithDescriptors++; 408 | } 409 | else { 410 | screenPrinter->debug("PSKr created packet without descriptors"); 411 | } 412 | 413 | mPackets.push(wholePacket); 414 | mSeq++; 415 | } 416 | 417 | return count; 418 | } 419 | 420 | void PSKReporter::terminate() { 421 | screenPrinter->debug("PSK Reporter interface terminating"); 422 | terminateFlag = true; 423 | } 424 | bool PSKReporter::isSameBand(const FrequencyHz f1, const FrequencyHz f2) { 425 | int divisor = 1000000; // 1e6 426 | if (f1 <= 1000000 || f2 <= 1000000) { 427 | divisor = 100000; // 1e5 428 | } 429 | const int c1 = f1 / divisor; 430 | const int c2 = f2 / divisor; 431 | return c1 == c2; 432 | } 433 | 434 | void PSKReporter::addHeaderToPacket(Packet& packet) { 435 | auto hdr = getHeader(); 436 | for (auto p : hdr) { 437 | packet.push_back(p); 438 | } 439 | } 440 | 441 | void PSKReporter::addDescriptorsToPacket(Packet& packet) { 442 | auto yy = getDescriptorReceiver(); 443 | for (auto p : yy) { 444 | packet.push_back(p); 445 | } 446 | auto xx = getDescriptorSender(REPORT_TYPE::TYPE_HAS_LOCATOR); 447 | for (auto p : xx) { 448 | packet.push_back(p); 449 | } 450 | auto qq = getDescriptorSender(REPORT_TYPE::TYPE_NO_LOCATOR); 451 | for (auto p : qq) { 452 | packet.push_back(p); 453 | } 454 | } 455 | 456 | std::vector PSKReporter::getDescriptorReceiver() { 457 | std::vector out = { 458 | 0x00, 0x03, 0x00, 0x24, 0x99, 0x92, 0x00, 0x03, 0x00, 0x00, 459 | 0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 460 | 0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 461 | 0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 462 | 0x00, 0x00, 463 | }; 464 | return out; 465 | } 466 | 467 | std::vector PSKReporter::getDescriptorSender(REPORT_TYPE reportType) { 468 | if (reportType == REPORT_TYPE::TYPE_HAS_LOCATOR) { 469 | // 3C is size 470 | return { 471 | 0x00, 0x02, 0x00, 0x3C, 0x64, 0xAF, 0x00, 0x07, 472 | 0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 473 | 0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F, 474 | 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 475 | 0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 476 | 0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, // locator 477 | 0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 478 | 0x00, 0x96, 0x00, 0x04, // flow start seconds 479 | }; 480 | } 481 | else 482 | { 483 | return { 484 | // 2E is size 485 | 0x00, 0x02, 0x00, 0x2E, 0x62, 0xA7, 0x00, 0x06, 486 | 0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 487 | 0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F, 488 | 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 489 | 0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 490 | 0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 491 | 0x00, 0x96, 0x00, 0x04, // flow start seconds 492 | }; 493 | } 494 | } 495 | 496 | 497 | }; // namespace 498 | --------------------------------------------------------------------------------