diff --git a/.gitignore b/.gitignore index 3edd80b..e96cd60 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ .qmake.stash debug release +wfview-debug +wfserver-debug +wfview-release +wfserver-release ui_* \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index 22ac0c0..a10f418 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,44 @@ # CHANGELOG +- 20220819 + version bumped to 1.4 + temporary squashed logs; may redo later + audio fixes at exit + introduction of peak decays at the scope + resizing of top and bottom scope/waterfall + various fixes on the spectrum display + + +- 20220414 + + this is a cherrypicked CHANGELOG add as there are loads of changes due to different + audio transport ideas etc. For a full changelog exec: git log" + + About box updated to include Patreon site + + added 500 Hz step for VFO + + Added clock and UTC toggle. + + Added forced manual RTS setting + + Add RIT function and other rigctl fixes + + Adjusted window size for radios without spectrum. Thanks K5TUX. + + Allow dynamic restarting of server + + New Settings tab + + Enable High DPI Scaling + + More multi-radio support + + many rigctl improvements/cmpat adds + + N1MM+ TCP connection added + - 20211201 Another "minor" update for RX only rigs @@ -94,7 +132,7 @@ Added an IF Shift-like control for radios with Twin PBF. Added support for IF Shift and Twin Pass-Band Filters. Currently - accessable only via the debug button. + accessible only via the debug button. - 20211104 Added IC-736 FM mode @@ -494,7 +532,7 @@ Just helps keep the UI in sync with changes taking place at the rig. The polling is slow enough that it doesn't impact anything. But quick enough - that it catches discrepencies pretty quickly. + that it catches discrepancies pretty quickly. - 20210619 @@ -535,7 +573,7 @@ Changed collision detection code so that we can more easily see what message was missed. - Added collision detection for serial commands. Collisions are aparently + Added collision detection for serial commands. Collisions are apparently frequent for true 1-wire CI-V radios. We now calculate polling rates immediately upon receiveCommReady for @@ -689,7 +727,7 @@ - 20210601 - Stop deleting audio after last client disconects + Stop deleting audio after last client disconnects Change udpserver packet handling to be similar to udphandler @@ -807,7 +845,7 @@ Add debugging and fix silly error in audiooutput combobox - reenable audio buffers + re-enable audio buffers Attempt to fix crash @@ -830,7 +868,7 @@ Fix for stuttering audio on mac - Fixed missing break in switchs. + Fixed missing break in switches. Typo in message about CI-V @@ -866,7 +904,7 @@ Fixed issue where unknown rigs were identified as 0xff. All rigs are now identified using rigCaps.modelID, populated with the raw response from the Rig ID query. Enumerations of known rig types continue to fall into - rigCaps.model, and unknwn rigs will match to rigCaps.model=modelUnknown, + rigCaps.model, and unknown rigs will match to rigCaps.model=modelUnknown, as before. Serial baud rate is in the UI now. Added some enable/disable code to @@ -1055,7 +1093,7 @@ Additional bands added, Airband, WFM - added 630/2200m for 705; probably like the 7300 hardly useable for TX though. Also added auomatic switching to the view pane after non-BSR bands are selected + added 630/2200m for 705; probably like the 7300 hardly usable for TX though. Also added auomatic switching to the view pane after non-BSR bands are selected added 630/2200m to 7300/7610/785x @@ -1131,7 +1169,7 @@ - 20210411 Added ATU feature on 7610. Now grabs scope state on startup. Found bug, did not fix, in the - frequency parsing code. Worked aroud it by using doubles for now. + frequency parsing code. Worked around it by using doubles for now. - 20210410 Preamp and attenuator are now queried on startup. Additionally, the @@ -1258,7 +1296,7 @@ S-meter ballistics: Turned up the speed. Once you see fast meters, there's no going back. - Tested at 5ms without any issues, comitting at 10ms for now. Note that + Tested at 5ms without any issues, committing at 10ms for now. Note that 50ms in the first 'fixed' meter code is the same as 25ms now due to how the command queue is structured. So 10ms is only a bit faster than before. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 901803c..7804ed1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,3 +6,12 @@ the following people currently contribute to this Project: - Jim PA8E - Phil M0VSE - Roeland PA3MET + + +Also contributions by: + +Daniele Forsi +Russ Woodman - K5TUX +(and some others I will add too) + + diff --git a/INSTALL.md b/INSTALL.md index 970ddab..70612cc 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -14,6 +14,9 @@ sudo apt-get install libqt5multimedia5-plugins sudo apt-get install qtmultimedia5-dev sudo apt-get install git sudo apt-get install libopus-dev +sudo apt-get install libeigen3-dev +sudo apt-get install portaudio19-dev +sudo apt-get install librtaudio-dev ~~~ Now you need to install qcustomplot. There are two versions that are commonly found in linux distros: 1.3 and 2.0. Either will work fine. If you are not sure which version your linux install comes with, simply run both commands. One will work and the other will fail, and that's fine! @@ -70,12 +73,12 @@ sudo usermod -aG dialout $USER ### opensuse/sles/tumbleweed install --- -install wfview on suse 15.x sles 15.x or tumbleweed; this was done on a clean install/updated OS. +install wfview on suse 15.3 & up, sles 15.x or tumbleweed; this was done on a clean install/updated OS. we need to add packages to be able to build the stuff. - sudo zypper in --type pattern devel_basis -- sudo zypper in libQt5Widgets-devel libqt5-qtbase-common-devel libqt5-qtserialport-devel libQt5SerialPort5 qcustomplot-devel libqcustomplot2 libQt5PrintSupport-devel libqt5-qtmultimedia-devel lv2-devel libopus-devel +- sudo zypper in libQt5Widgets-devel libqt5-qtbase-common-devel libqt5-qtserialport-devel libQt5SerialPort5 qcustomplot-devel libqcustomplot2 libQt5PrintSupport-devel libqt5-qtmultimedia-devel lv2-devel libopus-devel eigen3-devel optional (mainly for development specifics): get and install qt5: @@ -94,9 +97,9 @@ in this case, use your homedir: - mkdir -p ~/src/build && cd src - git clone https://gitlab.com/eliggett/wfview.git - cd build -- qmake-qt5 ../wfview/wfview.pro +- qmake-qt5 ../wfview/wfview.pro (wfserver.pro if you build the server version) - make -j -- sudo ./install.sh +- sudo make install wfview is now installed in /usr/local/bin diff --git a/INSTALL_PREBUILT_BINARY.md b/INSTALL_PREBUILT_BINARY.md index 6c8fc7e..6f0dd57 100644 --- a/INSTALL_PREBUILT_BINARY.md +++ b/INSTALL_PREBUILT_BINARY.md @@ -22,8 +22,8 @@ Debian 11 (Debian 10 is outdated) Fedora 33 Fedora 34 mint 20.1 (and up?) -openSUSE 15.2 -openSUSE 15.3 (see notes at the end) +openSUSE 15.2 (outdated/deprecated) +openSUSE 15.3/15.4) openSUSE Tumbleweed(s) SLES 15.x Ubuntu 20.04.2 @@ -86,16 +86,8 @@ sudo ln -s /lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /lib/x86_64-linux-gnu/l ~~~ -### openSUSE/Tumbleweed/SLES based on 15.2: +### openSUSE/Tumbleweed/SLES based on 15.3/15.4: ~~~ -sudo zypper in libqcustomplot2 libQt5SerialPort5 -wfview -~~~ - -### openSUSE/Tumbleweed/SLES based on 15.3: -~~~ - -SEE THE NOTES AT THE END. You need wfview153 here sudo zypper in libqcustomplot2 libQt5SerialPort5 wfview @@ -119,14 +111,8 @@ sudo ln -s /lib/x86_64-linux-gnu/libqcustomplot.so.2.0.1 /lib/x86_64-linux-gnu/l ### notes: ~~~ -Some newer versions of mint. ubuntu, openSUSE have different kernels and such which cause wfview to segfault. +openSUSE 15.2 is deprecated; old binary has been removed -For these cases we created two binaries: one for current systems ("wfview") and one for the new systems ("wfview153") - -So if you encounter a SEGFAULT at start: - -go in to the dist directory, rename wfview to wfvie152; rename wfview153 to wfview and re-execute the install.sh -script ~~~ diff --git a/README.md b/README.md index 074905f..2e69d58 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Other models to be tested/added (including the IC-705).. website - [WFVIEW](https://wfview.org/) wfview.org -wfview supports viewing the spectrum display waterfall and most normal radio controls. Using wfview, the radio can be operated using the mouse, or just the keyboard (great for those with visual impairments), or even a touch screen display. The gorgous waterfall spectrum can be displayed on a monitor of any size, and can even projected onto a wall for a presentation. Even a VNC session can make use of wfview for interesting remote rig posibilities. wfview runs on humble hardware, ranging from the $35 Raspberry Pi, to laptops, to desktops. wfview is designed to run on GNU Linux, but can probably be adapted to run on other operating systems. In fact we do have working example in windows as well. +wfview supports viewing the spectrum display waterfall and most normal radio controls. Using wfview, the radio can be operated using the mouse, or just the keyboard (great for those with visual impairments), or even a touch screen display. The gorgous waterfall spectrum can be displayed on a monitor of any size, and can even projected onto a wall for a presentation. Even a VNC session can make use of wfview for interesting remote rig possibilities. wfview runs on humble hardware, ranging from the $35 Raspberry Pi, to laptops, to desktops. wfview is designed to run on GNU Linux, but can probably be adapted to run on other operating systems. In fact we do have working example in windows as well. @@ -29,16 +29,16 @@ wfview is copyright 2017-2020 Elliott H. Liggett. All rights reserved. wfview so 2. Double-click anywhere on the bandscope or waterfall to tune the radio. 3. Entry of frequency is permitted under the "Frequency" tab. Buttons are provided for touch-screen control 4. Bandscope parameters (span and mode) are adjustable. -5. Full [keyboard](https://gitlab.com/eliggett/wfview/-/wikis/Keystrokes) and mouse control. Operate in whichever way you like. Most radio functions can be operated from a numberic keypad! This also enables those with visual impairments to use the IC-7300. +5. Full [keyboard](https://gitlab.com/eliggett/wfview/-/wikis/Keystrokes) and mouse control. Operate in whichever way you like. Most radio functions can be operated from a numeric keypad! This also enables those with visual impairments to use the IC-7300. 6. 100 user memories stored in plain text on the computer 7. Stylable GUI using CSS 8. pseudo-terminal device, which allows for secondary program to control the radio while wfview is running -9. works for radios that support the ethernet interface with compareable waterfall speeds as on the radio itself. +9. works for radios that support the ethernet interface with comparable waterfall speeds as on the radio itself. ### Build Requirements: 1. gcc / g++ / make 2. qmake -3. qt5 (proably the package named "qt5-default") +3. qt5 (probably the package named "qt5-default") 4. libqt5serialport5-dev 5. libqcustomplot-dev diff --git a/USERMANUAL/wfview-1.0.txt b/USERMANUAL/wfview-1.0.txt index 75049df..2dd8b6c 100644 --- a/USERMANUAL/wfview-1.0.txt +++ b/USERMANUAL/wfview-1.0.txt @@ -42,7 +42,7 @@ on/off switch for the scope/waterfall wf theme Currently fixed selections how the scope and waterfall look like -with colors. At som epoint we may add the ability to accept the +with colors. At some point we may add the ability to accept the RGB values like the rig does. diff --git a/WHATSNEW b/WHATSNEW index 30289b7..270f5ae 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -1,22 +1,31 @@ The following highlights are in this 1.x-release: - many changes/mods/updates/enhancements to rigctld - rigctld box added in the UI - build process changed: you can add the install prefix (derSuessmann) - added "do not ask again" for switching off rig and exiting wfview - added opus as audio transport - dual meter support - rigctl basic split support - rigctl prevents switching off civ transceive - added 25 kHz step - as a temporary measure sending multiple TX/FREQ change commands to the rig - when we use rigctld. - people should use "fake it" in wsjtx as the split code is not reliable. - tidied up udp server function for better reliability - added some IC736 stuff - added portaudio (you need t change wfview.pro to select - and that lowers the latency to maybe less than 50 ms - added PBT and IF-shift - several bugs fixed - RTS as PTT for several radios like the 706/718/736… + New major change is the audio transport mechanism. Lower latencies. + About box updated to include Patreon site + added 500 Hz step for VFO + Added clock and UTC toggle. + Added forced manual RTS setting + Add RIT function and other rigctl fixes + Adjusted window size for radios without spectrum. Thanks K5TUX. + Allow dynamic restarting of server + New Settings tab + Enable High DPI Scaling + More multi-radio support (mostly working!) + Split LAN waterfall data for N1MM+ Spectrum Scope support: There is a combobox that allows you to select + split/combine (or default). Split only makes sense for LAN and Combine for USB. + added radio status display with meters for audio (speaker/mic) + selector for audio: QT, PortAudio, RealTime audio ++ version bumped to 1.4 -- rethinking of a new version schema that makes more sense ++ temporary squashed logs; may redo later ++ audio fixes at exit ++ introduction of peak decays at the scope ++ resizing of top and bottom scope/waterfall ++ various fixes on the spectrum display + + +Notes: + +- We know about high CPU usage on RPi. +- try the audiostack you like the most/what works for you + diff --git a/audioconverter.cpp b/audioconverter.cpp new file mode 100644 index 0000000..2316229 --- /dev/null +++ b/audioconverter.cpp @@ -0,0 +1,313 @@ +#include "audioconverter.h" +#include "logcategories.h" +#include "ulaw.h" + +audioConverter::audioConverter(QObject* parent) : QObject(parent) +{ +} + +bool audioConverter::init(QAudioFormat inFormat, QAudioFormat outFormat, quint8 opusComplexity, quint8 resampleQuality) +{ + + this->inFormat = inFormat; + this->outFormat = outFormat; + this->opusComplexity = opusComplexity; + this->resampleQuality = resampleQuality; + + qInfo(logAudioConverter) << "Starting audioConverter() Input:" << inFormat.channelCount() << "Channels of" << inFormat.codec() << inFormat.sampleRate() << inFormat.sampleType() << inFormat.sampleSize() << + "Output:" << outFormat.channelCount() << "Channels of" << outFormat.codec() << outFormat.sampleRate() << outFormat.sampleType() << outFormat.sampleSize(); + + if (inFormat.byteOrder() != outFormat.byteOrder()) { + qInfo(logAudioConverter) << "Byteorder mismatch in:" << inFormat.byteOrder() << "out:" << outFormat.byteOrder(); + } + + + if (inFormat.codec() == "audio/opus") + { + // Create instance of opus decoder + int opus_err = 0; + opusDecoder = opus_decoder_create(inFormat.sampleRate(), inFormat.channelCount(), &opus_err); + qInfo(logAudioConverter()) << "Creating opus decoder: " << opus_strerror(opus_err); + } + + if (outFormat.codec() == "audio/opus") + { + // Create instance of opus encoder + int opus_err = 0; + opusEncoder = opus_encoder_create(outFormat.sampleRate(), outFormat.channelCount(), OPUS_APPLICATION_AUDIO, &opus_err); + //opus_encoder_ctl(opusEncoder, OPUS_SET_LSB_DEPTH(16)); + //opus_encoder_ctl(opusEncoder, OPUS_SET_INBAND_FEC(1)); + //opus_encoder_ctl(opusEncoder, OPUS_SET_DTX(1)); + //opus_encoder_ctl(opusEncoder, OPUS_SET_PACKET_LOSS_PERC(5)); + opus_encoder_ctl(opusEncoder, OPUS_SET_COMPLEXITY(opusComplexity)); // Reduce complexity to maybe lower CPU? + qInfo(logAudioConverter()) << "Creating opus encoder: " << opus_strerror(opus_err); + } + + if (inFormat.sampleRate() != outFormat.sampleRate()) + { + int resampleError = 0; + unsigned int ratioNum; + unsigned int ratioDen; + // Sample rate conversion required. + resampler = wf_resampler_init(outFormat.channelCount(), inFormat.sampleRate(), outFormat.sampleRate(), resampleQuality, &resampleError); + wf_resampler_get_ratio(resampler, &ratioNum, &ratioDen); + resampleRatio = static_cast(ratioDen) / ratioNum; + qInfo(logAudioConverter()) << "wf_resampler_init() returned: " << resampleError << " resampleRatio: " << resampleRatio; + } + return true; +} + +audioConverter::~audioConverter() +{ + + qInfo(logAudioConverter) << "Closing audioConverter() Input:" << inFormat.channelCount() << "Channels of" << inFormat.codec() << inFormat.sampleRate() << inFormat.sampleType() << inFormat.sampleSize() << + "Output:" << outFormat.channelCount() << "Channels of" << outFormat.codec() << outFormat.sampleRate() << outFormat.sampleType() << outFormat.sampleSize(); + + if (opusEncoder != Q_NULLPTR) { + qInfo(logAudioConverter()) << "Destroying opus encoder"; + opus_encoder_destroy(opusEncoder); + } + + if (opusDecoder != Q_NULLPTR) { + qInfo(logAudioConverter()) << "Destroying opus decoder"; + opus_decoder_destroy(opusDecoder); + } + + if (resampler != Q_NULLPTR) { + speex_resampler_destroy(resampler); + qDebug(logAudioConverter()) << "Resampler closed"; + } + +} + +bool audioConverter::convert(audioPacket audio) +{ + + // If inFormat and outFormat are identical, just emit the data back (removed as it doesn't then process amplitude) + if (audio.data.size() > 0) + { + + if (inFormat.codec() == "audio/opus") + { + unsigned char* in = (unsigned char*)audio.data.data(); + + //Decode the frame. + int nSamples = opus_packet_get_nb_samples(in, audio.data.size(), inFormat.sampleRate()); + if (nSamples == -1) { + // No opus data yet? + return false; + } + QByteArray outPacket(nSamples * sizeof(float) * inFormat.channelCount(), (char)0xff); // Preset the output buffer size. + float* out = (float*)outPacket.data(); + + //if (audio.seq > lastAudioSequence + 1) { + // nSamples = opus_decode_float(opusDecoder, Q_NULLPTR, 0, out, nSamples, 1); + //} + //else { + nSamples = opus_decode_float(opusDecoder, in, audio.data.size(), out, nSamples, 0); + //} + //lastAudioSequence = audio.seq; + audio.data.clear(); + audio.data = outPacket; // Replace incoming data with converted. + } + else if (inFormat.codec() == "audio/PCMU") + { + // Current packet is "technically" 8bit so need to create a new buffer that is 16bit + QByteArray outPacket((int)audio.data.length() * 2, (char)0xff); + qint16* out = (qint16*)outPacket.data(); + for (int f = 0; f < audio.data.length(); f++) + { + *out++ = ulaw_decode[(quint8)audio.data[f]]; + } + audio.data.clear(); + audio.data = outPacket; // Replace incoming data with converted. + // Make sure that sample size/type is set correctly + } + + Eigen::VectorXf samplesF; + + if (inFormat.sampleType() == QAudioFormat::SignedInt && inFormat.sampleSize() == 32) + { + Eigen::Ref samplesI = Eigen::Map(reinterpret_cast(audio.data.data()), audio.data.size() / int(sizeof(qint32))); + samplesF = samplesI.cast() / float(std::numeric_limits::max()); + } + else if (inFormat.sampleType() == QAudioFormat::SignedInt && inFormat.sampleSize() == 16) + { + Eigen::Ref samplesI = Eigen::Map(reinterpret_cast(audio.data.data()), audio.data.size() / int(sizeof(qint16))); + samplesF = samplesI.cast() / float(std::numeric_limits::max()); + } + else if (inFormat.sampleType() == QAudioFormat::SignedInt && inFormat.sampleSize() == 8) + { + Eigen::Ref samplesI = Eigen::Map(reinterpret_cast(audio.data.data()), audio.data.size() / int(sizeof(qint8))); + samplesF = samplesI.cast() / float(std::numeric_limits::max());; + } + else if (inFormat.sampleType() == QAudioFormat::UnSignedInt && inFormat.sampleSize() == 8) + { + Eigen::Ref samplesI = Eigen::Map(reinterpret_cast(audio.data.data()), audio.data.size() / int(sizeof(quint8))); + samplesF = samplesI.cast() / float(std::numeric_limits::max());; + } + else if (inFormat.sampleType() == QAudioFormat::Float) { + samplesF = Eigen::Map(reinterpret_cast(audio.data.data()), audio.data.size() / int(sizeof(float))); + } + else + { + qInfo(logAudio()) << "Unsupported Input Sample Type:" << inFormat.sampleType() << "Size:" << inFormat.sampleSize(); + } + + if (samplesF.size() > 0) + + { + audio.amplitude = samplesF.array().abs().maxCoeff(); + // Set the volume + samplesF *= audio.volume; + + /* + samplesF is now an Eigen Vector of the current samples in float format + The next step is to convert to the correct number of channels in outFormat.channelCount() + */ + + + if (inFormat.channelCount() == 2 && outFormat.channelCount() == 1) { + // If we need to drop one of the audio channels, do it now + Eigen::VectorXf samplesTemp(samplesF.size() / 2); + samplesTemp = Eigen::Map >(samplesF.data(), samplesF.size() / 2); + samplesF = samplesTemp; + } + else if (inFormat.channelCount() == 1 && outFormat.channelCount() == 2) { + // Convert mono to stereo if required + Eigen::VectorXf samplesTemp(samplesF.size() * 2); + Eigen::Map >(samplesTemp.data(), samplesF.size()) = samplesF; + Eigen::Map >(samplesTemp.data() + 1, samplesF.size()) = samplesF; + samplesF = samplesTemp; + } + + /* + Next step is to resample (if needed) + */ + + if (resampler != Q_NULLPTR && resampleRatio != 1.0) + { + quint32 outFrames = ((samplesF.size() / outFormat.channelCount()) * resampleRatio); + quint32 inFrames = (samplesF.size() / outFormat.channelCount()); + QByteArray outPacket(outFrames * outFormat.channelCount() * sizeof(float), (char)0xff); // Preset the output buffer size. + const float* in = (float*)samplesF.data(); + float* out = (float*)outPacket.data(); + + int err = 0; + if (outFormat.channelCount() == 1) { + err = wf_resampler_process_float(resampler, 0, in, &inFrames, out, &outFrames); + } + else { + err = wf_resampler_process_interleaved_float(resampler, in, &inFrames, out, &outFrames); + } + + if (err) { + qInfo(logAudioConverter()) << "Resampler error " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; + } + samplesF = Eigen::Map(reinterpret_cast(outPacket.data()), outPacket.size() / int(sizeof(float))); + } + + /* + If output is Opus so encode it now, don't do any more conversion on the output of Opus. + */ + + if (outFormat.codec() == "audio/opus") + { + float* in = (float*)samplesF.data(); + QByteArray outPacket(1275, (char)0xff); // Preset the output buffer size to MAXIMUM possible Opus frame size + unsigned char* out = (unsigned char*)outPacket.data(); + + int nbBytes = opus_encode_float(opusEncoder, in, (samplesF.size() / outFormat.channelCount()), out, outPacket.length()); + if (nbBytes < 0) + { + qInfo(logAudioConverter()) << "Opus encode failed:" << opus_strerror(nbBytes) << "Num Samples:" << samplesF.size(); + return false; + } + else { + outPacket.resize(nbBytes); + audio.data.clear(); + audio.data = outPacket; // Copy output packet back to input buffer. + //samplesF = Eigen::Map(reinterpret_cast(outPacket.data()), outPacket.size() / int(sizeof(float))); + } + + } + else { + + + /* + Now convert back into the output format required + */ + audio.data.clear(); + + if (outFormat.sampleType() == QAudioFormat::UnSignedInt && outFormat.sampleSize() == 8) + { + Eigen::VectorXf samplesITemp = samplesF * float(std::numeric_limits::max()); + samplesITemp.array() += 127; + VectorXuint8 samplesI = samplesITemp.cast(); + audio.data = QByteArray(reinterpret_cast(samplesI.data()), int(samplesI.size()) * int(sizeof(quint8))); + } + else if (outFormat.sampleType() == QAudioFormat::SignedInt && outFormat.sampleSize() == 8) + { + Eigen::VectorXf samplesITemp = samplesF * float(std::numeric_limits::max()); + VectorXint8 samplesI = samplesITemp.cast(); + audio.data = QByteArray(reinterpret_cast(samplesI.data()), int(samplesI.size()) * int(sizeof(qint8))); + } + else if (outFormat.sampleType() == QAudioFormat::SignedInt && outFormat.sampleSize() == 16) + { + Eigen::VectorXf samplesITemp = samplesF * float(std::numeric_limits::max()); + VectorXint16 samplesI = samplesITemp.cast(); + audio.data = QByteArray(reinterpret_cast(samplesI.data()), int(samplesI.size()) * int(sizeof(qint16))); + } + else if (outFormat.sampleType() == QAudioFormat::SignedInt && outFormat.sampleSize() == 32) + { + Eigen::VectorXf samplesITemp = samplesF * float(std::numeric_limits::max()); + VectorXint32 samplesI = samplesITemp.cast(); + audio.data = QByteArray(reinterpret_cast(samplesI.data()), int(samplesI.size()) * int(sizeof(qint32))); + } + else if (outFormat.sampleType() == QAudioFormat::Float) + { + audio.data = QByteArray(reinterpret_cast(samplesF.data()), int(samplesF.size()) * int(sizeof(float))); + } + else { + qInfo(logAudio()) << "Unsupported Output Sample Type:" << outFormat.sampleType() << "Size:" << outFormat.sampleSize(); + } + + /* + As we currently don't have a float based uLaw encoder, this must be done + after all other conversion has taken place. + */ + + if (outFormat.codec() == "audio/PCMU") + { + QByteArray outPacket((int)audio.data.length() / 2, (char)0xff); + qint16* in = (qint16*)audio.data.data(); + for (int f = 0; f < outPacket.length(); f++) + { + qint16 sample = *in++; + int sign = (sample >> 8) & 0x80; + if (sign) + sample = (short)-sample; + if (sample > cClip) + sample = cClip; + sample = (short)(sample + cBias); + int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; + int mantissa = (sample >> (exponent + 3)) & 0x0F; + int compressedByte = ~(sign | (exponent << 4) | mantissa); + outPacket[f] = (quint8)compressedByte; + } + audio.data.clear(); + audio.data = outPacket; // Copy output packet back to input buffer. + + } + } + } + else + { + qDebug(logAudioConverter) << "Detected empty packet"; + } + } + + emit converted(audio); + return true; +} + diff --git a/audioconverter.h b/audioconverter.h new file mode 100644 index 0000000..a0d2953 --- /dev/null +++ b/audioconverter.h @@ -0,0 +1,137 @@ +#ifndef AUDIOCONVERTER_H +#define AUDIOCONVERTER_H +#include +#include +#include +#include +#include +#include +#include + +/* Opus and Eigen */ +#ifdef Q_OS_WIN +#include "opus.h" +#include +#else +#include "opus/opus.h" +#include +#endif + +enum audioType {qtAudio,portAudio,rtAudio}; + +#include "resampler/speex_resampler.h" + +#include "packettypes.h" + +struct audioPacket { + quint32 seq; + QTime time; + quint16 sent; + QByteArray data; + quint8 guid[GUIDLEN]; + float amplitude; + qreal volume = 1.0; +}; + +struct audioSetup { + audioType type; + QString name; + quint16 latency; + quint8 codec; + bool ulaw = false; + bool isinput; + quint32 sampleRate; + QAudioDeviceInfo port; + int portInt; + quint8 resampleQuality; + unsigned char localAFgain; + quint16 blockSize = 20; // Each 'block' of audio is 20ms long by default. + quint8 guid[GUIDLEN]; +}; + +class audioConverter : public QObject +{ + Q_OBJECT + +public: + explicit audioConverter(QObject* parent = nullptr);; + ~audioConverter(); + +public slots: + bool init(QAudioFormat inFormat, QAudioFormat outFormat, quint8 opusComplexity, quint8 resampleQuality); + bool convert(audioPacket audio); + +signals: + void converted(audioPacket audio); + +protected: + QAudioFormat inFormat; + QAudioFormat outFormat; + OpusEncoder* opusEncoder = Q_NULLPTR; + OpusDecoder* opusDecoder = Q_NULLPTR; + SpeexResamplerState* resampler = Q_NULLPTR; + quint8 opusComplexity; + quint8 resampleQuality = 4; + double resampleRatio=1.0; // Default resample ratio is 1:1 + quint32 lastAudioSequence; +}; + + +// Various audio handling functions declared inline + +typedef Eigen::Matrix VectorXuint8; +typedef Eigen::Matrix VectorXint8; +typedef Eigen::Matrix VectorXint16; +typedef Eigen::Matrix VectorXint32; + +static inline QAudioFormat toQAudioFormat(quint8 codec, quint32 sampleRate) +{ + QAudioFormat format; + + /* + 0x01 uLaw 1ch 8bit + 0x02 PCM 1ch 8bit + 0x04 PCM 1ch 16bit + 0x08 PCM 2ch 8bit + 0x10 PCM 2ch 16bit + 0x20 uLaw 2ch 8bit + 0x40 Opus 1ch + 0x80 Opus 2ch + */ + + format.setByteOrder(QAudioFormat::LittleEndian); + format.setCodec("audio/pcm"); + format.setSampleRate(sampleRate); + + if (codec == 0x01 || codec == 0x20) { + /* Set sample to be what is expected by the encoder and the output of the decoder */ + format.setSampleSize(16); + format.setSampleType(QAudioFormat::SignedInt); + format.setCodec("audio/PCMU"); + } + + if (codec == 0x02 || codec == 0x08) { + format.setSampleSize(8); + format.setSampleType(QAudioFormat::UnSignedInt); + } + if (codec == 0x08 || codec == 0x10 || codec == 0x20 || codec == 0x80) { + format.setChannelCount(2); + } else { + format.setChannelCount(1); + } + + if (codec == 0x04 || codec == 0x10) { + format.setSampleSize(16); + format.setSampleType(QAudioFormat::SignedInt); + } + + if (codec == 0x40 || codec == 0x80) { + format.setSampleSize(32); + format.setSampleType(QAudioFormat::Float); + format.setCodec("audio/opus"); + } + + return format; +} + +#endif diff --git a/audiohandler.cpp b/audiohandler.cpp index e28fbb4..2aa17fa 100644 --- a/audiohandler.cpp +++ b/audiohandler.cpp @@ -1,5 +1,5 @@ /* - This class handles both RX and TX audio, each is created as a seperate instance of the class + This class handles both RX and TX audio, each is created as a separate instance of the class but as the setup/handling if output (RX) and input (TX) devices is so similar I have combined them. */ @@ -8,12 +8,9 @@ #include "logcategories.h" #include "ulaw.h" -#if defined(Q_OS_WIN) && defined(PORTAUDIO) -#include -#endif -audioHandler::audioHandler(QObject* parent) +audioHandler::audioHandler(QObject* parent) : QObject(parent) { Q_UNUSED(parent) } @@ -21,716 +18,330 @@ audioHandler::audioHandler(QObject* parent) audioHandler::~audioHandler() { + if (converterThread != Q_NULLPTR) { + converterThread->quit(); + converterThread->wait(); + } + if (isInitialized) { -#if defined(RTAUDIO) - - try { - audio->abortStream(); - audio->closeStream(); - } - catch (RtAudioError& e) { - qInfo(logAudio()) << "Error closing stream:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); - } - delete audio; -#elif defined(PORTAUDIO) - Pa_StopStream(audio); - Pa_CloseStream(audio); -#else stop(); -#endif } - if (ringBuf != Q_NULLPTR) { - delete ringBuf; + if (audioInput != Q_NULLPTR) { + delete audioInput; + audioInput = Q_NULLPTR; + } + if (audioOutput != Q_NULLPTR) { + delete audioOutput; + audioOutput = Q_NULLPTR; } - if (resampler != Q_NULLPTR) { - speex_resampler_destroy(resampler); - qDebug(logAudio()) << "Resampler closed"; - } - if (encoder != Q_NULLPTR) { - qInfo(logAudio()) << "Destroying opus encoder"; - opus_encoder_destroy(encoder); - } - if (decoder != Q_NULLPTR) { - qInfo(logAudio()) << "Destroying opus decoder"; - opus_decoder_destroy(decoder); - } -} - -bool audioHandler::init(audioSetup setupIn) +}bool audioHandler::init(audioSetup setup) { if (isInitialized) { return false; } - /* - 0x01 uLaw 1ch 8bit - 0x02 PCM 1ch 8bit - 0x04 PCM 1ch 16bit - 0x08 PCM 2ch 8bit - 0x10 PCM 2ch 16bit - 0x20 uLaw 2ch 8bit - */ - - setup = setupIn; - setup.radioChan = 1; - setup.bits = 8; - - if (setup.codec == 0x01 || setup.codec == 0x20) { - setup.ulaw = true; - } - if (setup.codec == 0x08 || setup.codec == 0x10 || setup.codec == 0x20 || setup.codec == 0x80) { - setup.radioChan = 2; - } - if (setup.codec == 0x04 || setup.codec == 0x10 || setup.codec == 0x40 || setup.codec == 0x80) { - setup.bits = 16; - } - - ringBuf = new wilt::Ring(setupIn.latency / 8 + 1); // Should be customizable. - - tempBuf.sent = 0; - - if(!setup.isinput) - { - this->setVolume(setup.localAFgain); - } - -#if defined(RTAUDIO) -#if !defined(Q_OS_MACX) - options.flags = ((!RTAUDIO_HOG_DEVICE) | (RTAUDIO_MINIMIZE_LATENCY)); -#endif - -#if defined(Q_OS_LINUX) - audio = new RtAudio(RtAudio::Api::LINUX_ALSA); -#elif defined(Q_OS_WIN) - audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); -#elif defined(Q_OS_MACX) - audio = new RtAudio(RtAudio::Api::MACOSX_CORE); -#endif - - if (setup.port > 0) { - aParams.deviceId = setup.port; - } - else if (setup.isinput) { - aParams.deviceId = audio->getDefaultInputDevice(); - } - else { - aParams.deviceId = audio->getDefaultOutputDevice(); - } - aParams.firstChannel = 0; - - try { - info = audio->getDeviceInfo(aParams.deviceId); - } - catch (RtAudioError& e) { - qInfo(logAudio()) << "Device error:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); - return isInitialized; - } - - if (info.probed) - { - // Always use the "preferred" sample rate - // We can always resample if needed - this->nativeSampleRate = info.preferredSampleRate; - - // Per channel chunk size. - this->chunkSize = (this->nativeSampleRate / 50); - - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") successfully probed"; - if (info.nativeFormats == 0) - { - qInfo(logAudio()) << " No natively supported data formats!"; - return false; - } - else { - qDebug(logAudio()) << " Supported formats:" << - (info.nativeFormats & RTAUDIO_SINT8 ? "8-bit int," : "") << - (info.nativeFormats & RTAUDIO_SINT16 ? "16-bit int," : "") << - (info.nativeFormats & RTAUDIO_SINT24 ? "24-bit int," : "") << - (info.nativeFormats & RTAUDIO_SINT32 ? "32-bit int," : "") << - (info.nativeFormats & RTAUDIO_FLOAT32 ? "32-bit float," : "") << - (info.nativeFormats & RTAUDIO_FLOAT64 ? "64-bit float," : ""); - - qInfo(logAudio()) << " Preferred sample rate:" << info.preferredSampleRate; - if (setup.isinput) { - devChannels = info.inputChannels; - } - else { - devChannels = info.outputChannels; - } - qInfo(logAudio()) << " Channels:" << devChannels; - if (devChannels > 2) { - devChannels = 2; - } - aParams.nChannels = devChannels; - } - - qInfo(logAudio()) << " chunkSize: " << chunkSize; - try { - if (setup.isinput) { - audio->openStream(NULL, &aParams, RTAUDIO_SINT16, this->nativeSampleRate, &this->chunkSize, &staticWrite, this, &options); - } - else { - audio->openStream(&aParams, NULL, RTAUDIO_SINT16, this->nativeSampleRate, &this->chunkSize, &staticRead, this, &options); - } - audio->startStream(); - isInitialized = true; - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "detected latency:" << audio->getStreamLatency(); - } - catch (RtAudioError& e) { - qInfo(logAudio()) << "Error opening:" << QString::fromStdString(e.getMessage()); - } - } - else - { - qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") could not be probed, check audio configuration!"; - } - - -#elif defined(PORTAUDIO) - - PaError err; - -#ifdef Q_OS_WIN - CoInitialize(0); -#endif - - memset(&aParams, 0,sizeof(PaStreamParameters)); - - if (setup.port > 0) { - aParams.device = setup.port; - } - else if (setup.isinput) { - aParams.device = Pa_GetDefaultInputDevice(); - } - else { - aParams.device = Pa_GetDefaultOutputDevice(); - } - - info = Pa_GetDeviceInfo(aParams.device); - - aParams.channelCount = 2; - aParams.hostApiSpecificStreamInfo = NULL; - aParams.sampleFormat = paInt16; - if (setup.isinput) { - aParams.suggestedLatency = info->defaultLowInputLatency; - } - else { - aParams.suggestedLatency = info->defaultLowOutputLatency; - } - - aParams.hostApiSpecificStreamInfo = NULL; - - // Always use the "preferred" sample rate (unless it is 44100) - // We can always resample if needed - if (info->defaultSampleRate == 44100) { - this->nativeSampleRate = 48000; - } - else { - this->nativeSampleRate = info->defaultSampleRate; - } - // Per channel chunk size. - this->chunkSize = (this->nativeSampleRate / 50); - - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << info->name << "(" << aParams.device << ") successfully probed"; - if (setup.isinput) { - devChannels = info->maxInputChannels; - } - else { - devChannels = info->maxOutputChannels; - } - if (devChannels > 2) { - devChannels = 2; - } - aParams.channelCount = devChannels; - - qInfo(logAudio()) << " Channels:" << devChannels; - qInfo(logAudio()) << " chunkSize: " << chunkSize; - qInfo(logAudio()) << " sampleRate: " << nativeSampleRate; - - if (setup.isinput) { - err=Pa_OpenStream(&audio, &aParams, 0, this->nativeSampleRate, this->chunkSize, paNoFlag, &audioHandler::staticWrite, (void*)this); - } - else { - err=Pa_OpenStream(&audio, 0, &aParams, this->nativeSampleRate, this->chunkSize, paNoFlag, &audioHandler::staticRead, (void*)this); - } - - if (err == paNoError) { - err = Pa_StartStream(audio); - } - if (err == paNoError) { - isInitialized = true; - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; - } - else { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "failed to open device" << Pa_GetErrorText(err); - } - - -#else - -#if QT_VERSION < 0x060000 -format.setSampleSize(16); - format.setChannelCount(2); - format.setSampleRate(INTERNAL_SAMPLE_RATE); - format.setCodec("audio/pcm"); - format.setByteOrder(QAudioFormat::LittleEndian); - format.setSampleType(QAudioFormat::SignedInt); -#else - format.setSampleFormat(QAudioFormat::Int16); - format.setChannelCount(2); - format.setSampleRate(INTERNAL_SAMPLE_RATE); -#endif + this->setup = setup; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "audio handler starting:" << setup.name; if (setup.port.isNull()) { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "No audio device was found. You probably need to install libqt5multimedia-plugins."; return false; } - else if (!setup.port.isFormatSupported(format)) - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Format not supported, choosing nearest supported format - which may not work!"; -#if QT_VERSION < 0x060000 - format=setup.port.nearestFormat(format); -#endif + + qDebug(logAudio()) << "Creating" << (setup.isinput ? "Input" : "Output") << "audio device:" << setup.name << + ", bits" << inFormat.sampleSize() << + ", codec" << setup.codec << + ", latency" << setup.latency << + ", localAFGain" << setup.localAFgain << + ", radioChan" << inFormat.channelCount() << + ", resampleQuality" << setup.resampleQuality << + ", samplerate" << inFormat.sampleRate() << + ", uLaw" << setup.ulaw; + + inFormat = toQAudioFormat(setup.codec, setup.sampleRate); + + + outFormat = setup.port.preferredFormat(); + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Preferred Format: SampleSize" << outFormat.sampleSize() << "Channel Count" << outFormat.channelCount() << + "Sample Rate" << outFormat.sampleRate() << "Codec" << outFormat.codec() << "Sample Type" << outFormat.sampleType(); + if (outFormat.channelCount() > 2) { + outFormat.setChannelCount(2); } - if (format.channelCount() > 2) { - format.setChannelCount(2); - } - else if (format.channelCount() < 1) + else if (outFormat.channelCount() < 1) { qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "No channels found, aborting setup."; return false; } - devChannels = format.channelCount(); - nativeSampleRate = format.sampleRate(); - // chunk size is always relative to Internal Sample Rate. - this->chunkSize = (nativeSampleRate / 50); + if (outFormat.channelCount() == 1 && inFormat.channelCount() == 2) { + outFormat.setChannelCount(2); + if (!setup.port.isFormatSupported(outFormat)) { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request stereo reverting to mono"; + outFormat.setChannelCount(1); + } + } - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Internal: sample rate" << format.sampleRate() << "channel count" << format.channelCount(); + if (outFormat.sampleRate() < 48000) { + int tempRate=outFormat.sampleRate(); + outFormat.setSampleRate(48000); + if (!setup.port.isFormatSupported(outFormat)) { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request 48K, reverting to "<< tempRate; + outFormat.setSampleRate(tempRate); + } + } + + if (outFormat.sampleType() == QAudioFormat::UnSignedInt && outFormat.sampleSize()==8) { + outFormat.setSampleType(QAudioFormat::SignedInt); + outFormat.setSampleSize(16); + + if (!setup.port.isFormatSupported(outFormat)) { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request 16bit Signed samples, reverting to 8bit Unsigned"; + outFormat.setSampleType(QAudioFormat::UnSignedInt); + outFormat.setSampleSize(8); + } + } + + + /* + + if (outFormat.sampleType()==QAudioFormat::SignedInt) { + outFormat.setSampleType(QAudioFormat::Float); + outFormat.setSampleSize(32); + if (!setup.port.isFormatSupported(outFormat)) { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Attempt to select 32bit Float failed, reverting to SignedInt"; + outFormat.setSampleType(QAudioFormat::SignedInt); + outFormat.setSampleSize(16); + } + + } + */ + + if (outFormat.sampleSize() == 24) { + // We can't convert this easily so use 32 bit instead. + outFormat.setSampleSize(32); + if (!setup.port.isFormatSupported(outFormat)) { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "24 bit requested and 32 bit audio not supported, try 16 bit instead"; + outFormat.setSampleSize(16); + } + } + + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Selected format: SampleSize" << outFormat.sampleSize() << "Channel Count" << outFormat.channelCount() << + "Sample Rate" << outFormat.sampleRate() << "Codec" << outFormat.codec() << "Sample Type" << outFormat.sampleType(); // We "hopefully" now have a valid format that is supported so try connecting + converter = new audioConverter(); + converterThread = new QThread(this); + if (setup.isinput) { + converterThread->setObjectName("audioConvIn()"); + } + else { + converterThread->setObjectName("audioConvOut()"); + } + converter->moveToThread(converterThread); + + connect(this, SIGNAL(setupConverter(QAudioFormat,QAudioFormat,quint8,quint8)), converter, SLOT(init(QAudioFormat,QAudioFormat,quint8,quint8))); + connect(converterThread, SIGNAL(finished()), converter, SLOT(deleteLater())); + connect(this, SIGNAL(sendToConverter(audioPacket)), converter, SLOT(convert(audioPacket))); + converterThread->start(QThread::TimeCriticalPriority); + if (setup.isinput) { #if QT_VERSION < 0x060000 - audioInput = new QAudioInput(setup.port, format, this); + audioInput = new QAudioInput(setup.port, outFormat, this); #else - audioInput = new QAudioSource(setup.port, format, this); + audioInput = new QAudioSource(setup.port, outFormat, this); #endif connect(audioInput, SIGNAL(stateChanged(QAudio::State)), SLOT(stateChanged(QAudio::State))); - isInitialized = true; + emit setupConverter(outFormat, inFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedInput(audioPacket))); } else { #if QT_VERSION < 0x060000 - audioOutput = new QAudioOutput(setup.port, format, this); + audioOutput = new QAudioOutput(setup.port, outFormat, this); #else - audioOutput = new QAudioSink(setup.port, format, this); + audioOutput = new QAudioSink(setup.port, outFormat, this); #endif -#ifdef Q_OS_MAC - audioOutput->setBufferSize(chunkSize*4); -#endif + connect(audioOutput, SIGNAL(stateChanged(QAudio::State)), SLOT(stateChanged(QAudio::State))); - isInitialized = true; + emit setupConverter(inFormat, outFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedOutput(audioPacket))); } -#endif - // Setup resampler and opus if they are needed. - int resample_error = 0; - int opus_err = 0; - if (setup.isinput) { - resampler = wf_resampler_init(devChannels, nativeSampleRate, setup.samplerate, setup.resampleQuality, &resample_error); - if (setup.codec == 0x40 || setup.codec == 0x80) { - // Opus codec - encoder = opus_encoder_create(setup.samplerate, setup.radioChan, OPUS_APPLICATION_AUDIO, &opus_err); - opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(16)); - opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1)); - opus_encoder_ctl(encoder, OPUS_SET_DTX(1)); - opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5)); - qInfo(logAudio()) << "Creating opus encoder: " << opus_strerror(opus_err); - } - } - else { - resampler = wf_resampler_init(devChannels, setup.samplerate, this->nativeSampleRate, setup.resampleQuality, &resample_error); - if (setup.codec == 0x40 || setup.codec == 0x80) { - // Opus codec - decoder = opus_decoder_create(setup.samplerate, setup.radioChan, &opus_err); - qInfo(logAudio()) << "Creating opus decoder: " << opus_strerror(opus_err); - } - } - unsigned int ratioNum; - unsigned int ratioDen; - wf_resampler_get_ratio(resampler, &ratioNum, &ratioDen); - resampleRatio = static_cast(ratioDen) / ratioNum; - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "wf_resampler_init() returned: " << resample_error << " resampleRatio: " << resampleRatio; - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "thread id" << QThread::currentThreadId(); + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "thread id" << QThread::currentThreadId(); -#if !defined (RTAUDIO) && !defined(PORTAUDIO) - if (isInitialized) { - this->start(); - } -#endif + underTimer = new QTimer(); + underTimer->setSingleShot(true); + connect(underTimer, SIGNAL(timeout()), this, SLOT(clearUnderrun())); - return isInitialized; + this->setVolume(setup.localAFgain); + + this->start(); + + return true; } -#if !defined (RTAUDIO) && !defined(PORTAUDIO) void audioHandler::start() { qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "start() running"; - if ((audioOutput == Q_NULLPTR || audioOutput->state() != QAudio::StoppedState) && - (audioInput == Q_NULLPTR || audioInput->state() != QAudio::StoppedState)) { + if (setup.isinput) { + //this->open(QIODevice::WriteOnly); + //audioInput->start(this); +#ifdef Q_OS_WIN + audioInput->setBufferSize(inFormat.bytesForDuration(setup.latency * 100)); +#else + audioInput->setBufferSize(inFormat.bytesForDuration(setup.latency * 1000)); +#endif + audioDevice = audioInput->start(); + connect(audioInput, SIGNAL(destroyed()), audioDevice, SLOT(deleteLater()), Qt::UniqueConnection); + connect(audioDevice, SIGNAL(readyRead()), this, SLOT(getNextAudioChunk()), Qt::UniqueConnection); + //audioInput->setNotifyInterval(setup.blockSize/2); + //connect(audioInput, SIGNAL(notify()), this, SLOT(getNextAudioChunk()), Qt::UniqueConnection); + } + else { + // Buffer size must be set before audio is started. +#ifdef Q_OS_WIN + audioOutput->setBufferSize(outFormat.bytesForDuration(setup.latency * 100)); +#else + audioOutput->setBufferSize(outFormat.bytesForDuration(setup.latency * 1000)); +#endif + audioDevice = audioOutput->start(); + connect(audioOutput, SIGNAL(destroyed()), audioDevice, SLOT(deleteLater()), Qt::UniqueConnection); + } + if (!audioDevice) { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio device failed to start()"; return; } - if (setup.isinput) { -#ifndef Q_OS_WIN - this->open(QIODevice::WriteOnly); -#else - this->open(QIODevice::WriteOnly | QIODevice::Unbuffered); -#endif - audioInput->start(this); - } - else { -#ifndef Q_OS_WIN - this->open(QIODevice::ReadOnly); -#else - this->open(QIODevice::ReadOnly | QIODevice::Unbuffered); -#endif - audioOutput->start(this); - } } -#endif + + +void audioHandler::stop() +{ + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "stop() running"; + + if (audioOutput != Q_NULLPTR && audioOutput->state() != QAudio::StoppedState) { + // Stop audio output + audioOutput->stop(); + } + + if (audioInput != Q_NULLPTR && audioInput->state() != QAudio::StoppedState) { + // Stop audio output + audioInput->stop(); + } + audioDevice = Q_NULLPTR; +} void audioHandler::setVolume(unsigned char volume) { - //this->volume = (qreal)volume/255.0; this->volume = audiopot[volume]; - - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "setVolume: " << volume << "(" << this->volume << ")"; + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "setVolume: " << volume << "(" << this->volume << ")"; } - -/// -/// This function processes the incoming audio FROM the radio and pushes it into the playback buffer *data -/// -/// -/// -/// -#if defined(RTAUDIO) -int audioHandler::readData(void* outputBuffer, void* inputBuffer, - unsigned int nFrames, double streamTime, RtAudioStreamStatus status) +void audioHandler::incomingAudio(audioPacket packet) { - Q_UNUSED(inputBuffer); - Q_UNUSED(streamTime); - if (status == RTAUDIO_OUTPUT_UNDERFLOW) - qDebug(logAudio()) << "Underflow detected"; - int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels - quint8* buffer = (quint8*)outputBuffer; -#elif defined(PORTAUDIO) -int audioHandler::readData(const void* inputBuffer, void* outputBuffer, - unsigned long nFrames, const PaStreamCallbackTimeInfo * streamTime, PaStreamCallbackFlags status) -{ - Q_UNUSED(inputBuffer); - Q_UNUSED(streamTime); - Q_UNUSED(status); - int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels - quint8* buffer = (quint8*)outputBuffer; -#else -qint64 audioHandler::readData(char* buffer, qint64 nBytes) -{ -#endif - // Calculate output length, always full samples - int sentlen = 0; - if (!isReady) { - isReady = true; - } - if (ringBuf->size()>0) - { - // Output buffer is ALWAYS 16 bit. - //qDebug(logAudio()) << "Read: nFrames" << nFrames << "nBytes" << nBytes; - while (sentlen < nBytes) - { - audioPacket packet; - if (!ringBuf->try_read(packet)) - { - qDebug(logAudio()) << "No more data available but buffer is not full! sentlen:" << sentlen << " nBytes:" << nBytes ; - break; - } - currentLatency = packet.time.msecsTo(QTime::currentTime()); + if (audioDevice != Q_NULLPTR && packet.data.size() > 0) { + packet.volume = volume; - // This shouldn't be required but if we did output a partial packet - // This will add the remaining packet data to the output buffer. - if (tempBuf.sent != tempBuf.data.length()) - { - int send = qMin((int)nBytes - sentlen, tempBuf.data.length() - tempBuf.sent); - memcpy(buffer + sentlen, tempBuf.data.constData() + tempBuf.sent, send); - tempBuf.sent = tempBuf.sent + send; - sentlen = sentlen + send; - if (tempBuf.sent != tempBuf.data.length()) - { - // We still don't have enough buffer space for this? - break; - } - //qDebug(logAudio()) << "Adding partial:" << send; - } - - if (currentLatency > setup.latency) { - qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Packet " << hex << packet.seq << - " arrived too late (increase output latency!) " << - packet.time.msecsTo(QTime::currentTime()) << "ms"; - lastSeq = packet.seq; - if (!ringBuf->try_read(packet)) - break; - currentLatency = packet.time.msecsTo(QTime::currentTime()); - } - - int send = qMin((int)nBytes - sentlen, packet.data.length()); - memcpy(buffer + sentlen, packet.data.constData(), send); - sentlen = sentlen + send; - if (send < packet.data.length()) - { - //qDebug(logAudio()) << "Asking for partial, sent:" << send << "packet length" << packet.data.length(); - tempBuf = packet; - tempBuf.sent = tempBuf.sent + send; - lastSeq = packet.seq; - break; - } - - /* - if (packet.seq <= lastSeq) { - qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Duplicate/early audio packet: " << hex << lastSeq << " got " << hex << packet.seq; - } - else if (packet.seq != lastSeq + 1) { - qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Missing audio packet(s) from: " << hex << lastSeq + 1 << " to " << hex << packet.seq - 1; - } - */ - lastSeq = packet.seq; - } - } - //qDebug(logAudio()) << "looking for: " << nBytes << " got: " << sentlen; - - // fill the rest of the buffer with silence - if (nBytes > sentlen) { - memset(buffer+sentlen,0,nBytes-sentlen); - } -#if defined(RTAUDIO) - return 0; -#elif defined(PORTAUDIO) - return 0; -#else - return nBytes; -#endif -} - -#if defined(RTAUDIO) -int audioHandler::writeData(void* outputBuffer, void* inputBuffer, - unsigned int nFrames, double streamTime, RtAudioStreamStatus status) -{ - Q_UNUSED(outputBuffer); - Q_UNUSED(streamTime); - Q_UNUSED(status); - int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels - const char* data = (const char*)inputBuffer; -#elif defined(PORTAUDIO) -int audioHandler::writeData(const void* inputBuffer, void* outputBuffer, - unsigned long nFrames, const PaStreamCallbackTimeInfo * streamTime, - PaStreamCallbackFlags status) -{ - Q_UNUSED(outputBuffer); - Q_UNUSED(streamTime); - Q_UNUSED(status); - int nBytes = nFrames * devChannels * 2; // This is ALWAYS 2 bytes per sample and 2 channels - const char* data = (const char*)inputBuffer; -#else -qint64 audioHandler::writeData(const char* data, qint64 nBytes) -{ -#endif - if (!isReady) { - isReady = true; - } - int sentlen = 0; - //qDebug(logAudio()) << "nFrames" << nFrames << "nBytes" << nBytes; - int chunkBytes = chunkSize * devChannels * 2; - while (sentlen < nBytes) { - if (tempBuf.sent != chunkBytes) - { - int send = qMin((int)(nBytes - sentlen), chunkBytes - tempBuf.sent); - tempBuf.data.append(QByteArray::fromRawData(data + sentlen, send)); - sentlen = sentlen + send; - tempBuf.seq = 0; // Not used in TX - tempBuf.time = QTime::currentTime(); - tempBuf.sent = tempBuf.sent + send; - } - else { - //ringBuf->write(tempBuf); - - if (!ringBuf->try_write(tempBuf)) - { - qDebug(logAudio()) << "outgoing audio buffer full!"; - break; - } - tempBuf.data.clear(); - tempBuf.sent = 0; - } + emit sendToConverter(packet); } - //qDebug(logAudio()) << "sentlen" << sentlen; -#if defined(RTAUDIO) - return 0; -#elif defined(PORTAUDIO) - return 0; -#else - return nBytes; -#endif -} - -void audioHandler::incomingAudio(audioPacket inPacket) -{ - // No point buffering audio until stream is actually running. - // Regardless of the radio stream format, the buffered audio will ALWAYS be - // 16bit sample interleaved stereo 48K (or whatever the native sample rate is) - - if (!isInitialized && !isReady) - { - qDebug(logAudio()) << "Packet received when stream was not ready"; - return; - } - - if (setup.codec == 0x40 || setup.codec == 0x80) { - unsigned char* in = (unsigned char*)inPacket.data.data(); - - /* Decode the frame. */ - QByteArray outPacket((setup.samplerate / 50) * sizeof(qint16) * setup.radioChan, (char)0xff); // Preset the output buffer size. - qint16* out = (qint16*)outPacket.data(); - int nSamples = opus_packet_get_nb_samples(in, inPacket.data.size(),setup.samplerate); - if (nSamples != setup.samplerate / 50) - { - qInfo(logAudio()) << "Opus nSamples=" << nSamples << " expected:" << (setup.samplerate / 50); - return; - } - nSamples = opus_decode(decoder, in, inPacket.data.size(), out, (setup.samplerate / 50), 0); - - if (nSamples < 0) - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decode failed:" << opus_strerror(nSamples) << "packet size" << inPacket.data.length(); - return; - } - else { - if (int(nSamples * sizeof(qint16) * setup.radioChan) != outPacket.size()) - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decoder mismatch: nBytes:" << nSamples * sizeof(qint16) * setup.radioChan << "outPacket:" << outPacket.size(); - outPacket.resize(nSamples * sizeof(qint16) * setup.radioChan); - } - //qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus decoded" << inPacket.data.size() << "bytes, into" << outPacket.length() << "bytes"; - inPacket.data.clear(); - inPacket.data = outPacket; // Replace incoming data with converted. - } - } - - //qDebug(logAudio()) << "Got" << setup.bits << "bits, length" << inPacket.data.length(); - // Incoming data is 8bits? - if (setup.bits == 8) - { - // Current packet is 8bit so need to create a new buffer that is 16bit - QByteArray outPacket((int)inPacket.data.length() * 2 * (devChannels / setup.radioChan), (char)0xff); - qint16* out = (qint16*)outPacket.data(); - for (int f = 0; f < inPacket.data.length(); f++) - { - int samp = (quint8)inPacket.data[f]; - for (int g = setup.radioChan; g <= devChannels; g++) - { - if (setup.ulaw) - *out++ = ulaw_decode[samp] * this->volume; - else - *out++ = (qint16)((samp - 128) << 8) * this->volume; - } - } - inPacket.data.clear(); - inPacket.data = outPacket; // Replace incoming data with converted. - } - else - { - // This is already a 16bit stream, do we need to convert to stereo? - if (setup.radioChan == 1 && devChannels > 1) { - // Yes - QByteArray outPacket(inPacket.data.length() * 2, (char)0xff); // Preset the output buffer size. - qint16* in = (qint16*)inPacket.data.data(); - qint16* out = (qint16*)outPacket.data(); - for (int f = 0; f < inPacket.data.length() / 2; f++) - { - *out++ = (qint16)*in * this->volume; - *out++ = (qint16)*in++ * this->volume; - } - inPacket.data.clear(); - inPacket.data = outPacket; // Replace incoming data with converted. - } - else - { - // We already have the same number of channels so just update volume. - qint16* in = (qint16*)inPacket.data.data(); - for (int f = 0; f < inPacket.data.length() / 2; f++) - { - *in = *in * this->volume; - in++; - } - } - - } - - /* We now have an array of 16bit samples in the NATIVE samplerate of the radio - If the radio sample rate is below 48000, we need to resample. - */ - //qDebug(logAudio()) << "Now 16 bit stereo, length" << inPacket.data.length(); - - if (resampleRatio != 1.0) { - - // We need to resample - // We have a stereo 16bit stream. - quint32 outFrames = ((inPacket.data.length() / 2 / devChannels) * resampleRatio); - quint32 inFrames = (inPacket.data.length() / 2 / devChannels); - QByteArray outPacket(outFrames * 4, (char)0xff); // Preset the output buffer size. - - const qint16* in = (qint16*)inPacket.data.constData(); - qint16* out = (qint16*)outPacket.data(); - - int err = 0; - err = wf_resampler_process_interleaved_int(resampler, in, &inFrames, out, &outFrames); - if (err) { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Resampler error " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; - } - inPacket.data.clear(); - inPacket.data = outPacket; // Replace incoming data with converted. - } - - //qDebug(logAudio()) << "Adding packet to buffer:" << inPacket.seq << ": " << inPacket.data.length(); - lastSentSeq = inPacket.seq; - - if (!ringBuf->try_write(inPacket)) - { - qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Buffer full! capacity:" << ringBuf->capacity() << "length" << ringBuf->size(); - } return; } +void audioHandler::convertedOutput(audioPacket packet) { + + if (packet.data.size() > 0 ) { + + currentLatency = packet.time.msecsTo(QTime::currentTime()) + (outFormat.durationForBytes(audioOutput->bufferSize() - audioOutput->bytesFree()) / 1000); + if (audioDevice != Q_NULLPTR) { + if (audioDevice->write(packet.data) < packet.data.size()) { + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Buffer full!"; + isOverrun=true; + } else { + isOverrun = false; + } + if (lastReceived.msecsTo(QTime::currentTime()) > 100) { + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Time since last audio packet" << lastReceived.msecsTo(QTime::currentTime()) << "Expected around" << setup.blockSize; + } + lastReceived = QTime::currentTime(); + } + /*if ((packet.seq > lastSentSeq + 1) && (setup.codec == 0x40 || setup.codec == 0x80)) { + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Attempting FEC on packet" << packet.seq << "as last is" << lastSentSeq; + lastSentSeq = packet.seq; + incomingAudio(packet); // Call myself again to run the packet a second time (FEC) + } + */ + lastSentSeq = packet.seq; + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); + + amplitude = packet.amplitude; + } +} + +void audioHandler::getNextAudioChunk() +{ + if (audioDevice) { + tempBuf.data.append(audioDevice->readAll()); + } + if (tempBuf.data.length() >= outFormat.bytesForDuration(setup.blockSize * 1000)) { + audioPacket packet; + packet.time = QTime::currentTime(); + packet.sent = 0; + packet.volume = volume; + memcpy(&packet.guid, setup.guid, GUIDLEN); + //QTime startProcessing = QTime::currentTime(); + packet.data.clear(); + packet.data = tempBuf.data.mid(0, outFormat.bytesForDuration(setup.blockSize * 1000)); + tempBuf.data.remove(0, outFormat.bytesForDuration(setup.blockSize * 1000)); + + emit sendToConverter(packet); + } + + /* If there is still enough data in the buffer, call myself again in 20ms */ + if (tempBuf.data.length() >= outFormat.bytesForDuration(setup.blockSize * 1000)) { + QTimer::singleShot(setup.blockSize, this, &audioHandler::getNextAudioChunk); + } + + return; +} + + +void audioHandler::convertedInput(audioPacket audio) +{ + if (audio.data.size() > 0) { + emit haveAudioData(audio); + if (lastReceived.msecsTo(QTime::currentTime()) > 100) { + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Time since last audio packet" << lastReceived.msecsTo(QTime::currentTime()) << "Expected around" << setup.blockSize ; + } + lastReceived = QTime::currentTime(); + amplitude = audio.amplitude; + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); + } +} + void audioHandler::changeLatency(const quint16 newSize) { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Changing latency to: " << newSize << " from " << setup.latency; - setup.latency = newSize; - delete ringBuf; - ringBuf = new wilt::Ring(setup.latency / 8 + 1); // Should be customizable. -} + setup.latency = newSize; + + if (!setup.isinput) { + stop(); + start(); + } + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Configured latency: " << setup.latency << "Buffer Duration:" << outFormat.durationForBytes(audioOutput->bufferSize())/1000 << "ms"; + +} int audioHandler::getLatency() { return currentLatency; @@ -738,202 +349,49 @@ int audioHandler::getLatency() -void audioHandler::getNextAudioChunk(QByteArray& ret) +quint16 audioHandler::getAmplitude() { - audioPacket packet; - packet.sent = 0; - - if (isInitialized && ringBuf != Q_NULLPTR && ringBuf->try_read(packet)) - { - currentLatency = packet.time.msecsTo(QTime::currentTime()); - - if (currentLatency > setup.latency) { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Packet " << hex << packet.seq << - " arrived too late (increase output latency!) " << - packet.time.msecsTo(QTime::currentTime()) << "ms"; - // if (!ringBuf->try_read(packet)) - // break; - // currentLatency = packet.time.msecsTo(QTime::currentTime()); - } - - //qDebug(logAudio) << "Chunksize" << this->chunkSize << "Packet size" << packet.data.length(); - // Packet will arrive as stereo interleaved 16bit 48K - if (resampleRatio != 1.0) - { - quint32 outFrames = ((packet.data.length() / 2 / devChannels) * resampleRatio); - quint32 inFrames = (packet.data.length() / 2 / devChannels); - QByteArray outPacket((int)outFrames * 2 * devChannels, (char)0xff); - - const qint16* in = (qint16*)packet.data.constData(); - qint16* out = (qint16*)outPacket.data(); - - int err = 0; - err = wf_resampler_process_interleaved_int(resampler, in, &inFrames, out, &outFrames); - if (err) { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Resampler error " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; - } - //qInfo(logAudio()) << "Resampler run " << err << " inFrames:" << inFrames << " outFrames:" << outFrames; - //qInfo(logAudio()) << "Resampler run inLen:" << packet->datain.length() << " outLen:" << packet->dataout.length(); - packet.data.clear(); - packet.data = outPacket; // Copy output packet back to input buffer. - } - - //qDebug(logAudio()) << "Now resampled, length" << packet.data.length(); - - // Do we need to convert mono to stereo? - if (setup.radioChan == 1 && devChannels > 1) - { - // Strip out right channel? - QByteArray outPacket(packet.data.length()/2, (char)0xff); - const qint16* in = (qint16*)packet.data.constData(); - qint16* out = (qint16*)outPacket.data(); - for (int f = 0; f < outPacket.length()/2; f++) - { - *out++ = *in++; - in++; // Skip each even channel. - } - packet.data.clear(); - packet.data = outPacket; // Copy output packet back to input buffer. - } - - //qDebug(logAudio()) << "Now mono, length" << packet.data.length(); - - if (setup.codec == 0x40 || setup.codec == 0x80) - { - //Are we using the opus codec? - qint16* in = (qint16*)packet.data.data(); - - /* Encode the frame. */ - QByteArray outPacket(1275, (char)0xff); // Preset the output buffer size to MAXIMUM possible Opus frame size - unsigned char* out = (unsigned char*)outPacket.data(); - - int nbBytes = opus_encode(encoder, in, (setup.samplerate / 50), out, outPacket.length()); - if (nbBytes < 0) - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Opus encode failed:" << opus_strerror(nbBytes); - return; - } - else { - outPacket.resize(nbBytes); - packet.data.clear(); - packet.data = outPacket; // Replace incoming data with converted. - } - - } - else if (setup.bits == 8) - { - // Do we need to convert 16-bit to 8-bit? - QByteArray outPacket((int)packet.data.length() / 2, (char)0xff); - qint16* in = (qint16*)packet.data.data(); - for (int f = 0; f < outPacket.length(); f++) - { - qint16 sample = *in++; - if (setup.ulaw) { - int sign = (sample >> 8) & 0x80; - if (sign) - sample = (short)-sample; - if (sample > cClip) - sample = cClip; - sample = (short)(sample + cBias); - int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; - int mantissa = (sample >> (exponent + 3)) & 0x0F; - int compressedByte = ~(sign | (exponent << 4) | mantissa); - outPacket[f] = (quint8)compressedByte; - } - else { - int compressedByte = (((sample + 32768) >> 8) & 0xff); - outPacket[f] = (quint8)compressedByte; - } - } - packet.data.clear(); - packet.data = outPacket; // Copy output packet back to input buffer. - } - - ret = packet.data; - //qDebug(logAudio()) << "Now radio format, length" << packet.data.length(); - } - - - return; - + return static_cast(amplitude * 255.0); } -#if !defined (RTAUDIO) && !defined(PORTAUDIO) - -qint64 audioHandler::bytesAvailable() const -{ - return 0; -} - -bool audioHandler::isSequential() const -{ - return true; -} - -void audioHandler::notified() -{ -} - void audioHandler::stateChanged(QAudio::State state) { // Process the state switch (state) { - case QAudio::IdleState: - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in idle state: " << audioBuffer.size() << " packets in buffer"; - if (audioOutput != Q_NULLPTR && audioOutput->error() == QAudio::UnderrunError) - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "buffer underrun"; - //audioOutput->suspend(); - } - break; + case QAudio::IdleState: + { + isUnderrun = true; + if (underTimer->isActive()) { + underTimer->stop(); } - case QAudio::ActiveState: - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in active state: " << audioBuffer.size() << " packets in buffer"; - break; - } - case QAudio::SuspendedState: - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in suspended state: " << audioBuffer.size() << " packets in buffer"; - break; - } - case QAudio::StoppedState: - { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio now in stopped state: " << audioBuffer.size() << " packets in buffer"; - break; - } - default: { - qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Unhandled audio state: " << audioBuffer.size() << " packets in buffer"; + break; + } + case QAudio::ActiveState: + { + //qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Audio started!"; + if (!underTimer->isActive()) { + underTimer->start(500); } + break; + } + case QAudio::SuspendedState: + { + break; + } + case QAudio::StoppedState: + { + break; + } + default: { + } + break; } } -void audioHandler::stop() +void audioHandler::clearUnderrun() { - if (audioOutput != Q_NULLPTR && audioOutput->state() != QAudio::StoppedState) { - // Stop audio output - audioOutput->stop(); - this->stop(); - this->close(); - delete audioOutput; - audioOutput = Q_NULLPTR; - } - - if (audioInput != Q_NULLPTR && audioInput->state() != QAudio::StoppedState) { - // Stop audio output - audioInput->stop(); - this->stop(); - this->close(); - delete audioInput; - audioInput = Q_NULLPTR; - } - isInitialized = false; + isUnderrun = false; } - -#endif - - diff --git a/audiohandler.h b/audiohandler.h index 27d4704..f6d36fa 100644 --- a/audiohandler.h +++ b/audiohandler.h @@ -1,23 +1,20 @@ #ifndef AUDIOHANDLER_H #define AUDIOHANDLER_H +/* QT Headers */ #include - #include #include #include #include +#include +#include +#include +#include +#include -#if defined(RTAUDIO) -#ifdef Q_OS_WIN -#include "RtAudio.h" -#else -#include "rtaudio/RtAudio.h" -#endif -#elif defined (PORTAUDIO) -#include "portaudio.h" -//#error "PORTAUDIO is not currently supported" -#else +/* QT Audio Headers */ +#include #include #if QT_VERSION < 0x060000 @@ -32,205 +29,115 @@ #endif #include -#endif -typedef signed short MY_TYPE; -#define FORMAT RTAUDIO_SINT16 -#define SCALE 32767.0 -#define LOG100 4.60517018599 +/* wfview Packet types */ +#include "packettypes.h" -#include -#include -#include -#include - -#include "resampler/speex_resampler.h" -#include "ring/ring.h" - -#ifdef Q_OS_WIN -#include "opus.h" -#else -#include "opus/opus.h" -#endif +/* Logarithmic taper for volume control */ #include "audiotaper.h" -#include +/* Audio converter class*/ +#include "audioconverter.h" -//#define BUFFER_SIZE (32*1024) - -#define INTERNAL_SAMPLE_RATE 48000 #define MULAW_BIAS 33 #define MULAW_MAX 0x1fff -struct audioPacket { - quint32 seq; - QTime time; - quint16 sent; - QByteArray data; -}; - -struct audioSetup { - QString name; - quint8 bits; - quint8 radioChan; - quint16 samplerate; - quint16 latency; - quint8 codec; - bool ulaw; - bool isinput; -#if defined(RTAUDIO) || defined(PORTAUDIO) - int port; -#else - -#if QT_VERSION < 0x060000 - QAudioDeviceInfo port; -#else - QAudioDevice port; -#endif - -#endif - quint8 resampleQuality; - unsigned char localAFgain; -}; // For QtMultimedia, use a native QIODevice -#if !defined(PORTAUDIO) && !defined(RTAUDIO) -class audioHandler : public QIODevice -#else +//class audioHandler : public QIODevice class audioHandler : public QObject -#endif { Q_OBJECT public: - audioHandler(QObject* parent = 0); - ~audioHandler(); + audioHandler(QObject* parent = nullptr); + virtual ~audioHandler(); - int getLatency(); + virtual int getLatency(); -#if !defined (RTAUDIO) && !defined(PORTAUDIO) + virtual void start(); + virtual void stop(); - void start(); - void flush(); - void stop(); - qint64 bytesAvailable() const; - bool isSequential() const; -#endif - - void getNextAudioChunk(QByteArray &data); + virtual quint16 getAmplitude(); public slots: - bool init(audioSetup setup); - void changeLatency(const quint16 newSize); - void setVolume(unsigned char volume); - void incomingAudio(const audioPacket data); + virtual bool init(audioSetup setup); + virtual void changeLatency(const quint16 newSize); + virtual void setVolume(unsigned char volume); + virtual void incomingAudio(const audioPacket data); + virtual void convertedInput(audioPacket audio); + virtual void convertedOutput(audioPacket audio); private slots: -#if !defined (RTAUDIO) && !defined(PORTAUDIO) - void notified(); - void stateChanged(QAudio::State state); -#endif + virtual void stateChanged(QAudio::State state); + virtual void clearUnderrun(); + virtual void getNextAudioChunk(); signals: void audioMessage(QString message); void sendLatency(quint16 newSize); - void haveAudioData(const QByteArray& data); + void haveAudioData(const audioPacket& data); + void haveLevels(quint16 amplitude,quint16 latency,quint16 current,bool under,bool over); + void setupConverter(QAudioFormat in, QAudioFormat out, quint8 opus, quint8 resamp); + void sendToConverter(audioPacket audio); private: -#if defined(RTAUDIO) - int readData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); + //qint64 readData(char* data, qint64 nBytes); + //qint64 writeData(const char* data, qint64 nBytes); - static int staticRead(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { - return static_cast(userData)->readData(outputBuffer, inputBuffer, nFrames, streamTime, status); - } - int writeData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); - - static int staticWrite(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { - return static_cast(userData)->writeData(outputBuffer, inputBuffer, nFrames, streamTime, status); - } -#elif defined(PORTAUDIO) - int readData(const void* inputBuffer, void* outputBuffer, - unsigned long nFrames, - const PaStreamCallbackTimeInfo* streamTime, - PaStreamCallbackFlags status); - static int staticRead(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status, void* userData) { - return ((audioHandler*)userData)->readData(inputBuffer, outputBuffer, nFrames, streamTime, status); - } - - int writeData(const void* inputBuffer, void* outputBuffer, - unsigned long nFrames, - const PaStreamCallbackTimeInfo* streamTime, - PaStreamCallbackFlags status); - static int staticWrite(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status, void* userData) { - return ((audioHandler*)userData)->writeData(inputBuffer, outputBuffer, nFrames, streamTime, status); - } - -#else - qint64 readData(char* data, qint64 nBytes); - qint64 writeData(const char* data, qint64 nBytes); -#endif - - void reinit(); + bool isUnderrun = false; + bool isOverrun = false; bool isInitialized=false; bool isReady = false; + bool audioBuffered = false; -#if defined(RTAUDIO) - RtAudio* audio = Q_NULLPTR; - int audioDevice = 0; - RtAudio::StreamParameters aParams; - RtAudio::StreamOptions options; - RtAudio::DeviceInfo info; -#elif defined(PORTAUDIO) - PaStream* audio = Q_NULLPTR; - PaStreamParameters aParams; - const PaDeviceInfo *info; -#else - QAudioFormat format; + QAudioOutput* audioOutput=Q_NULLPTR; + QAudioInput* audioInput=Q_NULLPTR; + QIODevice* audioDevice=Q_NULLPTR; + QAudioFormat inFormat; + QAudioFormat outFormat; #if QT_VERSION < 0x060000 QAudioDeviceInfo deviceInfo; - QAudioOutput* audioOutput = Q_NULLPTR; - QAudioInput* audioInput = Q_NULLPTR; #else - QMediaDevices deviceInfo; - QAudioSink* audioOutput = Q_NULLPTR; - QAudioSource* audioInput = Q_NULLPTR; + QAudioDevice deviceInfo; #endif -#endif - SpeexResamplerState* resampler = Q_NULLPTR; + + audioConverter* converter=Q_NULLPTR; + QThread* converterThread = Q_NULLPTR; + QTime lastReceived; + //r8b::CFixedBuffer* resampBufs; + //r8b::CPtrKeeper* resamps; quint16 audioLatency; - unsigned int chunkSize; - bool chunkAvailable; quint32 lastSeq; quint32 lastSentSeq=0; + qint64 elapsedMs = 0; - quint16 nativeSampleRate=0; - quint8 radioSampleBits; - quint8 radioChannels; - - QMapaudioBuffer; + int delayedPackets=0; double resampleRatio; - wilt::Ring *ringBuf=Q_NULLPTR; - volatile bool ready = false; audioPacket tempBuf; quint16 currentLatency; - qreal volume=1.0; - int devChannels; + float amplitude=0.0; + qreal volume = 1.0; + audioSetup setup; - OpusEncoder* encoder=Q_NULLPTR; - OpusDecoder* decoder=Q_NULLPTR; + + OpusEncoder* encoder = Q_NULLPTR; + OpusDecoder* decoder = Q_NULLPTR; + QTimer* underTimer; }; + #endif // AUDIOHANDLER_H diff --git a/calibrationwindow.ui b/calibrationwindow.ui index bb68a7e..5d8228f 100644 --- a/calibrationwindow.ui +++ b/calibrationwindow.ui @@ -173,7 +173,7 @@ false - <html><head/><body><p>Load the calibration data fromthe indicated slot in the preference file. </p></body></html> + <html><head/><body><p>Load the calibration data from the indicated slot in the preference file. </p></body></html> Load diff --git a/commhandler.cpp b/commhandler.cpp index e678317..3b500ef 100644 --- a/commhandler.cpp +++ b/commhandler.cpp @@ -3,17 +3,11 @@ #include -// Copytight 2017-2020 Elliott H. Liggett +// Copyright 2017-2020 Elliott H. Liggett -commHandler::commHandler() +commHandler::commHandler(QObject* parent) : QObject(parent) { //constructor - // grab baud rate and other comm port details - // if they need to be changed later, please - // destroy this and create a new one. - port = new QSerialPort(); - - // TODO: The following should become arguments and/or functions // Add signal/slot everywhere for comm port setup. // Consider how to "re-setup" and how to save the state for next time. @@ -22,23 +16,21 @@ commHandler::commHandler() portName = "/dev/ttyUSB0"; this->PTTviaRTS = false; - setupComm(); // basic parameters - openPort(); - //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); - //port->setReadBufferSize(1024); // manually. 256 never saw any return from the radio. why... - //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); - - connect(port, SIGNAL(readyRead()), this, SLOT(receiveDataIn())); + init(); } -commHandler::commHandler(QString portName, quint32 baudRate) +commHandler::commHandler(QString portName, quint32 baudRate, quint8 wfFormat, QObject* parent) : QObject(parent) { //constructor // grab baud rate and other comm port details // if they need to be changed later, please // destroy this and create a new one. - port = new QSerialPort(); + + if (wfFormat == 1) { // Single waterfall packet + combineWf = true; + qDebug(logSerial()) << "*********** Combine Waterfall Mode Enabled!"; + } // TODO: The following should become arguments and/or functions // Add signal/slot everywhere for comm port setup. @@ -47,7 +39,19 @@ commHandler::commHandler(QString portName, quint32 baudRate) stopbits = 1; this->portName = portName; this->PTTviaRTS = false; + init(); +} + +void commHandler::init() +{ + if (port != Q_NULLPTR) { + delete port; + port = Q_NULLPTR; + isConnected = false; + } + + port = new QSerialPort(); setupComm(); // basic parameters openPort(); // qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); @@ -55,12 +59,17 @@ commHandler::commHandler(QString portName, quint32 baudRate) //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize(); connect(port, SIGNAL(readyRead()), this, SLOT(receiveDataIn())); + connect(port, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(handleError(QSerialPort::SerialPortError))); + lastDataReceived = QTime::currentTime(); } - commHandler::~commHandler() { - this->closePort(); + qInfo(logSerial()) << "Closing serial port: " << port->portName(); + if (isConnected) { + this->closePort(); + } + delete port; } void commHandler::setupComm() @@ -78,6 +87,13 @@ void commHandler::receiveDataFromUserToRig(const QByteArray &data) void commHandler::sendDataOut(const QByteArray &writeData) { + // Recycle port to attempt reconnection. + if (lastDataReceived.msecsTo(QTime::currentTime()) > 2000) { + qDebug(logSerial()) << "Serial port error? Attempting reconnect..."; + lastDataReceived = QTime::currentTime(); + QTimer::singleShot(500, this, SLOT(init())); + return; + } mutex.lock(); @@ -127,7 +143,7 @@ void commHandler::sendDataOut(const QByteArray &writeData) } bytesWritten = port->write(writeData); - + if(bytesWritten != (qint64)writeData.size()) { qDebug(logSerial()) << "bytesWritten: " << bytesWritten << " length of byte array: " << writeData.length()\ @@ -146,6 +162,7 @@ void commHandler::receiveDataIn() // because we know what constitutes a valid "frame" of data. // new code: + lastDataReceived = QTime::currentTime(); port->startTransaction(); inPortData = port->readAll(); @@ -165,7 +182,7 @@ void commHandler::receiveDataIn() } - if(inPortData.startsWith("\xFE\xFE")) + if (inPortData.startsWith("\xFE\xFE")) { if(inPortData.contains("\xFC")) { @@ -178,17 +195,74 @@ void commHandler::receiveDataIn() { // good! port->commitTransaction(); + + //payloadIn = data.right(data.length() - 4); + + // Do we need to combine waterfall into single packet? + int combined = 0; + if (combineWf) { + int pos = inPortData.indexOf(QByteArrayLiteral("\x27\x00\x00")); + int fdPos = inPortData.mid(pos).indexOf(QByteArrayLiteral("\xfd")); + //printHex(inPortData, false, true); + while (pos > -1 && fdPos > -1) { + combined++; + spectrumDivisionNumber = 0; + spectrumDivisionNumber = inPortData[pos + 3] & 0x0f; + spectrumDivisionNumber += ((inPortData[pos + 3] & 0xf0) >> 4) * 10; + + if (spectrumDivisionNumber == 1) { + // This is the first waveform data. + spectrumDivisionMax = 0; + spectrumDivisionMax = inPortData[pos + 4] & 0x0f; + spectrumDivisionMax += ((inPortData[pos + 4] & 0xf0) >> 4) * 10; + spectrumData.clear(); + spectrumData = inPortData.mid(pos - 4, fdPos+4); // Don't include terminating FD + spectrumData[8] = spectrumData[7]; // Make max = current; + //qDebug() << "New Spectrum seq:" << spectrumDivisionNumber << "pos = " << pos << "len" << fdPos; + + } + else if (spectrumDivisionNumber > lastSpectrum && spectrumDivisionNumber <= spectrumDivisionMax) { + spectrumData.insert(spectrumData.length(), inPortData.mid(pos + 4, fdPos-5)); + //qDebug() << "Added spectrum seq:" << spectrumDivisionNumber << "len" << fdPos-5; + //printHex(inPortData.mid((pos+4),fdPos - (pos+5)), false, true); + } + else { + qDebug() << "Invalid Spectrum Division received" << spectrumDivisionNumber << "last Spectrum" << lastSpectrum; + } + + lastSpectrum = spectrumDivisionNumber; + + if (spectrumDivisionNumber == spectrumDivisionMax) { + //qDebug() << "Got Spectrum! length=" << spectrumData.length(); + spectrumData.append("\xfd"); // Need to add FD on the end. + //printHex(spectrumData, false, true); + emit haveDataFromPort(spectrumData); + lastSpectrum = 0; + } + inPortData = inPortData.remove(pos-4, fdPos+5); + pos = inPortData.indexOf(QByteArrayLiteral("\x27\x00\x00")); + fdPos = inPortData.mid(pos).indexOf(QByteArrayLiteral("\xfd")); + } + // If we still have data left, let the main function deal with it, any spectrum data has been removed + if (inPortData.length() == 0) + { + return; + } + // qDebug() << "Got extra data!"; + //printHex(inPortData, false, true); + } + // emit haveDataFromPort(inPortData); if(rolledBack) { - // qInfo(logSerial()) << "Rolled back and was successfull. Length: " << inPortData.length(); + // qInfo(logSerial()) << "Rolled back and was successful. Length: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = false; } } else { // did not receive the entire thing so roll back: - // qInfo(logSerial()) << "Rolling back transaction. End not detected. Lenth: " << inPortData.length(); + // qInfo(logSerial()) << "Rolling back transaction. End not detected. Length: " << inPortData.length(); //printHex(inPortData, false, true); port->rollbackTransaction(); rolledBack = true; @@ -230,7 +304,6 @@ void commHandler::setUseRTSforPTT(bool PTTviaRTS) void commHandler::openPort() { bool success; - // port->open(); success = port->open(QIODevice::ReadWrite); if(success) { @@ -253,7 +326,7 @@ void commHandler::closePort() if(port) { port->close(); - delete port; + //delete port; } isConnected = false; } @@ -299,3 +372,17 @@ void commHandler::printHex(const QByteArray &pdata, bool printVert, bool printHo } +void commHandler::handleError(QSerialPort::SerialPortError err) +{ + switch (err) { + case QSerialPort::NoError: + break; + default: + if (lastDataReceived.msecsTo(QTime::currentTime()) > 2000) { + qDebug(logSerial()) << "Serial port" << port->portName() << "Error, attempting disconnect/reconnect"; + lastDataReceived = QTime::currentTime(); + QTimer::singleShot(500, this, SLOT(init())); + } + break; + } +} diff --git a/commhandler.h b/commhandler.h index c76e23f..acfc8be 100644 --- a/commhandler.h +++ b/commhandler.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include // This class abstracts the comm port in a useful way and connects to // the command creator and command parser. @@ -15,8 +17,8 @@ class commHandler : public QObject Q_OBJECT public: - commHandler(); - commHandler(QString portName, quint32 baudRate); + commHandler(QObject* parent = nullptr); + commHandler(QString portName, quint32 baudRate, quint8 wfFormat,QObject* parent = nullptr); bool serialError; bool rtsStatus(); @@ -25,6 +27,8 @@ public: public slots: void setUseRTSforPTT(bool useRTS); void setRTS(bool rtsOn); + void handleError(QSerialPort::SerialPortError error); + void init(); private slots: void receiveDataIn(); // from physical port @@ -58,7 +62,7 @@ private: unsigned char buffer[256]; QString portName; - QSerialPort *port; + QSerialPort *port=Q_NULLPTR; qint32 baudrate; unsigned char stopbits; bool rolledBack; @@ -74,7 +78,15 @@ private: bool isConnected; // port opened mutable QMutex mutex; void printHex(const QByteArray &pdata, bool printVert, bool printHoriz); - + bool combineWf = false; + QByteArray spectrumData; + quint8 spectrumDivisionNumber; + quint8 spectrumDivisionMax; + quint8 spectrumCenterOrFixed; + quint8 spectrumInformation; + quint8 spectrumOutOfRange; + quint8 lastSpectrum = 0; + QTime lastDataReceived; }; #endif // COMMHANDLER_H diff --git a/freqmemory.cpp b/freqmemory.cpp index 9a137c0..660a8f8 100644 --- a/freqmemory.cpp +++ b/freqmemory.cpp @@ -1,7 +1,7 @@ #include "freqmemory.h" #include "logcategories.h" -// Copytight 2017-2020 Elliott H. Liggett +// Copyright 2017-2020 Elliott H. Liggett freqMemory::freqMemory() { diff --git a/keyboard.cpp b/keyboard.cpp new file mode 100644 index 0000000..f6f4b15 --- /dev/null +++ b/keyboard.cpp @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +#include "keyboard.h" + +keyboard::keyboard(void) +{ +} + +keyboard::~keyboard(void) +{ +} + +void keyboard::run() +{ + while (true) + { + char key = getchar(); + if (key == 'q') { + QCoreApplication::quit(); + } + } + return; +} \ No newline at end of file diff --git a/keyboard.h b/keyboard.h new file mode 100644 index 0000000..1fab712 --- /dev/null +++ b/keyboard.h @@ -0,0 +1,11 @@ +#include +#include +#include +class keyboard : public QThread +{ + Q_OBJECT +public: + keyboard(void); + ~keyboard(void); + void run(); +}; diff --git a/logcategories.cpp b/logcategories.cpp index 96d0a97..da4c6d7 100644 --- a/logcategories.cpp +++ b/logcategories.cpp @@ -8,3 +8,5 @@ Q_LOGGING_CATEGORY(logAudio, "audio") Q_LOGGING_CATEGORY(logUdp, "udp") Q_LOGGING_CATEGORY(logUdpServer, "udp.server") Q_LOGGING_CATEGORY(logRigCtlD, "rigctld") +Q_LOGGING_CATEGORY(logTcpServer, "tcpserver") +Q_LOGGING_CATEGORY(logAudioConverter, "audioconverter") diff --git a/logcategories.h b/logcategories.h index db6bfbb..7c33a6a 100644 --- a/logcategories.h +++ b/logcategories.h @@ -11,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logAudio) Q_DECLARE_LOGGING_CATEGORY(logUdp) Q_DECLARE_LOGGING_CATEGORY(logUdpServer) Q_DECLARE_LOGGING_CATEGORY(logRigCtlD) +Q_DECLARE_LOGGING_CATEGORY(logTcpServer) +Q_DECLARE_LOGGING_CATEGORY(logAudioConverter) #if defined(Q_OS_WIN) && !defined(__PRETTY_FUNCTION__) diff --git a/main.cpp b/main.cpp index bae6e35..8e945ae 100644 --- a/main.cpp +++ b/main.cpp @@ -1,25 +1,68 @@ +#ifdef BUILD_WFSERVER +#include +#include "keyboard.h" +#else #include +#endif + +#ifdef Q_OS_WIN +#include +#include +#endif + #include #include "wfmain.h" #include "logcategories.h" -// Copytight 2017-2021 Elliott H. Liggett +// Copyright 2017-2021 Elliott H. Liggett // Smart pointer to log file QScopedPointer m_logFile; QMutex logMutex; bool debugMode=false; +#ifdef BUILD_WFSERVER + servermain* w=Q_NULLPTR; + + #ifdef Q_OS_WIN + bool __stdcall cleanup(DWORD sig) + #else + static void cleanup(int sig) + #endif + { + Q_UNUSED(sig) + qDebug() << "Exiting via SIGNAL"; + if (w!=Q_NULLPTR) w->deleteLater(); + QCoreApplication::quit(); + + #ifdef Q_OS_WIN + return true; + #else + return; + #endif + } + +#endif + void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); int main(int argc, char *argv[]) { - QApplication a(argc, argv); - //a.setStyle( "Fusion" ); +#ifdef BUILD_WFSERVER + QCoreApplication a(argc, argv); + a.setOrganizationName("wfview"); + a.setOrganizationDomain("wfview.org"); + a.setApplicationName("wfserver"); + keyboard* kb = new keyboard(); + kb->start(); +#else + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication a(argc, argv); a.setOrganizationName("wfview"); a.setOrganizationDomain("wfview.org"); a.setApplicationName("wfview"); +#endif #ifdef QT_DEBUG debugMode = true; @@ -38,12 +81,19 @@ int main(int argc, char *argv[]) const QString helpText = QString("\nUsage: -p --port /dev/port, -h --host remotehostname, -c --civ 0xAddr, -l --logfile filename.log, -s --settings filename.ini, -d --debug, -v --version\n"); // TODO... +#ifdef BUILD_WFSERVER + const QString version = QString("wfserver version: %1 (Git:%2 on %3 at %4 by %5@%6)\nOperating System: %7 (%8)\nBuild Qt Version %9. Current Qt Version: %10\n") + .arg(QString(WFVIEW_VERSION)) + .arg(GITSHORT).arg(__DATE__).arg(__TIME__).arg(UNAME).arg(HOST) + .arg(QSysInfo::prettyProductName()).arg(QSysInfo::buildCpuArchitecture()) + .arg(QT_VERSION_STR).arg(qVersion()); +#else const QString version = QString("wfview version: %1 (Git:%2 on %3 at %4 by %5@%6)\nOperating System: %7 (%8)\nBuild Qt Version %9. Current Qt Version: %10\n") - .arg(QString(WFVIEW_VERSION)) - .arg(GITSHORT).arg(__DATE__).arg(__TIME__).arg(UNAME).arg(HOST) - .arg(QSysInfo::prettyProductName()).arg(QSysInfo::buildCpuArchitecture()) - .arg(QT_VERSION_STR).arg(qVersion()); - + .arg(QString(WFVIEW_VERSION)) + .arg(GITSHORT).arg(__DATE__).arg(__TIME__).arg(UNAME).arg(HOST) + .arg(QSysInfo::prettyProductName()).arg(QSysInfo::buildCpuArchitecture()) + .arg(QT_VERSION_STR).arg(qVersion()); +#endif for(int c=1; c +#endif + + +paHandler::paHandler(QObject* parent) +{ + Q_UNUSED(parent) +} + +paHandler::~paHandler() +{ + + if (converterThread != Q_NULLPTR) { + converterThread->quit(); + converterThread->wait(); + } + + if (isInitialized) { + Pa_StopStream(audio); + Pa_CloseStream(audio); + } + +} + +bool paHandler::init(audioSetup setup) +{ + if (isInitialized) { + return false; + } + + this->setup = setup; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "PortAudio handler starting:" << setup.name; + + if (setup.portInt == -1) + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "No audio device was found."; + return false; + } + + inFormat = toQAudioFormat(setup.codec, setup.sampleRate); + + qDebug(logAudio()) << "Creating" << (setup.isinput ? "Input" : "Output") << "audio device:" << setup.name << + ", bits" << inFormat.sampleSize() << + ", codec" << setup.codec << + ", latency" << setup.latency << + ", localAFGain" << setup.localAFgain << + ", radioChan" << inFormat.channelCount() << + ", resampleQuality" << setup.resampleQuality << + ", samplerate" << inFormat.sampleRate() << + ", uLaw" << setup.ulaw; + + PaError err; +#ifdef Q_OS_WIN + CoInitialize(0); +#endif + + //err = Pa_Initialize(); + //if (err != paNoError) + //{ + // qDebug(logAudio()) << "Portaudio initialized"; + //} + + memset(&aParams, 0, sizeof(PaStreamParameters)); + + aParams.device = setup.portInt; + info = Pa_GetDeviceInfo(aParams.device); + + qDebug(logAudio()) << "PortAudio" << (setup.isinput ? "Input" : "Output") << setup.portInt << "Input Channels" << info->maxInputChannels << "Output Channels" << info->maxOutputChannels; + + + if (setup.isinput) { + outFormat.setChannelCount(info->maxInputChannels); + } + else { + outFormat.setChannelCount(info->maxOutputChannels); + } + + aParams.suggestedLatency = (float)setup.latency / 1000.0f; + outFormat.setSampleRate(info->defaultSampleRate); + aParams.sampleFormat = paFloat32; + outFormat.setSampleSize(32); + outFormat.setSampleType(QAudioFormat::Float); + outFormat.setByteOrder(QAudioFormat::LittleEndian); + outFormat.setCodec("audio/pcm"); + + + if (outFormat.channelCount() > 2) { + outFormat.setChannelCount(2); + } + else if (outFormat.channelCount() < 1) + { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "No channels found, aborting setup."; + return false; + } + + if (inFormat.channelCount() < outFormat.channelCount()) { + outFormat.setChannelCount(inFormat.channelCount()); + } + + aParams.channelCount = outFormat.channelCount(); + + if (outFormat.sampleRate() < 44100) { + outFormat.setSampleRate(48000); + } + + + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Selected format: SampleSize" << outFormat.sampleSize() << "Channel Count" << outFormat.channelCount() << + "Sample Rate" << outFormat.sampleRate() << "Codec" << outFormat.codec() << "Sample Type" << outFormat.sampleType(); + + // We "hopefully" now have a valid format that is supported so try connecting + + converter = new audioConverter(); + converterThread = new QThread(this); + if (setup.isinput) { + converterThread->setObjectName("audioConvIn()"); + } + else { + converterThread->setObjectName("audioConvOut()"); + } + converter->moveToThread(converterThread); + + connect(this, SIGNAL(setupConverter(QAudioFormat, QAudioFormat, quint8, quint8)), converter, SLOT(init(QAudioFormat, QAudioFormat, quint8, quint8))); + connect(converterThread, SIGNAL(finished()), converter, SLOT(deleteLater())); + connect(this, SIGNAL(sendToConverter(audioPacket)), converter, SLOT(convert(audioPacket))); + converterThread->start(QThread::TimeCriticalPriority); + + aParams.hostApiSpecificStreamInfo = NULL; + + // Per channel chunk size. + this->chunkSize = (outFormat.bytesForDuration(setup.blockSize * 1000) / sizeof(float)) * outFormat.channelCount(); + + // Check the format is supported + + + if (setup.isinput) { + err = Pa_IsFormatSupported(&aParams, NULL, outFormat.sampleRate()); + } + else + { + err = Pa_IsFormatSupported(NULL,&aParams, outFormat.sampleRate()); + } + + if (err != paNoError) { + if (err == paInvalidChannelCount) + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Unsupported channel count" << aParams.channelCount; + if (aParams.channelCount == 2) { + aParams.channelCount = 1; + outFormat.setChannelCount(1); + } + else { + aParams.channelCount = 2; + outFormat.setChannelCount(2); + } + } + else if (err == paInvalidSampleRate) + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Unsupported sample rate" << outFormat.sampleRate(); + outFormat.setSampleRate(44100); + } + else if (err == paSampleFormatNotSupported) + { + aParams.sampleFormat = paInt16; + outFormat.setSampleType(QAudioFormat::SignedInt); + outFormat.setSampleSize(16); + } + + if (setup.isinput) { + err = Pa_IsFormatSupported(&aParams, NULL, outFormat.sampleRate()); + } + else + { + err = Pa_IsFormatSupported(NULL, &aParams, outFormat.sampleRate()); + } + if (err != paNoError) { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot find suitable format, aborting:" << Pa_GetErrorText(err); + return false; + } + } + + if (setup.isinput) { + + err = Pa_OpenStream(&audio, &aParams, 0, outFormat.sampleRate(), this->chunkSize, paNoFlag, &paHandler::staticWrite, (void*)this); + emit setupConverter(outFormat, inFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedInput(audioPacket))); + } + else { + err = Pa_OpenStream(&audio, 0, &aParams, outFormat.sampleRate(), this->chunkSize, paNoFlag, NULL, NULL); + emit setupConverter(inFormat, outFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedOutput(audioPacket))); + } + + if (err == paNoError) { + err = Pa_StartStream(audio); + } + if (err == paNoError) { + isInitialized = true; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; + } + else { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "failed to open device" << Pa_GetErrorText(err); + } + + this->setVolume(setup.localAFgain); + + return isInitialized; +} + + +void paHandler::setVolume(unsigned char volume) +{ + +#ifdef Q_OS_WIN + this->volume = audiopot[volume] * 5; +#else + this->volume = audiopot[volume]; +#endif + + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "setVolume: " << volume << "(" << this->volume << ")"; +} + +void paHandler::incomingAudio(audioPacket packet) +{ + packet.volume = volume; + emit sendToConverter(packet); + return; +} + + + +int paHandler::writeData(const void* inputBuffer, void* outputBuffer, + unsigned long nFrames, const PaStreamCallbackTimeInfo * streamTime, + PaStreamCallbackFlags status) +{ + Q_UNUSED(outputBuffer); + Q_UNUSED(streamTime); + Q_UNUSED(status); + audioPacket packet; + packet.time = QTime::currentTime(); + packet.sent = 0; + packet.volume = volume; + memcpy(&packet.guid, setup.guid, GUIDLEN); + packet.data.append((char*)inputBuffer, nFrames*inFormat.channelCount()*sizeof(float)); + emit sendToConverter(packet); + + if (status == paInputUnderflow) { + isUnderrun = true; + } + else if (status == paInputOverflow) { + isOverrun = true; + } + else + { + isUnderrun = false; + isOverrun = false; + } + + return paContinue; +} + + +void paHandler::convertedOutput(audioPacket packet) { + + if (packet.data.size() > 0) { + + if (Pa_IsStreamActive(audio) == 1) { + PaError err = Pa_WriteStream(audio, (char*)packet.data.data(), packet.data.size() / sizeof(float) / outFormat.channelCount()); + + if (err != paNoError) { + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Error writing audio!"; + } + const PaStreamInfo* info = Pa_GetStreamInfo(audio); + currentLatency = packet.time.msecsTo(QTime::currentTime()) + (info->outputLatency * 1000); + } + + amplitude = packet.amplitude; + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); + } +} + + + +void paHandler::convertedInput(audioPacket packet) +{ + if (packet.data.size() > 0) { + emit haveAudioData(packet); + amplitude = packet.amplitude; + const PaStreamInfo* info = Pa_GetStreamInfo(audio); + currentLatency = packet.time.msecsTo(QTime::currentTime()) + (info->inputLatency * 1000); + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); + } +} + + + +void paHandler::changeLatency(const quint16 newSize) +{ + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Changing latency to: " << newSize << " from " << setup.latency; +} + +int paHandler::getLatency() +{ + return currentLatency; +} + + +quint16 paHandler::getAmplitude() +{ + return static_cast(amplitude * 255.0); +} diff --git a/pahandler.h b/pahandler.h new file mode 100644 index 0000000..a8fac05 --- /dev/null +++ b/pahandler.h @@ -0,0 +1,96 @@ +#ifndef PAHANDLER_H +#define PAHANDLER_H + +#include +#include +#include + +#include "portaudio.h" + +#include +#include +#include + + +/* wfview Packet types */ +#include "packettypes.h" + +/* Logarithmic taper for volume control */ +#include "audiotaper.h" + +#include "audiohandler.h" + +/* Audio converter class*/ +#include "audioconverter.h" + +#include + + +class paHandler : public audioHandler +{ + Q_OBJECT + +public: + paHandler(QObject* parent = 0); + ~paHandler(); + + int getLatency(); + + + void getNextAudioChunk(QByteArray& data); + quint16 getAmplitude(); + +public slots: + bool init(audioSetup setup); + void changeLatency(const quint16 newSize); + void setVolume(unsigned char volume); + void convertedInput(audioPacket audio); + void convertedOutput(audioPacket audio); + void incomingAudio(const audioPacket data); + + +private slots: + +signals: + void audioMessage(QString message); + void sendLatency(quint16 newSize); + void haveAudioData(const audioPacket& data); + void haveLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void setupConverter(QAudioFormat in, QAudioFormat out, quint8 opus, quint8 resamp); + void sendToConverter(audioPacket audio); + +private: + + int writeData(const void* inputBuffer, void* outputBuffer, + unsigned long nFrames, + const PaStreamCallbackTimeInfo* streamTime, + PaStreamCallbackFlags status); + static int staticWrite(const void* inputBuffer, void* outputBuffer, unsigned long nFrames, const PaStreamCallbackTimeInfo* streamTime, PaStreamCallbackFlags status, void* userData) { + return ((paHandler*)userData)->writeData(inputBuffer, outputBuffer, nFrames, streamTime, status); + } + + bool isInitialized = false; + PaStream* audio = Q_NULLPTR; + PaStreamParameters aParams; + const PaDeviceInfo* info; + + quint16 audioLatency; + unsigned int chunkSize; + + quint32 lastSeq; + quint32 lastSentSeq = 0; + + quint16 currentLatency; + float amplitude=0.0; + qreal volume = 1.0; + + audioSetup setup; + QAudioFormat inFormat; + QAudioFormat outFormat; + audioConverter* converter = Q_NULLPTR; + QThread* converterThread = Q_NULLPTR; + bool isUnderrun = false; + bool isOverrun = false; +}; + +#endif // PAHANDLER_H diff --git a/pttyhandler.cpp b/pttyhandler.cpp index bea880d..60ea0ae 100644 --- a/pttyhandler.cpp +++ b/pttyhandler.cpp @@ -13,7 +13,7 @@ // Copyright 2017-2021 Elliott H. Liggett & Phil Taylor -pttyHandler::pttyHandler(QString pty) +pttyHandler::pttyHandler(QString pty, QObject* parent) : QObject(parent) { //constructor if (pty == "" || pty.toLower() == "none") @@ -214,12 +214,12 @@ void pttyHandler::receiveDataIn(int fd) { if (civId == 0 && inPortData.length() > lastFE + 2 && (quint8)inPortData[lastFE + 2] > (quint8)0xdf && (quint8)inPortData[lastFE + 2] < (quint8)0xef) { // This is (should be) the remotes CIV id. civId = (quint8)inPortData[lastFE + 2]; - qInfo(logSerial()) << "pty detected remote CI-V:" << hex << civId; + qInfo(logSerial()) << "pty detected remote CI-V:" << QString("0x%1").arg(civId,0,16); } else if (civId != 0 && inPortData.length() > lastFE + 2 && (quint8)inPortData[lastFE + 2] != civId) { civId = (quint8)inPortData[lastFE + 2]; - qInfo(logSerial()) << "pty remote CI-V changed:" << hex << (quint8)civId; + qInfo(logSerial()) << "pty remote CI-V changed:" << QString("0x%1").arg((quint8)civId,0,16); } // filter C-IV transceive command before forwarding on. if (inPortData.contains(rigCaps.transceiveCommand)) @@ -245,14 +245,14 @@ void pttyHandler::receiveDataIn(int fd) { if (rolledBack) { - // qInfo(logSerial()) << "Rolled back and was successfull. Length: " << inPortData.length(); + // qInfo(logSerial()) << "Rolled back and was successful. Length: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = false; } } else { // did not receive the entire thing so roll back: - // qInfo(logSerial()) << "Rolling back transaction. End not detected. Lenth: " << inPortData.length(); + // qInfo(logSerial()) << "Rolling back transaction. End not detected. Length: " << inPortData.length(); //printHex(inPortData, false, true); rolledBack = true; #ifdef Q_OS_WIN diff --git a/pttyhandler.h b/pttyhandler.h index 73aa80e..d056e77 100644 --- a/pttyhandler.h +++ b/pttyhandler.h @@ -19,7 +19,7 @@ class pttyHandler : public QObject Q_OBJECT public: - pttyHandler(QString portName); + explicit pttyHandler(QString portName, QObject* parent = nullptr); pttyHandler(QString portName, quint32 baudRate); bool serialError; @@ -62,7 +62,7 @@ private: bool rolledBack; int ptfd; // pseudo-terminal file desc. - int ptKeepAlive=0; // Used to keep the pty alive after client disconects. + int ptKeepAlive=0; // Used to keep the pty alive after client disconnects. bool havePt; QString ptDevSlave; diff --git a/repeatersetup.cpp b/repeatersetup.cpp index 2439f15..78c3d4a 100644 --- a/repeatersetup.cpp +++ b/repeatersetup.cpp @@ -25,7 +25,7 @@ repeaterSetup::repeaterSetup(QWidget *parent) : repeaterSetup::~repeaterSetup() { - // Trying this for more consistant destruction + // Trying this for more consistent destruction rig.inputs.clear(); rig.preamps.clear(); rig.attenuators.clear(); diff --git a/resampler/resample.c b/resampler/resample.c index 5144434..cab633c 100644 --- a/resampler/resample.c +++ b/resampler/resample.c @@ -104,7 +104,7 @@ static void speex_free(void* ptr) { free(ptr); } #include "resample_neon.h" #endif -/* Numer of elements to allocate on the stack */ +/* Number of elements to allocate on the stack */ #ifdef VAR_ARRAYS #define FIXED_STACK_ALLOC 8192 #else diff --git a/rigcommander.cpp b/rigcommander.cpp index d497c0d..c0d3ea6 100644 --- a/rigcommander.cpp +++ b/rigcommander.cpp @@ -4,7 +4,7 @@ #include "rigidentities.h" #include "logcategories.h" -// Copytight 2017-2020 Elliott H. Liggett +// Copyright 2017-2020 Elliott H. Liggett // This file parses data from the radio and also forms commands to the radio. // The radio physical interface is handled by the commHandler() instance "comm" @@ -20,18 +20,27 @@ // Note: When sending \x00, must use QByteArray.setRawData() -rigCommander::rigCommander() +rigCommander::rigCommander(QObject* parent) : QObject(parent) { - state.set(SCOPEFUNC,true,false); + qInfo(logRig()) << "creating instance of rigCommander()"; + state.set(SCOPEFUNC, true, false); +} + +rigCommander::rigCommander(quint8 guid[GUIDLEN], QObject* parent) : QObject(parent) +{ + qInfo(logRig()) << "creating instance of rigCommander()"; + state.set(SCOPEFUNC, true, false); + memcpy(this->guid, guid, GUIDLEN); } rigCommander::~rigCommander() { + qInfo(logRig()) << "closing instance of rigCommander()"; closeComm(); } -void rigCommander::commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate, QString vsp) +void rigCommander::commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate, QString vsp,quint16 tcpPort, quint8 wf) { // construct // TODO: Bring this parameter and the comm port from the UI. @@ -41,23 +50,37 @@ void rigCommander::commSetup(unsigned char rigCivAddr, QString rigSerialPort, qu civAddr = rigCivAddr; // address of the radio. Decimal is 148. usingNativeLAN = false; + //qInfo(logRig()) << "Opening connection to Rig:" << QString("0x%1").arg((unsigned char)rigCivAddr,0,16) << "on serial port" << rigSerialPort << "at baud rate" << rigBaudRate; // --- setup(); // --- this->rigSerialPort = rigSerialPort; this->rigBaudRate = rigBaudRate; + rigCaps.baudRate = rigBaudRate; + + comm = new commHandler(rigSerialPort, rigBaudRate,wf,this); + ptty = new pttyHandler(vsp,this); + + if (tcpPort > 0) { + tcp = new tcpServer(this); + tcp->startServer(tcpPort); + } - comm = new commHandler(rigSerialPort, rigBaudRate); - ptty = new pttyHandler(vsp); // data from the comm port to the program: connect(comm, SIGNAL(haveDataFromPort(QByteArray)), this, SLOT(handleNewData(QByteArray))); // data from the ptty to the rig: connect(ptty, SIGNAL(haveDataFromPort(QByteArray)), comm, SLOT(receiveDataFromUserToRig(QByteArray))); + // data from the program to the comm port: connect(this, SIGNAL(dataForComm(QByteArray)), comm, SLOT(receiveDataFromUserToRig(QByteArray))); + if (tcpPort > 0) { + // data from the tcp port to the rig: + connect(tcp, SIGNAL(receiveData(QByteArray)), comm, SLOT(receiveDataFromUserToRig(QByteArray))); + connect(comm, SIGNAL(haveDataFromPort(QByteArray)), tcp, SLOT(sendData(QByteArray))); + } connect(this, SIGNAL(toggleRTS(bool)), comm, SLOT(setRTS(bool))); // data from the rig to the ptty: @@ -76,7 +99,7 @@ void rigCommander::commSetup(unsigned char rigCivAddr, QString rigSerialPort, qu } -void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp) +void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp, quint16 tcpPort) { // construct // TODO: Bring this parameter and the comm port from the UI. @@ -85,16 +108,17 @@ void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, aud // civAddr = 0x94; // address of the radio. Decimal is 148. civAddr = rigCivAddr; // address of the radio. Decimal is 148. usingNativeLAN = true; - - // --- + // -- setup(); // --- + if (udp == Q_NULLPTR) { udp = new udpHandler(prefs,rxSetup,txSetup); udpHandlerThread = new QThread(this); + udpHandlerThread->setObjectName("udpHandler()"); udp->moveToThread(udpHandlerThread); @@ -108,8 +132,12 @@ void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, aud //this->rigSerialPort = rigSerialPort; //this->rigBaudRate = rigBaudRate; - ptty = new pttyHandler(vsp); + ptty = new pttyHandler(vsp,this); + if (tcpPort > 0) { + tcp = new tcpServer(this); + tcp->startServer(tcpPort); + } // Data from UDP to the program connect(udp, SIGNAL(haveDataFromPort(QByteArray)), this, SLOT(handleNewData(QByteArray))); @@ -125,19 +153,28 @@ void rigCommander::commSetup(unsigned char rigCivAddr, udpPreferences prefs, aud // data from the ptty to the rig: connect(ptty, SIGNAL(haveDataFromPort(QByteArray)), udp, SLOT(receiveDataFromUserToRig(QByteArray))); + if (tcpPort > 0) { + // data from the tcp port to the rig: + connect(tcp, SIGNAL(receiveData(QByteArray)), udp, SLOT(receiveDataFromUserToRig(QByteArray))); + connect(udp, SIGNAL(haveDataFromPort(QByteArray)), tcp, SLOT(sendData(QByteArray))); + } + connect(this, SIGNAL(haveChangeLatency(quint16)), udp, SLOT(changeLatency(quint16))); connect(this, SIGNAL(haveSetVolume(unsigned char)), udp, SLOT(setVolume(unsigned char))); connect(udp, SIGNAL(haveBaudRate(quint32)), this, SLOT(receiveBaudRate(quint32))); // Connect for errors/alerts connect(udp, SIGNAL(haveNetworkError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); - connect(udp, SIGNAL(haveNetworkStatus(QString)), this, SLOT(handleStatusUpdate(QString))); + connect(udp, SIGNAL(haveNetworkStatus(networkStatus)), this, SLOT(handleStatusUpdate(networkStatus))); connect(ptty, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(handleSerialPortError(QString, QString))); connect(this, SIGNAL(getMoreDebug()), ptty, SLOT(debugThis())); connect(this, SIGNAL(discoveredRigID(rigCapabilities)), ptty, SLOT(receiveFoundRigID(rigCapabilities))); + connect(udp, SIGNAL(requestRadioSelection(QList)), this, SLOT(radioSelection(QList))); + connect(udp, SIGNAL(setRadioUsage(quint8, quint8, QString, QString)), this, SLOT(radioUsage(quint8, quint8, QString, QString))); + connect(this, SIGNAL(selectedRadio(quint8)), udp, SLOT(setCurrentRadio(quint8))); emit haveAfGain(rxSetup.localAFgain); localVolume = rxSetup.localAFgain; } @@ -203,9 +240,9 @@ void rigCommander::handleSerialPortError(const QString port, const QString error emit haveSerialPortError(port, errorText); } -void rigCommander::handleStatusUpdate(const QString text) +void rigCommander::handleStatusUpdate(const networkStatus status) { - emit haveStatusUpdate(text); + emit haveStatusUpdate(status); } bool rigCommander::usingLAN() @@ -214,6 +251,7 @@ bool rigCommander::usingLAN() } void rigCommander::receiveBaudRate(quint32 baudrate) { + rigCaps.baudRate = baudrate; emit haveBaudRate(baudrate); } @@ -1407,7 +1445,7 @@ void rigCommander::parseLevels() case '\x0A': // TX RF level emit haveTxPower(level); - state.set(TXPOWER, level, false); + state.set(RFPOWER, level, false); break; case '\x0B': // Mic Gain @@ -1456,6 +1494,9 @@ void rigCommander::parseLevels() { switch(payloadIn[1]) { + case '\x01': + // noise or s-meter sequelch status + break; case '\x02': // S-Meter emit haveMeter(meterS, level); @@ -1466,6 +1507,9 @@ void rigCommander::parseLevels() emit haveMeter(meterCenter, level); state.set(SMETER, level, false); break; + case '\x05': + // Various squelch (tone etc.) + break; case '\x11': // RF-Power meter emit haveMeter(meterPower, level); @@ -3492,6 +3536,34 @@ void rigCommander::determineRigCaps() createMode(modeCW, 0x03, "CW"), createMode(modeCW_R, 0x07, "CW-R"), }; break; + case model746: + rigCaps.modelName = QString("IC-746"); + rigCaps.rigctlModel = 3023; + rigCaps.hasSpectrum = false; + rigCaps.inputs.clear(); + rigCaps.hasLan = false; + rigCaps.hasEthernet = false; + rigCaps.hasWiFi = false; + rigCaps.hasFDcomms = false; + rigCaps.hasATU = true; + rigCaps.hasTBPF = true; + rigCaps.hasIFShift = true; + rigCaps.hasCTCSS = true; + rigCaps.hasDTCS = true; + rigCaps.hasAntennaSel = true; + rigCaps.preamps.push_back('\x01'); + rigCaps.preamps.push_back('\x02'); + rigCaps.attenuators.insert(rigCaps.attenuators.end(),{ '\x20'}); + // There are two HF and VHF ant, 12-01 adn 12-02 select the HF, the VHF is auto selected + // this incorrectly shows up as 2 and 3 in the drop down. + rigCaps.antennas = {0x01, 0x02}; + rigCaps.bands = standardHF; + rigCaps.bands.push_back(band2m); + rigCaps.bands.push_back(bandGen); + rigCaps.modes = commonModes; + rigCaps.transceiveCommand = QByteArrayLiteral("\x1a\x05\x00\x00"); + break; + case model756pro: rigCaps.modelName = QString("IC-756 Pro"); rigCaps.rigctlModel = 3027; @@ -3609,6 +3681,9 @@ void rigCommander::determineRigCaps() } haveRigCaps = true; + // Copy received guid so we can recognise this radio. + memcpy(rigCaps.guid, this->guid, GUIDLEN); + if(!usingNativeLAN) { if(useRTSforPTT_isSet) @@ -3629,7 +3704,7 @@ void rigCommander::determineRigCaps() payloadPrefix.append(civAddr); payloadPrefix.append((char)compCivAddr); // if there is a compile-time error, remove the following line, the "hex" part is the issue: - qInfo(logRig()) << "Using incomingCIVAddr: (int): " << this->civAddr << " hex: " << hex << this->civAddr; + qInfo(logRig()) << "Using incomingCIVAddr: (int): " << this->civAddr << " hex: " << QString("0x%1").arg(this->civAddr,0,16); emit discoveredRigID(rigCaps); } else { if(!foundRig) @@ -3730,7 +3805,7 @@ void rigCommander::parseSpectrum() spectrumEndFreq = fEnd.MHzDouble; if(scopeMode == spectModeCenter) { - // "center" mode, start is actuall center, end is bandwidth. + // "center" mode, start is actual center, end is bandwidth. spectrumStartFreq -= spectrumEndFreq; spectrumEndFreq = spectrumStartFreq + 2*(spectrumEndFreq); // emit haveSpectrumCenterSpan(span); @@ -4029,7 +4104,7 @@ void rigCommander::getPreamp() void rigCommander::getAntenna() { - // This one might neet some thought + // This one might need some thought // as it seems each antenna has to be checked. // Maybe 0x12 alone will do it. QByteArray payload("\x12"); @@ -4273,6 +4348,19 @@ void rigCommander::sendState() emit stateInfo(&state); } +void rigCommander::radioSelection(QList radios) +{ + emit requestRadioSelection(radios); +} + +void rigCommander::radioUsage(quint8 radio, quint8 busy, QString user, QString ip) { + emit setRadioUsage(radio, busy, user, ip); +} + +void rigCommander::setCurrentRadio(quint8 radio) { + emit selectedRadio(radio); +} + void rigCommander::stateUpdated() { // A remote process has updated the rigState @@ -4397,9 +4485,9 @@ void rigCommander::stateUpdated() } getSql(); break; - case TXPOWER: + case RFPOWER: if (i.value()._valid) { - setTxPower(state.getChar(TXPOWER)); + setTxPower(state.getChar(RFPOWER)); } getTxLevel(); break; @@ -4539,6 +4627,7 @@ void rigCommander::stateUpdated() break; // All meters can only be updated from the rig end. case SMETER: + case SWRMETER: case POWERMETER: case ALCMETER: case COMPMETER: @@ -4655,14 +4744,17 @@ void rigCommander::printHex(const QByteArray &pdata, bool printVert, bool printH void rigCommander::dataFromServer(QByteArray data) { - //qInfo(logRig()) << "emit dataForComm()"; + //qInfo(logRig()) << "***************** emit dataForComm()" << data; emit dataForComm(data); } +quint8* rigCommander::getGUID() { + return guid; +} + - diff --git a/rigcommander.h b/rigcommander.h index 6da1744..7921b24 100644 --- a/rigcommander.h +++ b/rigcommander.h @@ -12,6 +12,7 @@ #include "rigidentities.h" #include "repeaterattributes.h" #include "freqmemory.h" +#include "tcpserver.h" #include "rigstate.h" @@ -68,15 +69,18 @@ class rigCommander : public QObject Q_OBJECT public: - rigCommander(); + explicit rigCommander(QObject* parent=nullptr); + explicit rigCommander(quint8 guid[GUIDLEN], QObject* parent = nullptr); ~rigCommander(); bool usingLAN(); + quint8* getGUID(); + public slots: void process(); - void commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp); - void commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp); + void commSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate, QString vsp, quint16 tcp, quint8 wf); + void commSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp, quint16 tcp); void closeComm(); void stateUpdated(); void setRTSforPTT(bool enabled); @@ -272,7 +276,10 @@ public slots: void sayAll(); // Housekeeping: - void handleStatusUpdate(const QString text); + void handleStatusUpdate(const networkStatus status); + void radioSelection(QList radios); + void radioUsage(quint8 radio, quint8 busy, QString name, QString ip); + void setCurrentRadio(quint8 radio); void sendState(); void getDebug(); @@ -280,7 +287,7 @@ signals: // Communication: void commReady(); void haveSerialPortError(const QString port, const QString errorText); - void haveStatusUpdate(const QString text); + void haveStatusUpdate(const networkStatus status); void dataForComm(const QByteArray &outData); void toggleRTS(bool rtsOn); @@ -365,6 +372,9 @@ signals: void stateInfo(rigstate* state); // Housekeeping: + void requestRadioSelection(QList radios); + void setRadioUsage(quint8 radio, quint8 busy, QString user, QString ip); + void selectedRadio(quint8 radio); void getMoreDebug(); void finished(); @@ -416,6 +426,7 @@ private: commHandler* comm = Q_NULLPTR; pttyHandler* ptty = Q_NULLPTR; + tcpServer* tcp = Q_NULLPTR; udpHandler* udp=Q_NULLPTR; QThread* udpHandlerThread = Q_NULLPTR; @@ -469,7 +480,7 @@ private: QString serialPortError; unsigned char localVolume=0; - + quint8 guid[GUIDLEN] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; }; diff --git a/rigctld.cpp b/rigctld.cpp index 0c32bda..6b81eaf 100644 --- a/rigctld.cpp +++ b/rigctld.cpp @@ -199,7 +199,7 @@ void rigCtlClient::socketReadyRead() response.append("1"); // rigctld protocol version response.append(QString("%1").arg(rigCaps.rigctlModel)); response.append("0"); // Print something - bandType lastBand; + bandType lastBand=(bandType)-1; for (bandType band : rigCaps.bands) { if (band != lastBand) @@ -710,7 +710,7 @@ void rigCtlClient::socketReadyRead() resp.append(QString("%1").arg((float)rigState->getChar(ANTIVOXGAIN) / 255.0)); } else if (command[1] == "RFPOWER") { - resp.append(QString("%1").arg((float)rigState->getChar(TXPOWER) / 255.0)); + resp.append(QString("%1").arg((float)rigState->getChar(RFPOWER) / 255.0)); } else if (command[1] == "PREAMP") { resp.append(QString("%1").arg(rigState->getChar(PREAMP)*10)); @@ -736,6 +736,10 @@ void rigCtlClient::socketReadyRead() value = command[2].toFloat() * 255; rigState->set(RFGAIN, value, true); } + else if (command[1] == "RFPOWER") { + value = command[2].toFloat() * 255; + rigState->set(RFPOWER, value, true); + } else if (command[1] == "SQL") { value = command[2].toFloat() * 255; rigState->set(SQUELCH, value, true); diff --git a/rigctld.h b/rigctld.h index d86818d..2018576 100644 --- a/rigctld.h +++ b/rigctld.h @@ -326,7 +326,7 @@ class rigCtlD : public QTcpServer Q_OBJECT public: - explicit rigCtlD(QObject *parent=Q_NULLPTR); + explicit rigCtlD(QObject *parent=nullptr); virtual ~rigCtlD(); int startServer(qint16 port); diff --git a/rigidentities.cpp b/rigidentities.cpp index a48a44d..62ea5aa 100644 --- a/rigidentities.cpp +++ b/rigidentities.cpp @@ -1,7 +1,7 @@ #include "rigidentities.h" #include "logcategories.h" -// Copytight 2017-2021 Elliott H. Liggett +// Copyright 2017-2021 Elliott H. Liggett model_kind determineRadioModel(unsigned char rigID) { @@ -58,8 +58,8 @@ model_kind determineRadioModel(unsigned char rigID) case model736: rig = model736; break; - case model910h: - rig = model910h; + case model746: + rig = model746; break; case model756pro: rig = model756pro; @@ -70,6 +70,9 @@ model_kind determineRadioModel(unsigned char rigID) case model756proiii: rig = model756proiii; break; + case model910h: + rig = model910h; + break; case model9100: rig = model9100; break; diff --git a/rigidentities.h b/rigidentities.h index cae1b75..d238a08 100644 --- a/rigidentities.h +++ b/rigidentities.h @@ -7,6 +7,7 @@ #include #include "freqmemory.h" +#include "packettypes.h" // Credit for parts of CIV list: // http://www.docksideradio.com/Icom%20Radio%20Hex%20Addresses.htm @@ -30,6 +31,7 @@ enum model_kind { model706 = 0x58, model718 = 0x5E, model736 = 0x40, + model746 = 0x56, model756pro = 0x5C, model756proii = 0x64, model756proiii = 0x6E, @@ -138,6 +140,8 @@ struct rigCapabilities { std::vector modes; QByteArray transceiveCommand; + quint8 guid[GUIDLEN] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; + quint32 baudRate; }; diff --git a/rigstate.h b/rigstate.h index 8722f9e..13c66cf 100644 --- a/rigstate.h +++ b/rigstate.h @@ -13,7 +13,7 @@ // Meters at the end as they are ALWAYS updated from the rig! enum stateTypes { VFOAFREQ, VFOBFREQ, CURRENTVFO, PTT, MODE, FILTER, DUPLEX, DATAMODE, ANTENNA, RXANTENNA, CTCSS, TSQL, DTCS, CSQL, - PREAMP, AGC, ATTENUATOR, MODINPUT, AFGAIN, RFGAIN, SQUELCH, TXPOWER, MICGAIN, COMPLEVEL, MONITORLEVEL, VOXGAIN, ANTIVOXGAIN, + PREAMP, AGC, ATTENUATOR, MODINPUT, AFGAIN, RFGAIN, SQUELCH, RFPOWER, MICGAIN, COMPLEVEL, MONITORLEVEL, VOXGAIN, ANTIVOXGAIN, FAGCFUNC, NBFUNC, COMPFUNC, VOXFUNC, TONEFUNC, TSQLFUNC, SBKINFUNC, FBKINFUNC, ANFFUNC, NRFUNC, AIPFUNC, APFFUNC, MONFUNC, MNFUNC,RFFUNC, AROFUNC, MUTEFUNC, VSCFUNC, REVFUNC, SQLFUNC, ABMFUNC, BCFUNC, MBCFUNC, RITFUNC, AFCFUNC, SATMODEFUNC, SCOPEFUNC, NBLEVEL, NBDEPTH, NBWIDTH, NRLEVEL, RIGINPUT, POWERONOFF, RITVALUE, @@ -124,4 +124,4 @@ private: QMutex _mutex; }; -#endif \ No newline at end of file +#endif diff --git a/ring/LICENSE b/ring/LICENSE deleted file mode 100644 index b9a4d39..0000000 --- a/ring/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Trevor Wilson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/ring/README.md b/ring/README.md deleted file mode 100644 index 2cdbbe9..0000000 --- a/ring/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Ring Library - -## Overview - -This library provides source for a multi-producer multi-consumer lock-free ring buffer. It provides a very simple interface for writing and reading from the buffer. The source includes a `Ring_` class, that provides the raw implementation and C-like facilities, as well as a templated `Ring` class for typed reads and writes. - - -## Contact - -If you have any questions, concerns, or recommendations please feel free to e-mail me at kmdreko@gmail.com. If you notice a bug or defect, create an issue to report it. diff --git a/ring/ring.cpp b/ring/ring.cpp deleted file mode 100644 index d50555d..0000000 --- a/ring/ring.cpp +++ /dev/null @@ -1,290 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// FILE: ring.cpp -// DATE: 2016-02-25 -// AUTH: Trevor Wilson -// DESC: Implements a lock-free, multi-consumer, multi-producer ring buffer -// class - -//////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2016 Trevor Wilson -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#include "ring.h" -using namespace wilt; - -#include -// - std::memcpy - -Ring_::Ring_() - : beg_(nullptr) - , end_(nullptr) -{ - std::atomic_init(&used_, static_cast(0)); - std::atomic_init(&free_, static_cast(0)); - std::atomic_init(&rbuf_, static_cast(0)); - std::atomic_init(&rptr_, static_cast(0)); - std::atomic_init(&wptr_, static_cast(0)); - std::atomic_init(&wbuf_, static_cast(0)); -} - -Ring_::Ring_(std::size_t size) - : beg_(new char[size]) - , end_(beg_ + size) -{ - std::atomic_init(&used_, static_cast(0)); - std::atomic_init(&free_, static_cast(size)); - std::atomic_init(&rbuf_, beg_); - std::atomic_init(&rptr_, beg_); - std::atomic_init(&wptr_, beg_); - std::atomic_init(&wbuf_, beg_); -} - -Ring_::Ring_(Ring_&& ring) - : beg_(ring.beg_) - , end_(ring.end_) -{ - std::atomic_init(&used_, ring.used_.load()); - std::atomic_init(&free_, ring.free_.load()); - std::atomic_init(&rbuf_, ring.rbuf_.load()); - std::atomic_init(&rptr_, ring.rptr_.load()); - std::atomic_init(&wptr_, ring.wptr_.load()); - std::atomic_init(&wbuf_, ring.wbuf_.load()); - - ring.beg_ = nullptr; - ring.end_ = nullptr; - - ring.used_.store(0); - ring.free_.store(0); - ring.rbuf_.store(nullptr); - ring.rptr_.store(nullptr); - ring.wptr_.store(nullptr); - ring.wbuf_.store(nullptr); -} - -Ring_& Ring_::operator= (Ring_&& ring) -{ - delete[] beg_; - - beg_ = ring.beg_; - end_ = ring.end_; - - used_.store(ring.used_.load()); - free_.store(ring.free_.load()); - rbuf_.store(ring.rbuf_.load()); - rptr_.store(ring.rptr_.load()); - wptr_.store(ring.wptr_.load()); - wbuf_.store(ring.wbuf_.load()); - - ring.beg_ = nullptr; - ring.end_ = nullptr; - - ring.used_.store(0); - ring.free_.store(0); - ring.rbuf_.store(nullptr); - ring.rptr_.store(nullptr); - ring.wptr_.store(nullptr); - ring.wbuf_.store(nullptr); - - return *this; -} - -Ring_::~Ring_() -{ - delete[] beg_; -} - -std::size_t Ring_::size() const -{ - // The 'used' space can be negative in an over-reserved case, but it can be - // clamped to 0 for simplicity. - - auto s = used_.load(); - return s < 0 ? 0 : static_cast(s); -} - -std::size_t Ring_::capacity() const -{ - return static_cast(end_ - beg_); -} - -void Ring_::read(void* data, std::size_t length) noexcept -{ - auto block = acquire_read_block_(length); - - copy_read_block_(block, (char*)data, length); - release_read_block_(block, length); -} - -void Ring_::write(const void* data, std::size_t length) noexcept -{ - auto block = acquire_write_block_(length); - - copy_write_block_(block, (const char*)data, length); - release_write_block_(block, length); -} - -bool Ring_::try_read(void* data, std::size_t length) noexcept -{ - auto block = try_acquire_read_block_(length); - if (block == nullptr) - return false; - - copy_read_block_(block, (char*)data, length); - release_read_block_(block, length); - - return true; -} - -bool Ring_::try_write(const void* data, std::size_t length) noexcept -{ - auto block = try_acquire_write_block_(length); - if (block == nullptr) - return false; - - copy_write_block_(block, (const char*)data, length); - release_write_block_(block, length); - - return true; -} - -char* Ring_::normalize_(char* ptr) -{ - return ptr < end_ ? ptr : ptr - capacity(); -} - -char* Ring_::acquire_read_block_(std::size_t length) -{ - auto size = static_cast(length); - while (true) // loop while conflict - { - auto old_rptr = rptr_.load(std::memory_order_consume); // read rptr - while (used_.load(std::memory_order_consume) < size) // check for data - ; // spin until success - - auto new_rptr = normalize_(old_rptr + size); // get block end - used_.fetch_sub(size); // reserve - if (rptr_.compare_exchange_strong(old_rptr, new_rptr)) // try commit - return old_rptr; // committed - - used_.fetch_add(size, std::memory_order_relaxed); // un-reserve - } -} - -char* Ring_::try_acquire_read_block_(std::size_t length) -{ - auto size = static_cast(length); - while (true) // loop while conflict - { - auto old_rptr = rptr_.load(std::memory_order_consume); // read rptr - if (used_.load(std::memory_order_consume) < size) // check for data - return nullptr; // return failure - - auto new_rptr = normalize_(old_rptr + size); // get block end - used_.fetch_sub(size); // reserve - if (rptr_.compare_exchange_strong(old_rptr, new_rptr)) // try commit - return old_rptr; // committed - - used_.fetch_add(size, std::memory_order_relaxed); // un-reserve - } -} - -void Ring_::copy_read_block_(const char* block, char* data, std::size_t length) -{ - if (block + length < end_) - { - std::memcpy(data, block, length); - } - else - { - auto first = end_ - block; - std::memcpy(data, block, first); - std::memcpy(data + first, beg_, length - first); - } -} - -void Ring_::release_read_block_(char* old_rptr, std::size_t length) -{ - auto new_rptr = normalize_(old_rptr + length); // get block end - while (rbuf_.load() != old_rptr) // check for earlier reads - ; // spin until reads complete - - rbuf_.store(new_rptr); // finish commit - free_.fetch_add(length, std::memory_order_relaxed); // add to free space -} - -char* Ring_::acquire_write_block_(std::size_t length) -{ - auto size = static_cast(length); - while (true) // loop while conflict - { - auto old_wbuf = wbuf_.load(std::memory_order_consume); // read wbuf - while (free_.load(std::memory_order_consume) < size) // check for space - ; // spin until success - - auto new_wbuf = normalize_(old_wbuf + size); // get block end - free_.fetch_sub(size); // reserve - if (wbuf_.compare_exchange_strong(old_wbuf, new_wbuf)) // try commit - return old_wbuf; // committed - - free_.fetch_add(size, std::memory_order_relaxed); // un-reserve - } -} - -char* Ring_::try_acquire_write_block_(std::size_t length) -{ - auto size = static_cast(length); - while (true) // loop while conflict - { - auto old_wbuf = wbuf_.load(std::memory_order_consume); // read wbuf - if (free_.load(std::memory_order_consume) < size) // check for space - return nullptr; // return failure - - auto new_wbuf = normalize_(old_wbuf + size); // get block end - free_.fetch_sub(size); // reserve - if (wbuf_.compare_exchange_strong(old_wbuf, new_wbuf)) // try commit - return old_wbuf; // committed - - free_.fetch_add(size, std::memory_order_relaxed); // un-reserve - } -} - -void Ring_::copy_write_block_(char* block, const char* data, std::size_t length) -{ - if (block + length < end_) - { - std::memcpy(block, data, length); - } - else - { - auto first = end_ - block; - std::memcpy(block, data, first); - std::memcpy(beg_, data + first, length - first); - } -} - -void Ring_::release_write_block_(char* old_wbuf, std::size_t length) -{ - auto new_wbuf = normalize_(old_wbuf + length); // get block end - while (wptr_.load() != old_wbuf) // wait for earlier writes - ; // spin until writes complete - - wptr_.store(new_wbuf); // finish commit - used_.fetch_add(length, std::memory_order_relaxed); // add to used space -} diff --git a/ring/ring.h b/ring/ring.h deleted file mode 100644 index cec5852..0000000 --- a/ring/ring.h +++ /dev/null @@ -1,440 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// FILE: ring.h -// DATE: 2016-02-25 -// AUTH: Trevor Wilson -// DESC: Defines a lock-free, multi-consumer, multi-producer ring buffer class - -//////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2016 Trevor Wilson -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#ifndef WILT_RING_H -#define WILT_RING_H - -#include -// - std::atomic -#include -// - std::size_t -// - std::ptrdiff_t -#include -// - ::new(ptr) -#include -// - std::is_nothrow_copy_constructible -// - std::is_nothrow_move_constructible -// - std::is_nothrow_move_assignable -// - std::is_nothrow_destructible -#include -// - std::move - -namespace wilt -{ - ////////////////////////////////////////////////////////////////////////////// - // This structure aims to access elements in a ring buffer from multiple - // concurrent readers and writers in a lock-free manner. - // - // The class works by allocating the array and storing two pointers (for the - // beginning and end of the allocated space). Two atomic pointers are used to - // track the beginning and end of the currently used storage space. To - // facilitate concurrent reads and writes, theres a read buffer pointer before - // the read pointer for data currently being read, and a corresponding write - // buffer pointer beyond the write pointer for data currently being written. - // These buffer pointers cannot overlap. Just using these pointers suffer from - // some minute inefficiencies and a few ABA problems. Therfore, atomic - // integers are used to store the currently used and currently free sizes. - // - // It allows multiple readers and multiple writers by implementing a reserve- - // commit system. A thread wanting to read will check the used size to see if - // there's enough data. If there is, it subtracts from the used size to - // 'reserve' the read. It then does a compare-exchange to 'commit' by - // increasing the read pointer. If that fails, then it backs out ('un- - // reserves') by adding back to the used size and tries again. If it - // succeeds, then it proceeds to read the data. In order to complete, the - // reader must update the read buffer pointer to where it just finished - // reading from. However, because other readers that started before may not be - // done yet, the reader must wait until the read buffer pointer points to - // where the read started. Only, then is the read buffer pointer updated, and - // the free size increased. So while this implementation is lock-free, it is - // not wait-free. This same principle works the same when writing (ammended - // for the appropriate pointers). - // - // If two readers try to read at the same time and there is only enough data - // for one of them. The used size MAY be negative because they both 'reserve' - // the data. This is an over-reserved state. But the compare-exchange will - // only allow one reader to 'commit' to the read and the other will 'un- - // reserve' the read. - // - // |beg |rptr used=5 |wbuf - unused - // |----|----|++++|====|====|====|====|====|++++|----| + modifying - // free=3 |rbuf |wptr |end = used - // - // The diagram above shows a buffer of size 10 storing 5 bytes with a reader - // reading one byte and one writer reading one byte. - // - // Out of the box, the class works by reading and writing raw bytes from POD - // data types and arrays. A wrapper could allow for a nicer interface for - // pushing and popping elements. As it stands, this structure cannot be easily - // modified to store types of variable size. - - class Ring_ - { - private: - //////////////////////////////////////////////////////////////////////////// - // TYPE DEFINITIONS - //////////////////////////////////////////////////////////////////////////// - - typedef char* data_ptr; - typedef std::atomic size_type; - typedef std::atomic atom_ptr; - - private: - //////////////////////////////////////////////////////////////////////////// - // PRIVATE MEMBERS - //////////////////////////////////////////////////////////////////////////// - // Beginning and end pointers don't need to be atomic because they don't - // change. used_ and free_ can be negative in certain cases (and that's ok). - - data_ptr beg_; // pointer to beginning of data block - data_ptr end_; // pointer to end of data block - - alignas(64) - size_type used_; // size of unreserved used space - - alignas(64) - size_type free_; // size of unreserved free space - - alignas(64) - atom_ptr rbuf_; // pointer to beginning of data being read - atom_ptr rptr_; // pointer to beginning of data - - alignas(64) - atom_ptr wptr_; // pointer to end of data - atom_ptr wbuf_; // pointer to end of data being written - - public: - //////////////////////////////////////////////////////////////////////////// - // CONSTRUCTORS AND DESTRUCTORS - //////////////////////////////////////////////////////////////////////////// - - // Constructs a ring without a buffer (capacity() == 0) - Ring_(); - - // Constructs a ring with a buffer with a size - Ring_(std::size_t size); - - // Moves the buffer between rings, assumes no concurrent operations - Ring_(Ring_&& ring); - - // Moves the buffer between rings, assumes no concurrent operations on - // either ring. Deallocates the buffer - Ring_& operator= (Ring_&& ring); - - // No copying - Ring_(const Ring_&) = delete; - Ring_& operator= (const Ring_&) = delete; - - // Deallocates the buffer - ~Ring_(); - - public: - //////////////////////////////////////////////////////////////////////////// - // QUERY FUNCTIONS - //////////////////////////////////////////////////////////////////////////// - // Functions only report on the state of the ring - - // Returns the current amount of non-reserved used space (amount of written - // data that a read hasn't yet reserved). Over-reserved scenarios mean this - // number is not the ultimate source of truth with concurrent operations, - // but its the closest safe approximation. This, of course, doesn't report - // writes that have not completed. - std::size_t size() const; - - // Maximum amount of data that can be held - std::size_t capacity() const; - - public: - //////////////////////////////////////////////////////////////////////////// - // ACCESSORS AND MODIFIERS - //////////////////////////////////////////////////////////////////////////// - // All operations assume object has not been moved. Blocking operations run - // until operation is completed. Non-blocking operations fail if there is - // not enough space - - void read(void* data, std::size_t length) noexcept; - void write(const void* data, std::size_t length) noexcept; - bool try_read(void* data, std::size_t length) noexcept; - bool try_write(const void* data, std::size_t length) noexcept; - - protected: - //////////////////////////////////////////////////////////////////////////// - // PROTECTED FUNCTIONS - //////////////////////////////////////////////////////////////////////////// - // Helper functions - - // Wraps a pointer within the array. Assumes 'beg_ <= ptr < end_+capacity()' - char* normalize_(char*); - - char* acquire_read_block_(std::size_t length); - char* try_acquire_read_block_(std::size_t length); - void copy_read_block_(const char* block, char* data, std::size_t length); - void release_read_block_(char* block, std::size_t length); - - char* acquire_write_block_(std::size_t length); - char* try_acquire_write_block_(std::size_t length); - void copy_write_block_(char* block, const char* data, std::size_t length); - void release_write_block_(char* block, std::size_t length); - - char* begin_alloc_() { return beg_; } - const char* begin_alloc_() const { return beg_; } - char* end_alloc_() { return end_; } - const char* end_alloc_() const { return end_; } - char* begin_data_() { return rptr_; } - const char* begin_data_() const { return rptr_; } - char* end_data_() { return wptr_; } - const char* end_data_() const { return wptr_; } - - }; // class Ring_ - - template - class Ring : protected Ring_ - { - public: - //////////////////////////////////////////////////////////////////////////// - // CONSTRUCTORS AND DESTRUCTORS - //////////////////////////////////////////////////////////////////////////// - - // Constructs a ring without a buffer (capacity() == 0) - Ring(); - - // Constructs a ring with a buffer with a size - Ring(std::size_t size); - - // Moves the buffer between rings, assumes no concurrent operations - Ring(Ring&& ring); - - // Moves the buffer between rings, assumes no concurrent operations on - // either ring. Deallocates the buffer - Ring& operator= (Ring&& ring); - - // No copying - Ring(const Ring_&) = delete; - Ring& operator= (const Ring_&) = delete; - - // Deallocates the buffer, destructs stored data. - ~Ring(); - - public: - //////////////////////////////////////////////////////////////////////////// - // QUERY FUNCTIONS - //////////////////////////////////////////////////////////////////////////// - // Functions only report on the state of the ring - - // Returns the current amount of non-reserved used space (amount of written - // data that a read hasn't yet reserved). Over-reserved scenarios mean this - // number is not the ultimate source of truth with concurrent operations, - // but its the closest safe approximation. This, of course, doesn't report - // writes that have not completed. - std::size_t size() const; - - // Maximum amount of data that can be held - std::size_t capacity() const; - - public: - //////////////////////////////////////////////////////////////////////////// - // ACCESSORS AND MODIFIERS - //////////////////////////////////////////////////////////////////////////// - // All operations assume object has not been moved. Blocking operations run - // until operation is completed. Non-blocking operations fail if there is - // not enough space - - void read(T& data) noexcept; // blocking read - void write(const T& data) noexcept; // blocking write - void write(T&& data) noexcept; // blocking write - bool try_read(T& data) noexcept; // non-blocking read - bool try_write(const T& data) noexcept; // non-blocking write - bool try_write(T&& data) noexcept; // non-blocking write - - private: - //////////////////////////////////////////////////////////////////////////// - // PRIVATE HELPER FUNCTIONS - //////////////////////////////////////////////////////////////////////////// - - void destruct_(); - - }; // class Ring - - template - Ring::Ring() - : Ring_() - { } - - template - Ring::Ring(std::size_t size) - : Ring_(size * sizeof(T)) - { } - - template - Ring::Ring(Ring&& ring) - : Ring_(std::move(ring)) - { } - - template - Ring& Ring::operator= (Ring&& ring) - { - destruct_(); - - Ring_::operator= (ring); - - return *this; - } - - template - Ring::~Ring() - { - destruct_(); - } - - template - void Ring::destruct_() - { - if (size() == 0) - return; - - auto itr = begin_data_(); - auto end = end_data_(); - do - { - auto t = reinterpret_cast(itr); - t->~T(); - - itr = normalize_(itr + sizeof(T)); - } while (itr != end); - } - - template - std::size_t Ring::size() const - { - return Ring_::size() / sizeof(T); - } - - template - std::size_t Ring::capacity() const - { - return Ring_::capacity() / sizeof(T); - } - - template - void Ring::read(T& data) noexcept - { - static_assert(std::is_nothrow_move_assignable::value, "T move assignment must not throw"); - static_assert(std::is_nothrow_destructible::value, "T destructor must not throw"); - - auto block = acquire_read_block_(sizeof(T)); - - // critical section - auto t = reinterpret_cast(block); - data = std::move(*t); - t->~T(); - - release_read_block_(block, sizeof(T)); - } - - template - void Ring::write(const T& data) noexcept - { - static_assert(std::is_nothrow_copy_constructible::value, "T copy constructor must not throw"); - - auto block = acquire_write_block_(sizeof(T)); - - // critical section - new(block) T(data); - - release_write_block_(block, sizeof(T)); - } - - template - void Ring::write(T&& data) noexcept - { - static_assert(std::is_nothrow_move_constructible::value, "T move constructor must not throw"); - - auto block = acquire_write_block_(sizeof(T)); - - // critical section - new(block) T(std::move(data)); - - release_write_block_(block, sizeof(T)); - } - - template - bool Ring::try_read(T& data) noexcept - { - static_assert(std::is_nothrow_move_assignable::value, "T move assignment must not throw"); - static_assert(std::is_nothrow_destructible::value, "T destructor must not throw"); - - auto block = try_acquire_read_block_(sizeof(T)); - if (block == nullptr) - return false; - - // critical section - auto t = reinterpret_cast(block); - data = std::move(*t); - t->~T(); - - release_read_block_(block, sizeof(T)); - - return true; - } - - template - bool Ring::try_write(const T& data) noexcept - { - static_assert(std::is_nothrow_copy_constructible::value, "T copy constructor must not throw"); - - auto block = try_acquire_write_block_(sizeof(T)); - if (block == nullptr) - return false; - - // critical section - new(block) T(data); - - release_write_block_(block, sizeof(T)); - - return true; - } - - template - bool Ring::try_write(T&& data) noexcept - { - static_assert(std::is_nothrow_move_constructible::value, "T move constructor must not throw"); - - auto block = try_acquire_write_block_(sizeof(T)); - if (block == nullptr) - return false; - - // critical section - new(block) T(std::move(data)); - - release_write_block_(block, sizeof(T)); - - return true; - } - -} // namespace wilt - -#endif // !WILT_RING_H \ No newline at end of file diff --git a/rthandler.cpp b/rthandler.cpp new file mode 100644 index 0000000..f848c38 --- /dev/null +++ b/rthandler.cpp @@ -0,0 +1,367 @@ +#include "rthandler.h" + +#include "logcategories.h" + +#if defined(Q_OS_WIN) +#include +#endif + +#define RT_EXCEPTION + +rtHandler::rtHandler(QObject* parent) +{ + Q_UNUSED(parent) +} + +rtHandler::~rtHandler() +{ + + if (converterThread != Q_NULLPTR) { + converterThread->quit(); + converterThread->wait(); + } + + if (isInitialized) { + +#ifdef RT_EXCEPTION + try { +#endif + audio->abortStream(); + audio->closeStream(); +#ifdef RT_EXCEPTION + } + catch (RtAudioError& e) { + qInfo(logAudio()) << "Error closing stream:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); + } +#endif + delete audio; + + } + +} + +bool rtHandler::init(audioSetup setup) +{ + if (isInitialized) { + return false; + } + + this->setup = setup; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "RTAudio handler starting:" << setup.name; + + if (setup.portInt==-1) + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "No audio device was found."; + return false; + } + + inFormat = toQAudioFormat(setup.codec, setup.sampleRate); + + qDebug(logAudio()) << "Creating" << (setup.isinput ? "Input" : "Output") << "audio device:" << setup.name << + ", bits" << inFormat.sampleSize() << + ", codec" << setup.codec << + ", latency" << setup.latency << + ", localAFGain" << setup.localAFgain << + ", radioChan" << inFormat.channelCount() << + ", resampleQuality" << setup.resampleQuality << + ", samplerate" << inFormat.sampleRate() << + ", uLaw" << setup.ulaw; + +#if !defined(Q_OS_MACX) + options.flags = ((!RTAUDIO_HOG_DEVICE) | (RTAUDIO_MINIMIZE_LATENCY)); + //options.flags = RTAUDIO_MINIMIZE_LATENCY; +#endif + +#if defined(Q_OS_LINUX) + audio = new RtAudio(RtAudio::Api::LINUX_ALSA); +#elif defined(Q_OS_WIN) + audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); +#elif defined(Q_OS_MACX) + audio = new RtAudio(RtAudio::Api::MACOSX_CORE); +#endif + + options.numberOfBuffers = int(setup.latency/setup.blockSize); + + if (setup.portInt > 0) { + aParams.deviceId = setup.portInt; + } + else if (setup.isinput) { + aParams.deviceId = audio->getDefaultInputDevice(); + } + else { + aParams.deviceId = audio->getDefaultOutputDevice(); + } + aParams.firstChannel = 0; + +#ifdef RT_EXCEPTION + try { +#endif + info = audio->getDeviceInfo(aParams.deviceId); +#ifdef RT_EXCEPTION + } + catch (RtAudioError e) { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Device exception:" << aParams.deviceId << ":" << QString::fromStdString(e.getMessage()); + goto errorHandler; + } +#endif + if (info.probed) + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") successfully probed"; + + RtAudioFormat sampleFormat; + outFormat.setByteOrder(QAudioFormat::LittleEndian); + outFormat.setCodec("audio/pcm"); + + if (info.nativeFormats == 0) + { + qCritical(logAudio()) << " No natively supported data formats!"; + goto errorHandler; + } + else { + qDebug(logAudio()) << " Supported formats:" << + (info.nativeFormats & RTAUDIO_SINT8 ? "8-bit int," : "") << + (info.nativeFormats & RTAUDIO_SINT16 ? "16-bit int," : "") << + (info.nativeFormats & RTAUDIO_SINT24 ? "24-bit int," : "") << + (info.nativeFormats & RTAUDIO_SINT32 ? "32-bit int," : "") << + (info.nativeFormats & RTAUDIO_FLOAT32 ? "32-bit float," : "") << + (info.nativeFormats & RTAUDIO_FLOAT64 ? "64-bit float," : ""); + + qInfo(logAudio()) << " Preferred sample rate:" << info.preferredSampleRate; + if (setup.isinput) { + outFormat.setChannelCount(info.inputChannels); + } + else { + outFormat.setChannelCount(info.outputChannels); + } + + qInfo(logAudio()) << " Channels:" << outFormat.channelCount(); + + if (outFormat.channelCount() > 2) { + outFormat.setChannelCount(2); + } + else if (outFormat.channelCount() < 1) + { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "No channels found, aborting setup."; + goto errorHandler; + } + + aParams.nChannels = outFormat.channelCount(); + + + outFormat.setSampleRate(info.preferredSampleRate); + + if (outFormat.sampleRate() < 44100) { + outFormat.setSampleRate(48000); + } + + if (info.nativeFormats & RTAUDIO_FLOAT32) { + outFormat.setSampleType(QAudioFormat::Float); + outFormat.setSampleSize(32); + sampleFormat = RTAUDIO_FLOAT32; + } + else if (info.nativeFormats & RTAUDIO_SINT32) { + outFormat.setSampleType(QAudioFormat::SignedInt); + outFormat.setSampleSize(32); + sampleFormat = RTAUDIO_SINT32; + } + else if (info.nativeFormats & RTAUDIO_SINT16) { + outFormat.setSampleType(QAudioFormat::SignedInt); + outFormat.setSampleSize(16); + sampleFormat = RTAUDIO_SINT16; + } + else { + qCritical(logAudio()) << "Cannot find supported sample format!"; + goto errorHandler; + } + } + + + qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Selected format: SampleSize" << outFormat.sampleSize() << "Channel Count" << outFormat.channelCount() << + "Sample Rate" << outFormat.sampleRate() << "Codec" << outFormat.codec() << "Sample Type" << outFormat.sampleType(); + + // We "hopefully" now have a valid format that is supported so try connecting + converter = new audioConverter(); + converterThread = new QThread(this); + if (setup.isinput) { + converterThread->setObjectName("audioConvIn()"); + } + else { + converterThread->setObjectName("audioConvOut()"); + } + converter->moveToThread(converterThread); + + connect(this, SIGNAL(setupConverter(QAudioFormat, QAudioFormat, quint8, quint8)), converter, SLOT(init(QAudioFormat, QAudioFormat, quint8, quint8))); + connect(converterThread, SIGNAL(finished()), converter, SLOT(deleteLater())); + connect(this, SIGNAL(sendToConverter(audioPacket)), converter, SLOT(convert(audioPacket))); + converterThread->start(QThread::TimeCriticalPriority); + + + // Per channel chunk size. + this->chunkSize = (outFormat.bytesForDuration(setup.blockSize * 1000) / (outFormat.sampleSize()/8) / outFormat.channelCount()); + +#ifdef RT_EXCEPTION + try { +#endif + if (setup.isinput) { + audio->openStream(NULL, &aParams, sampleFormat, outFormat.sampleRate(), &this->chunkSize, &staticWrite, this, &options); + emit setupConverter(outFormat, inFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedInput(audioPacket))); + } + else { + audio->openStream(&aParams, NULL, sampleFormat, outFormat.sampleRate(), &this->chunkSize, &staticRead, this , &options); + emit setupConverter(inFormat, outFormat, 7, setup.resampleQuality); + connect(converter, SIGNAL(converted(audioPacket)), this, SLOT(convertedOutput(audioPacket))); + } + audio->startStream(); + isInitialized = true; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "device successfully opened"; + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "detected latency:" << audio->getStreamLatency(); +#ifdef RT_EXCEPTION + } + catch (RtAudioError& e) { + qInfo(logAudio()) << "Error opening:" << QString::fromStdString(e.getMessage()); + // Try again? + goto errorHandler; + } +#endif + } + else + { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << QString::fromStdString(info.name) << "(" << aParams.deviceId << ") could not be probed, check audio configuration!"; + goto errorHandler; + } + + this->setVolume(setup.localAFgain); + + + return isInitialized; + +errorHandler: + if (retryConnectCount < 10) { + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "*** Attempting to reconnect to audio device in 500ms"; + QTimer::singleShot(500, this, std::bind(&rtHandler::init, this, setup)); + retryConnectCount++; + } + else + { + qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "*** Retry count exceeded, giving up!"; + } + return false; + +} + + +void rtHandler::setVolume(unsigned char volume) +{ + + this->volume = audiopot[volume]; + + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "setVolume: " << volume << "(" << this->volume << ")"; +} + +void rtHandler::incomingAudio(audioPacket packet) +{ + packet.volume = volume; + emit sendToConverter(packet); + return; +} + +int rtHandler::readData(void* outputBuffer, void* inputBuffer, + unsigned int nFrames, double streamTime, RtAudioStreamStatus status) +{ + Q_UNUSED(inputBuffer); + Q_UNUSED(streamTime); + int nBytes = nFrames * outFormat.channelCount() * (outFormat.sampleSize()/8); + + //lastSentSeq = packet.seq; + if (arrayBuffer.length() >= nBytes) { + if (audioMutex.tryLock(0)) { + std::memcpy(outputBuffer, arrayBuffer.constData(), nBytes); + arrayBuffer.remove(0, nBytes); + audioMutex.unlock(); + } + } + + if (status == RTAUDIO_INPUT_OVERFLOW) { + isUnderrun = true; + } + else if (status == RTAUDIO_OUTPUT_UNDERFLOW) { + isOverrun = true; + } + else + { + isUnderrun = false; + isOverrun = false; + } + return 0; +} + + +int rtHandler::writeData(void* outputBuffer, void* inputBuffer, + unsigned int nFrames, double streamTime, RtAudioStreamStatus status) +{ + Q_UNUSED(outputBuffer); + Q_UNUSED(streamTime); + Q_UNUSED(status); + audioPacket packet; + packet.time = QTime::currentTime(); + packet.sent = 0; + packet.volume = volume; + memcpy(&packet.guid, setup.guid, GUIDLEN); + packet.data.append((char*)inputBuffer, nFrames *outFormat.channelCount() * (outFormat.sampleSize()/8)); + emit sendToConverter(packet); + if (status == RTAUDIO_INPUT_OVERFLOW) { + isUnderrun = true; + } + else if (status == RTAUDIO_OUTPUT_UNDERFLOW) { + isOverrun = true; + } + else + { + isUnderrun = false; + isOverrun = false; + } + + return 0; +} + + +void rtHandler::convertedOutput(audioPacket packet) +{ + audioMutex.lock(); + arrayBuffer.append(packet.data); + audioMutex.unlock(); + amplitude = packet.amplitude; + currentLatency = packet.time.msecsTo(QTime::currentTime()) + (outFormat.durationForBytes(audio->getStreamLatency() * (outFormat.sampleSize() / 8) * outFormat.channelCount())/1000); + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); +} + + + +void rtHandler::convertedInput(audioPacket packet) +{ + if (packet.data.size() > 0) { + emit haveAudioData(packet); + amplitude = packet.amplitude; + currentLatency = packet.time.msecsTo(QTime::currentTime()) + (outFormat.durationForBytes(audio->getStreamLatency() * (outFormat.sampleSize() / 8) * outFormat.channelCount())/1000); + emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun); + } +} + + + +void rtHandler::changeLatency(const quint16 newSize) +{ + qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Changing latency to: " << newSize << " from " << setup.latency; +} + +int rtHandler::getLatency() +{ + return currentLatency; +} + + +quint16 rtHandler::getAmplitude() +{ + return static_cast(amplitude * 255.0); +} diff --git a/rthandler.h b/rthandler.h new file mode 100644 index 0000000..c48a2b4 --- /dev/null +++ b/rthandler.h @@ -0,0 +1,115 @@ +#ifndef rtHandler_H +#define rtHandler_H + +#include +#include +#include +#include + +#ifndef Q_OS_LINUX +#include "RtAudio.h" +#else +#include "rtaudio/RtAudio.h" +#endif + + +#include +#include +#include +#include + + +/* wfview Packet types */ +#include "packettypes.h" + +/* Logarithmic taper for volume control */ +#include "audiotaper.h" + +#include "audiohandler.h" +/* Audio converter class*/ +#include "audioconverter.h" + +#include + + +class rtHandler : public audioHandler +{ + Q_OBJECT + +public: + rtHandler(QObject* parent = 0); + ~rtHandler(); + + int getLatency(); + + + void getNextAudioChunk(QByteArray& data); + quint16 getAmplitude(); + +public slots: + bool init(audioSetup setup); + void changeLatency(const quint16 newSize); + void setVolume(unsigned char volume); + void convertedInput(audioPacket audio); + void convertedOutput(audioPacket audio); + void incomingAudio(const audioPacket data); + + +private slots: + +signals: + void audioMessage(QString message); + void sendLatency(quint16 newSize); + void haveAudioData(const audioPacket& data); + void haveLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void setupConverter(QAudioFormat in, QAudioFormat out, quint8 opus, quint8 resamp); + void sendToConverter(audioPacket audio); + +private: + + + int readData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); + + static int staticRead(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { + return static_cast(userData)->readData(outputBuffer, inputBuffer, nFrames, streamTime, status); + } + + + int writeData(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status); + + static int staticWrite(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { + return static_cast(userData)->writeData(outputBuffer, inputBuffer, nFrames, streamTime, status); + } + + + bool isInitialized = false; + + RtAudio* audio = Q_NULLPTR; + int audioDevice = 0; + RtAudio::StreamParameters aParams; + RtAudio::StreamOptions options; + RtAudio::DeviceInfo info; + + quint16 audioLatency; + unsigned int chunkSize; + + quint32 lastSeq; + quint32 lastSentSeq = 0; + + quint16 currentLatency; + float amplitude = 0.0; + qreal volume = 1.0; + + audioSetup setup; + QAudioFormat inFormat; + QAudioFormat outFormat; + audioConverter* converter = Q_NULLPTR; + QThread* converterThread = Q_NULLPTR; + QByteArray arrayBuffer; + bool isUnderrun = false; + bool isOverrun = false; + QMutex audioMutex; + int retryConnectCount = 0; +}; + +#endif // rtHandler_H diff --git a/selectradio.cpp b/selectradio.cpp new file mode 100644 index 0000000..61e0192 --- /dev/null +++ b/selectradio.cpp @@ -0,0 +1,81 @@ +#include "logcategories.h" +#include "selectradio.h" +#include "ui_selectradio.h" + + +selectRadio::selectRadio(QWidget* parent) : + QDialog(parent), + ui(new Ui::selectRadio) +{ + ui->setupUi(this); +} + +selectRadio::~selectRadio() +{ + delete ui; +} + +void selectRadio::populate(QList radios) +{ + ui->table->clearContents(); + for (int row = ui->table->rowCount() - 1;row>=0; row--) { + ui->table->removeRow(row); + } + + for (int row = 0; row < radios.count(); row++) { + ui->table->insertRow(ui->table->rowCount()); + ui->table->setItem(row, 0, new QTableWidgetItem(QString(radios[row].name))); + ui->table->setItem(row, 1, new QTableWidgetItem(QString("%1").arg((unsigned char)radios[row].civ, 2, 16, QLatin1Char('0')).toUpper())); + ui->table->setItem(row, 2, new QTableWidgetItem(QString::number(qFromBigEndian(radios[row].baudrate)))); + } + if (radios.count() > 1) { + this->setVisible(true); + } +} + +void selectRadio::setInUse(quint8 radio, quint8 busy, QString user, QString ip) +{ + //if ((radio > 0)&& !this->isVisible()) { + // qInfo() << "setInUse: radio:" << radio <<"busy" << busy << "user" << user << "ip"<setVisible(true); + //} + ui->table->setItem(radio, 3, new QTableWidgetItem(user)); + ui->table->setItem(radio, 4, new QTableWidgetItem(ip)); + for (int f = 0; f < 5; f++) { + if (busy == 1) + { + ui->table->item(radio, f)->setBackground(Qt::darkGreen); + } + else if (busy == 2) + { + ui->table->item(radio, f)->setBackground(Qt::red); + } + else + { + ui->table->item(radio, f)->setBackground(Qt::black); + } + } + +} + +void selectRadio::on_table_cellClicked(int row, int col) { + qInfo() << "Clicked on " << row << "," << col; + if (ui->table->item(row, col)->backgroundColor() != Qt::darkGreen) { + ui->table->selectRow(row); + emit selectedRadio(row); + this->setVisible(false); + } +} + + +void selectRadio::on_cancelButton_clicked() { + this->setVisible(false); +} + +void selectRadio::audioOutputLevel(quint16 level) { + ui->afLevel->setValue(level); +} + +void selectRadio::audioInputLevel(quint16 level) { + ui->modLevel->setValue(level); +} diff --git a/selectradio.h b/selectradio.h new file mode 100644 index 0000000..5e585ab --- /dev/null +++ b/selectradio.h @@ -0,0 +1,38 @@ +#ifndef SELECTRADIO_H +#define SELECTRADIO_H + +#include +#include +#include +#include +#include "packettypes.h" + +namespace Ui { + class selectRadio; +} + +class selectRadio : public QDialog +{ + Q_OBJECT + +public: + explicit selectRadio(QWidget* parent = 0); + ~selectRadio(); + void populate(QList radios); + void audioOutputLevel(quint16 level); + void audioInputLevel(quint16 level); + +public slots: + void on_table_cellClicked(int row, int col); + void setInUse(quint8 radio, quint8 busy, QString user, QString ip); + void on_cancelButton_clicked(); + +signals: + void selectedRadio(quint8 radio); + +private: + Ui::selectRadio* ui; + +}; + +#endif // SELECTRADIO_H diff --git a/selectradio.ui b/selectradio.ui new file mode 100644 index 0000000..46be235 --- /dev/null +++ b/selectradio.ui @@ -0,0 +1,157 @@ + + + selectRadio + + + + 0 + 0 + 590 + 206 + + + + Select Radio From List + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + + Rig Name + + + + + CI-V + + + + + Baud Rate + + + + + Current User + + + + + User IP Address + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + AF + + + + + + + 255 + + + 0 + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + MOD + + + + + + + 255 + + + 0 + + + false + + + + + + + + + + + + diff --git a/servermain.cpp b/servermain.cpp new file mode 100644 index 0000000..c2942c5 --- /dev/null +++ b/servermain.cpp @@ -0,0 +1,780 @@ +#include "servermain.h" + +#include "commhandler.h" +#include "rigidentities.h" +#include "logcategories.h" +#include + +// This code is copyright 2017-2020 Elliott H. Liggett +// All rights reserved + +servermain::servermain(const QString serialPortCL, const QString hostCL, const QString settingsFile) +{ + this->serialPortCL = serialPortCL; + this->hostCL = hostCL; + + qRegisterMetaType (); // Needs to be registered early. + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType (); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + + setDefPrefs(); + + getSettingsFilePath(settingsFile); + + loadSettings(); // Look for saved preferences + + setInitialTiming(); + + openRig(); + + setServerToPrefs(); + + amTransmitting = false; + +} + +servermain::~servermain() +{ + for (RIGCONFIG* radio : serverConfig.rigs) + { + if (radio->rigThread != Q_NULLPTR) + { + radio->rigThread->quit(); + radio->rigThread->wait(); + } + delete radio; // This has been created by new in loadSettings(); + } + serverConfig.rigs.clear(); + if (serverThread != Q_NULLPTR) { + serverThread->quit(); + serverThread->wait(); + } + + delete settings; + +#if defined(PORTAUDIO) + Pa_Terminate(); +#endif + +} + +void servermain::openRig() +{ + // This function is intended to handle opening a connection to the rig. + // the connection can be either serial or network, + // and this function is also responsible for initiating the search for a rig model and capabilities. + // Any errors, such as unable to open connection or unable to open port, are to be reported to the user. + + + makeRig(); + + for (RIGCONFIG* radio : serverConfig.rigs) + { + //qInfo(logSystem()) << "Opening rig"; + if (radio->rigThread != Q_NULLPTR) + { + //qInfo(logSystem()) << "Got rig"; + QMetaObject::invokeMethod(radio->rig, [=]() { + radio->rig->commSetup(radio->civAddr, radio->serialPort, radio->baudRate, QString("none"),prefs.tcpPort,radio->waterfallFormat); + }, Qt::QueuedConnection); + } + } +} + +void servermain::makeRig() +{ + for (RIGCONFIG* radio : serverConfig.rigs) + { + if (radio->rigThread == Q_NULLPTR) + { + qInfo(logSystem()) << "Creating new rigThread()"; + radio->rig = new rigCommander(radio->guid); + radio->rigThread = new QThread(this); + radio->rigThread->setObjectName("rigCommander()"); + + // Thread: + radio->rig->moveToThread(radio->rigThread); + connect(radio->rigThread, SIGNAL(started()), radio->rig, SLOT(process())); + connect(radio->rigThread, SIGNAL(finished()), radio->rig, SLOT(deleteLater())); + radio->rigThread->start(); + // Rig status and Errors: + connect(radio->rig, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(receiveSerialPortError(QString, QString))); + connect(radio->rig, SIGNAL(haveStatusUpdate(networkStatus)), this, SLOT(receiveStatusUpdate(networkStatus))); + + // Rig comm setup: + connect(this, SIGNAL(setRTSforPTT(bool)), radio->rig, SLOT(setRTSforPTT(bool))); + + connect(radio->rig, SIGNAL(haveBaudRate(quint32)), this, SLOT(receiveBaudRate(quint32))); + + connect(this, SIGNAL(sendCloseComm()), radio->rig, SLOT(closeComm())); + connect(this, SIGNAL(sendChangeLatency(quint16)), radio->rig, SLOT(changeLatency(quint16))); + //connect(this, SIGNAL(getRigCIV()), radio->rig, SLOT(findRigs())); + //connect(this, SIGNAL(setRigID(unsigned char)), radio->rig, SLOT(setRigID(unsigned char))); + connect(radio->rig, SIGNAL(discoveredRigID(rigCapabilities)), this, SLOT(receiveFoundRigID(rigCapabilities))); + connect(radio->rig, SIGNAL(commReady()), this, SLOT(receiveCommReady())); + + connect(this, SIGNAL(requestRigState()), radio->rig, SLOT(sendState())); + connect(this, SIGNAL(stateUpdated()), radio->rig, SLOT(stateUpdated())); + connect(radio->rig, SIGNAL(stateInfo(rigstate*)), this, SLOT(receiveStateInfo(rigstate*))); + + //Other connections + connect(this, SIGNAL(setCIVAddr(unsigned char)), radio->rig, SLOT(setCIVAddr(unsigned char))); + + connect(radio->rig, SIGNAL(havePTTStatus(bool)), this, SLOT(receivePTTstatus(bool))); + connect(this, SIGNAL(setPTT(bool)), radio->rig, SLOT(setPTT(bool))); + connect(this, SIGNAL(getPTT()), radio->rig, SLOT(getPTT())); + connect(this, SIGNAL(getDebug()), radio->rig, SLOT(getDebug())); + if (radio->rigThread->isRunning()) { + qInfo(logSystem()) << "Rig thread is running"; + } + else { + qInfo(logSystem()) << "Rig thread is not running"; + } + + } + + } + +} + +void servermain::removeRig() +{ + for (RIGCONFIG* radio : serverConfig.rigs) + { + if (radio->rigThread != Q_NULLPTR) + { + radio->rigThread->disconnect(); + radio->rig->disconnect(); + delete radio->rigThread; + delete radio->rig; + radio->rig = Q_NULLPTR; + } + } +} + + +void servermain::findSerialPort() +{ + // Find the ICOM radio connected, or, if none, fall back to OS default. + // qInfo(logSystem()) << "Searching for serial port..."; + QDirIterator it73("/dev/serial/by-id", QStringList() << "*IC-7300*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it97("/dev/serial", QStringList() << "*IC-9700*A*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it785x("/dev/serial", QStringList() << "*IC-785*A*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it705("/dev/serial", QStringList() << "*IC-705*A", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it7610("/dev/serial", QStringList() << "*IC-7610*A", QDir::Files, QDirIterator::Subdirectories); + QDirIterator itR8600("/dev/serial", QStringList() << "*IC-R8600*A", QDir::Files, QDirIterator::Subdirectories); + + if(!it73.filePath().isEmpty()) + { + // IC-7300 + serialPortRig = it73.filePath(); // first + } else if(!it97.filePath().isEmpty()) + { + // IC-9700 + serialPortRig = it97.filePath(); + } else if(!it785x.filePath().isEmpty()) + { + // IC-785x + serialPortRig = it785x.filePath(); + } else if(!it705.filePath().isEmpty()) + { + // IC-705 + serialPortRig = it705.filePath(); + } else if(!it7610.filePath().isEmpty()) + { + // IC-7610 + serialPortRig = it7610.filePath(); + } else if(!itR8600.filePath().isEmpty()) + { + // IC-R8600 + serialPortRig = itR8600.filePath(); + } else { + //fall back: + qInfo(logSystem()) << "Could not find Icom serial port. Falling back to OS default. Use --port to specify, or modify preferences."; +#ifdef Q_OS_MAC + serialPortRig = QString("/dev/tty.SLAB_USBtoUART"); +#endif +#ifdef Q_OS_LINUX + serialPortRig = QString("/dev/ttyUSB0"); +#endif +#ifdef Q_OS_WIN + serialPortRig = QString("COM1"); +#endif + } +} + +void servermain::receiveStatusUpdate(networkStatus status) +{ + if (status.message != lastMessage) { + std::cout << status.message.toLocal8Bit().toStdString() << "\n"; + lastMessage = status.message; + } +} + + +void servermain::receiveCommReady() +{ + rigCommander* sender = qobject_cast(QObject::sender()); + + // Use the GUID to determine which radio the response is from + + for (RIGCONFIG* radio : serverConfig.rigs) + { + if (sender != Q_NULLPTR && radio->rig != Q_NULLPTR && !memcmp(sender->getGUID(), radio->guid, GUIDLEN)) + { + + qInfo(logSystem()) << "Received CommReady!! "; + if (radio->civAddr == 0) + { + // tell rigCommander to broadcast a request for all rig IDs. + // qInfo(logSystem()) << "Beginning search from wfview for rigCIV (auto-detection broadcast)"; + if (!radio->rigAvailable) { + if (radio->connectTimer == Q_NULLPTR) { + radio->connectTimer = new QTimer(); + connect(radio->connectTimer, &QTimer::timeout, this, std::bind(&servermain::connectToRig, this, radio)); + } + radio->connectTimer->start(500); + } + } + else { + // don't bother, they told us the CIV they want, stick with it. + // We still query the rigID to find the model, but at least we know the CIV. + qInfo(logSystem()) << "Skipping automatic CIV, using user-supplied value of " << radio->civAddr; + QMetaObject::invokeMethod(radio->rig, [=]() { + radio->rig->setRigID(radio->civAddr); + }, Qt::QueuedConnection); + } + } + } +} + +void servermain::connectToRig(RIGCONFIG* rig) +{ + if (!rig->rigAvailable) { + qDebug(logSystem()) << "Searching for rig on" << rig->serialPort; + QMetaObject::invokeMethod(rig->rig, [=]() { + rig->rig->findRigs(); + }, Qt::QueuedConnection); + } + else { + rig->connectTimer->stop(); + } +} + +void servermain::receiveFoundRigID(rigCapabilities rigCaps) +{ + // Entry point for unknown rig being identified at the start of the program. + //now we know what the rig ID is: + + rigCommander* sender = qobject_cast(QObject::sender()); + + // Use the GUID to determine which radio the response is from + for (RIGCONFIG* radio : serverConfig.rigs) + { + + if (sender != Q_NULLPTR && radio->rig != Q_NULLPTR && !radio->rigAvailable && !memcmp(sender->getGUID(), radio->guid, GUIDLEN)) + { + + qDebug(logSystem()) << "Rig name: " << rigCaps.modelName; + qDebug(logSystem()) << "Has LAN capabilities: " << rigCaps.hasLan; + qDebug(logSystem()) << "Rig ID received into servermain: spectLenMax: " << rigCaps.spectLenMax; + qDebug(logSystem()) << "Rig ID received into servermain: spectAmpMax: " << rigCaps.spectAmpMax; + qDebug(logSystem()) << "Rig ID received into servermain: spectSeqMax: " << rigCaps.spectSeqMax; + qDebug(logSystem()) << "Rig ID received into servermain: hasSpectrum: " << rigCaps.hasSpectrum; + qDebug(logSystem()).noquote() << QString("Rig ID received into servermain: GUID: {%1%2%3%4-%5%6-%7%8-%9%10-%11%12%13%14%15%16}") + .arg(rigCaps.guid[0], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[1], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[2], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[3], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[4], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[5], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[6], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[7], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[8], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[9], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[10], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[11], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[12], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[13], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[14], 2, 16, QLatin1Char('0')) + .arg(rigCaps.guid[15], 2, 16, QLatin1Char('0')) + ; + radio->rigCaps = rigCaps; + // Added so that server receives rig capabilities. + emit sendRigCaps(rigCaps); + } + } + return; +} + +void servermain::receiveSerialPortError(QString port, QString errorText) +{ + qInfo(logSystem()) << "servermain: received serial port error for port: " << port << " with message: " << errorText; + + // TODO: Dialog box, exit, etc +} + + +void servermain::getSettingsFilePath(QString settingsFile) +{ + if (settingsFile.isNull()) { + settings = new QSettings(); + } + else + { + QString file = settingsFile; + QFile info(settingsFile); + QString path=""; + if (!QFileInfo(info).isAbsolute()) + { + path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (path.isEmpty()) + { + path = QDir::homePath(); + } + path = path + "/"; + file = info.fileName(); + } + + qInfo(logSystem()) << "Loading settings from:" << path + file; + settings = new QSettings(path + file, QSettings::Format::IniFormat); + } +} + + +void servermain::setInitialTiming() +{ + loopTickCounter = 0; + pttTimer = new QTimer(this); + pttTimer->setInterval(180*1000); // 3 minute max transmit time in ms + pttTimer->setSingleShot(true); + connect(pttTimer, SIGNAL(timeout()), this, SLOT(handlePttLimit())); +} + +void servermain::setServerToPrefs() +{ + + // Start server if enabled in config + if (serverThread != Q_NULLPTR) { + serverThread->quit(); + serverThread->wait(); + serverThread = Q_NULLPTR; + udp = Q_NULLPTR; + } + + udp = new udpServer(&serverConfig); + + serverThread = new QThread(this); + serverThread->setObjectName("udpServer()"); + + udp->moveToThread(serverThread); + + + connect(this, SIGNAL(initServer()), udp, SLOT(init())); + connect(serverThread, SIGNAL(finished()), udp, SLOT(deleteLater())); + + // Step through all radios and connect them to the server, + // server will then use GUID to determine which actual radio it belongs to. + + for (RIGCONFIG* radio : serverConfig.rigs) + { + if (radio->rigThread != Q_NULLPTR) + { + + if (radio->rig != Q_NULLPTR) { + connect(radio->rig, SIGNAL(haveAudioData(audioPacket)), udp, SLOT(receiveAudioData(audioPacket))); + connect(radio->rig, SIGNAL(haveDataForServer(QByteArray)), udp, SLOT(dataForServer(QByteArray))); + //connect(udp, SIGNAL(haveDataFromServer(QByteArray)), radio->rig, SLOT(dataFromServer(QByteArray))); + connect(this, SIGNAL(sendRigCaps(rigCapabilities)), udp, SLOT(receiveRigCaps(rigCapabilities))); + } + } + } + + connect(udp, SIGNAL(haveNetworkStatus(networkStatus)), this, SLOT(receiveStatusUpdate(networkStatus))); + + serverThread->start(); + + emit initServer(); +} + + +void servermain::setDefPrefs() +{ + defPrefs.radioCIVAddr = 0x00; // previously was 0x94 for 7300. + defPrefs.CIVisRadioModel = false; + defPrefs.forceRTSasPTT = false; + defPrefs.serialPortRadio = QString("auto"); + defPrefs.serialPortBaud = 115200; + defPrefs.localAFgain = 255; + defPrefs.tcpPort = 0; + defPrefs.audioSystem = qtAudio; + defPrefs.rxAudio.name = QString("default"); + defPrefs.txAudio.name = QString("default"); + + udpDefPrefs.ipAddress = QString(""); + udpDefPrefs.controlLANPort = 50001; + udpDefPrefs.serialLANPort = 50002; + udpDefPrefs.audioLANPort = 50003; + udpDefPrefs.username = QString(""); + udpDefPrefs.password = QString(""); + udpDefPrefs.clientName = QHostInfo::localHostName(); +} + +void servermain::loadSettings() +{ + qInfo(logSystem()) << "Loading settings from " << settings->fileName(); + prefs.audioSystem = static_cast(settings->value("AudioSystem", defPrefs.audioSystem).toInt()); + + int numRadios = settings->beginReadArray("Radios"); + if (numRadios == 0) { + settings->endArray(); + + // We assume that QSettings is empty as there are no radios configured, create new: + qInfo(logSystem()) << "Creating new settings file " << settings->fileName(); + settings->setValue("AudioSystem", defPrefs.audioSystem); + numRadios = 1; + settings->beginWriteArray("Radios"); + for (int i = 0; i < numRadios; i++) + { + settings->setArrayIndex(i); + settings->setValue("RigCIVuInt", defPrefs.radioCIVAddr); + settings->setValue("ForceRTSasPTT", defPrefs.forceRTSasPTT); + settings->setValue("SerialPortRadio", defPrefs.serialPortRadio); + settings->setValue("RigName", ""); + settings->setValue("SerialPortBaud", defPrefs.serialPortBaud); + settings->setValue("AudioInput", "default"); + settings->setValue("AudioOutput", "default"); + settings->setValue("WaterfallFormat", 0); + } + settings->endArray(); + + settings->beginGroup("Server"); + settings->setValue("ServerEnabled", true); + settings->setValue("ServerControlPort", udpDefPrefs.controlLANPort); + settings->setValue("ServerCivPort", udpDefPrefs.serialLANPort); + settings->setValue("ServerAudioPort", udpDefPrefs.audioLANPort); + + settings->beginWriteArray("Users"); + settings->setArrayIndex(0); + settings->setValue("Username", "user"); + QByteArray pass; + passcode("password", pass); + settings->setValue("Password", QString(pass)); + settings->setValue("UserType", 0); + + settings->endArray(); + + settings->endGroup(); + settings->sync(); + + } else { + settings->endArray(); + } + + numRadios = settings->beginReadArray("Radios"); + int tempNum = numRadios; + + for (int i = 0; i < numRadios; i++) { + settings->setArrayIndex(i); + RIGCONFIG* tempPrefs = new RIGCONFIG(); + tempPrefs->civAddr = (unsigned char)settings->value("RigCIVuInt", defPrefs.radioCIVAddr).toInt(); + tempPrefs->forceRTSasPTT = (bool)settings->value("ForceRTSasPTT", defPrefs.forceRTSasPTT).toBool(); + tempPrefs->serialPort = settings->value("SerialPortRadio", defPrefs.serialPortRadio).toString(); + tempPrefs->rigName = settings->value("RigName", "").toString(); + tempPrefs->baudRate = (quint32)settings->value("SerialPortBaud", defPrefs.serialPortBaud).toInt(); + tempPrefs->rxAudioSetup.name = settings->value("AudioInput", "default").toString(); + tempPrefs->txAudioSetup.name = settings->value("AudioOutput", "default").toString(); + tempPrefs->waterfallFormat = settings->value("WaterfallFormat", 0).toInt(); + tempPrefs->rxAudioSetup.type = prefs.audioSystem; + tempPrefs->txAudioSetup.type = prefs.audioSystem; + + QString tempPort = "auto"; + if (tempPrefs->rigName=="") + { + foreach(const QSerialPortInfo & serialPortInfo, QSerialPortInfo::availablePorts()) + { + qDebug(logSystem()) << "Serial Port found: " << serialPortInfo.portName() << "Manufacturer:" << serialPortInfo.manufacturer() << "Product ID" << serialPortInfo.description() << "S/N" << serialPortInfo.serialNumber(); + if ((serialPortInfo.portName() == tempPrefs->serialPort || tempPrefs->serialPort == "auto") && !serialPortInfo.serialNumber().isEmpty()) + { + if (serialPortInfo.serialNumber().startsWith("IC-")) { + tempPrefs->rigName = serialPortInfo.serialNumber(); + tempPort = serialPortInfo.portName(); + } + } + } + } + tempPrefs->serialPort = tempPort; + + QString guid = settings->value("GUID", "").toString(); + if (guid.isEmpty()) { + guid = QUuid::createUuid().toString(); + settings->setValue("GUID", guid); + } +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + memcpy(tempPrefs->guid, QUuid::fromString(guid).toRfc4122().constData(), GUIDLEN); +#endif + tempPrefs->rxAudioSetup.isinput = true; + tempPrefs->txAudioSetup.isinput = false; + tempPrefs->rxAudioSetup.localAFgain = 255; + tempPrefs->txAudioSetup.localAFgain = 255; + tempPrefs->rxAudioSetup.resampleQuality = 4; + tempPrefs->txAudioSetup.resampleQuality = 4; + + tempPrefs->rig = Q_NULLPTR; + tempPrefs->rigThread = Q_NULLPTR; + serverConfig.rigs.append(tempPrefs); + if (tempNum == 0) { + settings->endGroup(); + } + } + if (tempNum > 0) { + settings->endArray(); + } + + + /* + Now we have an array of rig objects, we need to match the configured audio devices with physical devices + */ + switch (prefs.audioSystem) + { + case rtAudio: + { +#if defined(Q_OS_LINUX) + RtAudio* audio = new RtAudio(RtAudio::Api::LINUX_ALSA); +// RtAudio* audio = new RtAudio(RtAudio::Api::LINUX_PULSE); +#elif defined(Q_OS_WIN) + RtAudio* audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); +#elif defined(Q_OS_MACX) + RtAudio* audio = new RtAudio(RtAudio::Api::MACOSX_CORE); +#endif + + // Enumerate audio devices, need to do before settings are loaded. + std::map apiMap; + apiMap[RtAudio::MACOSX_CORE] = "OS-X Core Audio"; + apiMap[RtAudio::WINDOWS_ASIO] = "Windows ASIO"; + apiMap[RtAudio::WINDOWS_DS] = "Windows DirectSound"; + apiMap[RtAudio::WINDOWS_WASAPI] = "Windows WASAPI"; + apiMap[RtAudio::UNIX_JACK] = "Jack Client"; + apiMap[RtAudio::LINUX_ALSA] = "Linux ALSA"; + apiMap[RtAudio::LINUX_PULSE] = "Linux PulseAudio"; + apiMap[RtAudio::LINUX_OSS] = "Linux OSS"; + apiMap[RtAudio::RTAUDIO_DUMMY] = "RtAudio Dummy"; + + std::vector< RtAudio::Api > apis; + RtAudio::getCompiledApi(apis); + + qInfo(logAudio()) << "RtAudio Version " << QString::fromStdString(RtAudio::getVersion()); + + qInfo(logAudio()) << "Compiled APIs:"; + for (unsigned int i = 0; i < apis.size(); i++) { + qInfo(logAudio()) << " " << QString::fromStdString(apiMap[apis[i]]); + } + + RtAudio::DeviceInfo info; + + qInfo(logAudio()) << "Current API: " << QString::fromStdString(apiMap[audio->getCurrentApi()]); + + unsigned int devices = audio->getDeviceCount(); + qInfo(logAudio()) << "Found " << devices << " audio device(s) *=default"; + + for (unsigned int i = 1; i < devices; i++) { + info = audio->getDeviceInfo(i); + for (RIGCONFIG* rig : serverConfig.rigs) + { + qDebug(logAudio()) << "Rig" << rig->rigName << "rxAudio device:" << rig->rxAudioSetup.name; + qDebug(logAudio()) << "Rig" << rig->rigName << "txAudio device:" << rig->txAudioSetup.name; + if (info.outputChannels > 0) + { + qInfo(logAudio()) << (info.isDefaultOutput ? "*" : " ") << "(" << i << ") Output Device : " << QString::fromStdString(info.name); + if (rig->txAudioSetup.name.toStdString() == info.name) { + rig->txAudioSetup.portInt = i; + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected txAudio device:" << QString(info.name.c_str()); + } + } + if (info.inputChannels > 0) + { + qInfo(logAudio()) << (info.isDefaultInput ? "*" : " ") << "(" << i << ") Input Device : " << QString::fromStdString(info.name); + if (rig->rxAudioSetup.name.toStdString() == info.name) { + rig->rxAudioSetup.portInt = i; + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected rxAudio device:" << QString(info.name.c_str()); + } + } + } + } + break; + } + case portAudio: + { + // Use PortAudio device enumeration + + PaError err; + + err = Pa_Initialize(); + + if (err != paNoError) + { + qInfo(logAudio()) << "ERROR: Cannot initialize Portaudio"; + } + + qInfo(logAudio()) << "PortAudio version: " << Pa_GetVersionInfo()->versionText; + + int numDevices; + numDevices = Pa_GetDeviceCount(); + qInfo(logAudio()) << "Pa_CountDevices returned" << numDevices; + + const PaDeviceInfo* info; + for (int i = 0; i < numDevices; i++) + { + info = Pa_GetDeviceInfo(i); + for (RIGCONFIG* rig : serverConfig.rigs) + { + qDebug(logAudio()) << "Rig" << rig->rigName << "rxAudio device:" << rig->rxAudioSetup.name; + qDebug(logAudio()) << "Rig" << rig->rigName << "txAudio device:" << rig->txAudioSetup.name; + if (info->maxInputChannels > 0) { + qDebug(logAudio()) << (i == Pa_GetDefaultInputDevice() ? "*" : " ") << "(" << i << ") Input Device : " << info->name; + + if (rig->rxAudioSetup.name == info->name) { + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected rxAudio device:" << QString(info->name); + rig->rxAudioSetup.portInt = i; + } + } + if (info->maxOutputChannels > 0) { + if (rig->txAudioSetup.name == info->name) { + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected txAudio device:" << QString(info->name); + rig->txAudioSetup.portInt = i; + } + } + } + } + break; + } + case qtAudio: + { + const auto audioOutputs = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + const auto audioInputs = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); + //qInfo(logAudio()) << "Looking for audio input devices"; + for (const QAudioDeviceInfo& deviceInfo : audioInputs) { + qDebug(logSystem()) << "Found Audio input: " << deviceInfo.deviceName(); + for (RIGCONFIG* rig : serverConfig.rigs) + { + qDebug(logAudio()) << "Rig" << rig->rigName << "rxAudio device:" << rig->rxAudioSetup.name; + qDebug(logAudio()) << "Rig" << rig->rigName << "txAudio device:" << rig->txAudioSetup.name; + if (deviceInfo.deviceName() == rig->rxAudioSetup.name +#ifdef Q_OS_WIN + && deviceInfo.realm() == "wasapi" +#endif + ) + { + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected rxAudio device:" << deviceInfo.deviceName(); + rig->rxAudioSetup.port = deviceInfo; + } + } + } + + //qInfo(logAudio()) << "Looking for audio output devices"; + for (const QAudioDeviceInfo& deviceInfo : audioOutputs) { + qDebug(logSystem()) << "Found Audio output: " << deviceInfo.deviceName(); + for (RIGCONFIG* rig : serverConfig.rigs) + { + if (deviceInfo.deviceName() == rig->txAudioSetup.name +#ifdef Q_OS_WIN + && deviceInfo.realm() == "wasapi" +#endif + ) + { + qDebug(logAudio()) << "Rig" << rig->rigName << "Selected txAudio device:" << deviceInfo.deviceName(); + rig->txAudioSetup.port = deviceInfo; + } + } + } + break; + } + } + + + + settings->beginGroup("Server"); + serverConfig.enabled = settings->value("ServerEnabled", false).toBool(); + serverConfig.controlPort = settings->value("ServerControlPort", 50001).toInt(); + serverConfig.civPort = settings->value("ServerCivPort", 50002).toInt(); + serverConfig.audioPort = settings->value("ServerAudioPort", 50003).toInt(); + + serverConfig.users.clear(); + + int numUsers = settings->beginReadArray("Users"); + if (numUsers > 0) { + { + for (int f = 0; f < numUsers; f++) + { + settings->setArrayIndex(f); + SERVERUSER user; + user.username = settings->value("Username", "").toString(); + user.password = settings->value("Password", "").toString(); + user.userType = settings->value("UserType", 0).toInt(); + serverConfig.users.append(user); + } + } + settings->endArray(); + } + + settings->endGroup(); + settings->sync(); + +#if defined(RTAUDIO) + delete audio; +#endif + +} + + +void servermain::receivePTTstatus(bool pttOn) +{ + // This is the only place where amTransmitting and the transmit button text should be changed: + //qInfo(logSystem()) << "PTT status: " << pttOn; + amTransmitting = pttOn; +} + +void servermain::handlePttLimit() +{ + // ptt time exceeded! + std::cout << "Transmit timeout at 3 minutes. Sending PTT OFF command now.\n"; + emit setPTT(false); + emit getPTT(); +} + +void servermain::receiveBaudRate(quint32 baud) +{ + qInfo() << "Received serial port baud rate from remote server:" << baud; + prefs.serialPortBaud = baud; +} + +void servermain::powerRigOn() +{ + emit sendPowerOn(); +} + +void servermain::powerRigOff() +{ + emit sendPowerOff(); +} + +void servermain::receiveStateInfo(rigstate* state) +{ + qInfo("Received rig state for wfmain"); + Q_UNUSED(state); + //rigState = state; +} diff --git a/servermain.h b/servermain.h new file mode 100644 index 0000000..1b7e3a8 --- /dev/null +++ b/servermain.h @@ -0,0 +1,307 @@ +#ifndef WFMAIN_H +#define WFMAIN_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "logcategories.h" +#include "commhandler.h" +#include "rigcommander.h" +#include "rigstate.h" +#include "freqmemory.h" +#include "rigidentities.h" +#include "repeaterattributes.h" + +#include "udpserver.h" +#include "rigctld.h" +#include "signal.h" + +#include + +#include +#include + +#include +#ifdef Q_OS_WIN +#include "RtAudio.h" +#else +#include "rtaudio/RtAudio.h" +#endif + +namespace Ui { +class wfmain; +} + +class servermain : public QObject +{ + Q_OBJECT + +public: + servermain(const QString serialPortCL, const QString hostCL, const QString settingsFile); + QString serialPortCL; + QString hostCL; + ~servermain(); + +signals: + // Basic to rig: + void setCIVAddr(unsigned char newRigCIVAddr); + void setRigID(unsigned char rigID); + void setRTSforPTT(bool enabled); + + // Power + void sendPowerOn(); + void sendPowerOff(); + + // Frequency, mode, band: + void getFrequency(); + void setFrequency(unsigned char vfo, freqt freq); + void getMode(); + void setMode(unsigned char modeIndex, unsigned char modeFilter); + void setMode(mode_info); + void setDataMode(bool dataOn, unsigned char filter); + void getDataMode(); + void getModInput(bool dataOn); + void setModInput(rigInput input, bool dataOn); + void getBandStackReg(char band, char regCode); + void getDebug(); + void getRitEnabled(); + void getRitValue(); + void setRitValue(int ritValue); + void setRitEnable(bool ritEnabled); + + // Repeater: + void getDuplexMode(); + void getTone(); + void getTSQL(); + void getDTCS(); + void getRptAccessMode(); + + // Level get: + void getLevels(); // get all levels + void getRfGain(); + void getAfGain(); + void getSql(); + void getIfShift(); + void getTPBFInner(); + void getTPBFOuter(); + void getTxPower(); + void getMicGain(); + void getSpectrumRefLevel(); + void getModInputLevel(rigInput input); + + // Level set: + void setRfGain(unsigned char level); + void setAfGain(unsigned char level); + void setSql(unsigned char level); + void setIFShift(unsigned char level); + void setTPBFInner(unsigned char level); + void setTPBFOuter(unsigned char level); + + void setIFShiftWindow(unsigned char level); + void setTPBFInnerWindow(unsigned char level); + void setTPBFOuterWindow(unsigned char level); + void setMicGain(unsigned char); + void setCompLevel(unsigned char); + void setTxPower(unsigned char); + void setMonitorLevel(unsigned char); + void setVoxGain(unsigned char); + void setAntiVoxGain(unsigned char); + void setSpectrumRefLevel(int); + + void setModLevel(rigInput input, unsigned char level); + void setACCGain(unsigned char level); + void setACCAGain(unsigned char level); + void setACCBGain(unsigned char level); + void setUSBGain(unsigned char level); + void setLANGain(unsigned char level); + + void getMeters(meterKind meter); + + + // PTT, ATU, ATT, Antenna, Preamp: + void getPTT(); + void setPTT(bool pttOn); + void getAttenuator(); + void getPreamp(); + void getAntenna(); + void setAttenuator(unsigned char att); + void setPreamp(unsigned char pre); + void setAntenna(unsigned char ant, bool rx); + void startATU(); + void setATU(bool atuEnabled); + void getATUStatus(); + + // Time and date: + void setTime(timekind t); + void setDate(datekind d); + void setUTCOffset(timekind t); + + void getRigID(); // this is the model of the rig + void getRigCIV(); // get the rig's CIV addr + void spectOutputEnable(); + void spectOutputDisable(); + void scopeDisplayEnable(); + void scopeDisplayDisable(); + void setScopeMode(spectrumMode spectMode); + void setScopeSpan(char span); + void setScopeEdge(char edge); + void setScopeFixedEdge(double startFreq, double endFreq, unsigned char edgeNumber); + void getScopeMode(); + void getScopeEdge(); + void getScopeSpan(); + void sayFrequency(); + void sayMode(); + void sayAll(); + void sendCommSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp, quint16 tcp, quint8 wf); + void sendCommSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp, quint16 tcp); + void sendCloseComm(); + void sendChangeLatency(quint16 latency); + void initServer(); + void sendRigCaps(rigCapabilities caps); + void requestRigState(); + void stateUpdated(); + +private slots: + + void receiveCommReady(); + void receivePTTstatus(bool pttOn); + + void receiveFoundRigID(rigCapabilities rigCaps); + void receiveSerialPortError(QString port, QString errorText); + void receiveBaudRate(quint32 baudrate); + + void handlePttLimit(); + void receiveStatusUpdate(networkStatus status); + void receiveStateInfo(rigstate* state); + void connectToRig(RIGCONFIG* rig); + +private: + QSettings *settings=Q_NULLPTR; + void loadSettings(); + + void openRig(); + void powerRigOff(); + void powerRigOn(); + QStringList portList; + QString serialPortRig; + + QTimer * delayedCommand; + QTimer * pttTimer; + uint16_t loopTickCounter; + uint16_t slowCmdNum; + QString lastMessage=""; + + void makeRig(); + void removeRig(); + void findSerialPort(); + + void setServerToPrefs(); + void setInitialTiming(); + void getSettingsFilePath(QString settingsFile); + + QStringList modes; + int currentModeIndex; + QStringList spans; + QStringList edges; + QStringList commPorts; + + quint16 spectWidth; + quint16 wfLength; + bool spectrumDrawLock; + + QByteArray spectrumPeaks; + + QVector wfimage; + unsigned int wfLengthMax; + + bool onFullscreen; + bool drawPeaks; + bool freqTextSelected; + void checkFreqSel(); + + double oldLowerFreq; + double oldUpperFreq; + freqt freq; + float tsKnobMHz; + + unsigned char setModeVal=0; + unsigned char setFilterVal=0; + + bool usingLAN = false; + + struct preferences { + unsigned char radioCIVAddr; + bool CIVisRadioModel; + bool forceRTSasPTT; + QString serialPortRadio; + quint32 serialPortBaud; + unsigned char localAFgain; + audioSetup rxAudio; + audioSetup txAudio; + rigCapabilities rigCaps; + bool haveRigCaps = false; + quint16 tcpPort; + audioType audioSystem; + } prefs; + + preferences defPrefs; + udpPreferences udpPrefs; + udpPreferences udpDefPrefs; + + // Configuration for audio output and input. + audioSetup rxSetup; + audioSetup txSetup; + + audioSetup serverRxSetup; + audioSetup serverTxSetup; + + + void setDefPrefs(); // populate default values to default prefs + + bool amTransmitting; + bool usingDataMode = false; + + unsigned char micGain=0; + unsigned char accAGain=0; + unsigned char accBGain=0; + unsigned char accGain=0; + unsigned char usbGain=0; + unsigned char lanGain=0; + + + udpServer* udp = Q_NULLPTR; + rigCtlD* rigCtl = Q_NULLPTR; + QThread* serverThread = Q_NULLPTR; + + rigstate* rigState = Q_NULLPTR; + + SERVERCONFIG serverConfig; +}; + +Q_DECLARE_METATYPE(struct rigCapabilities) +Q_DECLARE_METATYPE(struct freqt) +Q_DECLARE_METATYPE(struct mode_info) +Q_DECLARE_METATYPE(struct udpPreferences) +Q_DECLARE_METATYPE(struct audioPacket) +Q_DECLARE_METATYPE(struct audioSetup) +Q_DECLARE_METATYPE(struct SERVERCONFIG) +Q_DECLARE_METATYPE(struct timekind) +Q_DECLARE_METATYPE(struct datekind) +Q_DECLARE_METATYPE(struct networkStatus) +Q_DECLARE_METATYPE(enum rigInput) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(enum meterKind) +Q_DECLARE_METATYPE(enum spectrumMode) +Q_DECLARE_METATYPE(rigstate*) + + +#endif // WFMAIN_H diff --git a/tcpserver.cpp b/tcpserver.cpp new file mode 100644 index 0000000..8dea503 --- /dev/null +++ b/tcpserver.cpp @@ -0,0 +1,99 @@ +#include "tcpserver.h" + +#include "logcategories.h" + +tcpServer::tcpServer(QObject* parent) : QTcpServer(parent) +{ +} + +tcpServer::~tcpServer() +{ + qInfo(logTcpServer()) << "closing tcpServer"; +} + +int tcpServer::startServer(qint16 port) { + + if (!this->listen(QHostAddress::Any, port)) { + qInfo(logTcpServer()) << "could not start on port " << port; + return -1; + } + else + { + qInfo(logTcpServer()) << "started on port " << port; + } + + return 0; +} + +void tcpServer::incomingConnection(qintptr socket) { + tcpServerClient* client = new tcpServerClient(socket, this); + connect(this, SIGNAL(onStopped()), client, SLOT(closeSocket())); + emit newClient(socket); // Signal par +} + +void tcpServer::stopServer() +{ + qInfo(logTcpServer()) << "stopping server"; + emit onStopped(); +} + + +void tcpServer::receiveDataFromClient(QByteArray data) +{ + emit receiveData(data); +} + +void tcpServer::sendData(QByteArray data) { + + emit sendDataToClient(data); + +} + +tcpServerClient::tcpServerClient(int socketId, tcpServer* parent) : QObject(parent) +{ + sessionId = socketId; + socket = new QTcpSocket(this); + this->parent = parent; + if (!socket->setSocketDescriptor(sessionId)) + { + qInfo(logTcpServer()) << " error binding socket: " << sessionId; + return; + } + connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()), Qt::DirectConnection); + connect(socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()), Qt::DirectConnection); + connect(parent, SIGNAL(sendDataToClient(QByteArray)), this, SLOT(receiveDataToClient(QByteArray)), Qt::DirectConnection); + connect(this, SIGNAL(sendDataFromClient(QByteArray)), parent, SLOT(receiveDataFromClient(QByteArray)), Qt::DirectConnection); + qInfo(logTcpServer()) << " session connected: " << sessionId; + +} + +void tcpServerClient::socketReadyRead() { + QByteArray data; + if (socket->bytesAvailable()) { + data=socket->readAll(); + emit sendDataFromClient(data); + } +} + +void tcpServerClient::socketDisconnected() { + qInfo(logTcpServer()) << sessionId << "disconnected"; + socket->deleteLater(); + this->deleteLater(); +} + +void tcpServerClient::closeSocket() +{ + socket->close(); +} + +void tcpServerClient::receiveDataToClient(QByteArray data) { + + if (socket != Q_NULLPTR && socket->isValid() && socket->isOpen()) + { + socket->write(data); + } + else + { + qInfo(logTcpServer()) << "socket not open!"; + } +} \ No newline at end of file diff --git a/tcpserver.h b/tcpserver.h new file mode 100644 index 0000000..e699a90 --- /dev/null +++ b/tcpserver.h @@ -0,0 +1,63 @@ +#ifndef TCPSERVER_H +#define TCPSERVER_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class tcpServer : public QTcpServer +{ + Q_OBJECT + +public: + explicit tcpServer(QObject* parent = Q_NULLPTR); + ~tcpServer(); + int startServer(qint16 port); + void stopServer(); + +public slots: + virtual void incomingConnection(qintptr socketDescriptor); + void receiveDataFromClient(QByteArray data); + void sendData(QByteArray data); +signals: + void onStarted(); + void onStopped(); + void receiveData(QByteArray data); // emit this when we have data from tcp client, connect to rigcommander + void sendDataToClient(QByteArray data); + void newClient(int socketId); + +private: + QTcpServer* server; + QTcpSocket* socket = Q_NULLPTR; +}; + +class tcpServerClient : public QObject +{ + Q_OBJECT + +public: + explicit tcpServerClient(int socket, tcpServer* parent = Q_NULLPTR); +public slots: + void socketReadyRead(); + void socketDisconnected(); + void closeSocket(); + void receiveDataToClient(QByteArray); + +signals: + void sendDataFromClient(QByteArray data); +protected: + int sessionId; + QTcpSocket* socket = Q_NULLPTR; + +private: + tcpServer* parent; +}; + +#endif \ No newline at end of file diff --git a/udpaudio.cpp b/udpaudio.cpp new file mode 100644 index 0000000..dc1e5c5 --- /dev/null +++ b/udpaudio.cpp @@ -0,0 +1,301 @@ +#include "udpaudio.h" +#include "logcategories.h" + +// Audio stream +udpAudio::udpAudio(QHostAddress local, QHostAddress ip, quint16 audioPort, quint16 lport, audioSetup rxSetup, audioSetup txSetup) +{ + qInfo(logUdp()) << "Starting udpAudio"; + this->localIP = local; + this->port = audioPort; + this->radioIP = ip; + this->rxSetup = rxSetup; + this->txSetup = txSetup; + + if (txSetup.sampleRate == 0) { + enableTx = false; + } + + init(lport); // Perform connection + + QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpAudio::dataReceived); + + startAudio(); + + watchdogTimer = new QTimer(); + connect(watchdogTimer, &QTimer::timeout, this, &udpAudio::watchdog); + watchdogTimer->start(WATCHDOG_PERIOD); + + areYouThereTimer = new QTimer(); + connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); + areYouThereTimer->start(AREYOUTHERE_PERIOD); +} + +udpAudio::~udpAudio() +{ + + if (pingTimer != Q_NULLPTR) + { + qDebug(logUdp()) << "Stopping pingTimer"; + pingTimer->stop(); + delete pingTimer; + pingTimer = Q_NULLPTR; + } + + if (idleTimer != Q_NULLPTR) + { + qDebug(logUdp()) << "Stopping idleTimer"; + idleTimer->stop(); + delete idleTimer; + idleTimer = Q_NULLPTR; + } + + if (watchdogTimer != Q_NULLPTR) + { + qDebug(logUdp()) << "Stopping watchdogTimer"; + watchdogTimer->stop(); + delete watchdogTimer; + watchdogTimer = Q_NULLPTR; + } + + if (rxAudioThread != Q_NULLPTR) { + qDebug(logUdp()) << "Stopping rxaudio thread"; + rxAudioThread->quit(); + rxAudioThread->wait(); + } + + if (txAudioThread != Q_NULLPTR) { + qDebug(logUdp()) << "Stopping txaudio thread"; + txAudioThread->quit(); + txAudioThread->wait(); + } + qDebug(logUdp()) << "udpHandler successfully closed"; +} + +void udpAudio::watchdog() +{ + static bool alerted = false; + if (lastReceived.msecsTo(QTime::currentTime()) > 2000) + { + if (!alerted) { + /* Just log it at the moment, maybe try signalling the control channel that it needs to + try requesting civ/audio again? */ + + qInfo(logUdp()) << " Audio Watchdog: no audio data received for 2s, restart required?"; + alerted = true; + if (rxAudioThread != Q_NULLPTR) { + qDebug(logUdp()) << "Stopping rxaudio thread"; + rxAudioThread->quit(); + rxAudioThread->wait(); + rxAudioThread = Q_NULLPTR; + rxaudio = Q_NULLPTR; + } + + if (txAudioThread != Q_NULLPTR) { + qDebug(logUdp()) << "Stopping txaudio thread"; + txAudioThread->quit(); + txAudioThread->wait(); + txAudioThread = Q_NULLPTR; + txaudio = Q_NULLPTR; + } + } + } + else + { + alerted = false; + } +} + + +void udpAudio::sendTxAudio() +{ + if (txaudio == Q_NULLPTR) { + return; + } + +} + +void udpAudio::receiveAudioData(audioPacket audio) { + // I really can't see how this could be possible but a quick sanity check! + if (txaudio == Q_NULLPTR) { + return; + } + if (audio.data.length() > 0) { + int counter = 1; + int len = 0; + + while (len < audio.data.length()) { + QByteArray partial = audio.data.mid(len, 1364); + audio_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p) + partial.length(); + p.sentid = myId; + p.rcvdid = remoteId; + if (partial.length() == 0xa0) { + p.ident = 0x9781; + } + else { + p.ident = 0x0080; // TX audio is always this? + } + p.datalen = (quint16)qToBigEndian((quint16)partial.length()); + p.sendseq = (quint16)qToBigEndian((quint16)sendAudioSeq); // THIS IS BIG ENDIAN! + QByteArray tx = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); + tx.append(partial); + len = len + partial.length(); + //qInfo(logUdp()) << "Sending audio packet length: " << tx.length(); + sendTrackedPacket(tx); + sendAudioSeq++; + counter++; + } + } +} + +void udpAudio::changeLatency(quint16 value) +{ + emit haveChangeLatency(value); +} + +void udpAudio::setVolume(unsigned char value) +{ + emit haveSetVolume(value); +} + +void udpAudio::getRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over) { + + emit haveRxLevels(amplitude, latency, current, under, over); +} + +void udpAudio::getTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over) { + emit haveTxLevels(amplitude, latency, current, under, over); +} + +void udpAudio::dataReceived() +{ + + while (udp->hasPendingDatagrams()) { + QNetworkDatagram datagram = udp->receiveDatagram(); + //qInfo(logUdp()) << "Received: " << datagram.data().mid(0,10); + QByteArray r = datagram.data(); + + switch (r.length()) + { + case (16): // Response to control packet handled in udpBase + { + //control_packet_t in = (control_packet_t)r.constData(); + break; + } + default: + { + /* Audio packets start as follows: + PCM 16bit and PCM8/uLAW stereo: 0x44,0x02 for first packet and 0x6c,0x05 for second. + uLAW 8bit/PCM 8bit 0xd8,0x03 for all packets + PCM 16bit stereo 0x6c,0x05 first & second 0x70,0x04 third + + + */ + control_packet_t in = (control_packet_t)r.constData(); + + if (in->type != 0x01 && in->len >= 0x20) { + if (in->seq == 0) + { + // Seq number has rolled over. + seqPrefix++; + } + + // 0xac is the smallest possible audio packet. + lastReceived = QTime::currentTime(); + audioPacket tempAudio; + tempAudio.seq = (quint32)seqPrefix << 16 | in->seq; + tempAudio.time = lastReceived; + tempAudio.sent = 0; + tempAudio.data = r.mid(0x18); + // Prefer signal/slot to forward audio as it is thread/safe + // Need to do more testing but latency appears fine. + //rxaudio->incomingAudio(tempAudio); + if (rxAudioThread == Q_NULLPTR) + { + startAudio(); + } + emit haveAudioData(tempAudio); + } + break; + } + } + + udpBase::dataReceived(r); // Call parent function to process the rest. + r.clear(); + datagram.clear(); + } +} + +void udpAudio::startAudio() { + + if (rxSetup.type == qtAudio) { + rxaudio = new audioHandler(); + } + else if (rxSetup.type == portAudio) { + rxaudio = new paHandler(); + } + else if (rxSetup.type == rtAudio) { + rxaudio = new rtHandler(); + } + else + { + qCritical(logAudio()) << "Unsupported Receive Audio Handler selected!"; + } + + rxAudioThread = new QThread(this); + rxAudioThread->setObjectName("rxAudio()"); + + rxaudio->moveToThread(rxAudioThread); + + rxAudioThread->start(QThread::TimeCriticalPriority); + + connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup))); + + // signal/slot not currently used. + connect(this, SIGNAL(haveAudioData(audioPacket)), rxaudio, SLOT(incomingAudio(audioPacket))); + connect(this, SIGNAL(haveChangeLatency(quint16)), rxaudio, SLOT(changeLatency(quint16))); + connect(this, SIGNAL(haveSetVolume(unsigned char)), rxaudio, SLOT(setVolume(unsigned char))); + connect(rxaudio, SIGNAL(haveLevels(quint16, quint16, quint16, bool, bool)), this, SLOT(getRxLevels(quint16, quint16, quint16, bool, bool))); + connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater())); + + + sendControl(false, 0x03, 0x00); // First connect packet + + pingTimer = new QTimer(); + connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); + pingTimer->start(PING_PERIOD); // send ping packets every 100ms + + if (enableTx) { + if (txSetup.type == qtAudio) { + txaudio = new audioHandler(); + } + else if (txSetup.type == portAudio) { + txaudio = new paHandler(); + } + else if (txSetup.type == rtAudio) { + txaudio = new rtHandler(); + } + else + { + qCritical(logAudio()) << "Unsupported Transmit Audio Handler selected!"; + } + + txAudioThread = new QThread(this); + rxAudioThread->setObjectName("txAudio()"); + + txaudio->moveToThread(txAudioThread); + + txAudioThread->start(QThread::TimeCriticalPriority); + + connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup))); + connect(txaudio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); + connect(txaudio, SIGNAL(haveLevels(quint16, quint16, quint16, bool, bool)), this, SLOT(getTxLevels(quint16, quint16, quint16, bool, bool))); + + connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater())); + emit setupTxAudio(txSetup); + } + + emit setupRxAudio(rxSetup); + +} diff --git a/udpaudio.h b/udpaudio.h new file mode 100644 index 0000000..884edef --- /dev/null +++ b/udpaudio.h @@ -0,0 +1,88 @@ +#ifndef UDPAUDIO_H +#define UDPAUDIO_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Allow easy endian-ness conversions +#include + +// Needed for audio +#include +#include + +#include + +#include "packettypes.h" + +#include "udpbase.h" + +#include "audiohandler.h" +#include "pahandler.h" +#include "rthandler.h" + + +// Class for all audio communications. +class udpAudio : public udpBase +{ + Q_OBJECT + +public: + udpAudio(QHostAddress local, QHostAddress ip, quint16 aport, quint16 lport, audioSetup rxSetup, audioSetup txSetup); + ~udpAudio(); + + int audioLatency = 0; + +signals: + void haveAudioData(audioPacket data); + + void setupTxAudio(audioSetup setup); + void setupRxAudio(audioSetup setup); + + void haveChangeLatency(quint16 value); + void haveSetVolume(unsigned char value); + void haveRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void haveTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + +public slots: + void changeLatency(quint16 value); + void setVolume(unsigned char value); + void getRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void getTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void receiveAudioData(audioPacket audio); + +private: + + void sendTxAudio(); + void dataReceived(); + void watchdog(); + void startAudio(); + audioSetup rxSetup; + audioSetup txSetup; + + uint16_t sendAudioSeq = 0; + + audioHandler* rxaudio = Q_NULLPTR; + QThread* rxAudioThread = Q_NULLPTR; + + audioHandler* txaudio = Q_NULLPTR; + QThread* txAudioThread = Q_NULLPTR; + + QTimer* txAudioTimer = Q_NULLPTR; + bool enableTx = true; + + QMutex audioMutex; + +}; + +#endif \ No newline at end of file diff --git a/udpbase.cpp b/udpbase.cpp new file mode 100644 index 0000000..9381fd8 --- /dev/null +++ b/udpbase.cpp @@ -0,0 +1,545 @@ +#include "udpbase.h" +#include "logcategories.h" + +void udpBase::init(quint16 lport) +{ + //timeStarted.start(); + udp = new QUdpSocket(this); + udp->bind(lport); // Bind to random port. + localPort = udp->localPort(); + qInfo(logUdp()) << "UDP Stream bound to local port:" << localPort << " remote port:" << port; + uint32_t addr = localIP.toIPv4Address(); + myId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (localPort & 0xffff); + + retransmitTimer = new QTimer(); + connect(retransmitTimer, &QTimer::timeout, this, &udpBase::sendRetransmitRequest); + retransmitTimer->start(RETRANSMIT_PERIOD); +} + +udpBase::~udpBase() +{ + qInfo(logUdp()) << "Closing UDP stream :" << radioIP.toString() << ":" << port; + if (udp != Q_NULLPTR) { + sendControl(false, 0x05, 0x00); // Send disconnect + udp->close(); + delete udp; + } + if (areYouThereTimer != Q_NULLPTR) + { + areYouThereTimer->stop(); + delete areYouThereTimer; + } + + if (pingTimer != Q_NULLPTR) + { + pingTimer->stop(); + delete pingTimer; + } + if (idleTimer != Q_NULLPTR) + { + idleTimer->stop(); + delete idleTimer; + } + if (retransmitTimer != Q_NULLPTR) + { + retransmitTimer->stop(); + delete retransmitTimer; + } + + pingTimer = Q_NULLPTR; + idleTimer = Q_NULLPTR; + areYouThereTimer = Q_NULLPTR; + retransmitTimer = Q_NULLPTR; + +} + +// Base class! + +void udpBase::dataReceived(QByteArray r) +{ + if (r.length() < 0x10) + { + return; // Packet too small do to anything with? + } + + switch (r.length()) + { + case (CONTROL_SIZE): // Empty response used for simple comms and retransmit requests. + { + control_packet_t in = (control_packet_t)r.constData(); + if (in->type == 0x01 && in->len == 0x10) + { + // Single packet request + packetsLost++; + congestion = static_cast(packetsSent) / packetsLost * 100; + txBufferMutex.lock(); + auto match = txSeqBuf.find(in->seq); + if (match != txSeqBuf.end()) { + // Found matching entry? + // Send "untracked" as it has already been sent once. + // Don't constantly retransmit the same packet, give-up eventually + qDebug(logUdp()) << this->metaObject()->className() << ": Sending (single packet) retransmit of " << QString("0x%1").arg(match->seqNum, 0, 16); + match->retransmitCount++; + udpMutex.lock(); + udp->writeDatagram(match->data, radioIP, port); + udpMutex.unlock(); + } + else { + qDebug(logUdp()) << this->metaObject()->className() << ": Remote requested packet" + << QString("0x%1").arg(in->seq, 0, 16) << + "not found, have " << QString("0x%1").arg(txSeqBuf.firstKey(), 0, 16) << + "to" << QString("0x%1").arg(txSeqBuf.lastKey(), 0, 16); + } + txBufferMutex.unlock(); + } + if (in->type == 0x04) { + qInfo(logUdp()) << this->metaObject()->className() << ": Received I am here "; + areYouThereCounter = 0; + // I don't think that we will ever receive an "I am here" other than in response to "Are you there?" + remoteId = in->sentid; + if (areYouThereTimer != Q_NULLPTR && areYouThereTimer->isActive()) { + // send ping packets every second + areYouThereTimer->stop(); + } + sendControl(false, 0x06, 0x01); // Send Are you ready - untracked. + } + else if (in->type == 0x06) + { + // Just get the seqnum and ignore the rest. + } + break; + } + case (PING_SIZE): // ping packet + { + ping_packet_t in = (ping_packet_t)r.constData(); + if (in->type == 0x07) + { + // It is a ping request/response + if (in->reply == 0x00) + { + ping_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p); + p.type = 0x07; + p.sentid = myId; + p.rcvdid = remoteId; + p.reply = 0x01; + p.seq = in->seq; + p.time = in->time; + udpMutex.lock(); + udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); + udpMutex.unlock(); + } + else if (in->reply == 0x01) { + if (in->seq == pingSendSeq) + { + // This is response to OUR request so increment counter + pingSendSeq++; + } + } + else { + qInfo(logUdp()) << this->metaObject()->className() << "Unhandled response to ping. I have never seen this! 0x10=" << r[16]; + } + + } + break; + } + default: + { + + // All packets "should" be added to the incoming buffer. + // First check that we haven't already received it. + + + } + break; + + } + + + // All packets except ping and retransmit requests should trigger this. + control_packet_t in = (control_packet_t)r.constData(); + + // This is a variable length retransmit request! + if (in->type == 0x01 && in->len != 0x10) + { + + for (quint16 i = 0x10; i < r.length(); i = i + 2) + { + quint16 seq = (quint8)r[i] | (quint8)r[i + 1] << 8; + auto match = txSeqBuf.find(seq); + if (match == txSeqBuf.end()) { + qDebug(logUdp()) << this->metaObject()->className() << ": Remote requested packet" + << QString("0x%1").arg(seq, 0, 16) << + "not found, have " << QString("0x%1").arg(txSeqBuf.firstKey(), 0, 16) << + "to" << QString("0x%1").arg(txSeqBuf.lastKey(), 0, 16); + // Just send idle packet. + sendControl(false, 0, seq); + } + else { + // Found matching entry? + // Send "untracked" as it has already been sent once. + qDebug(logUdp()) << this->metaObject()->className() << ": Sending (multiple packet) retransmit of " << QString("0x%1").arg(match->seqNum, 0, 16); + match->retransmitCount++; + udpMutex.lock(); + udp->writeDatagram(match->data, radioIP, port); + udpMutex.unlock(); + packetsLost++; + congestion = static_cast(packetsSent) / packetsLost * 100; + } + } + } + else if (in->len != PING_SIZE && in->type == 0x00 && in->seq != 0x00) + { + rxBufferMutex.lock(); + if (rxSeqBuf.isEmpty()) { + rxSeqBuf.insert(in->seq, QTime::currentTime()); + } + else + { + if (in->seq < rxSeqBuf.firstKey() || in->seq - rxSeqBuf.lastKey() > MAX_MISSING) + { + qInfo(logUdp()) << this->metaObject()->className() << "Large seq number gap detected, previous highest: " << + QString("0x%1").arg(rxSeqBuf.lastKey(), 0, 16) << " current: " << QString("0x%1").arg(in->seq, 0, 16); + //seqPrefix++; + // Looks like it has rolled over so clear buffer and start again. + rxSeqBuf.clear(); + // Add this packet to the incoming buffer + rxSeqBuf.insert(in->seq, QTime::currentTime()); + rxBufferMutex.unlock(); + missingMutex.lock(); + rxMissing.clear(); + missingMutex.unlock(); + return; + } + + if (!rxSeqBuf.contains(in->seq)) + { + // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. + + if (in->seq > rxSeqBuf.lastKey() + 1) { + qInfo(logUdp()) << this->metaObject()->className() << "1 or more missing packets detected, previous: " << + QString("0x%1").arg(rxSeqBuf.lastKey(), 0, 16) << " current: " << QString("0x%1").arg(in->seq, 0, 16); + // We are likely missing packets then! + missingMutex.lock(); + //int missCounter = 0; + // Sanity check! + for (quint16 f = rxSeqBuf.lastKey() + 1; f <= in->seq; f++) + { + if (rxSeqBuf.size() > BUFSIZE) + { + rxSeqBuf.erase(rxSeqBuf.begin()); + } + rxSeqBuf.insert(f, QTime::currentTime()); + if (f != in->seq && !rxMissing.contains(f)) + { + rxMissing.insert(f, 0); + } + } + missingMutex.unlock(); + } + else { + if (rxSeqBuf.size() > BUFSIZE) + { + rxSeqBuf.erase(rxSeqBuf.begin()); + } + rxSeqBuf.insert(in->seq, QTime::currentTime()); + + } + } + else { + // This is probably one of our missing packets! + missingMutex.lock(); + auto s = rxMissing.find(in->seq); + if (s != rxMissing.end()) + { + qInfo(logUdp()) << this->metaObject()->className() << ": Missing SEQ has been received! " << QString("0x%1").arg(in->seq, 0, 16); + + s = rxMissing.erase(s); + } + missingMutex.unlock(); + + } + + } + rxBufferMutex.unlock(); + + } +} + + +void udpBase::sendRetransmitRequest() +{ + // Find all gaps in received packets and then send requests for them. + // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. + if (rxMissing.isEmpty()) { + return; + } + else if (rxMissing.size() > MAX_MISSING) { + qInfo(logUdp()) << "Too many missing packets," << rxMissing.size() << "flushing all buffers"; + missingMutex.lock(); + rxMissing.clear(); + missingMutex.unlock(); + + rxBufferMutex.lock(); + rxSeqBuf.clear(); + rxBufferMutex.unlock(); + return; + } + + QByteArray missingSeqs; + + missingMutex.lock(); + auto it = rxMissing.begin(); + while (it != rxMissing.end()) + { + if (it.key() != 0x0) + { + if (it.value() < 4) + { + missingSeqs.append(it.key() & 0xff); + missingSeqs.append(it.key() >> 8 & 0xff); + missingSeqs.append(it.key() & 0xff); + missingSeqs.append(it.key() >> 8 & 0xff); + it.value()++; + it++; + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": No response for missing packet" << QString("0x%1").arg(it.key(), 0, 16) << "deleting"; + it = rxMissing.erase(it); + } + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": found empty key in missing buffer"; + it++; + } + } + missingMutex.unlock(); + + if (missingSeqs.length() != 0) + { + control_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p); + p.type = 0x01; + p.seq = 0x0000; + p.sentid = myId; + p.rcvdid = remoteId; + if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control. + { + p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8); + qInfo(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << QString("0x%1").arg(p.seq, 0, 16); + udpMutex.lock(); + udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); + udpMutex.unlock(); + } + else + { + qInfo(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(':'); + missingMutex.lock(); + p.len = sizeof(p) + missingSeqs.size(); + missingSeqs.insert(0, p.packet, sizeof(p)); + missingMutex.unlock(); + + udpMutex.lock(); + udp->writeDatagram(missingSeqs, radioIP, port); + udpMutex.unlock(); + } + } + +} + + + +// Used to send idle and other "control" style messages +void udpBase::sendControl(bool tracked = true, quint8 type = 0, quint16 seq = 0) +{ + control_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p); + p.type = type; + p.sentid = myId; + p.rcvdid = remoteId; + + if (!tracked) { + p.seq = seq; + udpMutex.lock(); + udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); + udpMutex.unlock(); + } + else { + sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); + } + return; +} + +// Send periodic ping packets +void udpBase::sendPing() +{ + ping_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p); + p.type = 0x07; + p.sentid = myId; + p.rcvdid = remoteId; + p.seq = pingSendSeq; + QTime now = QTime::currentTime(); + p.time = (quint32)now.msecsSinceStartOfDay(); + lastPingSentTime = QDateTime::currentDateTime(); + udpMutex.lock(); + udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); + udpMutex.unlock(); + return; +} + + +void udpBase::sendTrackedPacket(QByteArray d) +{ + // As the radio can request retransmission of these packets, store them in a buffer + d[6] = sendSeq & 0xff; + d[7] = (sendSeq >> 8) & 0xff; + SEQBUFENTRY s; + s.seqNum = sendSeq; + s.timeSent = QTime::currentTime(); + s.retransmitCount = 0; + s.data = d; + if (txBufferMutex.tryLock(100)) + { + + if (sendSeq == 0) { + // We are either the first ever sent packet or have rolled-over so clear the buffer. + txSeqBuf.clear(); + congestion = 0; + } + if (txSeqBuf.size() > BUFSIZE) + { + txSeqBuf.erase(txSeqBuf.begin()); + } + txSeqBuf.insert(sendSeq, s); + + txBufferMutex.unlock(); + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; + } + // Stop using purgeOldEntries() as it is likely slower than just removing the earliest packet. + //qInfo(logUdp()) << this->metaObject()->className() << "RX:" << rxSeqBuf.size() << "TX:" <writeDatagram(d, radioIP, port); + if (congestion > 10) { // Poor quality connection? + udp->writeDatagram(d, radioIP, port); + if (congestion > 20) // Even worse so send again. + udp->writeDatagram(d, radioIP, port); + } + if (idleTimer != Q_NULLPTR && idleTimer->isActive()) { + idleTimer->start(IDLE_PERIOD); // Reset idle counter if it's running + } + udpMutex.unlock(); + packetsSent++; + return; +} + + +/// +/// Once a packet has reached PURGE_SECONDS old (currently 10) then it is not likely to be any use. +/// +void udpBase::purgeOldEntries() +{ + // Erase old entries from the tx packet buffer + if (txBufferMutex.tryLock(100)) + { + if (!txSeqBuf.isEmpty()) + { + // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS + for (auto it = txSeqBuf.begin(); it != txSeqBuf.end();) { + if (it.value().timeSent.secsTo(QTime::currentTime()) > PURGE_SECONDS) { + txSeqBuf.erase(it++); + } + else { + break; + } + } + } + txBufferMutex.unlock(); + + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; + } + + + + if (rxBufferMutex.tryLock(100)) + { + if (!rxSeqBuf.isEmpty()) { + // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS + for (auto it = rxSeqBuf.begin(); it != rxSeqBuf.end();) { + if (it.value().secsTo(QTime::currentTime()) > PURGE_SECONDS) { + rxSeqBuf.erase(it++); + } + else { + break; + } + } + } + rxBufferMutex.unlock(); + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": rxBuffer mutex is locked"; + } + + if (missingMutex.tryLock(100)) + { + // Erase old entries from the missing packets buffer + if (!rxMissing.isEmpty() && rxMissing.size() > 50) { + for (size_t i = 0; i < 25; ++i) { + rxMissing.erase(rxMissing.begin()); + } + } + missingMutex.unlock(); + } + else { + qInfo(logUdp()) << this->metaObject()->className() << ": missingBuffer mutex is locked"; + } +} + +void udpBase::printHex(const QByteArray& pdata) +{ + printHex(pdata, false, true); +} + +void udpBase::printHex(const QByteArray& pdata, bool printVert, bool printHoriz) +{ + qDebug(logUdp()) << "---- Begin hex dump -----:"; + QString sdata("DATA: "); + QString index("INDEX: "); + QStringList strings; + + for (int i = 0; i < pdata.length(); i++) + { + strings << QString("[%1]: %2").arg(i, 8, 10, QChar('0')).arg((unsigned char)pdata[i], 2, 16, QChar('0')); + sdata.append(QString("%1 ").arg((unsigned char)pdata[i], 2, 16, QChar('0'))); + index.append(QString("%1 ").arg(i, 2, 10, QChar('0'))); + } + + if (printVert) + { + for (int i = 0; i < strings.length(); i++) + { + //sdata = QString(strings.at(i)); + qDebug(logUdp()) << strings.at(i); + } + } + + if (printHoriz) + { + qDebug(logUdp()) << index; + qDebug(logUdp()) << sdata; + } + qDebug(logUdp()) << "----- End hex dump -----"; +} + + diff --git a/udpbase.h b/udpbase.h new file mode 100644 index 0000000..f042b4d --- /dev/null +++ b/udpbase.h @@ -0,0 +1,207 @@ +#ifndef UDPBASE_H +#define UDPBASE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Allow easy endian-ness conversions +#include + +// Needed for audio +#include +#include + +#include + +#include "packettypes.h" + + + +struct udpPreferences { + QString ipAddress; + quint16 controlLANPort; + quint16 serialLANPort; + quint16 audioLANPort; + QString username; + QString password; + QString clientName; + quint8 waterfallFormat; +}; + +struct networkStatus { + quint8 rxAudioBufferPercent; + quint8 txAudioBufferPercent; + quint8 rxAudioLevel; + quint8 txAudioLevel; + quint16 rxLatency; + quint16 txLatency; + bool rxUnderrun; + bool txUnderrun; + bool rxOverrun; + bool txOverrun; + quint16 rxCurrentLatency; + quint16 txCurrentLatency; + quint32 packetsSent = 0; + quint32 packetsLost = 0; + quint16 rtt = 0; + quint32 networkLatency = 0; + QString message; +}; + + +// Parent class that contains all common items. +class udpBase : public QObject +{ + + +public: + ~udpBase(); + + void init(quint16 local); + + void reconnect(); + + void dataReceived(QByteArray r); + void sendPing(); + void sendRetransmitRange(quint16 first, quint16 second, quint16 third, quint16 fourth); + + void sendControl(bool tracked, quint8 id, quint16 seq); + + void printHex(const QByteArray& pdata); + void printHex(const QByteArray& pdata, bool printVert, bool printHoriz); + + + //QTime timeStarted; + + QUdpSocket* udp = Q_NULLPTR; + uint32_t myId = 0; + uint32_t remoteId = 0; + uint16_t authSeq = 0x30; + uint16_t sendSeqB = 0; + uint16_t sendSeq = 1; + uint16_t lastReceivedSeq = 1; + uint16_t pkt0SendSeq = 0; + uint16_t periodicSeq = 0; + quint64 latency = 0; + + QString username = ""; + QString password = ""; + QHostAddress radioIP; + QHostAddress localIP; + bool isAuthenticated = false; + quint16 localPort = 0; + quint16 port = 0; + bool periodicRunning = false; + bool sentPacketConnect2 = false; + QTime lastReceived = QTime::currentTime(); + QMutex udpMutex; + QMutex txBufferMutex; + QMutex rxBufferMutex; + QMutex missingMutex; + + struct SEQBUFENTRY { + QTime timeSent; + uint16_t seqNum; + QByteArray data; + quint8 retransmitCount; + }; + + QMap rxSeqBuf; + QMap txSeqBuf; + QMap rxMissing; + + void sendTrackedPacket(QByteArray d); + void purgeOldEntries(); + + QTimer* areYouThereTimer = Q_NULLPTR; // Send are-you-there packets every second until a response is received. + QTimer* pingTimer = Q_NULLPTR; // Start sending pings immediately. + QTimer* idleTimer = Q_NULLPTR; // Start watchdog once we are connected. + + QTimer* watchdogTimer = Q_NULLPTR; + QTimer* retransmitTimer = Q_NULLPTR; + + QDateTime lastPingSentTime; + uint16_t pingSendSeq = 0; + + quint16 areYouThereCounter = 0; + + quint32 packetsSent = 0; + quint32 packetsLost = 0; + + quint16 seqPrefix = 0; + QString connectionType = ""; + int congestion = 0; + + +private: + void sendRetransmitRequest(); + +}; + + +/// +/// passcode function used to generate secure (ish) code +/// +/// +/// pointer to encoded username or password +static inline void passcode(QString in, QByteArray& out) +{ + const quint8 sequence[] = + { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0x47,0x5d,0x4c,0x42,0x66,0x20,0x23,0x46,0x4e,0x57,0x45,0x3d,0x67,0x76,0x60,0x41,0x62,0x39,0x59,0x2d,0x68,0x7e, + 0x7c,0x65,0x7d,0x49,0x29,0x72,0x73,0x78,0x21,0x6e,0x5a,0x5e,0x4a,0x3e,0x71,0x2c,0x2a,0x54,0x3c,0x3a,0x63,0x4f, + 0x43,0x75,0x27,0x79,0x5b,0x35,0x70,0x48,0x6b,0x56,0x6f,0x34,0x32,0x6c,0x30,0x61,0x6d,0x7b,0x2f,0x4b,0x64,0x38, + 0x2b,0x2e,0x50,0x40,0x3f,0x55,0x33,0x37,0x25,0x77,0x24,0x26,0x74,0x6a,0x28,0x53,0x4d,0x69,0x22,0x5c,0x44,0x31, + 0x36,0x58,0x3b,0x7a,0x51,0x5f,0x52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + }; + + QByteArray ba = in.toLocal8Bit(); + uchar* ascii = (uchar*)ba.constData(); + for (int i = 0; i < in.length() && i < 16; i++) + { + int p = ascii[i] + i; + if (p > 126) + { + p = 32 + p % 127; + } + out.append(sequence[p]); + } + return; +} + +/// +/// returns a QByteArray of a null terminated string +/// +/// +/// +/// +static inline QByteArray parseNullTerminatedString(QByteArray c, int s) +{ + //QString res = ""; + QByteArray res; + for (int i = s; i < c.length(); i++) + { + if (c[i] != '\0') + { + res.append(c[i]); + } + else + { + break; + } + } + return res; +} + +#endif diff --git a/udpcivdata.cpp b/udpcivdata.cpp new file mode 100644 index 0000000..f6cd51d --- /dev/null +++ b/udpcivdata.cpp @@ -0,0 +1,268 @@ +#include "udpcivdata.h" +#include "logcategories.h" + +// Class that manages all Civ Data to/from the rig +udpCivData::udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort, bool splitWf, quint16 localPort = 0) +{ + qInfo(logUdp()) << "Starting udpCivData"; + localIP = local; + port = civPort; + radioIP = ip; + splitWaterfall = splitWf; + + udpBase::init(localPort); // Perform connection + + QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpCivData::dataReceived); + + sendControl(false, 0x03, 0x00); // First connect packet + + /* + Connect various timers + */ + pingTimer = new QTimer(); + idleTimer = new QTimer(); + areYouThereTimer = new QTimer(); + startCivDataTimer = new QTimer(); + watchdogTimer = new QTimer(); + + connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); + connect(watchdogTimer, &QTimer::timeout, this, &udpCivData::watchdog); + connect(idleTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, true, 0, 0)); + connect(startCivDataTimer, &QTimer::timeout, this, std::bind(&udpCivData::sendOpenClose, this, false)); + connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); + watchdogTimer->start(WATCHDOG_PERIOD); + // Start sending are you there packets - will be stopped once "I am here" received + // send ping packets every 100 ms (maybe change to less frequent?) + pingTimer->start(PING_PERIOD); + // Send idle packets every 100ms, this timer will be reset every time a non-idle packet is sent. + idleTimer->start(IDLE_PERIOD); + areYouThereTimer->start(AREYOUTHERE_PERIOD); +} + +udpCivData::~udpCivData() +{ + sendOpenClose(true); + if (startCivDataTimer != Q_NULLPTR) + { + startCivDataTimer->stop(); + delete startCivDataTimer; + startCivDataTimer = Q_NULLPTR; + } + if (pingTimer != Q_NULLPTR) + { + pingTimer->stop(); + delete pingTimer; + pingTimer = Q_NULLPTR; + } + if (idleTimer != Q_NULLPTR) + { + idleTimer->stop(); + delete idleTimer; + idleTimer = Q_NULLPTR; + } + if (watchdogTimer != Q_NULLPTR) + { + watchdogTimer->stop(); + delete watchdogTimer; + watchdogTimer = Q_NULLPTR; + } +} + +void udpCivData::watchdog() +{ + static bool alerted = false; + if (lastReceived.msecsTo(QTime::currentTime()) > 2000) + { + if (!alerted) { + qInfo(logUdp()) << " CIV Watchdog: no CIV data received for 2s, requesting data start."; + if (startCivDataTimer != Q_NULLPTR) + { + startCivDataTimer->start(100); + } + alerted = true; + } + } + else + { + alerted = false; + } +} + +void udpCivData::send(QByteArray d) +{ + //qInfo(logUdp()) << "Sending: (" << d.length() << ") " << d; + data_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p) + d.length(); + p.sentid = myId; + p.rcvdid = remoteId; + p.reply = (char)0xc1; + p.datalen = d.length(); + p.sendseq = qToBigEndian(sendSeqB); // THIS IS BIG ENDIAN! + + QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); + t.append(d); + sendTrackedPacket(t); + sendSeqB++; + return; +} + + +void udpCivData::sendOpenClose(bool close) +{ + uint8_t magic = 0x04; + + if (close) + { + magic = 0x00; + } + + openclose_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p); + p.sentid = myId; + p.rcvdid = remoteId; + p.data = 0x01c0; // Not sure what other values are available: + p.sendseq = qToBigEndian(sendSeqB); + p.magic = magic; + + sendSeqB++; + + sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); + return; +} + + + +void udpCivData::dataReceived() +{ + while (udp->hasPendingDatagrams()) + { + QNetworkDatagram datagram = udp->receiveDatagram(); + //qInfo(logUdp()) << "Received: " << datagram.data(); + QByteArray r = datagram.data(); + + + switch (r.length()) + { + case (CONTROL_SIZE): // Control packet + { + control_packet_t in = (control_packet_t)r.constData(); + if (in->type == 0x04) + { + areYouThereTimer->stop(); + } + else if (in->type == 0x06) + { + // Update remoteId + remoteId = in->sentid; + // Manually send a CIV start request and start the timer if it isn't received. + // The timer will be stopped as soon as valid CIV data is received. + sendOpenClose(false); + if (startCivDataTimer != Q_NULLPTR) { + startCivDataTimer->start(100); + } + } + break; + } + default: + { + if (r.length() > 21) { + data_packet_t in = (data_packet_t)r.constData(); + if (in->type != 0x01) { + // Process this packet, any re-transmit requests will happen later. + //uint16_t gotSeq = qFromLittleEndian(r.mid(6, 2)); + // We have received some Civ data so stop sending Start packets! + if (startCivDataTimer != Q_NULLPTR) { + startCivDataTimer->stop(); + } + lastReceived = QTime::currentTime(); + if (quint16(in->datalen + 0x15) == (quint16)in->len) + { + //if (r.mid(0x15).length() != 157) + // Find data length + int pos = r.indexOf(QByteArrayLiteral("\x27\x00\x00")) + 2; + int len = r.mid(pos).indexOf(QByteArrayLiteral("\xfd")); + //splitWaterfall = false; + if (splitWaterfall && pos > 1 && len > 100) { + // We need to split waterfall data into its component parts + // There are only 2 types that we are currently aware of + int numDivisions = 0; + if (len == 490) // IC705, IC9700, IC7300(LAN) + { + numDivisions = 11; + } + else if (len == 704) // IC7610, IC7851, ICR8600 + { + numDivisions = 15; + } + else { + qInfo(logUdp()) << "Unknown spectrum size" << len; + break; + } + // (sequence #1) includes center/fixed mode at [05]. No pixels. + // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 " + // "DATA: 27 00 00 01 11 01 00 00 00 14 00 00 00 35 14 00 00 fd " + // (sequences 2-10, 50 pixels) + // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 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 " + // "DATA: 27 00 00 07 11 27 13 15 01 00 22 21 09 08 06 19 0e 20 23 25 2c 2d 17 27 29 16 14 1b 1b 21 27 1a 18 17 1e 21 1b 24 21 22 23 13 19 23 2f 2d 25 25 0a 0e 1e 20 1f 1a 0c fd " + // ^--^--(seq 7/11) + // ^-- start waveform data 0x00 to 0xA0, index 05 to 54 + // (sequence #11) + // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 " + // "DATA: 27 00 00 11 11 0b 13 21 23 1a 1b 22 1e 1a 1d 13 21 1d 26 28 1f 19 1a 18 09 2c 2c 2c 1a 1b fd " + + int divSize = (len / numDivisions) + 6; + QByteArray wfPacket; + for (int i = 0; i < numDivisions; i++) { + + wfPacket = r.mid(pos - 6, 9); // First part of packet + + wfPacket = r.mid(pos - 6, 9); // First part of packet + char tens = ((i + 1) / 10); + char units = ((i + 1) - (10 * tens)); + wfPacket[7] = units | (tens << 4); + + tens = (numDivisions / 10); + units = (numDivisions - (10 * tens)); + wfPacket[8] = units | (tens << 4); + + if (i == 0) { + //Just send initial data, first BCD encode the max number: + wfPacket.append(r.mid(pos + 3, 12)); + } + else + { + wfPacket.append(r.mid((pos + 15) + ((i - 1) * divSize), divSize)); + } + if (i < numDivisions - 1) { + wfPacket.append('\xfd'); + } + //printHex(wfPacket, false, true); + + emit receive(wfPacket); + wfPacket.clear(); + + } + //qDebug(logUdp()) << "Waterfall packet len" << len << "Num Divisions" << numDivisions << "Division Size" << divSize; + } + else { + // Not waterfall data or split not enabled. + emit receive(r.mid(0x15)); + } + //qDebug(logUdp()) << "Got incoming CIV datagram" << r.mid(0x15).length(); + + } + + } + } + break; + } + } + udpBase::dataReceived(r); // Call parent function to process the rest. + + r.clear(); + datagram.clear(); + + } +} diff --git a/udpcivdata.h b/udpcivdata.h new file mode 100644 index 0000000..410f969 --- /dev/null +++ b/udpcivdata.h @@ -0,0 +1,56 @@ +// Class for all (pseudo) serial communications +#ifndef UDPCIVDATA_H +#define UDPCIVDATA_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Allow easy endian-ness conversions +#include + +// Needed for audio +#include +#include + +#include + +#include "packettypes.h" + +#include "udpbase.h" + +class udpCivData : public udpBase +{ + Q_OBJECT + +public: + udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort, bool splitWf, quint16 lport); + ~udpCivData(); + QMutex serialmutex; + +signals: + int receive(QByteArray); + +public slots: + void send(QByteArray d); + + +private: + void watchdog(); + void dataReceived(); + void sendOpenClose(bool close); + + QTimer* startCivDataTimer = Q_NULLPTR; + bool splitWaterfall = false; +}; + + +#endif \ No newline at end of file diff --git a/udphandler.cpp b/udphandler.cpp index 5238a86..f0ffd60 100644 --- a/udphandler.cpp +++ b/udphandler.cpp @@ -8,18 +8,26 @@ udpHandler::udpHandler(udpPreferences prefs, audioSetup rx, audioSetup tx) : controlPort(prefs.controlLANPort), civPort(0), audioPort(0), + civLocalPort(0), + audioLocalPort(0), rxSetup(rx), txSetup(tx) { - - this->port = this->controlPort; this->username = prefs.username; this->password = prefs.password; this->compName = prefs.clientName.mid(0,8) + "-wfview"; - qInfo(logUdp()) << "Starting udpHandler user:" << username << " rx latency:" << rxSetup.latency << " tx latency:" << txSetup.latency << " rx sample rate: " << rxSetup.samplerate << - " rx codec: " << rxSetup.codec << " tx sample rate: " << txSetup.samplerate << " tx codec: " << txSetup.codec; + if (prefs.waterfallFormat == 2) + { + splitWf = true; + } + else + { + splitWf = false; + } + qInfo(logUdp()) << "Starting udpHandler user:" << username << " rx latency:" << rxSetup.latency << " tx latency:" << txSetup.latency << " rx sample rate: " << rxSetup.sampleRate << + " rx codec: " << rxSetup.codec << " tx sample rate: " << txSetup.sampleRate << " tx codec: " << txSetup.codec; // Try to set the IP address, if it is a hostname then perform a DNS lookup. if (!radioIP.setAddress(prefs.ipAddress)) @@ -55,7 +63,7 @@ udpHandler::udpHandler(udpPreferences prefs, audioSetup rx, audioSetup tx) : void udpHandler::init() { - udpBase::init(); // Perform UDP socket initialization. + udpBase::init(0); // Perform UDP socket initialization. // Connect socket to my dataReceived function. QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpHandler::dataReceived); @@ -82,10 +90,12 @@ udpHandler::~udpHandler() if (streamOpened) { if (audio != Q_NULLPTR) { delete audio; + audio = Q_NULLPTR; } if (civ != Q_NULLPTR) { delete civ; + civ = Q_NULLPTR; } qInfo(logUdp()) << "Sending token removal packet"; sendToken(0x01); @@ -133,6 +143,22 @@ void udpHandler::receiveDataFromUserToRig(QByteArray data) } } +void udpHandler::getRxLevels(quint16 amplitude,quint16 latency,quint16 current, bool under, bool over) { + status.rxAudioLevel = amplitude; + status.rxLatency = latency; + status.rxCurrentLatency = current; + status.rxUnderrun = under; + status.rxOverrun = over; +} + +void udpHandler::getTxLevels(quint16 amplitude,quint16 latency, quint16 current, bool under, bool over) { + status.txAudioLevel = amplitude; + status.txLatency = latency; + status.txCurrentLatency = current; + status.txUnderrun = under; + status.txOverrun = over; +} + void udpHandler::dataReceived() { while (udp->hasPendingDatagrams()) { @@ -170,39 +196,48 @@ void udpHandler::dataReceived() if (in->type == 0x07 && in->reply == 0x01 && streamOpened) { // This is a response to our ping request so measure latency - latency += lastPingSentTime.msecsTo(QDateTime::currentDateTime()); - latency /= 2; - quint32 totalsent = packetsSent; - quint32 totallost = packetsLost; + status.networkLatency += lastPingSentTime.msecsTo(QDateTime::currentDateTime()); + status.networkLatency /= 2; + status.packetsSent = packetsSent; + status.packetsLost = packetsLost; if (audio != Q_NULLPTR) { - totalsent = totalsent + audio->packetsSent; - totallost = totallost + audio->packetsLost; + status.packetsSent = status.packetsSent + audio->packetsSent; + status.packetsLost = status.packetsLost + audio->packetsLost; } if (civ != Q_NULLPTR) { - totalsent = totalsent + civ->packetsSent; - totallost = totallost + civ->packetsLost; + status.packetsSent = status.packetsSent + civ->packetsSent; + status.packetsLost = status.packetsLost + civ->packetsLost; } QString tempLatency; - if (rxSetup.latency > audio->audioLatency) + if (status.rxCurrentLatency <= status.rxLatency && !status.rxUnderrun && !status.rxOverrun) { - tempLatency = QString("%1 ms").arg(audio->audioLatency,3); + tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3); } - else { - tempLatency = QString("%1 ms").arg(audio->audioLatency,3); + else if (status.rxUnderrun){ + tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3); + } + else if (status.rxOverrun){ + tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3); + } else + { + tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3); } QString txString=""; if (txSetup.codec == 0) { txString = "(no tx)"; } - emit haveNetworkStatus(QString("
%1 rx latency: %2 / rtt: %3 ms / loss: %4/%5
").arg(txString).arg(tempLatency).arg(latency, 3).arg(totallost, 3).arg(totalsent, 3)); + status.message = QString("
%1 rx latency: %2 / rtt: %3 ms / loss: %4/%5
").arg(txString).arg(tempLatency).arg(status.networkLatency, 3).arg(status.packetsLost, 3).arg(status.packetsSent, 3); + + emit haveNetworkStatus(status); + } break; } case (TOKEN_SIZE): // Response to Token request { token_packet_t in = (token_packet_t)r.constData(); - if (in->res == 0x05 && in->type != 0x01) + if (in->requesttype == 0x05 && in->requestreply == 0x02 && in->type != 0x01) { if (in->response == 0x0000) { @@ -211,7 +246,7 @@ void udpHandler::dataReceived() gotAuthOK = true; if (!streamOpened) { - sendRequestStream(); + sendRequestStream(); } } @@ -257,13 +292,38 @@ void udpHandler::dataReceived() delete civ; civ = Q_NULLPTR; } - + streamOpened = false; } } else { civPort = qFromBigEndian(in->civport); audioPort = qFromBigEndian(in->audioport); + if (!streamOpened) { + + civ = new udpCivData(localIP, radioIP, civPort, splitWf, civLocalPort); + QObject::connect(civ, SIGNAL(receive(QByteArray)), this, SLOT(receiveFromCivStream(QByteArray))); + + // TX is not supported + if (txSampleRates < 2) { + txSetup.sampleRate=0; + txSetup.codec = 0; + } + streamOpened = true; + } + if (audio == Q_NULLPTR) { + audio = new udpAudio(localIP, radioIP, audioPort, audioLocalPort, rxSetup, txSetup); + + QObject::connect(audio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); + QObject::connect(this, SIGNAL(haveChangeLatency(quint16)), audio, SLOT(changeLatency(quint16))); + QObject::connect(this, SIGNAL(haveSetVolume(unsigned char)), audio, SLOT(setVolume(unsigned char))); + QObject::connect(audio, SIGNAL(haveRxLevels(quint16, quint16, quint16, bool, bool)), this, SLOT(getRxLevels(quint16, quint16, quint16, bool, bool))); + QObject::connect(audio, SIGNAL(haveTxLevels(quint16, quint16, quint16, bool, bool)), this, SLOT(getTxLevels(quint16, quint16, quint16, bool, bool))); + } + + qInfo(logUdp()) << this->metaObject()->className() << "Got serial and audio request success, device name: " << devName; + + } } break; @@ -295,7 +355,7 @@ void udpHandler::dataReceived() if (in->error == 0xfeffffff) { - emit haveNetworkStatus("Invalid Username/Password"); + status.message = "Invalid Username/Password"; qInfo(logUdp()) << this->metaObject()->className() << ": Invalid Username/Password"; } else if (!isAuthenticated) @@ -303,7 +363,7 @@ void udpHandler::dataReceived() if (in->tokrequest == tokRequest) { - emit haveNetworkStatus("Radio Login OK!"); + status.message="Radio Login OK!"; qInfo(logUdp()) << this->metaObject()->className() << ": Received matching token response to our request"; token = in->token; sendToken(0x02); @@ -322,89 +382,97 @@ void udpHandler::dataReceived() } case (CONNINFO_SIZE): { - conninfo_packet_t in = (conninfo_packet_t)r.constData(); - if (in->type != 0x01) { + // Once connected, the server will send a conninfo packet for each radio that is connected - devName = in->name; - QHostAddress ip = QHostAddress(qToBigEndian(in->ipaddress)); - if (!streamOpened && in->busy) + conninfo_packet_t in = (conninfo_packet_t)r.constData(); + QHostAddress ip = QHostAddress(qToBigEndian(in->ipaddress)); + + qInfo(logUdp()) << "Got Connection status for:" << in->name << "Busy:" << in->busy << "Computer" << in->computer << "IP" << ip.toString(); + + // First we need to find this radio in our capabilities packet, there aren't many so just step through + for (unsigned char f = 0; f < radios.size(); f++) + { + + if ((radios[f].commoncap == 0x8010 && + radios[f].macaddress[0] == in->macaddress[0] && + radios[f].macaddress[1] == in->macaddress[1] && + radios[f].macaddress[2] == in->macaddress[2] && + radios[f].macaddress[3] == in->macaddress[3] && + radios[f].macaddress[4] == in->macaddress[4] && + radios[f].macaddress[5] == in->macaddress[5]) || + !memcmp(radios[f].guid,in->guid, GUIDLEN)) + { + emit setRadioUsage(f, in->busy, QString(in->computer), ip.toString()); + qDebug(logUdp()) << "Set radio usage num:" << f << in->name << "Busy:" << in->busy << "Computer" << in->computer << "IP" << ip.toString(); + } + } + + if (!streamOpened && radios.size()==1) { + + qDebug(logUdp()) << "Single radio available, can I connect to it?"; + + if (in->busy) { if (in->ipaddress != 0x00 && strcmp(in->computer, compName.toLocal8Bit())) { - emit haveNetworkStatus(devName + " in use by: " + in->computer + " (" + ip.toString() + ")"); + status.message = devName + " in use by: " + in->computer + " (" + ip.toString() + ")"; sendControl(false, 0x00, in->seq); // Respond with an idle } else { - civ = new udpCivData(localIP, radioIP, civPort); - - // TX is not supported - if (txSampleRates <2 ) { - txSetup.samplerate = 0; - txSetup.codec = 0; - } - - audio = new udpAudio(localIP, radioIP, audioPort, rxSetup, txSetup); - - QObject::connect(civ, SIGNAL(receive(QByteArray)), this, SLOT(receiveFromCivStream(QByteArray))); - QObject::connect(audio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); - QObject::connect(this, SIGNAL(haveChangeLatency(quint16)), audio, SLOT(changeLatency(quint16))); - QObject::connect(this, SIGNAL(haveSetVolume(unsigned char)), audio, SLOT(setVolume(unsigned char))); - - streamOpened = true; - - emit haveNetworkStatus(devName); - - qInfo(logUdp()) << this->metaObject()->className() << "Got serial and audio request success, device name: " << devName; - - // Stuff can change in the meantime because of a previous login... - remoteId = in->sentid; - myId = in->rcvdid; - tokRequest = in->tokrequest; - token = in->token; } } - else if (!streamOpened && !in->busy) + else if (!in->busy) { - emit haveNetworkStatus(devName + " available"); - - identa = in->identa; - identb = in->identb; - - sendRequestStream(); - } - else if (streamOpened) - /* If another client connects/disconnects from the server, the server will emit - a CONNINFO packet, send our details to confirm we still want the stream */ - { - // Received while stream is open. - sendRequestStream(); + qDebug(logUdp()) << "Attempting to connect to radio"; + status.message = devName + " available"; + + setCurrentRadio(0); } } + else if (streamOpened) + /* If another client connects/disconnects from the server, the server will emit + a CONNINFO packet, send our details to confirm we still want the stream */ + { + //qDebug(logUdp()) << "I am already connected????"; + // Received while stream is open. + //sendRequestStream(); + } break; } - case (CAPABILITIES_SIZE): + default: { - capabilities_packet_t in = (capabilities_packet_t)r.constData(); - if (in->type != 0x01) + if ((r.length() - CAPABILITIES_SIZE) % RADIO_CAP_SIZE != 0) { - audioType = in->audio; - devName = in->name; - civId = in->civ; - rxSampleRates = in->rxsample; - txSampleRates = in->txsample; - emit haveBaudRate(qFromBigEndian(in->baudrate)); - //replyId = r.mid(0x42, 16); - qInfo(logUdp()) << this->metaObject()->className() << "Received radio capabilities, Name:" << - devName << " Audio:" << - audioType << "CIV:" << hex << civId; - - if (txSampleRates < 2) - { - // TX not supported - qInfo(logUdp()) << this->metaObject()->className() << "TX audio is disabled"; - } + // Likely a retransmit request? + break; } + + + capabilities_packet_t in = (capabilities_packet_t)r.constData(); + numRadios = in->numradios; + + for (int f = CAPABILITIES_SIZE; f < r.length(); f = f + RADIO_CAP_SIZE) { + radio_cap_packet rad; + const char* tmpRad = r.constData(); + memcpy(&rad, tmpRad+f, RADIO_CAP_SIZE); + radios.append(rad); + } + for(const radio_cap_packet &radio : radios) + { + qInfo(logUdp()) << this->metaObject()->className() << "Received radio capabilities, Name:" << + radio.name << " Audio:" << + radio.audio << "CIV:" << QString("0x%1").arg((unsigned char)radio.civ,0, 16) << + "MAC:" << radio.macaddress[0] << + ":" << radio.macaddress[1] << + ":" << radio.macaddress[2] << + ":" << radio.macaddress[3] << + ":" << radio.macaddress[4] << + ":" << radio.macaddress[5] << + "CAPF" << radio.capf; + } + emit requestRadioSelection(radios); + break; } @@ -412,12 +480,64 @@ void udpHandler::dataReceived() udpBase::dataReceived(r); // Call parent function to process the rest. r.clear(); datagram.clear(); - } return; } +void udpHandler::setCurrentRadio(quint8 radio) { + + // If we are currently connected to a different radio, disconnect first + if (audio != Q_NULLPTR) { + delete audio; + audio = Q_NULLPTR; + } + + if (civ != Q_NULLPTR) { + delete civ; + civ = Q_NULLPTR; + } + + streamOpened = false; + + qInfo(logUdp()) << "Got Radio" << radio; + qInfo(logUdp()) << "Find available local ports"; + + /* This seems to be the only way to find some available local ports. + The problem is we need to know the local AND remote ports but send the + local port to the server first and it replies with the remote ports! */ + if (civLocalPort == 0 || audioLocalPort == 0) { + QUdpSocket* tempudp = new QUdpSocket(); + tempudp->bind(); // Bind to random port. + civLocalPort = tempudp->localPort(); + tempudp->close(); + tempudp->bind(); + audioLocalPort = tempudp->localPort(); + tempudp->close(); + delete tempudp; + } + int baudrate = qFromBigEndian(radios[radio].baudrate); + emit haveBaudRate(baudrate); + + if (radios[radio].commoncap == 0x8010) { + memcpy(&macaddress, radios[radio].macaddress, sizeof(macaddress)); + useGuid = false; + } + else { + useGuid = true; + memcpy(&guid, radios[radio].guid, GUIDLEN); + } + + devName =radios[radio].name; + audioType = radios[radio].audio; + civId = radios[radio].civ; + rxSampleRates = radios[radio].rxsample; + txSampleRates = radios[radio].txsample; + + sendRequestStream(); +} + + void udpHandler::sendRequestStream() { @@ -429,12 +549,18 @@ void udpHandler::sendRequestStream() p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; - p.code = 0x0180; - p.res = 0x03; - p.commoncap = 0x8010; - p.identa = identa; - p.identb = identb; - p.innerseq = authSeq++; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requesttype = 0x03; + p.requestreply = 0x01; + + if (!useGuid) { + p.commoncap = 0x8010; + memcpy(&p.macaddress, macaddress, 6); + } + else { + memcpy(&p.guid, guid, GUIDLEN); + } + p.innerseq = qToBigEndian(authSeq++); p.tokrequest = tokRequest; p.token = token; memcpy(&p.name, devName.toLocal8Bit().constData(), devName.length()); @@ -445,10 +571,10 @@ void udpHandler::sendRequestStream() } p.rxcodec = rxSetup.codec; memcpy(&p.username, usernameEncoded.constData(), usernameEncoded.length()); - p.rxsample = qToBigEndian((quint32)rxSetup.samplerate); - p.txsample = qToBigEndian((quint32)txSetup.samplerate); - p.civport = qToBigEndian((quint32)civPort); - p.audioport = qToBigEndian((quint32)audioPort); + p.rxsample = qToBigEndian((quint32)rxSetup.sampleRate); + p.txsample = qToBigEndian((quint32)txSetup.sampleRate); + p.civport = qToBigEndian((quint32)civLocalPort); + p.audioport = qToBigEndian((quint32)audioLocalPort); p.txbuffer = qToBigEndian((quint32)txSetup.latency); p.convert = 1; sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); @@ -460,7 +586,7 @@ void udpHandler::sendAreYouThere() if (areYouThereCounter == 20) { qInfo(logUdp()) << this->metaObject()->className() << ": Radio not responding."; - emit haveNetworkStatus("Radio not responding!"); + status.message = "Radio not responding!"; } qInfo(logUdp()) << this->metaObject()->className() << ": Sending Are You There..."; @@ -485,8 +611,11 @@ void udpHandler::sendLogin() // Only used on control stream. p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; - p.code = 0x0170; // Not sure what this is? - p.innerseq = authSeq++; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requesttype = 0x00; + p.requestreply = 0x01; + + p.innerseq = qToBigEndian(authSeq++); p.tokrequest = tokRequest; memcpy(p.username, usernameEncoded.constData(), usernameEncoded.length()); memcpy(p.password, passwordEncoded.constData(), passwordEncoded.length()); @@ -505,9 +634,10 @@ void udpHandler::sendToken(uint8_t magic) p.len = sizeof(p); p.sentid = myId; p.rcvdid = remoteId; - p.code = 0x0130; // Not sure what this is? - p.res = magic; - p.innerseq = authSeq++; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requesttype = magic; + p.requestreply = 0x01; + p.innerseq = qToBigEndian(authSeq++); p.tokrequest = tokRequest; p.token = token; @@ -517,1025 +647,3 @@ void udpHandler::sendToken(uint8_t magic) return; } - -// Class that manages all Civ Data to/from the rig -udpCivData::udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort) -{ - qInfo(logUdp()) << "Starting udpCivData"; - localIP = local; - port = civPort; - radioIP = ip; - - udpBase::init(); // Perform connection - - QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpCivData::dataReceived); - - sendControl(false, 0x03, 0x00); // First connect packet - - /* - Connect various timers - */ - pingTimer = new QTimer(); - idleTimer = new QTimer(); - areYouThereTimer = new QTimer(); - startCivDataTimer = new QTimer(); - watchdogTimer = new QTimer(); - - connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); - connect(watchdogTimer, &QTimer::timeout, this, &udpCivData::watchdog); - connect(idleTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, true, 0, 0)); - connect(startCivDataTimer, &QTimer::timeout, this, std::bind(&udpCivData::sendOpenClose, this, false)); - connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); - watchdogTimer->start(WATCHDOG_PERIOD); - // Start sending are you there packets - will be stopped once "I am here" received - // send ping packets every 100 ms (maybe change to less frequent?) - pingTimer->start(PING_PERIOD); - // Send idle packets every 100ms, this timer will be reset everytime a non-idle packet is sent. - idleTimer->start(IDLE_PERIOD); - areYouThereTimer->start(AREYOUTHERE_PERIOD); -} - -udpCivData::~udpCivData() -{ - sendOpenClose(true); - if (startCivDataTimer != Q_NULLPTR) - { - startCivDataTimer->stop(); - delete startCivDataTimer; - startCivDataTimer = Q_NULLPTR; - } - if (pingTimer != Q_NULLPTR) - { - pingTimer->stop(); - delete pingTimer; - pingTimer = Q_NULLPTR; - } - if (idleTimer != Q_NULLPTR) - { - idleTimer->stop(); - delete idleTimer; - idleTimer = Q_NULLPTR; - } - if (watchdogTimer != Q_NULLPTR) - { - watchdogTimer->stop(); - delete watchdogTimer; - watchdogTimer = Q_NULLPTR; - } -} - -void udpCivData::watchdog() -{ - static bool alerted = false; - if (lastReceived.msecsTo(QTime::currentTime()) > 2000) - { - if (!alerted) { - qInfo(logUdp()) << " CIV Watchdog: no CIV data received for 2s, requesting data start."; - if (startCivDataTimer != Q_NULLPTR) - { - startCivDataTimer->start(100); - } - alerted = true; - } - } - else - { - alerted = false; - } -} - -void udpCivData::send(QByteArray d) -{ - //qInfo(logUdp()) << "Sending: (" << d.length() << ") " << d; - data_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p)+d.length(); - p.sentid = myId; - p.rcvdid = remoteId; - p.reply = (char)0xc1; - p.datalen = d.length(); - p.sendseq = qToBigEndian(sendSeqB); // THIS IS BIG ENDIAN! - - QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); - t.append(d); - sendTrackedPacket(t); - sendSeqB++; - return; -} - - -void udpCivData::sendOpenClose(bool close) -{ - uint8_t magic = 0x04; - - if (close) - { - magic = 0x00; - } - - openclose_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); - p.sentid = myId; - p.rcvdid = remoteId; - p.data = 0x01c0; // Not sure what other values are available: - p.sendseq = qToBigEndian(sendSeqB); - p.magic = magic; - - sendSeqB++; - - sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); - return; -} - - - -void udpCivData::dataReceived() -{ - while (udp->hasPendingDatagrams()) - { - QNetworkDatagram datagram = udp->receiveDatagram(); - //qInfo(logUdp()) << "Received: " << datagram.data(); - QByteArray r = datagram.data(); - - - switch (r.length()) - { - case (CONTROL_SIZE): // Control packet - { - control_packet_t in = (control_packet_t)r.constData(); - if (in->type == 0x04) - { - areYouThereTimer->stop(); - } - else if (in->type == 0x06) - { - // Update remoteId - remoteId = in->sentid; - // Manually send a CIV start request and start the timer if it isn't received. - // The timer will be stopped as soon as valid CIV data is received. - sendOpenClose(false); - if (startCivDataTimer != Q_NULLPTR) { - startCivDataTimer->start(100); - } - } - break; - } - default: - { - if (r.length() > 21) { - data_packet_t in = (data_packet_t)r.constData(); - if (in->type != 0x01) { - // Process this packet, any re-transmit requests will happen later. - //uint16_t gotSeq = qFromLittleEndian(r.mid(6, 2)); - // We have received some Civ data so stop sending Start packets! - if (startCivDataTimer != Q_NULLPTR) { - startCivDataTimer->stop(); - } - lastReceived = QTime::currentTime(); - if (quint16(in->datalen + 0x15) == (quint16)in->len) - { - //if (r.mid(0x15).length() != 157) - emit receive(r.mid(0x15)); - //qDebug(logUdp()) << "Got incoming CIV datagram" << r.mid(0x15).length(); - } - - } - } - break; - } - } - udpBase::dataReceived(r); // Call parent function to process the rest. - - r.clear(); - datagram.clear(); - - } -} - - -// Audio stream -udpAudio::udpAudio(QHostAddress local, QHostAddress ip, quint16 audioPort, audioSetup rxSetup, audioSetup txSetup) -{ - qInfo(logUdp()) << "Starting udpAudio"; - this->localIP = local; - this->port = audioPort; - this->radioIP = ip; - - if (txSetup.samplerate == 0) { - enableTx = false; - } - - init(); // Perform connection - - QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpAudio::dataReceived); - - rxaudio = new audioHandler(); - rxAudioThread = new QThread(this); - - rxaudio->moveToThread(rxAudioThread); - - rxAudioThread->start(QThread::TimeCriticalPriority); - - connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup))); - - // signal/slot not currently used. - connect(this, SIGNAL(haveAudioData(audioPacket)), rxaudio, SLOT(incomingAudio(audioPacket))); - connect(this, SIGNAL(haveChangeLatency(quint16)), rxaudio, SLOT(changeLatency(quint16))); - connect(this, SIGNAL(haveSetVolume(unsigned char)), rxaudio, SLOT(setVolume(unsigned char))); - connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater())); - - txSetup.radioChan = 1; - - txaudio = new audioHandler(); - txAudioThread = new QThread(this); - - txaudio->moveToThread(txAudioThread); - - txAudioThread->start(QThread::TimeCriticalPriority); - - connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup))); - - connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater())); - - sendControl(false, 0x03, 0x00); // First connect packet - - pingTimer = new QTimer(); - connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing); - pingTimer->start(PING_PERIOD); // send ping packets every 100ms - - if (enableTx) { - emit setupTxAudio(txSetup); - } - - emit setupRxAudio(rxSetup); - - watchdogTimer = new QTimer(); - connect(watchdogTimer, &QTimer::timeout, this, &udpAudio::watchdog); - watchdogTimer->start(WATCHDOG_PERIOD); - - txAudioTimer = new QTimer(); - txAudioTimer->setTimerType(Qt::PreciseTimer); - connect(txAudioTimer, &QTimer::timeout, this, &udpAudio::sendTxAudio); - - areYouThereTimer = new QTimer(); - connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0)); - areYouThereTimer->start(AREYOUTHERE_PERIOD); -} - -udpAudio::~udpAudio() -{ - if (pingTimer != Q_NULLPTR) - { - qDebug(logUdp()) << "Stopping pingTimer"; - pingTimer->stop(); - delete pingTimer; - pingTimer = Q_NULLPTR; - } - - if (idleTimer != Q_NULLPTR) - { - qDebug(logUdp()) << "Stopping idleTimer"; - idleTimer->stop(); - delete idleTimer; - idleTimer = Q_NULLPTR; - } - - if (watchdogTimer != Q_NULLPTR) - { - qDebug(logUdp()) << "Stopping watchdogTimer"; - watchdogTimer->stop(); - delete watchdogTimer; - watchdogTimer = Q_NULLPTR; - } - - if (txAudioTimer != Q_NULLPTR) - { - qDebug(logUdp()) << "Stopping txaudio timer"; - txAudioTimer->stop(); - delete txAudioTimer; - } - - if (rxAudioThread != Q_NULLPTR) { - qDebug(logUdp()) << "Stopping rxaudio thread"; - rxAudioThread->quit(); - rxAudioThread->wait(); - } - - if (txAudioThread != Q_NULLPTR) { - qDebug(logUdp()) << "Stopping txaudio thread"; - txAudioThread->quit(); - txAudioThread->wait(); - } - qDebug(logUdp()) << "udpHandler successfully closed"; -} - -void udpAudio::watchdog() -{ - static bool alerted = false; - if (lastReceived.msecsTo(QTime::currentTime()) > 2000) - { - if (!alerted) { - /* Just log it at the moment, maybe try signalling the control channel that it needs to - try requesting civ/audio again? */ - - qInfo(logUdp()) << " Audio Watchdog: no audio data received for 2s, restart required?"; - alerted = true; - } - } - else - { - alerted = false; - } -} - - -void udpAudio::sendTxAudio() -{ - if (txaudio == Q_NULLPTR) { - return; - } - QByteArray audio; - if (audioMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - txaudio->getNextAudioChunk(audio); - // Now we have the next audio chunk, we can release the mutex. - audioMutex.unlock(); - - if (audio.length() > 0) { - int counter = 1; - int len = 0; - - while (len < audio.length()) { - QByteArray partial = audio.mid(len, 1364); - audio_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p) + partial.length(); - p.sentid = myId; - p.rcvdid = remoteId; - if (partial.length() == 0xa0) { - p.ident = 0x9781; - } - else { - p.ident = 0x0080; // TX audio is always this? - } - p.datalen = (quint16)qToBigEndian((quint16)partial.length()); - p.sendseq = (quint16)qToBigEndian((quint16)sendAudioSeq); // THIS IS BIG ENDIAN! - QByteArray tx = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); - tx.append(partial); - len = len + partial.length(); - //qInfo(logUdp()) << "Sending audio packet length: " << tx.length(); - sendTrackedPacket(tx); - sendAudioSeq++; - counter++; - } - } - } - else { - qInfo(logUdpServer()) << "Unable to lock mutex for rxaudio"; - } -} - -void udpAudio::changeLatency(quint16 value) -{ - emit haveChangeLatency(value); -} - -void udpAudio::setVolume(unsigned char value) -{ - emit haveSetVolume(value); -} - -void udpAudio::dataReceived() -{ - while (udp->hasPendingDatagrams()) { - QNetworkDatagram datagram = udp->receiveDatagram(); - //qInfo(logUdp()) << "Received: " << datagram.data().mid(0,10); - QByteArray r = datagram.data(); - - switch (r.length()) - { - case (16): // Response to control packet handled in udpBase - { - control_packet_t in = (control_packet_t)r.constData(); - if (in->type == 0x04 && enableTx) - { - txAudioTimer->start(TXAUDIO_PERIOD); - } - - break; - } - default: - { - /* Audio packets start as follows: - PCM 16bit and PCM8/uLAW stereo: 0x44,0x02 for first packet and 0x6c,0x05 for second. - uLAW 8bit/PCM 8bit 0xd8,0x03 for all packets - PCM 16bit stereo 0x6c,0x05 first & second 0x70,0x04 third - - - */ - control_packet_t in = (control_packet_t)r.constData(); - - - if (in->type != 0x01 && in->len >= 0x20) { - if (in->seq == 0) - { - // Seq number has rolled over. - seqPrefix++; - } - - // 0xac is the smallest possible audio packet. - lastReceived = QTime::currentTime(); - audioPacket tempAudio; - tempAudio.seq = (quint32)seqPrefix << 16 | in->seq; - tempAudio.time = lastReceived; - tempAudio.sent = 0; - tempAudio.data = r.mid(0x18); - // Prefer signal/slot to forward audio as it is thread/safe - // Need to do more testing but latency appears fine. - //rxaudio->incomingAudio(tempAudio); - emit haveAudioData(tempAudio); - audioLatency = rxaudio->getLatency(); - } - break; - } - } - - udpBase::dataReceived(r); // Call parent function to process the rest. - r.clear(); - datagram.clear(); - } -} - - - -void udpBase::init() -{ -#if QT_VERSION < 0x060000 - timeStarted.start(); -#endif - udp = new QUdpSocket(this); - udp->bind(); // Bind to random port. - localPort = udp->localPort(); - qInfo(logUdp()) << "UDP Stream bound to local port:" << localPort << " remote port:" << port; - uint32_t addr = localIP.toIPv4Address(); - myId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (localPort & 0xffff); - - retransmitTimer = new QTimer(); - connect(retransmitTimer, &QTimer::timeout, this, &udpBase::sendRetransmitRequest); - retransmitTimer->start(RETRANSMIT_PERIOD); - -} - -udpBase::~udpBase() -{ - qInfo(logUdp()) << "Closing UDP stream :" << radioIP.toString() << ":" << port; - if (udp != Q_NULLPTR) { - sendControl(false, 0x05, 0x00); // Send disconnect - udp->close(); - delete udp; - } - if (areYouThereTimer != Q_NULLPTR) - { - areYouThereTimer->stop(); - delete areYouThereTimer; - } - - if (pingTimer != Q_NULLPTR) - { - pingTimer->stop(); - delete pingTimer; - } - if (idleTimer != Q_NULLPTR) - { - idleTimer->stop(); - delete idleTimer; - } - if (retransmitTimer != Q_NULLPTR) - { - retransmitTimer->stop(); - delete retransmitTimer; - } - - pingTimer = Q_NULLPTR; - idleTimer = Q_NULLPTR; - areYouThereTimer = Q_NULLPTR; - retransmitTimer = Q_NULLPTR; - -} - -// Base class! - -void udpBase::dataReceived(QByteArray r) -{ - if (r.length() < 0x10) - { - return; // Packet too small do to anything with? - } - - switch (r.length()) - { - case (CONTROL_SIZE): // Empty response used for simple comms and retransmit requests. - { - control_packet_t in = (control_packet_t)r.constData(); - if (in->type == 0x01) - { - // Single packet request - packetsLost++; - congestion ++; - txBufferMutex.lock(); - QMap::iterator match = txSeqBuf.find(in->seq); - if (match != txSeqBuf.end()) { - // Found matching entry? - // Send "untracked" as it has already been sent once. - // Don't constantly retransmit the same packet, give-up eventually - qDebug(logUdp()) << this->metaObject()->className() << ": Sending retransmit of " << hex << match->seqNum; - match->retransmitCount++; - udpMutex.lock(); - udp->writeDatagram(match->data, radioIP, port); - udpMutex.unlock(); - } - txBufferMutex.unlock(); - } - if (in->type == 0x04) { - qInfo(logUdp()) << this->metaObject()->className() << ": Received I am here "; - areYouThereCounter = 0; - // I don't think that we will ever receive an "I am here" other than in response to "Are you there?" - remoteId = in->sentid; - if (areYouThereTimer != Q_NULLPTR && areYouThereTimer->isActive()) { - // send ping packets every second - areYouThereTimer->stop(); - } - sendControl(false, 0x06, 0x01); // Send Are you ready - untracked. - } - else if (in->type == 0x06) - { - // Just get the seqnum and ignore the rest. - } - break; - } - case (PING_SIZE): // ping packet - { - ping_packet_t in = (ping_packet_t)r.constData(); - if (in->type == 0x07) - { - // It is a ping request/response - if (in->reply == 0x00) - { - ping_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); - p.type = 0x07; - p.sentid = myId; - p.rcvdid = remoteId; - p.reply = 0x01; - p.seq = in->seq; - p.time = in->time; - udpMutex.lock(); - udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); - udpMutex.unlock(); - } - else if (in->reply == 0x01) { - if (in->seq == pingSendSeq) - { - // This is response to OUR request so increment counter - pingSendSeq++; - } - else { - // Not sure what to do here, need to spend more time with the protocol but will try sending ping with same seq next time. - //qInfo(logUdp()) << this->metaObject()->className() << "Received out-of-sequence ping response. Sent:" << pingSendSeq << " received " << in->seq; - } - } - else { - qInfo(logUdp()) << this->metaObject()->className() << "Unhandled response to ping. I have never seen this! 0x10=" << r[16]; - } - - } - break; - } - default: - { - - // All packets "should" be added to the incoming buffer. - // First check that we haven't already received it. - - - } - break; - - } - - - // All packets except ping and retransmit requests should trigger this. - control_packet_t in = (control_packet_t)r.constData(); - - // This is a variable length retransmit request! - if (in->type == 0x01 && in->len != 0x10) - { - - for (quint16 i = 0x10; i < r.length(); i = i + 2) - { - quint16 seq = (quint8)r[i] | (quint8)r[i + 1] << 8; - QMap::iterator match = txSeqBuf.find(seq); - if (match == txSeqBuf.end()) { - qDebug(logUdp()) << this->metaObject()->className() << ": Requested packet " << hex << seq << " not found"; - // Just send idle packet. - sendControl(false, 0, seq); - } - else { - // Found matching entry? - // Send "untracked" as it has already been sent once. - qDebug(logUdp()) << this->metaObject()->className() << ": Sending retransmit (range) of " << hex << match->seqNum; - match->retransmitCount++; - udpMutex.lock(); - udp->writeDatagram(match->data, radioIP, port); - udpMutex.unlock(); - match++; - packetsLost++; - congestion++; - } - } - } - else if (in->len != PING_SIZE && in->type == 0x00 && in->seq != 0x00) - { - rxBufferMutex.lock(); - if (rxSeqBuf.isEmpty()) { - if (rxSeqBuf.size() > 400) - { - rxSeqBuf.erase(rxSeqBuf.begin()); - } - rxSeqBuf.insert(in->seq, QTime::currentTime()); - } - else - { - //std::sort(rxSeqBuf.begin(), rxSeqBuf.end()); - if (in->seq < rxSeqBuf.firstKey()) - { - qInfo(logUdp()) << this->metaObject()->className() << ": ******* seq number has rolled over ****** previous highest: " << hex << rxSeqBuf.lastKey() << " current: " << hex << in->seq; - //seqPrefix++; - // Looks like it has rolled over so clear buffer and start again. - rxSeqBuf.clear(); - rxMissing.clear(); - rxBufferMutex.unlock(); - return; - } - - if (!rxSeqBuf.contains(in->seq)) - { - // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. - rxSeqBuf.insert(in->seq, QTime::currentTime()); - if (rxSeqBuf.size() > 400) - { - rxSeqBuf.erase(rxSeqBuf.begin()); - } - } - else { - // This is probably one of our missing packets! - missingMutex.lock(); - QMap::iterator s = rxMissing.find(in->seq); - if (s != rxMissing.end()) - { - qDebug(logUdp()) << this->metaObject()->className() << ": Missing SEQ has been received! " << hex << in->seq; - s = rxMissing.erase(s); - } - missingMutex.unlock(); - - } - - } - rxBufferMutex.unlock(); - - } -} - -bool missing(quint16 i, quint16 j) { - return (i + 1 != j); -} - -void udpBase::sendRetransmitRequest() -{ - // Find all gaps in received packets and then send requests for them. - // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. - - QByteArray missingSeqs; - - rxBufferMutex.lock(); - if (!rxSeqBuf.empty() && rxSeqBuf.size() <= rxSeqBuf.lastKey() - rxSeqBuf.firstKey()) - { - if ((rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size()) > 20) - { - // Too many packets to process, flush buffers and start again! - qDebug(logUdp()) << "Too many missing packets, flushing buffer: " << rxSeqBuf.lastKey() << "missing=" << rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size() + 1; - rxMissing.clear(); - missingMutex.lock(); - rxSeqBuf.clear(); - missingMutex.unlock(); - } - else { - // We have at least 1 missing packet! - qDebug(logUdp()) << "Missing Seq: size=" << rxSeqBuf.size() << "firstKey=" << rxSeqBuf.firstKey() << "lastKey=" << rxSeqBuf.lastKey() << "missing=" << rxSeqBuf.lastKey() - rxSeqBuf.firstKey() - rxSeqBuf.size() + 1; - // We are missing packets so iterate through the buffer and add the missing ones to missing packet list - for (int i = 0; i < rxSeqBuf.keys().length() - 1; i++) { - missingMutex.lock(); - for (quint16 j = rxSeqBuf.keys()[i] + 1; j < rxSeqBuf.keys()[i + 1]; j++) { - auto s = rxMissing.find(j); - if (s == rxMissing.end()) - { - // We haven't seen this missing packet before - qDebug(logUdp()) << this->metaObject()->className() << ": Adding to missing buffer (len=" << rxMissing.size() << "): " << j; - if (rxMissing.size() > 25) - { - rxMissing.erase(rxMissing.begin()); - } - rxMissing.insert(j, 0); - if (rxSeqBuf.size() > 400) - { - rxSeqBuf.erase(rxSeqBuf.begin()); - } - rxSeqBuf.insert(j, QTime::currentTime()); // Add this missing packet to the rxbuffer as we now long about it. - packetsLost++; - } - else { - if (s.value() == 4) - { - // We have tried 4 times to request this packet, time to give up! - s = rxMissing.erase(s); - } - - } - } - missingMutex.unlock(); - } - } - } - rxBufferMutex.unlock(); - - missingMutex.lock(); - for (auto it = rxMissing.begin(); it != rxMissing.end(); ++it) - { - if (it.value() < 10) - { - missingSeqs.append(it.key() & 0xff); - missingSeqs.append(it.key() >> 8 & 0xff); - missingSeqs.append(it.key() & 0xff); - missingSeqs.append(it.key() >> 8 & 0xff); - it.value()++; - } - } - missingMutex.unlock(); - if (missingSeqs.length() != 0) - { - control_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.type = 0x01; - p.seq = 0x0000; - p.sentid = myId; - p.rcvdid = remoteId; - if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control. - { - p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8); - qDebug(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << hex << p.seq; - udpMutex.lock(); - udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); - udpMutex.unlock(); - } - else - { - qDebug(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(); - missingMutex.lock(); - missingSeqs.insert(0, p.packet, sizeof(p.packet)); - missingMutex.unlock(); - - udpMutex.lock(); - udp->writeDatagram(missingSeqs, radioIP, port); - udpMutex.unlock(); - } - } - -} - - - -// Used to send idle and other "control" style messages -void udpBase::sendControl(bool tracked = true, quint8 type = 0, quint16 seq = 0) -{ - control_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); - p.type = type; - p.sentid = myId; - p.rcvdid = remoteId; - - if (!tracked) { - p.seq = seq; - udpMutex.lock(); - udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); - udpMutex.unlock(); - } - else { - sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p))); - } - return; -} - -// Send periodic ping packets -void udpBase::sendPing() -{ - ping_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); - p.type = 0x07; - p.sentid = myId; - p.rcvdid = remoteId; - p.seq = pingSendSeq; - p.time = timeStarted.msecsSinceStartOfDay(); - lastPingSentTime = QDateTime::currentDateTime(); - udpMutex.lock(); - udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); - udpMutex.unlock(); - return; -} - -void udpBase::sendRetransmitRange(quint16 first, quint16 second, quint16 third, quint16 fourth) -{ - retransmit_range_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); - p.type = 0x00; - p.sentid = myId; - p.rcvdid = remoteId; - p.first = first; - p.second = second; - p.third = third; - p.fourth = fourth; - udpMutex.lock(); - udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port); - udpMutex.unlock(); - return; -} - - -void udpBase::sendTrackedPacket(QByteArray d) -{ - // As the radio can request retransmission of these packets, store them in a buffer - d[6] = sendSeq & 0xff; - d[7] = (sendSeq >> 8) & 0xff; - SEQBUFENTRY s; - s.seqNum = sendSeq; - s.timeSent = QTime::currentTime(); - s.retransmitCount = 0; - s.data = d; - if (txBufferMutex.tryLock(100)) - { - - if (sendSeq == 0) { - // We are either the first ever sent packet or have rolled-over so clear the buffer. - txSeqBuf.clear(); - congestion = 0; - } - txSeqBuf.insert(sendSeq,s); - if (txSeqBuf.size() > 400) - { - txSeqBuf.erase(txSeqBuf.begin()); - } - txBufferMutex.unlock(); - } else { - qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; - } - // Stop using purgeOldEntries() as it is likely slower than just removing the earliest packet. - //qInfo(logUdp()) << this->metaObject()->className() << "RX:" << rxSeqBuf.size() << "TX:" <writeDatagram(d, radioIP, port); - if (congestion>10) { // Poor quality connection? - udp->writeDatagram(d, radioIP, port); - if (congestion>20) // Even worse so send again. - udp->writeDatagram(d, radioIP, port); - } - if (idleTimer != Q_NULLPTR && idleTimer->isActive()) { - idleTimer->start(IDLE_PERIOD); // Reset idle counter if it's running - } - udpMutex.unlock(); - packetsSent++; - return; -} - - -/// -/// Once a packet has reached PURGE_SECONDS old (currently 10) then it is not likely to be any use. -/// -void udpBase::purgeOldEntries() -{ - // Erase old entries from the tx packet buffer - if (txBufferMutex.tryLock(100)) - { - if (!txSeqBuf.isEmpty()) - { - // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS - for (auto it = txSeqBuf.begin(); it != txSeqBuf.end();) { - if (it.value().timeSent.secsTo(QTime::currentTime()) > PURGE_SECONDS) { - txSeqBuf.erase(it++); - } - else { - break; - } - } - } - txBufferMutex.unlock(); - - } else { - qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked"; - } - - - - if (rxBufferMutex.tryLock(100)) - { - if (!rxSeqBuf.isEmpty()) { - // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS - for (auto it = rxSeqBuf.begin(); it != rxSeqBuf.end();) { - if (it.value().secsTo(QTime::currentTime()) > PURGE_SECONDS) { - rxSeqBuf.erase(it++); - } - else { - break; - } - } - } - rxBufferMutex.unlock(); - } else { - qInfo(logUdp()) << this->metaObject()->className() << ": rxBuffer mutex is locked"; - } - - if (missingMutex.tryLock(100)) - { - // Erase old entries from the missing packets buffer - if (!rxMissing.isEmpty() && rxMissing.size() > 50) { - for (size_t i = 0; i < 25; ++i) { - rxMissing.erase(rxMissing.begin()); - } - } - missingMutex.unlock(); - } else { - qInfo(logUdp()) << this->metaObject()->className() << ": missingBuffer mutex is locked"; - } -} - -/// -/// passcode function used to generate secure (ish) code -/// -/// -/// pointer to encoded username or password -void passcode(QString in, QByteArray& out) -{ - const quint8 sequence[] = - { - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0x47,0x5d,0x4c,0x42,0x66,0x20,0x23,0x46,0x4e,0x57,0x45,0x3d,0x67,0x76,0x60,0x41,0x62,0x39,0x59,0x2d,0x68,0x7e, - 0x7c,0x65,0x7d,0x49,0x29,0x72,0x73,0x78,0x21,0x6e,0x5a,0x5e,0x4a,0x3e,0x71,0x2c,0x2a,0x54,0x3c,0x3a,0x63,0x4f, - 0x43,0x75,0x27,0x79,0x5b,0x35,0x70,0x48,0x6b,0x56,0x6f,0x34,0x32,0x6c,0x30,0x61,0x6d,0x7b,0x2f,0x4b,0x64,0x38, - 0x2b,0x2e,0x50,0x40,0x3f,0x55,0x33,0x37,0x25,0x77,0x24,0x26,0x74,0x6a,0x28,0x53,0x4d,0x69,0x22,0x5c,0x44,0x31, - 0x36,0x58,0x3b,0x7a,0x51,0x5f,0x52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - }; - - QByteArray ba = in.toLocal8Bit(); - uchar* ascii = (uchar*)ba.constData(); - for (int i = 0; i < in.length() && i < 16; i++) - { - int p = ascii[i] + i; - if (p > 126) - { - p = 32 + p % 127; - } - out.append(sequence[p]); - } - return; -} - -/// -/// returns a QByteArray of a null terminated string -/// -/// -/// -/// -QByteArray parseNullTerminatedString(QByteArray c, int s) -{ - //QString res = ""; - QByteArray res; - for (int i = s; i < c.length(); i++) - { - if (c[i] != '\0') - { - res.append(c[i]); - } - else - { - break; - } - } - return res; -} - diff --git a/udphandler.h b/udphandler.h index 1a8e975..8300805 100644 --- a/udphandler.h +++ b/udphandler.h @@ -1,6 +1,7 @@ #ifndef UDPHANDLER_H #define UDPHANDLER_H + #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include // Allow easy endian-ness conversions #include @@ -21,186 +23,11 @@ #include -#include "audiohandler.h" #include "packettypes.h" - -#define PURGE_SECONDS 10 -#define TOKEN_RENEWAL 60000 -#define PING_PERIOD 100 -#define IDLE_PERIOD 100 -#define TXAUDIO_PERIOD 10 -#define AREYOUTHERE_PERIOD 500 -#define WATCHDOG_PERIOD 500 -#define RETRANSMIT_PERIOD 100 -#define LOCK_PERIOD 100 - -struct udpPreferences { - QString ipAddress; - quint16 controlLANPort; - quint16 serialLANPort; - quint16 audioLANPort; - QString username; - QString password; - QString clientName; -}; - -void passcode(QString in, QByteArray& out); -QByteArray parseNullTerminatedString(QByteArray c, int s); - -// Parent class that contains all common items. -class udpBase : public QObject -{ - - -public: - ~udpBase(); - - void init(); - - void dataReceived(QByteArray r); - void sendPing(); - void sendRetransmitRange(quint16 first, quint16 second, quint16 third,quint16 fourth); - - void sendControl(bool tracked,quint8 id, quint16 seq); - - QTime timeStarted; - - QUdpSocket* udp=Q_NULLPTR; - uint32_t myId = 0; - uint32_t remoteId = 0; - uint8_t authSeq = 0x00; - uint16_t sendSeqB = 0; - uint16_t sendSeq = 1; - uint16_t lastReceivedSeq = 1; - uint16_t pkt0SendSeq = 0; - uint16_t periodicSeq = 0; - quint64 latency = 0; - - QString username = ""; - QString password = ""; - QHostAddress radioIP; - QHostAddress localIP; - bool isAuthenticated = false; - quint16 localPort=0; - quint16 port=0; - bool periodicRunning = false; - bool sentPacketConnect2 = false; - QTime lastReceived =QTime::currentTime(); - QMutex udpMutex; - QMutex txBufferMutex; - QMutex rxBufferMutex; - QMutex missingMutex; - - struct SEQBUFENTRY { - QTime timeSent; - uint16_t seqNum; - QByteArray data; - quint8 retransmitCount; - }; - - QMap rxSeqBuf; - QMap txSeqBuf; - QMap rxMissing; - - void sendTrackedPacket(QByteArray d); - void purgeOldEntries(); - - QTimer* areYouThereTimer = Q_NULLPTR; // Send are-you-there packets every second until a response is received. - QTimer* pingTimer = Q_NULLPTR; // Start sending pings immediately. - QTimer* idleTimer = Q_NULLPTR; // Start watchdog once we are connected. - - QTimer* watchdogTimer = Q_NULLPTR; - QTimer* retransmitTimer = Q_NULLPTR; - - QDateTime lastPingSentTime; - uint16_t pingSendSeq = 0; - - quint16 areYouThereCounter=0; - - quint32 packetsSent=0; - quint32 packetsLost=0; - - quint16 seqPrefix = 0; - QString connectionType=""; - int congestion = 0; - - -private: - void sendRetransmitRequest(); - -}; - - -// Class for all (pseudo) serial communications -class udpCivData : public udpBase -{ - Q_OBJECT - -public: - udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort); - ~udpCivData(); - QMutex serialmutex; - -signals: - int receive(QByteArray); - -public slots: - void send(QByteArray d); - - -private: - void watchdog(); - void dataReceived(); - void sendOpenClose(bool close); - - QTimer* startCivDataTimer = Q_NULLPTR; -}; - - -// Class for all audio communications. -class udpAudio : public udpBase -{ - Q_OBJECT - -public: - udpAudio(QHostAddress local, QHostAddress ip, quint16 aport, audioSetup rxSetup, audioSetup txSetup); - ~udpAudio(); - - int audioLatency = 0; - -signals: - void haveAudioData(audioPacket data); - - void setupTxAudio(audioSetup setup); - void setupRxAudio(audioSetup setup); - - void haveChangeLatency(quint16 value); - void haveSetVolume(unsigned char value); - -public slots: - void changeLatency(quint16 value); - void setVolume(unsigned char value); - -private: - - void sendTxAudio(); - void dataReceived(); - void watchdog(); - - uint16_t sendAudioSeq = 0; - - audioHandler* rxaudio = Q_NULLPTR; - QThread* rxAudioThread = Q_NULLPTR; - - audioHandler* txaudio = Q_NULLPTR; - QThread* txAudioThread = Q_NULLPTR; - - QTimer* txAudioTimer=Q_NULLPTR; - bool enableTx = true; - - QMutex audioMutex; - -}; +#include "audiohandler.h" +#include "udpbase.h" +#include "udpcivdata.h" +#include "udpaudio.h" @@ -218,6 +45,8 @@ public: udpCivData* civ = Q_NULLPTR; udpAudio* audio = Q_NULLPTR; + unsigned char numRadios; + QList radios; public slots: void receiveDataFromUserToRig(QByteArray); // This slot will send data on to @@ -226,6 +55,10 @@ public slots: void changeLatency(quint16 value); void setVolume(unsigned char value); void init(); + void setCurrentRadio(quint8 radio); + void getRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + void getTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over); + signals: void haveDataFromPort(QByteArray data); // emit this when we have data, connect to rigcommander @@ -233,9 +66,10 @@ signals: void haveNetworkError(QString, QString); void haveChangeLatency(quint16 value); void haveSetVolume(unsigned char value); - void haveNetworkStatus(QString); + void haveNetworkStatus(networkStatus); void haveBaudRate(quint32 baudrate); - + void requestRadioSelection(QList radios); + void setRadioUsage(quint8, quint8 busy, QString name, QString mac); private: void sendAreYouThere(); @@ -259,6 +93,9 @@ private: quint16 civPort; quint16 audioPort; + quint16 civLocalPort; + quint16 audioLocalPort; + audioSetup rxSetup; audioSetup txSetup; @@ -270,9 +107,9 @@ private: quint16 tokRequest; quint32 token; // These are for stream ident info. - char identa; - quint32 identb; - + quint8 macaddress[8]; + quint8 guid[GUIDLEN]; + bool useGuid = false; QByteArray usernameEncoded; QByteArray passwordEncoded; @@ -283,6 +120,8 @@ private: quint8 civId = 0; quint16 rxSampleRates = 0; quint16 txSampleRates = 0; + networkStatus status; + bool splitWf = false; }; diff --git a/udpserver.cpp b/udpserver.cpp index 634902a..b99f2eb 100644 --- a/udpserver.cpp +++ b/udpserver.cpp @@ -3,10 +3,10 @@ #define STALE_CONNECTION 15 #define LOCK_PERIOD 10 // time to attempt to lock Mutex in ms -udpServer::udpServer(SERVERCONFIG config, audioSetup outAudio, audioSetup inAudio) : - config(config), - outAudio(outAudio), - inAudio(inAudio) +#define AUDIO_SEND_PERIOD 20 // +udpServer::udpServer(SERVERCONFIG* config, QObject* parent) : + QObject(parent), + config(config) { qInfo(logUdpServer()) << "Starting udp server"; } @@ -14,11 +14,33 @@ udpServer::udpServer(SERVERCONFIG config, audioSetup outAudio, audioSetup inAudi void udpServer::init() { - srand(time(NULL)); // Generate random key -#if QT_VERSION < 0x060000 - timeStarted.start(); -#endif - + for (RIGCONFIG* rig : config->rigs) + { + qDebug(logUdpServer()) << "CIV:" << rig->civAddr; + qDebug(logUdpServer()) << "Model:" << rig->modelName; + qDebug(logUdpServer()) << "Name:" << rig->rigName; + qDebug(logUdpServer()) << "CIV:" << rig->civAddr; + qDebug(logUdpServer()).noquote() << QString("GUID: {%1%2%3%4-%5%6-%7%8-%9%10-%11%12%13%14%15%16}") + .arg(rig->guid[0], 2, 16, QLatin1Char('0')) + .arg(rig->guid[1], 2, 16, QLatin1Char('0')) + .arg(rig->guid[2], 2, 16, QLatin1Char('0')) + .arg(rig->guid[3], 2, 16, QLatin1Char('0')) + .arg(rig->guid[4], 2, 16, QLatin1Char('0')) + .arg(rig->guid[5], 2, 16, QLatin1Char('0')) + .arg(rig->guid[6], 2, 16, QLatin1Char('0')) + .arg(rig->guid[7], 2, 16, QLatin1Char('0')) + .arg(rig->guid[8], 2, 16, QLatin1Char('0')) + .arg(rig->guid[9], 2, 16, QLatin1Char('0')) + .arg(rig->guid[10], 2, 16, QLatin1Char('0')) + .arg(rig->guid[11], 2, 16, QLatin1Char('0')) + .arg(rig->guid[12], 2, 16, QLatin1Char('0')) + .arg(rig->guid[13], 2, 16, QLatin1Char('0')) + .arg(rig->guid[14], 2, 16, QLatin1Char('0')) + .arg(rig->guid[15], 2, 16, QLatin1Char('0')) + ; + } + srand(time(NULL)); // Generate random + //timeStarted.start(); // Convoluted way to find the external IP address, there must be a better way???? QString localhostname = QHostInfo::localHostName(); QList hostList = QHostInfo::fromName(localhostname).addresses(); @@ -30,36 +52,40 @@ void udpServer::init() } } + QString macTemp; foreach(QNetworkInterface netInterface, QNetworkInterface::allInterfaces()) { // Return only the first non-loopback MAC Address if (!(netInterface.flags() & QNetworkInterface::IsLoopBack)) { - macAddress = netInterface.hardwareAddress(); + macTemp = netInterface.hardwareAddress(); } } + memcpy(&macAddress, macTemp.toLocal8Bit(), 6); + memcpy(&macAddress, QByteArrayLiteral("\x00\x90\xc7").constData(), 3); + uint32_t addr = localIP.toIPv4Address(); - qInfo(logUdpServer()) << " My IP Address: " << QHostAddress(addr).toString() << " My MAC Address: " << macAddress; + qInfo(logUdpServer()) << "My IP Address:" << QHostAddress(addr).toString(); - controlId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.controlPort & 0xffff); - civId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.civPort & 0xffff); - audioId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config.audioPort & 0xffff); + controlId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config->controlPort & 0xffff); + civId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config->civPort & 0xffff); + audioId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (config->audioPort & 0xffff); - qInfo(logUdpServer()) << "Server Binding Control to: " << config.controlPort; + qInfo(logUdpServer()) << "Server Binding Control to: " << config->controlPort; udpControl = new QUdpSocket(this); - udpControl->bind(config.controlPort); + udpControl->bind(config->controlPort); QUdpSocket::connect(udpControl, &QUdpSocket::readyRead, this, &udpServer::controlReceived); - qInfo(logUdpServer()) << "Server Binding CIV to: " << config.civPort; + qInfo(logUdpServer()) << "Server Binding CIV to: " << config->civPort; udpCiv = new QUdpSocket(this); - udpCiv->bind(config.civPort); + udpCiv->bind(config->civPort); QUdpSocket::connect(udpCiv, &QUdpSocket::readyRead, this, &udpServer::civReceived); - qInfo(logUdpServer()) << "Server Binding Audio to: " << config.audioPort; + qInfo(logUdpServer()) << "Server Binding Audio to: " << config->audioPort; udpAudio = new QUdpSocket(this); - udpAudio->bind(config.audioPort); + udpAudio->bind(config->audioPort); QUdpSocket::connect(udpAudio, &QUdpSocket::readyRead, this, &udpServer::audioReceived); wdTimer = new QTimer(); @@ -86,7 +112,7 @@ udpServer::~udpServer() deleteConnection(&audioClients, client); } - // Now all connections are deleted, close and delete the sockets. + // Now all connections are deleted, close and delete the sockets if (udpControl != Q_NULLPTR) { udpControl->close(); delete udpControl; @@ -99,17 +125,26 @@ udpServer::~udpServer() udpAudio->close(); delete udpAudio; } - emit haveNetworkStatus(QString("")); - + status.message = QString(""); + emit haveNetworkStatus(status); } void udpServer::receiveRigCaps(rigCapabilities caps) { - this->rigCaps = caps; + for (RIGCONFIG* rig: config->rigs) { + if (!memcmp(rig->guid, caps.guid, GUIDLEN) || config->rigs.size()==1) { + // Matching rig, fill-in missing details + rig->rigAvailable = true; + rig->modelName = caps.modelName; + rig->civAddr = caps.civ; + if (rig->rigName == "") { + rig->rigName = caps.modelName; + } + } + } } -#define RETRANSMIT_PERIOD 100 void udpServer::controlReceived() { @@ -141,8 +176,8 @@ void udpServer::controlReceived() current->timeConnected = QDateTime::currentDateTime(); current->ipAddress = datagram.senderAddress(); current->port = datagram.senderPort(); - current->civPort = config.civPort; - current->audioPort = config.audioPort; + current->civPort = config->civPort; + current->audioPort = config->audioPort; current->myId = controlId; current->remoteId = qFromLittleEndian(r.mid(8, 4)); current->socket = udpControl; @@ -160,12 +195,18 @@ void udpServer::controlReceived() connect(current->retransmitTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRetransmitRequest, this, current)); current->retransmitTimer->start(RETRANSMIT_PERIOD); - - current->commonCap = 0x8010; qInfo(logUdpServer()) << current->ipAddress.toString() << ": New Control connection created"; + if (connMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { + // Quick hack to replace the GUID with a MAC address. + if (config->rigs.size() == 1) { + memset(config->rigs.first()->guid, 0, GUIDLEN); + config->rigs.first()->commoncap = (quint16)0x8010; + memcpy(config->rigs.first()->macaddress, macAddress, 6); + memcpy(current->guid, config->rigs.first()->guid, GUIDLEN); + } controlClients.append(current); connMutex.unlock(); } @@ -211,15 +252,9 @@ void udpServer::controlReceived() sendPing(&controlClients, current, in->seq, true); } else if (in->reply == 0x01) { - if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) - { - // A Reply to our ping! - if (in->seq == current->pingSeq) { - current->pingSeq++; - } - else { - qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; - } + // A Reply to our ping! + if (in->seq == current->pingSeq) { + current->pingSeq++; } } } @@ -231,28 +266,34 @@ void udpServer::controlReceived() token_packet_t in = (token_packet_t)r.constData(); current->rxSeq = in->seq; current->authInnerSeq = in->innerseq; - current->identa = in->identa; - current->identb = in->identb; - if (in->res == 0x02) { + memcpy(current->guid, in->guid, GUIDLEN); + if (in->requesttype == 0x02 && in->requestreply == 0x01) { // Request for new token qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received create token request"; sendCapabilities(current); - sendConnectionInfo(current); + for (RIGCONFIG* radio : config->rigs) { + sendConnectionInfo(current, radio->guid); + } } - else if (in->res == 0x01) { + else if (in->requesttype == 0x01 && in->requestreply == 0x01) { // Token disconnect qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received token disconnect request"; - sendTokenResponse(current, in->res); + sendTokenResponse(current, in->requesttype); } - else if (in->res == 0x04) { + else if (in->requesttype == 0x04 && in->requestreply == 0x01) { // Disconnect audio/civ - sendTokenResponse(current, in->res); + sendTokenResponse(current, in->requesttype); current->isStreaming = false; - sendConnectionInfo(current); + for (RIGCONFIG* radio : config->rigs) { + if (!memcmp(radio->guid, current->guid, GUIDLEN) || config->rigs.size() == 1) + { + sendConnectionInfo(current, radio->guid); + } + } } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received token request"; - sendTokenResponse(current, in->res); + sendTokenResponse(current, in->requesttype); } break; } @@ -260,7 +301,7 @@ void udpServer::controlReceived() { login_packet_t in = (login_packet_t)r.constData(); qInfo(logUdpServer()) << current->ipAddress.toString() << ": Received 'login'"; - foreach(SERVERUSER user, config.users) + foreach(SERVERUSER user, config->users) { QByteArray usercomp; passcode(user.username, usercomp); @@ -303,67 +344,114 @@ void udpServer::controlReceived() current->txSampleRate = qFromBigEndian(in->txsample); current->txBufferLen = qFromBigEndian(in->txbuffer); current->authInnerSeq = in->innerseq; - current->identa = in->identa; - current->identb = in->identb; + + memcpy(current->guid, in->guid, GUIDLEN); sendStatus(current); current->authInnerSeq = 0x00; - sendConnectionInfo(current); + sendConnectionInfo(current,in->guid); qInfo(logUdpServer()) << current->ipAddress.toString() << ": rxCodec:" << current->rxCodec << " txCodec:" << current->txCodec << " rxSampleRate" << current->rxSampleRate << " txSampleRate" << current->txSampleRate << " txBufferLen" << current->txBufferLen; - if (!config.lan) { - // Radio is connected by USB/Serial and we assume that audio is connected as well. - // Create audio TX/RX threads if they don't already exist (first client chooses samplerate/codec) - audioSetup setup; - setup.resampleQuality = config.resampleQuality; - - if (txaudio == Q_NULLPTR) + audioSetup setup; + setup.resampleQuality = config->resampleQuality; + for (RIGCONFIG* radio : config->rigs) { + if ((!memcmp(radio->guid, current->guid, GUIDLEN) || config->rigs.size()==1) && radio->txaudio == Q_NULLPTR && !config->lan) { - outAudio.codec = current->txCodec; - outAudio.samplerate = current->txSampleRate; - outAudio.latency = current->txBufferLen; + radio->txAudioSetup.codec = current->txCodec; + radio->txAudioSetup.sampleRate=current->txSampleRate; + radio->txAudioSetup.isinput = false; + radio->txAudioSetup.latency = current->txBufferLen; - txaudio = new audioHandler(); - txAudioThread = new QThread(this); + outAudio.isinput = false; - txaudio->moveToThread(txAudioThread); - txAudioThread->start(QThread::TimeCriticalPriority); + if (radio->txAudioSetup.type == qtAudio) { + radio->txaudio = new audioHandler(); + } + else if (radio->txAudioSetup.type == portAudio) { + radio->txaudio = new paHandler(); + } + else if (radio->txAudioSetup.type == rtAudio) { + radio->txaudio = new rtHandler(); + } + else + { + qCritical(logAudio()) << "Unsupported Transmit Audio Handler selected!"; + } - connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup))); - connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater())); + //radio->txaudio = new audioHandler(); + radio->txAudioThread = new QThread(this); + radio->txAudioThread->setObjectName("txAudio()"); - emit setupTxAudio(outAudio); + + radio->txaudio->moveToThread(radio->txAudioThread); + + radio->txAudioThread->start(QThread::TimeCriticalPriority); + + connect(this, SIGNAL(setupTxAudio(audioSetup)), radio->txaudio, SLOT(init(audioSetup))); + connect(radio->txAudioThread, SIGNAL(finished()), radio->txaudio, SLOT(deleteLater())); + + // Not sure how we make this work in QT5.9? +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(radio->txaudio, [=]() { + radio->txaudio->init(radio->txAudioSetup); + }, Qt::QueuedConnection); +#else + emit setupTxAudio(radio->txAudioSetup); + #warning "QT 5.9 is not fully supported multiple rigs will NOT work!" +#endif hasTxAudio = datagram.senderAddress(); - connect(this, SIGNAL(haveAudioData(audioPacket)), txaudio, SLOT(incomingAudio(audioPacket))); + connect(this, SIGNAL(haveAudioData(audioPacket)), radio->txaudio, SLOT(incomingAudio(audioPacket))); } - if (rxaudio == Q_NULLPTR) + if ((!memcmp(radio->guid, current->guid, GUIDLEN) || config->rigs.size() == 1) && radio->rxaudio == Q_NULLPTR && !config->lan) { - inAudio.codec = current->rxCodec; - inAudio.samplerate = current->rxSampleRate; + radio->rxAudioSetup.codec = current->rxCodec; + radio->rxAudioSetup.sampleRate=current->rxSampleRate; + radio->rxAudioSetup.latency = current->txBufferLen; + radio->rxAudioSetup.isinput = true; + memcpy(radio->rxAudioSetup.guid, radio->guid, GUIDLEN); - rxaudio = new audioHandler(); + //radio->rxaudio = new audioHandler(); + if (radio->rxAudioSetup.type == qtAudio) { + radio->rxaudio = new audioHandler(); + } + else if (radio->rxAudioSetup.type == portAudio) { + radio->rxaudio = new paHandler(); + } + else if (radio->rxAudioSetup.type == rtAudio) { + radio->rxaudio = new rtHandler(); + } + else + { + qCritical(logAudio()) << "Unsupported Receive Audio Handler selected!"; + } - rxAudioThread = new QThread(this); - rxaudio->moveToThread(rxAudioThread); + radio->rxAudioThread = new QThread(this); + radio->rxAudioThread->setObjectName("rxAudio()"); - rxAudioThread->start(QThread::TimeCriticalPriority); + radio->rxaudio->moveToThread(radio->rxAudioThread); - connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup))); - connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater())); + radio->rxAudioThread->start(QThread::TimeCriticalPriority); - emit setupRxAudio(inAudio); + connect(radio->rxAudioThread, SIGNAL(finished()), radio->rxaudio, SLOT(deleteLater())); + connect(radio->rxaudio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket))); + +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(radio->rxaudio, [=]() { + radio->rxaudio->init(radio->rxAudioSetup); + }, Qt::QueuedConnection); +#else + //#warning "QT 5.9 is not fully supported multiple rigs will NOT work!" + connect(this, SIGNAL(setupRxAudio(audioSetup)), radio->rxaudio, SLOT(init(audioSetup))); + setupRxAudio(radio->rxAudioSetup); +#endif - rxAudioTimer = new QTimer(); - rxAudioTimer->setTimerType(Qt::PreciseTimer); - connect(rxAudioTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRxAudio, this)); - rxAudioTimer->start(20); } } @@ -419,6 +507,7 @@ void udpServer::civReceived() { current->controlClient = client; client->civClient = current; + memcpy(current->guid, client->guid, GUIDLEN); } } } @@ -486,15 +575,9 @@ void udpServer::civReceived() sendPing(&civClients, current, in->seq, true); } else if (in->reply == 0x01) { - if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) - { - // A Reply to our ping! - if (in->seq == current->pingSeq) { - current->pingSeq++; - } - else { - qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; - } + // A Reply to our ping! + if (in->seq == current->pingSeq) { + current->pingSeq++; } } } @@ -515,17 +598,34 @@ void udpServer::civReceived() if (current->civId == 0 && r.length() > lastFE + 2 && (quint8)r[lastFE+2] != 0xE1 && (quint8)r[lastFE + 2] > (quint8)0xdf && (quint8)r[lastFE + 2] < (quint8)0xef) { // This is (should be) the remotes CIV id. current->civId = (quint8)r[lastFE + 2]; - qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected remote CI-V:" << hex << current->civId; + qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected remote CI-V:" << QString("0x%1").arg(current->civId,0,16); } else if (current->civId != 0 && r.length() > lastFE + 2 && (quint8)r[lastFE+2] != 0xE1 && (quint8)r[lastFE + 2] != current->civId) { current->civId = (quint8)r[lastFE + 2]; - qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << hex << current->civId; qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << hex << current->civId; + qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << QString("0x%1").arg(current->civId,0,16); + qInfo(logUdpServer()) << current->ipAddress.toString() << ": Detected different remote CI-V:" << QString("0x%1").arg(current->civId,0,16); } else if (r.length() > lastFE+2 && (quint8)r[lastFE+2] != 0xE1) { - qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected invalid remote CI-V:" << hex << (quint8)r[lastFE+2]; - } + qDebug(logUdpServer()) << current->ipAddress.toString() << ": Detected invalid remote CI-V:" << QString("0x%1").arg((quint8)r[lastFE+2],0,16); + } + + for (RIGCONFIG* radio : config->rigs) { + if (!memcmp(radio->guid, current->guid, sizeof(radio->guid)) || config->rigs.size()==1) + { + // Only send to the rig that it belongs to! + //qDebug(logUdpServer()) << "Sending data" << r.mid(0x15); +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(radio->rig, [=]() { + radio->rig->dataFromServer(r.mid(0x15));; + }, Qt::DirectConnection); +#else + #warning "QT 5.9 is not fully supported, multiple rigs will NOT work!" + emit haveDataFromServer(r.mid(0x15)); +#endif + + } + } - emit haveDataFromServer(r.mid(0x15)); } else { qInfo(logUdpServer()) << current->ipAddress.toString() << ": Datalen mismatch " << quint16(in->datalen + 0x15) << ":" << (quint16)in->len; @@ -576,6 +676,7 @@ void udpServer::audioReceived() { current->controlClient = client; client->audioClient = current; + memcpy(current->guid, client->guid, GUIDLEN); } } } @@ -599,7 +700,7 @@ void udpServer::audioReceived() current->pingTimer = new QTimer(); connect(current->pingTimer, &QTimer::timeout, this, std::bind(&udpServer::sendPing, this, &audioClients, current, (quint16)0x00, false)); - current->pingTimer->start(100); + current->pingTimer->start(PING_PERIOD); current->retransmitTimer = new QTimer(); connect(current->retransmitTimer, &QTimer::timeout, this, std::bind(&udpServer::sendRetransmitRequest, this, current)); @@ -633,15 +734,9 @@ void udpServer::audioReceived() sendPing(&audioClients, current, in->seq, true); } else if (in->reply == 0x01) { - if (in->seq == current->pingSeq || in->seq == current->pingSeq - 1) - { - // A Reply to our ping! - if (in->seq == current->pingSeq) { - current->pingSeq++; - } - else { - qInfo(logUdpServer()) << current->ipAddress.toString() << ": got out of sequence ping reply. Got: " << in->seq << " expecting: " << current->pingSeq; - } + // A Reply to our ping! + if (in->seq == current->pingSeq) { + current->pingSeq++; } } } @@ -723,17 +818,15 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) if (current->idleTimer != Q_NULLPTR && !current->idleTimer->isActive()) { current->idleTimer->start(100); } - } // This is a retransmit request - else if (in->type == 0x01) + } // This is a single packet retransmit request + else if (in->type == 0x01 && in->len == 0x10) { - // Single packet request - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Received 'retransmit' request for " << in->seq; - QMap::iterator match = current->txSeqBuf.find(in->seq); + auto match = current->txSeqBuf.find(in->seq); if (match != current->txSeqBuf.end() && match->retransmitCount < 5) { // Found matching entry? // Don't constantly retransmit the same packet, give-up eventually - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending retransmit of " << hex << match->seqNum; + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending (single packet) retransmit of " << QString("0x%1").arg(match->seqNum, 0, 16); match->retransmitCount++; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { @@ -747,6 +840,10 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) } else { // Just send an idle! + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << + "): Requested (single) packet " << QString("0x%1").arg(in->seq, 0, 16) << + "not found, have " << QString("0x%1").arg(current->txSeqBuf.firstKey(), 0, 16) << + "to" << QString("0x%1").arg(current->txSeqBuf.lastKey(), 0, 16); sendControl(current, 0x00, in->seq); } } @@ -754,7 +851,7 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) } default: { - //break; + //break } } @@ -765,11 +862,14 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) { for (quint16 i = 0x10; i < r.length(); i = i + 2) { - auto match = std::find_if(current->txSeqBuf.begin(), current->txSeqBuf.end(), [&cs = in->seq](SEQBUFENTRY& s) { - return s.seqNum == cs; - }); + quint16 seq = (quint8)r[i] | (quint8)r[i + 1] << 8; + auto match = current->txSeqBuf.find(seq); if (match == current->txSeqBuf.end()) { - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Requested packet " << hex << in->seq << " not found"; + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << + "): Requested (multiple) packet " << QString("0x%1").arg(seq,0,16) << + "not found, have " << QString("0x%1").arg(current->txSeqBuf.firstKey(), 0, 16) << + "to" << QString("0x%1").arg(current->txSeqBuf.lastKey(), 0, 16); + // Just send idle packet. sendControl(current, 0, in->seq); } @@ -777,7 +877,7 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) { // Found matching entry? // Send "untracked" as it has already been sent once. - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending retransmit of " << hex << match->seqNum; + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Sending (multiple packet) retransmit of " << QString("0x%1").arg(match->seqNum,0,16); match->retransmitCount++; if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { @@ -787,8 +887,6 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } - - match++; } } } @@ -812,9 +910,10 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) else { - if (in->seq < current->rxSeqBuf.firstKey()) + if (in->seq < current->rxSeqBuf.firstKey() || in->seq - current->rxSeqBuf.lastKey() > MAX_MISSING) { - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): ******* seq number may have rolled over ****** previous highest: " << hex << current->rxSeqBuf.lastKey() << " current: " << hex << in->seq; + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "Large seq number gap detected, previous highest: " << + QString("0x%1").arg(current->rxSeqBuf.lastKey(), 0, 16) << " current: " << QString("0x%1").arg(in->seq, 0, 16); // Looks like it has rolled over so clear buffer and start again. if (current->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { @@ -839,14 +938,45 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) if (!current->rxSeqBuf.contains(in->seq)) { - // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. if (current->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - if (current->rxSeqBuf.size() > 400) - { - current->rxSeqBuf.remove(0); + // Add incoming packet to the received buffer and if it is in the missing buffer, remove it. + //int missCounter = 0; + if (in->seq > current->rxSeqBuf.lastKey() + 1) { + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "1 or more missing packets detected, previous: " << + QString("0x%1").arg(current->rxSeqBuf.lastKey(), 0, 16) << " current: " << QString("0x%1").arg(in->seq, 0, 16); + // We are likely missing packets then! + if (current->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) + { + + for (quint16 f = current->rxSeqBuf.lastKey() + 1; f <= in->seq; f++) + { + + if (current->rxSeqBuf.size() > BUFSIZE) + { + current->rxSeqBuf.remove(current->rxSeqBuf.firstKey()); + } + current->rxSeqBuf.insert(f, QTime::currentTime()); + + if (f != in->seq && !current->rxMissing.contains(f)) + { + current->rxMissing.insert(f, 0); + } + } + current->missMutex.unlock(); + } + else { + qInfo(logUdpServer()) << "Unable to lock missMutex()"; + } + } + else { + + if (current->rxSeqBuf.size() > BUFSIZE) + { + current->rxSeqBuf.remove(current->rxSeqBuf.firstKey()); + } + current->rxSeqBuf.insert(in->seq, QTime::currentTime()); } - current->rxSeqBuf.insert(in->seq, QTime::currentTime()); current->rxMutex.unlock(); } else { @@ -857,10 +987,10 @@ void udpServer::commonReceived(QList* l, CLIENT* current, QByteArray r) // Check whether this is one of our missing ones! if (current->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - QMap::iterator s = current->rxMissing.find(in->seq); + auto s = current->rxMissing.find(in->seq); if (s != current->rxMissing.end()) { - qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Missing SEQ has been received! " << hex << in->seq; + qInfo(logUdpServer()) << current->ipAddress.toString() << "(" << current->type << "): Missing SEQ has been received! " << QString("0x%1").arg(in->seq,0,16); s = current->rxMissing.erase(s); } current->missMutex.unlock(); @@ -891,13 +1021,22 @@ void udpServer::sendControl(CLIENT* c, quint8 type, quint16 seq) { p.seq = c->txSeq; SEQBUFENTRY s; - s.seqNum = seq; + s.seqNum = c->txSeq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - c->txSeqBuf.insert(seq, s); + if (c->txSeq == 0) { + c->txSeqBuf.clear(); + } + else + if (c->txSeqBuf.size() > BUFSIZE) + { + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); + } + + c->txSeqBuf.insert(c->txSeq, s); c->txSeq++; c->txMutex.unlock(); } @@ -955,7 +1094,8 @@ void udpServer::sendPing(QList* l, CLIENT* c, quint16 seq, bool reply) pingTime = c->rxPingTime; } else { - pingTime = (quint32)timeStarted.msecsSinceStartOfDay(); + QTime now=QTime::currentTime(); + pingTime = (quint32)now.msecsSinceStartOfDay(); seq = c->pingSeq; // Don't increment pingseq until we receive a reply. } @@ -999,7 +1139,9 @@ void udpServer::sendLoginResponse(CLIENT* c, bool allowed) p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; - p.code = 0x0250; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requesttype = 0x00; + p.requestreply = 0x01; if (!allowed) { @@ -1013,6 +1155,7 @@ void udpServer::sendLoginResponse(CLIENT* c, bool allowed) } else { strcpy(p.connection, "WFVIEW"); + //strcpy(p.connection, "FTTH"); } SEQBUFENTRY s; @@ -1022,6 +1165,14 @@ void udpServer::sendLoginResponse(CLIENT* c, bool allowed) s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { + if (c->txSeq == 0) { + c->txSeqBuf.clear(); + } + else if (c->txSeqBuf.size() > BUFSIZE) + { + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); + } + c->txSeqBuf.insert(c->txSeq, s); c->txSeq++; c->txMutex.unlock(); @@ -1047,11 +1198,8 @@ void udpServer::sendLoginResponse(CLIENT* c, bool allowed) void udpServer::sendCapabilities(CLIENT* c) { - qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Capabilities :" << c->txSeq; - capabilities_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p); p.type = 0x00; p.seq = c->txSeq; p.sentid = c->myId; @@ -1059,89 +1207,97 @@ void udpServer::sendCapabilities(CLIENT* c) p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; - p.code = 0x0298; - p.res = 0x02; - p.capa = 0x01; - p.commoncap = c->commonCap; - - memcpy(p.macaddress, macAddress.toLocal8Bit(), 6); - // IRU seems to expect an "Icom" mac address so replace the first 3 octets of our Mac with one in their range! - memcpy(p.macaddress, QByteArrayLiteral("\x00\x90\xc7").constData(), 3); - - - memcpy(p.name, rigCaps.modelName.toLocal8Bit(), rigCaps.modelName.length()); - memcpy(p.audio, QByteArrayLiteral("ICOM_VAUDIO").constData(), 11); - - if (rigCaps.hasWiFi && !rigCaps.hasEthernet) { - p.conntype = 0x0707; // 0x0707 for wifi rig. - } - else { - p.conntype = 0x073f; // 0x073f for ethernet rig. - } - - p.civ = rigCaps.civ; - p.baudrate = (quint32)qToBigEndian(config.baudRate); - /* - 0x80 = 12K only - 0x40 = 44.1K only - 0x20 = 22.05K only - 0x10 = 11.025K only - 0x08 = 48K only - 0x04 = 32K only - 0x02 = 16K only - 0x01 = 8K only - */ - if (rxaudio == Q_NULLPTR) { - p.rxsample = 0x8b01; // all rx sample frequencies supported - } - else { - if (rxSampleRate == 48000) { - p.rxsample = 0x0800; // fixed rx sample frequency - } - else if (rxSampleRate == 32000) { - p.rxsample = 0x0400; - } - else if (rxSampleRate == 24000) { - p.rxsample = 0x0001; - } - else if (rxSampleRate == 16000) { - p.rxsample = 0x0200; - } - else if (rxSampleRate == 12000) { - p.rxsample = 0x8000; - } - } - - if (txaudio == Q_NULLPTR) { - p.txsample = 0x8b01; // all tx sample frequencies supported - p.enablea = 0x01; // 0x01 enables TX 24K mode? - qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Client will have TX audio"; - } - else { - qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Disable tx audio for client"; - p.txsample = 0; - } - - // I still don't know what these are? - p.enableb = 0x01; // 0x01 doesn't seem to do anything? - p.enablec = 0x01; // 0x01 doesn't seem to do anything? - p.capf = 0x5001; - p.capg = 0x0190; - - - + p.requesttype = 0x02; + p.requestreply = 0x02; + p.numradios = qToBigEndian((quint16)config->rigs.size()); SEQBUFENTRY s; s.seqNum = p.seq; s.timeSent = QTime::currentTime(); s.retransmitCount = 0; - s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); + + for (RIGCONFIG* rig : config->rigs) { + qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending Capabilities :" << c->txSeq << "for" << rig->modelName; + radio_cap_packet r; + memset(r.packet, 0x0, sizeof(r)); // We can't be sure it is initialized with 0x00! + + memcpy(r.guid, rig->guid, GUIDLEN); + + memcpy(r.name, rig->rigName.toLocal8Bit(), sizeof(r.name)); + memcpy(r.audio, QByteArrayLiteral("ICOM_VAUDIO").constData(), 11); + + if (rig->hasWiFi && !rig->hasEthernet) { + r.conntype = 0x0707; // 0x0707 for wifi rig-> + } + else { + r.conntype = 0x073f; // 0x073f for ethernet rig-> + } + + r.civ = rig->civAddr; + r.baudrate = (quint32)qToBigEndian(rig->baudRate); + /* + 0x80 = 12K only + 0x40 = 44.1K only + 0x20 = 22.05K only + 0x10 = 11.025K only + 0x08 = 48K only + 0x04 = 32K only + 0x02 = 16K only + 0x01 = 8K only + */ + if (rig->rxaudio == Q_NULLPTR) { + r.rxsample = 0x8b01; // all rx sample frequencies supported + } + else { + if (rxSampleRate == 48000) { + r.rxsample = 0x0800; // fixed rx sample frequency + } + else if (rxSampleRate == 32000) { + r.rxsample = 0x0400; + } + else if (rxSampleRate == 24000) { + r.rxsample = 0x0001; + } + else if (rxSampleRate == 16000) { + r.rxsample = 0x0200; + } + else if (rxSampleRate == 12000) { + r.rxsample = 0x8000; + } + } + + if (rig->txaudio == Q_NULLPTR) { + r.txsample = 0x8b01; // all tx sample frequencies supported + r.enablea = 0x01; // 0x01 enables TX 24K mode? + qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Client will have TX audio"; + } + else { + qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Disable tx audio for client"; + r.txsample = 0; + } + + // I still don't know what these are? + r.enableb = 0x01; // 0x01 doesn't seem to do anything? + r.enablec = 0x01; // 0x01 doesn't seem to do anything? + r.capf = 0x5001; + r.capg = 0x0190; + s.data.append(QByteArray::fromRawData((const char*)r.packet, sizeof(r))); + } + + p.len = sizeof(p)+s.data.length(); + p.payloadsize = qToBigEndian((quint16)(sizeof(p) + s.data.length() - 0x10)); + + s.data.insert(0,QByteArray::fromRawData((const char*)p.packet, sizeof(p))); + if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - if (c->txSeqBuf.size() > 400) - { - c->txSeqBuf.remove(0); + if (c->txSeq == 0) { + c->txSeqBuf.clear(); } - c->txSeqBuf.insert(p.seq, s); + else if (c->txSeqBuf.size() > BUFSIZE) + { + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); + } + c->txSeqBuf.insert(c->txSeq, s); c->txSeq++; c->txMutex.unlock(); } @@ -1151,7 +1307,7 @@ void udpServer::sendCapabilities(CLIENT* c) if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); + c->socket->writeDatagram((const char*)s.data, s.data.length(), c->ipAddress, c->port); udpMutex.unlock(); } else { @@ -1167,70 +1323,85 @@ void udpServer::sendCapabilities(CLIENT* c) // When client has requested civ/audio connection, this will contain their details // Also used to display currently connected used information. -void udpServer::sendConnectionInfo(CLIENT* c) +void udpServer::sendConnectionInfo(CLIENT* c, quint8 guid[GUIDLEN]) { - qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending ConnectionInfo :" << c->txSeq; - conninfo_packet p; - memset(p.packet, 0x0, sizeof(p)); - p.len = sizeof(p); - p.type = 0x00; - p.seq = c->txSeq; - p.sentid = c->myId; - p.rcvdid = c->remoteId; - //p.innerseq = c->authInnerSeq; // Innerseq not used in user packet - p.tokrequest = c->tokenRx; - p.token = c->tokenTx; - p.code = 0x0380; - p.commoncap = c->commonCap; - p.identa = c->identa; - p.identb = c->identb; - - // 0x1a-0x1f is authid (random number? - // memcpy(p + 0x40, QByteArrayLiteral("IC-7851").constData(), 7); - - memcpy(p.packet + 0x40, rigCaps.modelName.toLocal8Bit(), rigCaps.modelName.length()); - - // This is the current streaming client (should we support multiple clients?) - if (c->isStreaming) { - p.busy = 0x01; - memcpy(p.computer, c->clientName.constData(), c->clientName.length()); - p.ipaddress = qToBigEndian(c->ipAddress.toIPv4Address()); - p.identa = c->identa; - p.identb = c->identb; - } - - - SEQBUFENTRY s; - s.seqNum = p.seq; - s.timeSent = QTime::currentTime(); - s.retransmitCount = 0; - s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); - - if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - if (c->txSeqBuf.size() > 400) + for (RIGCONFIG* radio : config->rigs) { + if (!memcmp(guid, radio->guid, GUIDLEN) || config->rigs.size()==1) { - c->txSeqBuf.remove(0); + qInfo(logUdpServer()) << c->ipAddress.toString() << "(" << c->type << "): Sending ConnectionInfo :" << c->txSeq; + conninfo_packet p; + memset(p.packet, 0x0, sizeof(p)); + p.len = sizeof(p); + p.type = 0x00; + p.seq = c->txSeq; + p.sentid = c->myId; + p.rcvdid = c->remoteId; + //p.innerseq = c->authInnerSeq; // Innerseq not used in user packet + p.tokrequest = c->tokenRx; + p.token = c->tokenTx; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requesttype = 0x00; + p.requestreply = 0x03; + + memcpy(p.guid, radio->guid, GUIDLEN); + memcpy(p.name, radio->rigName.toLocal8Bit(), sizeof(p.name)); + + if (radio->rigAvailable) { + if (c->isStreaming) { + p.busy = 0x01; + } + else { + p.busy = 0x00; + } + } + else + { + p.busy = 0x02; + } + + // This is the current streaming client (should we support multiple clients?) + if (c->isStreaming) { + memcpy(p.computer, c->clientName.constData(), c->clientName.length()); + p.ipaddress = qToBigEndian(c->ipAddress.toIPv4Address()); + + } + + + SEQBUFENTRY s; + s.seqNum = p.seq; + s.timeSent = QTime::currentTime(); + s.retransmitCount = 0; + s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); + + if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) + { + if (c->txSeq == 0) { + c->txSeqBuf.clear(); + } + else if (c->txSeqBuf.size() > BUFSIZE) + { + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); + } + c->txSeqBuf.insert(c->txSeq, s); + c->txSeq++; + c->txMutex.unlock(); + } + else { + qInfo(logUdpServer()) << "Unable to lock txMutex()"; + } + + + if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) + { + c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); + udpMutex.unlock(); + } + else { + qInfo(logUdpServer()) << "Unable to lock udpMutex()"; + } + } - c->txSeqBuf.insert(p.seq, s); - c->txSeq++; - c->txMutex.unlock(); } - else { - qInfo(logUdpServer()) << "Unable to lock txMutex()"; - } - - - if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - c->socket->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), c->ipAddress, c->port); - udpMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock udpMutex()"; - } - - if (c->idleTimer != Q_NULLPTR) c->idleTimer->start(100); @@ -1251,12 +1422,11 @@ void udpServer::sendTokenResponse(CLIENT* c, quint8 type) p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; - p.code = 0x0230; - p.identa = c->identa; - p.identb = c->identb; - p.commoncap = c->commonCap; - p.res = type; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + memcpy(p.guid, c->guid, GUIDLEN); + p.requesttype = type; + p.requestreply = 0x02; SEQBUFENTRY s; s.seqNum = p.seq; @@ -1266,11 +1436,14 @@ void udpServer::sendTokenResponse(CLIENT* c, quint8 type) if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - if (c->txSeqBuf.size() > 400) - { - c->txSeqBuf.remove(0); + if (c->txSeq == 0) { + c->txSeqBuf.clear(); } - c->txSeqBuf.insert(p.seq, s); + else if (c->txSeqBuf.size() > BUFSIZE) + { + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); + } + c->txSeqBuf.insert(c->txSeq, s); c->txSeq++; c->txMutex.unlock(); } @@ -1295,8 +1468,6 @@ void udpServer::sendTokenResponse(CLIENT* c, quint8 type) return; } -#define PURGE_SECONDS 60 - void udpServer::watchdog() { QDateTime now = QDateTime::currentDateTime(); @@ -1345,8 +1516,10 @@ void udpServer::watchdog() qInfo(logUdpServer()) << "Current client is NULL!"; } } - - emit haveNetworkStatus(QString("
Server connections: Control:%1 CI-V:%2 Audio:%3
").arg(controlClients.size()).arg(civClients.size()).arg(audioClients.size())); + if (!config->lan) { + status.message = QString("
Server connections: Control:%1 CI-V:%2 Audio:%3
").arg(controlClients.size()).arg(civClients.size()).arg(audioClients.size()); + emit haveNetworkStatus(status); + } } void udpServer::sendStatus(CLIENT* c) @@ -1364,12 +1537,11 @@ void udpServer::sendStatus(CLIENT* c) p.innerseq = c->authInnerSeq; p.tokrequest = c->tokenRx; p.token = c->tokenTx; - p.code = 0x0240; - p.res = 0x03; - p.unknown = 0x1000; - p.unusede = (char)0x80; - p.identa = c->identa; - p.identb = c->identb; + p.payloadsize = qToBigEndian((quint16)(sizeof(p) - 0x10)); + p.requestreply = 0x02; + p.requesttype = 0x03; + memcpy(p.guid, c->guid, GUIDLEN); // May be MAC address OR guid. + p.civport = qToBigEndian(c->civPort); p.audioport = qToBigEndian(c->audioPort); @@ -1384,12 +1556,15 @@ void udpServer::sendStatus(CLIENT* c) s.data = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); if (c->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - if (c->txSeqBuf.size() > 400) + if (c->txSeq == 0) { + c->txSeqBuf.clear(); + } + else if (c->txSeqBuf.size() > BUFSIZE) { - c->txSeqBuf.remove(0); + c->txSeqBuf.remove(c->txSeqBuf.firstKey()); } c->txSeq++; - c->txSeqBuf.insert(p.seq, s); + c->txSeqBuf.insert(c->txSeq, s); c->txMutex.unlock(); } else { @@ -1411,14 +1586,31 @@ void udpServer::sendStatus(CLIENT* c) void udpServer::dataForServer(QByteArray d) { + rigCommander* sender = qobject_cast(QObject::sender()); - //qInfo(logUdpServer()) << "Server got:" << d; - foreach(CLIENT * client, civClients) + if (sender == Q_NULLPTR) { + return; + } + + for (CLIENT* client : civClients) + { + if (client == Q_NULLPTR || !client->connected) + { + continue; + } + // Use the GUID to determine which radio the response is from + if (memcmp(sender->getGUID(), client->guid, GUIDLEN) && config->rigs.size()>1) + { + continue; // Rig guid doesn't match the one requested by the client. + } + int lastFE = d.lastIndexOf((quint8)0xfe); - if (client != Q_NULLPTR && client->connected && d.length() > lastFE + 2 && - ((quint8)d[lastFE + 1] == client->civId || (quint8)d[lastFE + 2] == client->civId || - (quint8)d[lastFE + 1] == 0x00 || (quint8)d[lastFE + 2]==0x00 || (quint8)d[lastFE + 1] == 0xE1 || (quint8)d[lastFE + 2] == 0xE1)) { + //qInfo(logUdpServer()) << "Server got CIV data from" << config->rigs.first()->rigName << "length" << d.length() << d.toHex(); + if (client->connected && d.length() > lastFE + 2 && + ((quint8)d[lastFE + 1] == client->civId || (quint8)d[lastFE + 2] == client->civId || + (quint8)d[lastFE + 1] == 0x00 || (quint8)d[lastFE + 2] == 0x00 || (quint8)d[lastFE + 1] == 0xE1 || (quint8)d[lastFE + 2] == 0xE1)) + { data_packet p; memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! p.len = (quint16)d.length() + sizeof(p); @@ -1439,13 +1631,16 @@ void udpServer::dataForServer(QByteArray d) if (client->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - if (client->txSeqBuf.size() > 400) - { - client->txSeqBuf.remove(0); + if (client->txSeq == 0) { + client->txSeqBuf.clear(); } - client->txSeqBuf.insert(p.seq, s); + else if (client->txSeqBuf.size() > BUFSIZE) + { + client->txSeqBuf.remove(client->txSeqBuf.firstKey()); + } + client->txSeqBuf.insert(client->txSeq, s); client->txSeq++; - client->innerSeq++; + //client->innerSeq = (qToBigEndian(qFromBigEndian(client->innerSeq) + 1)); client->txMutex.unlock(); } else { @@ -1461,89 +1656,82 @@ void udpServer::dataForServer(QByteArray d) else { qInfo(logUdpServer()) << "Unable to lock udpMutex()"; } - - } else { - qInfo(logUdpServer()) << "Got data for different ID" << hex << (quint8)d[lastFE+1] << ":" << hex << (quint8)d[lastFE+2]; - } - } - - return; -} - -void udpServer::sendRxAudio() -{ - QByteArray audio; - if (rxaudio) { - if (audioMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - audio.clear(); - rxaudio->getNextAudioChunk(audio); - // Now we have the next audio chunk, we can release the mutex. - audioMutex.unlock(); - int len = 0; - while (len < audio.length()) { - audioPacket partial; - partial.data = audio.mid(len, 1364); - receiveAudioData(partial); - len = len + partial.data.length(); - } } else { - qInfo(logUdpServer()) << "Unable to lock mutex for rxaudio"; + qInfo(logUdpServer()) << "Got data for different ID" << + QString("0x%1").arg((quint8)d[lastFE + 1],0,16) << ":" << QString("0x%1").arg((quint8)d[lastFE + 2],0,16); } } + return; } - void udpServer::receiveAudioData(const audioPacket& d) { + rigCommander* sender = qobject_cast(QObject::sender()); + quint8 guid[GUIDLEN]; + if (sender != Q_NULLPTR) + { + memcpy(guid, sender->getGUID(), GUIDLEN); + } + else { + memcpy(guid, d.guid, GUIDLEN); + } //qInfo(logUdpServer()) << "Server got:" << d.data.length(); foreach(CLIENT * client, audioClients) { - if (client != Q_NULLPTR && client->connected) { - audio_packet p; - memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! - p.len = sizeof(p) + d.data.length(); - p.sentid = client->myId; - p.rcvdid = client->remoteId; - p.ident = 0x0080; // audio is always this? - p.datalen = (quint16)qToBigEndian((quint16)d.data.length()); - p.sendseq = (quint16)qToBigEndian((quint16)client->sendAudioSeq); // THIS IS BIG ENDIAN! - p.seq = client->txSeq; - QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); - t.append(d.data); + int len = 0; + while (len < d.data.length()) { + QByteArray partial; + partial = d.data.mid(len, 1364); + len = len + partial.length(); + if (client != Q_NULLPTR && client->connected && (!memcmp(client->guid, guid, GUIDLEN) || config->rigs.size()== 1)) { + audio_packet p; + memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00! + p.len = sizeof(p) + partial.length(); + p.sentid = client->myId; + p.rcvdid = client->remoteId; + p.ident = 0x0080; // audio is always this? + p.datalen = (quint16)qToBigEndian((quint16)partial.length()); + p.sendseq = (quint16)qToBigEndian((quint16)client->sendAudioSeq); // THIS IS BIG ENDIAN! + p.seq = client->txSeq; + QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p)); + t.append(partial); - SEQBUFENTRY s; - s.seqNum = p.seq; - s.timeSent = QTime::currentTime(); - s.retransmitCount = 0; - s.data = t; - if (client->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - if (client->txSeqBuf.size() > 400) + SEQBUFENTRY s; + s.seqNum = p.seq; + s.timeSent = QTime::currentTime(); + s.retransmitCount = 0; + s.data = t; + if (client->txMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - client->txSeqBuf.remove(0); + if (client->txSeq == 0) { + client->txSeqBuf.clear(); + } + else if (client->txSeqBuf.size() > BUFSIZE) + { + client->txSeqBuf.remove(client->txSeqBuf.firstKey()); + } + client->txSeqBuf.insert(client->txSeq, s); + client->txSeq++; + client->sendAudioSeq++; + client->txMutex.unlock(); + } + else { + qInfo(logUdpServer()) << "Unable to lock txMutex()"; } - client->txSeqBuf.insert(p.seq, s); - client->txSeq++; - client->sendAudioSeq++; - client->txMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock txMutex()"; - } - if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - client->socket->writeDatagram(t, client->ipAddress, client->port); - udpMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock udpMutex()"; - } + if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) + { + client->socket->writeDatagram(t, client->ipAddress, client->port); + udpMutex.unlock(); + } + else { + qInfo(logUdpServer()) << "Unable to lock udpMutex()"; + } + } } } @@ -1560,102 +1748,50 @@ void udpServer::sendRetransmitRequest(CLIENT* c) // Find all gaps in received packets and then send requests for them. // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request. + if (c->rxMissing.isEmpty()) { + return; + } + else if (c->rxMissing.size() > MAX_MISSING) { + qInfo(logUdp()) << "Too many missing packets," << c->rxMissing.size() << "flushing all buffers"; + c->rxMutex.lock(); + c->rxSeqBuf.clear(); + c->rxMutex.unlock(); + c->missMutex.lock(); + c->rxMissing.clear(); + c->missMutex.unlock(); + return; + } + + QByteArray missingSeqs; - - if (!c->rxSeqBuf.empty() && c->rxSeqBuf.size() <= c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey()) - { - if ((c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size()) > 20) - { - // Too many packets to process, flush buffers and start again! - qDebug(logUdp()) << "Too many missing packets, flushing buffer: " << c->rxSeqBuf.lastKey() << "missing=" << c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size() + 1; - if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - c->rxMissing.clear(); - c->missMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock missMutex()"; - } - - if (c->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - c->rxSeqBuf.clear(); - c->rxMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock rxMutex()"; - } - - } - else { - // We have at least 1 missing packet! - qDebug(logUdp()) << "Missing Seq: size=" << c->rxSeqBuf.size() << "firstKey=" << c->rxSeqBuf.firstKey() << "lastKey=" << c->rxSeqBuf.lastKey() << "missing=" << c->rxSeqBuf.lastKey() - c->rxSeqBuf.firstKey() - c->rxSeqBuf.size() + 1; - // We are missing packets so iterate through the buffer and add the missing ones to missing packet list - for (int i = 0; i < c->rxSeqBuf.keys().length() - 1; i++) { - for (quint16 j = c->rxSeqBuf.keys()[i] + 1; j < c->rxSeqBuf.keys()[i + 1]; j++) { - auto s = c->rxMissing.find(j); - if (s == c->rxMissing.end()) - { - // We haven't seen this missing packet before - qDebug(logUdp()) << this->metaObject()->className() << ": Adding to missing buffer (len=" << c->rxMissing.size() << "): " << j; - - if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - c->rxMissing.insert(j, 0); - c->missMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock missMutex()"; - } - - - if (c->rxMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - if (c->rxSeqBuf.size() > 400) - { - c->rxSeqBuf.remove(0); - } - c->rxSeqBuf.insert(j, QTime::currentTime()); // Add this missing packet to the rxbuffer as we now long about it. - c->rxMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock rxMutex()"; - } - - } - else { - if (s.value() == 4) - { - // We have tried 4 times to request this packet, time to give up! - if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) - { - s = c->rxMissing.erase(s); - c->missMutex.unlock(); - } - else { - qInfo(logUdpServer()) << "Unable to lock missMutex()"; - } - - } - - } - } - } - } - } + //QTime missingTime = QTime::currentTime(); if (c->missMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - for (auto it = c->rxMissing.begin(); it != c->rxMissing.end(); ++it) + auto it = c->rxMissing.begin(); + while (it != c->rxMissing.end()) { - if (it.value() < 10) + if (it.key() != 0x00) { - missingSeqs.append(it.key() & 0xff); - missingSeqs.append(it.key() >> 8 & 0xff); - missingSeqs.append(it.key() & 0xff); - missingSeqs.append(it.key() >> 8 & 0xff); - it.value()++; + if (it.value() < 4) + { + missingSeqs.append(it.key() & 0xff); + missingSeqs.append(it.key() >> 8 & 0xff); + missingSeqs.append(it.key() & 0xff); + missingSeqs.append(it.key() >> 8 & 0xff); + it.value()++; + it++; + } + + else { + // We have tried 4 times to request this packet, time to give up! + qInfo(logUdp()) << this->metaObject()->className() << ": No response for missing packet" << QString("0x%1").arg(it.key(), 0, 16) << "deleting"; + it = c->rxMissing.erase(it); + } + } else { + qInfo(logUdp()) << this->metaObject()->className() << ": found empty key in missing buffer"; + it++; } } @@ -1669,8 +1805,9 @@ void udpServer::sendRetransmitRequest(CLIENT* c) p.rcvdid = c->remoteId; if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control. { + p.len = sizeof(p); p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8); - qDebug(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << hex << p.seq; + qInfo(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << QString("0x%1").arg(p.seq,0,16); if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { @@ -1684,11 +1821,13 @@ void udpServer::sendRetransmitRequest(CLIENT* c) } else { - qDebug(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(); + qInfo(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(); + + p.len = sizeof(p) + missingSeqs.size(); + missingSeqs.insert(0, p.packet, sizeof(p)); if (udpMutex.try_lock_for(std::chrono::milliseconds(LOCK_PERIOD))) { - missingSeqs.insert(0, p.packet, sizeof(p.packet)); c->socket->writeDatagram(missingSeqs, c->ipAddress, c->port); udpMutex.unlock(); } @@ -1703,7 +1842,6 @@ void udpServer::sendRetransmitRequest(CLIENT* c) else { qInfo(logUdpServer()) << "Unable to lock missMutex()"; } - } @@ -1716,6 +1854,10 @@ void udpServer::sendRetransmitRequest(CLIENT* c) void udpServer::deleteConnection(QList* l, CLIENT* c) { + quint8 guid[GUIDLEN]; + memcpy(guid, c->guid, GUIDLEN); + int len = l->length(); + qInfo(logUdpServer()) << "Deleting" << c->type << "connection to: " << c->ipAddress.toString() << ":" << QString::number(c->port); if (c->idleTimer != Q_NULLPTR) { c->idleTimer->stop(); @@ -1769,6 +1911,7 @@ void udpServer::deleteConnection(QList* l, CLIENT* c) if (client != Q_NULLPTR && client == c) { qInfo(logUdpServer()) << "Found" << client->type << "connection to: " << client->ipAddress.toString() << ":" << QString::number(client->port); it = l->erase(it); + len--; } else { ++it; @@ -1783,27 +1926,25 @@ void udpServer::deleteConnection(QList* l, CLIENT* c) qInfo(logUdpServer()) << "Unable to lock connMutex()"; } - if (l->length() == 0) { + if (len == 0) { + for (RIGCONFIG* radio : config->rigs) { + if (!memcmp(radio->guid, guid, GUIDLEN) || config->rigs.size() == 1) + { - if (rxAudioTimer != Q_NULLPTR) { - rxAudioTimer->stop(); - delete rxAudioTimer; - rxAudioTimer = Q_NULLPTR; + if (radio->rxAudioThread != Q_NULLPTR) { + radio->rxAudioThread->quit(); + radio->rxAudioThread->wait(); + radio->rxaudio = Q_NULLPTR; + radio->rxAudioThread = Q_NULLPTR; + } + + if (radio->txAudioThread != Q_NULLPTR) { + radio->txAudioThread->quit(); + radio->txAudioThread->wait(); + radio->txaudio = Q_NULLPTR; + radio->txAudioThread = Q_NULLPTR; + } + } } - - if (rxAudioThread != Q_NULLPTR) { - rxAudioThread->quit(); - rxAudioThread->wait(); - rxaudio = Q_NULLPTR; - rxAudioThread = Q_NULLPTR; - } - - if (txAudioThread != Q_NULLPTR) { - txAudioThread->quit(); - txAudioThread->wait(); - txaudio = Q_NULLPTR; - txAudioThread = Q_NULLPTR; - } - } } diff --git a/udpserver.h b/udpserver.h index 4da37de..3cbed99 100644 --- a/udpserver.h +++ b/udpserver.h @@ -21,7 +21,9 @@ #include "packettypes.h" #include "rigidentities.h" +#include "udphandler.h" #include "audiohandler.h" +#include "rigcommander.h" extern void passcode(QString in,QByteArray& out); extern QByteArray parseNullTerminatedString(QByteArray c, int s); @@ -40,6 +42,44 @@ struct SERVERUSER { quint8 userType; }; + +struct RIGCONFIG { + QString serialPort; + quint32 baudRate; + unsigned char civAddr; + bool civIsRadioModel; + bool forceRTSasPTT; + bool hasWiFi = false; + bool hasEthernet=false; + audioSetup rxAudioSetup; + audioSetup txAudioSetup; + QString modelName; + QString rigName; +#pragma pack(push, 1) + union { + struct { + quint8 unused[7]; // 0x22 + quint16 commoncap; // 0x27 + quint8 unusedb; // 0x29 + quint8 macaddress[6]; // 0x2a + }; + quint8 guid[GUIDLEN]; // 0x20 + }; +#pragma pack(pop) + bool rigAvailable=false; + rigCapabilities rigCaps; + rigCommander* rig = Q_NULLPTR; + QThread* rigThread = Q_NULLPTR; + audioHandler* rxaudio = Q_NULLPTR; + QThread* rxAudioThread = Q_NULLPTR; + audioHandler* txaudio = Q_NULLPTR; + QThread* txAudioThread = Q_NULLPTR; + QTimer* rxAudioTimer = Q_NULLPTR; + QTimer* connectTimer = Q_NULLPTR; + quint8 waterfallFormat; +}; + + struct SERVERCONFIG { bool enabled; bool lan; @@ -50,8 +90,8 @@ struct SERVERCONFIG { int audioInput; quint8 resampleQuality; quint32 baudRate; - QList users; + QList rigs; }; @@ -60,7 +100,7 @@ class udpServer : public QObject Q_OBJECT public: - udpServer(SERVERCONFIG config,audioSetup outAudio, audioSetup inAudio); + explicit udpServer(SERVERCONFIG* config, QObject* parent = nullptr); ~udpServer(); public slots: @@ -72,7 +112,7 @@ public slots: signals: void haveDataFromServer(QByteArray); void haveAudioData(audioPacket data); - void haveNetworkStatus(QString); + void haveNetworkStatus(networkStatus); void setupTxAudio(audioSetup); void setupRxAudio(audioSetup); @@ -100,12 +140,11 @@ private: quint16 connSeq; quint16 pingSeq; quint32 rxPingTime; // 32bit as has other info - quint8 authInnerSeq; + quint16 authInnerSeq; quint16 authSeq; quint16 innerSeq; quint16 sendAudioSeq; - quint8 identa; - quint32 identb; + quint8 macaddress[6]; quint16 tokenRx; quint32 tokenTx; quint32 commonCap; @@ -140,6 +179,7 @@ private: CLIENT* controlClient = Q_NULLPTR; CLIENT* civClient = Q_NULLPTR; CLIENT* audioClient = Q_NULLPTR; + quint8 guid[GUIDLEN]; }; void controlReceived(); @@ -151,28 +191,25 @@ private: void sendControl(CLIENT* c, quint8 type, quint16 seq); void sendLoginResponse(CLIENT* c, bool allowed); void sendCapabilities(CLIENT* c); - void sendConnectionInfo(CLIENT* c); + void sendConnectionInfo(CLIENT* c,quint8 guid[GUIDLEN]); void sendTokenResponse(CLIENT* c,quint8 type); void sendStatus(CLIENT* c); void sendRetransmitRequest(CLIENT* c); void watchdog(); - void sendRxAudio(); void deleteConnection(QList *l, CLIENT* c); - SERVERCONFIG config; + SERVERCONFIG *config; QUdpSocket* udpControl = Q_NULLPTR; QUdpSocket* udpCiv = Q_NULLPTR; QUdpSocket* udpAudio = Q_NULLPTR; QHostAddress localIP; - QString macAddress; + quint8 macAddress[6]; quint32 controlId = 0; quint32 civId = 0; quint32 audioId = 0; - quint8 rigciv = 0xa2; - QMutex udpMutex; // Used for critical operations. QMutex connMutex; QMutex audioMutex; @@ -180,19 +217,13 @@ private: QList controlClients = QList(); QList civClients = QList(); QList audioClients = QList(); - QTime timeStarted; - rigCapabilities rigCaps; - audioHandler* rxaudio = Q_NULLPTR; - QThread* rxAudioThread = Q_NULLPTR; + //QTime timeStarted; - audioHandler* txaudio = Q_NULLPTR; - QThread* txAudioThread = Q_NULLPTR; audioSetup outAudio; audioSetup inAudio; - QTimer* rxAudioTimer=Q_NULLPTR; quint16 rxSampleRate = 0; quint16 txSampleRate = 0; quint8 rxCodec = 0; @@ -200,7 +231,9 @@ private: QHostAddress hasTxAudio; QTimer* wdTimer; + + networkStatus status; }; -#endif // UDPSERVER_H \ No newline at end of file +#endif // UDPSERVER_H diff --git a/udpserversetup.ui b/udpserversetup.ui index f3d8d63..9379320 100644 --- a/udpserversetup.ui +++ b/udpserversetup.ui @@ -139,7 +139,7 @@
- Contol Port + Control Port diff --git a/wfmain.cpp b/wfmain.cpp index efdec3e..c55d6cc 100644 --- a/wfmain.cpp +++ b/wfmain.cpp @@ -28,6 +28,7 @@ wfmain::wfmain(const QString serialPortCL, const QString hostCL, const QString s sat = new satelliteSetup(); trxadj = new transceiverAdjustments(); abtBox = new aboutbox(); + selRad = new selectRadio(); qRegisterMetaType(); // Needs to be registered early. qRegisterMetaType(); @@ -40,10 +41,12 @@ wfmain::wfmain(const QString serialPortCL, const QString hostCL, const QString s qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType (); + qRegisterMetaType (); qRegisterMetaType (); qRegisterMetaType (); qRegisterMetaType(); - + qRegisterMetaType>(); + qRegisterMetaType(); haveRigCaps = false; @@ -53,8 +56,6 @@ wfmain::wfmain(const QString serialPortCL, const QString hostCL, const QString s setSerialDevicesUI(); - setAudioDevicesUI(); - setDefaultColors(); setDefPrefs(); @@ -62,24 +63,26 @@ wfmain::wfmain(const QString serialPortCL, const QString hostCL, const QString s setupPlots(); loadSettings(); // Look for saved preferences + + setAudioDevicesUI(); + setTuningSteps(); // TODO: Combine into preferences + qDebug(logSystem()) << "Running setUIToPrefs()"; setUIToPrefs(); - setServerToPrefs(); - + qDebug(logSystem()) << "Running setInititalTiming()"; setInitialTiming(); + qDebug(logSystem()) << "Running openRig()"; openRig(); + qDebug(logSystem()) << "Running rigConnections()"; rigConnections(); - if (serverConfig.enabled && udp != Q_NULLPTR) { - // Server - connect(rig, SIGNAL(haveAudioData(audioPacket)), udp, SLOT(receiveAudioData(audioPacket))); - connect(rig, SIGNAL(haveDataForServer(QByteArray)), udp, SLOT(dataForServer(QByteArray))); - connect(udp, SIGNAL(haveDataFromServer(QByteArray)), rig, SLOT(dataFromServer(QByteArray))); - } + + + setServerToPrefs(); amTransmitting = false; @@ -100,14 +103,14 @@ wfmain::~wfmain() if (rigCtl != Q_NULLPTR) { delete rigCtl; } + + if (prefs.audioSystem == portAudio) { + Pa_Terminate(); + } delete rpt; delete ui; delete settings; -#if defined(PORTAUDIO) - Pa_Terminate(); -#endif - } void wfmain::closeEvent(QCloseEvent *event) @@ -185,13 +188,13 @@ void wfmain::openRig() ui->lanEnableBtn->setChecked(true); usingLAN = true; // We need to setup the tx/rx audio: - emit sendCommSetup(prefs.radioCIVAddr, udpPrefs, rxSetup, txSetup, prefs.virtualSerialPort); + udpPrefs.waterfallFormat = prefs.waterfallFormat; + emit sendCommSetup(prefs.radioCIVAddr, udpPrefs, rxSetup, txSetup, prefs.virtualSerialPort, prefs.tcpPort); } else { ui->serialEnableBtn->setChecked(true); if( (prefs.serialPortRadio.toLower() == QString("auto")) && (serialPortCL.isEmpty())) { findSerialPort(); - } else { if(serialPortCL.isEmpty()) { @@ -201,7 +204,7 @@ void wfmain::openRig() } } usingLAN = false; - emit sendCommSetup(prefs.radioCIVAddr, serialPortRig, prefs.serialPortBaud,prefs.virtualSerialPort); + emit sendCommSetup(prefs.radioCIVAddr, serialPortRig, prefs.serialPortBaud,prefs.virtualSerialPort, prefs.tcpPort,prefs.waterfallFormat); ui->statusBar->showMessage(QString("Connecting to rig using serial port ").append(serialPortRig), 1000); } @@ -409,6 +412,8 @@ void wfmain::makeRig() { rig = new rigCommander(); rigThread = new QThread(this); + rigThread->setObjectName("rigCommander()"); + // Thread: rig->moveToThread(rigThread); @@ -418,11 +423,13 @@ void wfmain::makeRig() // Rig status and Errors: connect(rig, SIGNAL(haveSerialPortError(QString, QString)), this, SLOT(receiveSerialPortError(QString, QString))); - connect(rig, SIGNAL(haveStatusUpdate(QString)), this, SLOT(receiveStatusUpdate(QString))); - + connect(rig, SIGNAL(haveStatusUpdate(networkStatus)), this, SLOT(receiveStatusUpdate(networkStatus))); + connect(rig, SIGNAL(requestRadioSelection(QList)), this, SLOT(radioSelection(QList))); + connect(rig, SIGNAL(setRadioUsage(quint8, quint8, QString, QString)), selRad, SLOT(setInUse(quint8, quint8, QString, QString))); + connect(selRad, SIGNAL(selectedRadio(quint8)), rig, SLOT(setCurrentRadio(quint8))); // Rig comm setup: - connect(this, SIGNAL(sendCommSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString)), rig, SLOT(commSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString))); - connect(this, SIGNAL(sendCommSetup(unsigned char, QString, quint32,QString)), rig, SLOT(commSetup(unsigned char, QString, quint32,QString))); + connect(this, SIGNAL(sendCommSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString, quint16)), rig, SLOT(commSetup(unsigned char, udpPreferences, audioSetup, audioSetup, QString, quint16))); + connect(this, SIGNAL(sendCommSetup(unsigned char, QString, quint32,QString, quint16,quint8)), rig, SLOT(commSetup(unsigned char, QString, quint32,QString, quint16,quint8))); connect(this, SIGNAL(setRTSforPTT(bool)), rig, SLOT(setRTSforPTT(bool))); connect(rig, SIGNAL(haveBaudRate(quint32)), this, SLOT(receiveBaudRate(quint32))); @@ -441,6 +448,11 @@ void wfmain::makeRig() connect(rig, SIGNAL(stateInfo(rigstate*)), rigCtl, SLOT(receiveStateInfo(rigstate*))); connect(rigCtl, SIGNAL(stateUpdated()), rig, SLOT(stateUpdated())); } + // Create link for server so it can have easy access to rig. + if (serverConfig.rigs.first() != Q_NULLPTR) { + serverConfig.rigs.first()->rig = rig; + serverConfig.rigs.first()->rigThread = rigThread; + } } } @@ -454,7 +466,7 @@ void wfmain::removeRig() rigThread->disconnect(); rig->disconnect(); - + delete rigThread; delete rig; rig = Q_NULLPTR; @@ -467,49 +479,68 @@ void wfmain::findSerialPort() { // Find the ICOM radio connected, or, if none, fall back to OS default. // qInfo(logSystem()) << "Searching for serial port..."; - QDirIterator it73("/dev/serial/by-id", QStringList() << "*IC-7300*", QDir::Files, QDirIterator::Subdirectories); - QDirIterator it97("/dev/serial", QStringList() << "*IC-9700*A*", QDir::Files, QDirIterator::Subdirectories); - QDirIterator it785x("/dev/serial", QStringList() << "*IC-785*A*", QDir::Files, QDirIterator::Subdirectories); - QDirIterator it705("/dev/serial", QStringList() << "*IC-705*A", QDir::Files, QDirIterator::Subdirectories); - QDirIterator it7610("/dev/serial", QStringList() << "*IC-7610*A", QDir::Files, QDirIterator::Subdirectories); - QDirIterator itR8600("/dev/serial", QStringList() << "*IC-R8600*A", QDir::Files, QDirIterator::Subdirectories); + bool found = false; + // First try to find first Icom port: + foreach(const QSerialPortInfo & serialPortInfo, QSerialPortInfo::availablePorts()) + { + if (serialPortInfo.serialNumber().left(3) == "IC-") { + qInfo(logSystem()) << "Serial Port found: " << serialPortInfo.portName() << "Manufacturer:" << serialPortInfo.manufacturer() << "Product ID" << serialPortInfo.description() << "S/N" << serialPortInfo.serialNumber(); +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) + serialPortRig = (QString("/dev/") + serialPortInfo.portName()); +#else + serialPortRig = serialPortInfo.portName(); +#endif + found = true; + break; + } + } - if(!it73.filePath().isEmpty()) - { - // IC-7300 - serialPortRig = it73.filePath(); // first - } else if(!it97.filePath().isEmpty()) - { - // IC-9700 - serialPortRig = it97.filePath(); - } else if(!it785x.filePath().isEmpty()) - { - // IC-785x - serialPortRig = it785x.filePath(); - } else if(!it705.filePath().isEmpty()) - { - // IC-705 - serialPortRig = it705.filePath(); - } else if(!it7610.filePath().isEmpty()) - { - // IC-7610 - serialPortRig = it7610.filePath(); - } else if(!itR8600.filePath().isEmpty()) - { - // IC-R8600 - serialPortRig = itR8600.filePath(); - } else { - //fall back: - qInfo(logSystem()) << "Could not find Icom serial port. Falling back to OS default. Use --port to specify, or modify preferences."; + if (!found) { + QDirIterator it73("/dev/serial/by-id", QStringList() << "*IC-7300*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it97("/dev/serial", QStringList() << "*IC-9700*A*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it785x("/dev/serial", QStringList() << "*IC-785*A*", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it705("/dev/serial", QStringList() << "*IC-705*A", QDir::Files, QDirIterator::Subdirectories); + QDirIterator it7610("/dev/serial", QStringList() << "*IC-7610*A", QDir::Files, QDirIterator::Subdirectories); + QDirIterator itR8600("/dev/serial", QStringList() << "*IC-R8600*A", QDir::Files, QDirIterator::Subdirectories); + + if(!it73.filePath().isEmpty()) + { + // IC-7300 + serialPortRig = it73.filePath(); // first + } else if(!it97.filePath().isEmpty()) + { + // IC-9700 + serialPortRig = it97.filePath(); + } else if(!it785x.filePath().isEmpty()) + { + // IC-785x + serialPortRig = it785x.filePath(); + } else if(!it705.filePath().isEmpty()) + { + // IC-705 + serialPortRig = it705.filePath(); + } else if(!it7610.filePath().isEmpty()) + { + // IC-7610 + serialPortRig = it7610.filePath(); + } else if(!itR8600.filePath().isEmpty()) + { + // IC-R8600 + serialPortRig = itR8600.filePath(); + } + else { + //fall back: + qInfo(logSystem()) << "Could not find Icom serial port. Falling back to OS default. Use --port to specify, or modify preferences."; #ifdef Q_OS_MAC - serialPortRig = QString("/dev/tty.SLAB_USBtoUART"); + serialPortRig = QString("/dev/tty.SLAB_USBtoUART"); #endif #ifdef Q_OS_LINUX - serialPortRig = QString("/dev/ttyUSB0"); + serialPortRig = QString("/dev/ttyUSB0"); #endif #ifdef Q_OS_WIN - serialPortRig = QString("COM1"); + serialPortRig = QString("COM1"); #endif + } } } @@ -575,27 +606,32 @@ void wfmain::receiveSerialPortError(QString port, QString errorText) // TODO: Dialog box, exit, etc } -void wfmain::receiveStatusUpdate(QString text) +void wfmain::receiveStatusUpdate(networkStatus status) { - this->rigStatus->setText(text); + + this->rigStatus->setText(status.message); + selRad->audioOutputLevel(status.rxAudioLevel); + selRad->audioInputLevel(status.txAudioLevel); + //qInfo(logSystem()) << "Got Status Update" << status.rxAudioLevel; } void wfmain::setupPlots() { - -// Line 290-- spectrumDrawLock = true; - plot = ui->plot; // rename it waterfall. + plot = ui->plot; wf = ui->waterfall; freqIndicatorLine = new QCPItemLine(plot); freqIndicatorLine->setAntialiased(true); freqIndicatorLine->setPen(QPen(Qt::blue)); -// ui->plot->addGraph(); // primary - ui->plot->addGraph(0, 0); // secondary, peaks, same axis as first? + ui->plot->addGraph(0, 0); // secondary, peaks, same axis as first. + ui->plot->addLayer( "Top Layer", ui->plot->layer("main")); + ui->plot->graph(0)->setLayer("Top Layer"); + + ui->waterfall->addGraph(); colorMap = new QCPColorMap(wf->xAxis, wf->yAxis); @@ -865,13 +901,11 @@ void wfmain::updateSizes(int tabIndex) ui->tabWidget->widget(0)->setMaximumSize(ui->tabWidget->widget(0)->minimumSizeHint()); ui->tabWidget->widget(0)->adjustSize(); // tab this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - this->setMaximumSize(QSize(929, 270)); - this->setMinimumSize(QSize(929, 270)); + this->setMaximumSize(QSize(1024,350)); + this->setMinimumSize(QSize(1024,350)); resize(minimumSize()); adjustSize(); // main window - adjustSize(); - } else if(tabIndex==0 && rigCaps.hasSpectrum) { // At main tab (0) and we have spectrum: ui->tabWidget->widget(0)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); @@ -882,8 +916,9 @@ void wfmain::updateSizes(int tabIndex) // At some other tab, with or without spectrum: ui->tabWidget->widget(tabIndex)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - this->setMinimumSize(QSize(994, 455)); // not large enough for settings tab + this->setMinimumSize(QSize(1024, 600)); // not large enough for settings tab this->setMaximumSize(QSize(65535,65535)); + adjustSize(); } } else { ui->tabWidget->widget(tabIndex)->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); @@ -964,17 +999,29 @@ void wfmain::setServerToPrefs() if (serverConfig.enabled) { serverConfig.lan = prefs.enableLAN; - udp = new udpServer(serverConfig,serverTxSetup,serverRxSetup); + udp = new udpServer(&serverConfig); serverThread = new QThread(this); udp->moveToThread(serverThread); + connect(this, SIGNAL(initServer()), udp, SLOT(init())); connect(serverThread, SIGNAL(finished()), udp, SLOT(deleteLater())); - if (!prefs.enableLAN && udp != Q_NULLPTR) { - connect(udp, SIGNAL(haveNetworkStatus(QString)), this, SLOT(receiveStatusUpdate(QString))); + if (rig != Q_NULLPTR) { + connect(rig, SIGNAL(haveAudioData(audioPacket)), udp, SLOT(receiveAudioData(audioPacket))); + // Need to add a signal/slot for audio from the client to rig. + //connect(udp, SIGNAL(haveAudioData(audioPacket)), rig, SLOT(receiveAudioData(audioPacket))); + connect(rig, SIGNAL(haveDataForServer(QByteArray)), udp, SLOT(dataForServer(QByteArray))); + connect(udp, SIGNAL(haveDataFromServer(QByteArray)), rig, SLOT(dataFromServer(QByteArray))); + } + + if (serverConfig.lan) { + connect(udp, SIGNAL(haveNetworkStatus(networkStatus)), this, SLOT(receiveStatusUpdate(networkStatus))); + } else { + qInfo(logAudio()) << "Audio Input device " << serverConfig.rigs.first()->rxAudioSetup.name; + qInfo(logAudio()) << "Audio Output device " << serverConfig.rigs.first()->txAudioSetup.name; } serverThread->start(); @@ -982,6 +1029,7 @@ void wfmain::setServerToPrefs() emit initServer(); connect(this, SIGNAL(sendRigCaps(rigCapabilities)), udp, SLOT(receiveRigCaps(rigCapabilities))); + ui->statusBar->showMessage(QString("Server enabled"), 1000); } @@ -1002,6 +1050,28 @@ void wfmain::setUIToPrefs() on_drawPeakChk_clicked(prefs.drawPeaks); drawPeaks = prefs.drawPeaks; + underlayMode = prefs.underlayMode; + switch(underlayMode) + { + case underlayNone: + ui->underlayNone->setChecked(true); + break; + case underlayPeakHold: + ui->underlayPeakHold->setChecked(true); + break; + case underlayPeakBuffer: + ui->underlayPeakBuffer->setChecked(true); + break; + case underlayAverageBuffer: + ui->underlayAverageBuffer->setChecked(true); + break; + default: + break; + } + + ui->underlayBufferSlider->setValue(prefs.underlayBufferSize); + on_underlayBufferSlider_valueChanged(prefs.underlayBufferSize); + ui->wfAntiAliasChk->setChecked(prefs.wfAntiAlias); on_wfAntiAliasChk_clicked(prefs.wfAntiAlias); @@ -1011,6 +1081,12 @@ void wfmain::setUIToPrefs() ui->wfLengthSlider->setValue(prefs.wflength); prepareWf(prefs.wflength); + ui->topLevelSlider->setValue(prefs.plotCeiling); + ui->botLevelSlider->setValue(prefs.plotFloor); + + plot->yAxis->setRange(QCPRange(prefs.plotFloor, prefs.plotCeiling)); + colorMap->setDataRange(QCPRange(prefs.plotFloor, prefs.plotCeiling)); + ui->wfthemeCombo->setCurrentIndex(ui->wfthemeCombo->findData(prefs.wftheme)); colorMap->setGradient(static_cast(prefs.wftheme)); @@ -1019,121 +1095,6 @@ void wfmain::setUIToPrefs() ui->useCIVasRigIDChk->blockSignals(false); } -void wfmain::setAudioDevicesUI() -{ - -#if defined(RTAUDIO) - -#if defined(Q_OS_LINUX) - RtAudio* audio = new RtAudio(RtAudio::Api::LINUX_ALSA); -#elif defined(Q_OS_WIN) - RtAudio* audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); -#elif defined(Q_OS_MACX) - RtAudio* audio = new RtAudio(RtAudio::Api::MACOSX_CORE); -#endif - - - // Enumerate audio devices, need to do before settings are loaded. - std::map apiMap; - apiMap[RtAudio::MACOSX_CORE] = "OS-X Core Audio"; - apiMap[RtAudio::WINDOWS_ASIO] = "Windows ASIO"; - apiMap[RtAudio::WINDOWS_DS] = "Windows DirectSound"; - apiMap[RtAudio::WINDOWS_WASAPI] = "Windows WASAPI"; - apiMap[RtAudio::UNIX_JACK] = "Jack Client"; - apiMap[RtAudio::LINUX_ALSA] = "Linux ALSA"; - apiMap[RtAudio::LINUX_PULSE] = "Linux PulseAudio"; - apiMap[RtAudio::LINUX_OSS] = "Linux OSS"; - apiMap[RtAudio::RTAUDIO_DUMMY] = "RtAudio Dummy"; - - std::vector< RtAudio::Api > apis; - RtAudio::getCompiledApi(apis); - - qInfo(logAudio()) << "RtAudio Version " << QString::fromStdString(RtAudio::getVersion()); - - qInfo(logAudio()) << "Compiled APIs:"; - for (unsigned int i = 0; i < apis.size(); i++) { - qInfo(logAudio()) << " " << QString::fromStdString(apiMap[apis[i]]); - } - - RtAudio::DeviceInfo info; - - qInfo(logAudio()) << "Current API: " << QString::fromStdString(apiMap[audio->getCurrentApi()]); - - unsigned int devices = audio->getDeviceCount(); - qInfo(logAudio()) << "Found " << devices << " audio device(s) *=default"; - - for (unsigned int i = 1; i < devices; i++) { - info = audio->getDeviceInfo(i); - if (info.outputChannels > 0) { - qInfo(logAudio()) << (info.isDefaultOutput ? "*" : " ") << "(" << i << ") Output Device : " << QString::fromStdString(info.name); - ui->audioOutputCombo->addItem(QString::fromStdString(info.name), i); - ui->serverTXAudioOutputCombo->addItem(QString::fromStdString(info.name), i); - } - if (info.inputChannels > 0) { - qInfo(logAudio()) << (info.isDefaultInput ? "*" : " ") << "(" << i << ") Input Device : " << QString::fromStdString(info.name); - ui->audioInputCombo->addItem(QString::fromStdString(info.name), i); - ui->serverRXAudioInputCombo->addItem(QString::fromStdString(info.name), i); - } - } - - delete audio; - -#elif defined(PORTAUDIO) - // Use PortAudio device enumeration - - PaError err; - - err = Pa_Initialize(); - - if (err != paNoError) - { - qInfo(logAudio()) << "ERROR: Cannot initialize Portaudio"; - } - - qInfo(logAudio()) << "PortAudio version: " << Pa_GetVersionInfo()->versionText; - - int numDevices; - numDevices = Pa_GetDeviceCount(); - qInfo(logAudio()) << "Pa_CountDevices returned" << numDevices; - - const PaDeviceInfo* info; - for (int i = 0; i < numDevices; i++) - { - info = Pa_GetDeviceInfo(i); - if (info->maxInputChannels > 0) { - qInfo(logAudio()) << (i == Pa_GetDefaultInputDevice() ? "*" : " ") << "(" << i << ") Output Device : " << info->name; - ui->audioInputCombo->addItem(info->name, i); - ui->serverRXAudioInputCombo->addItem(info->name, i); - } - if (info->maxOutputChannels > 0) { - qInfo(logAudio()) << (i == Pa_GetDefaultOutputDevice() ? "*" : " ") << "(" << i << ") Input Device : " << info->name; - ui->audioOutputCombo->addItem(info->name, i); - ui->serverTXAudioOutputCombo->addItem(info->name, i); -} - } -#else - -// If no external library is configured, use QTMultimedia - // Enumerate audio devices, need to do before settings are loaded. - const auto audioOutputs = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - for (const QAudioDeviceInfo& deviceInfo : audioOutputs) { - ui->audioOutputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); - ui->serverTXAudioOutputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); - } - - const auto audioInputs = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); - for (const QAudioDeviceInfo& deviceInfo : audioInputs) { - ui->audioInputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); - ui->serverRXAudioInputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); - } - // Set these to default audio devices initially. - rxSetup.port = QAudioDeviceInfo::defaultOutputDevice(); - txSetup.port = QAudioDeviceInfo::defaultInputDevice(); - serverRxSetup.port = QAudioDeviceInfo::defaultOutputDevice(); - serverTxSetup.port = QAudioDeviceInfo::defaultInputDevice(); -#endif -} - void wfmain::setSerialDevicesUI() { ui->serialDeviceListCombo->blockSignals(true); @@ -1143,9 +1104,10 @@ void wfmain::setSerialDevicesUI() { portList.append(serialPortInfo.portName()); #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - ui->serialDeviceListCombo->addItem(QString("/dev/")+serialPortInfo.portName(), i++); + ui->serialDeviceListCombo->addItem(QString("/dev/") + serialPortInfo.portName(), i++); #else ui->serialDeviceListCombo->addItem(serialPortInfo.portName(), i++); + //qInfo(logSystem()) << "Serial Port found: " << serialPortInfo.portName() << "Manufacturer:" << serialPortInfo.manufacturer() << "Product ID" << serialPortInfo.description() << "S/N" << serialPortInfo.serialNumber(); #endif } #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) @@ -1167,18 +1129,18 @@ void wfmain::setSerialDevicesUI() #ifdef Q_OS_MAC QString vspName = QStandardPaths::standardLocations(QStandardPaths::DownloadLocation)[0] + "/rig-pty"; #else - QString vspName=QDir::homePath()+"/rig-pty"; + QString vspName = QDir::homePath() + "/rig-pty"; #endif - for (i=1;i<8;i++) { + for (i = 1; i < 8; i++) { ui->vspCombo->addItem(vspName + QString::number(i)); - if (QFile::exists(vspName+QString::number(i))) { - auto * model = qobject_cast(ui->vspCombo->model()); - auto *item = model->item(ui->vspCombo->count()-1); + if (QFile::exists(vspName + QString::number(i))) { + auto* model = qobject_cast(ui->vspCombo->model()); + auto* item = model->item(ui->vspCombo->count() - 1); item->setEnabled(false); } } - ui->vspCombo->addItem(vspName+QString::number(i)); + ui->vspCombo->addItem(vspName + QString::number(i)); ui->vspCombo->addItem(QString("None"), i++); #endif @@ -1316,6 +1278,8 @@ void wfmain::setDefPrefs() defPrefs.useDarkMode = true; defPrefs.useSystemTheme = false; defPrefs.drawPeaks = true; + defPrefs.underlayMode = underlayNone; + defPrefs.underlayBufferSize = 64; defPrefs.wfAntiAlias = false; defPrefs.wfInterpolate = true; defPrefs.stylesheetPath = QString("qdarkstyle/style.qss"); @@ -1332,9 +1296,14 @@ void wfmain::setDefPrefs() defPrefs.localAFgain = 255; defPrefs.wflength = 160; defPrefs.wftheme = static_cast(QCPColorGradient::gpJet); + defPrefs.plotFloor = 0; + defPrefs.plotCeiling = 160; defPrefs.confirmExit = true; defPrefs.confirmPowerOff = true; defPrefs.meter2Type = meterNone; + defPrefs.tcpPort = 0; + defPrefs.waterfallFormat = 0; + defPrefs.audioSystem = qtAudio; udpDefPrefs.ipAddress = QString(""); udpDefPrefs.controlLANPort = 50001; @@ -1356,7 +1325,15 @@ void wfmain::loadSettings() prefs.useDarkMode = settings->value("UseDarkMode", defPrefs.useDarkMode).toBool(); prefs.useSystemTheme = settings->value("UseSystemTheme", defPrefs.useSystemTheme).toBool(); prefs.wftheme = settings->value("WFTheme", defPrefs.wftheme).toInt(); + prefs.plotFloor = settings->value("plotFloor", defPrefs.plotFloor).toInt(); + prefs.plotCeiling = settings->value("plotCeiling", defPrefs.plotCeiling).toInt(); + plotFloor = prefs.plotFloor; + plotCeiling = prefs.plotCeiling; + wfFloor = prefs.plotFloor; + wfCeiling = prefs.plotCeiling; prefs.drawPeaks = settings->value("DrawPeaks", defPrefs.drawPeaks).toBool(); + prefs.underlayBufferSize = settings->value("underlayBufferSize", defPrefs.underlayBufferSize).toInt(); + prefs.underlayMode = static_cast(settings->value("underlayMode", defPrefs.underlayMode).toInt()); prefs.wfAntiAlias = settings->value("WFAntiAlias", defPrefs.wfAntiAlias).toBool(); prefs.wfInterpolate = settings->value("WFInterpolate", defPrefs.wfInterpolate).toBool(); prefs.wflength = (unsigned int)settings->value("WFLength", defPrefs.wflength).toInt(); @@ -1404,7 +1381,6 @@ void wfmain::loadSettings() prefs.colorScheme.Light_TuningLine = QColor::fromRgba(settings->value("Light_TuningLine", defaultColors.Light_TuningLine.rgba()).toUInt()); settings->endGroup(); - // Radio and Comms: C-IV addr, port to use settings->beginGroup("Radio"); prefs.radioCIVAddr = (unsigned char)settings->value("RigCIVuInt", defPrefs.radioCIVAddr).toInt(); @@ -1454,6 +1430,14 @@ void wfmain::loadSettings() prefs.localAFgain = (unsigned char)settings->value("localAFgain", defPrefs.localAFgain).toUInt(); rxSetup.localAFgain = prefs.localAFgain; + txSetup.localAFgain = 255; + + prefs.audioSystem = static_cast(settings->value("AudioSystem", defPrefs.audioSystem).toInt()); + ui->audioSystemCombo->blockSignals(true); + ui->audioSystemCombo->setCurrentIndex(prefs.audioSystem); + ui->audioSystemCombo->blockSignals(false); + + settings->endGroup(); // Misc. user settings (enable PTT, draw peaks, etc) @@ -1461,20 +1445,19 @@ void wfmain::loadSettings() prefs.enablePTT = settings->value("EnablePTT", defPrefs.enablePTT).toBool(); ui->pttEnableChk->setChecked(prefs.enablePTT); prefs.niceTS = settings->value("NiceTS", defPrefs.niceTS).toBool(); + settings->endGroup(); settings->beginGroup("LAN"); prefs.enableLAN = settings->value("EnableLAN", defPrefs.enableLAN).toBool(); - if (prefs.enableLAN) - { - ui->baudRateCombo->setEnabled(false); - ui->serialDeviceListCombo->setEnabled(false); - } - else { - ui->baudRateCombo->setEnabled(true); - ui->serialDeviceListCombo->setEnabled(true); - } + + // If LAN is enabled, server gets its audio straight from the LAN + ui->serverRXAudioInputCombo->setEnabled(!prefs.enableLAN); + ui->serverTXAudioOutputCombo->setEnabled(!prefs.enableLAN); + + ui->baudRateCombo->setEnabled(!prefs.enableLAN); + ui->serialDeviceListCombo->setEnabled(!prefs.enableLAN); ui->lanEnableBtn->setChecked(prefs.enableLAN); ui->connectBtn->setEnabled(true); @@ -1486,6 +1469,14 @@ void wfmain::loadSettings() // Call the function to start rigctld if enabled. on_enableRigctldChk_clicked(prefs.enableRigCtlD); + prefs.tcpPort = settings->value("TcpServerPort", defPrefs.tcpPort).toInt(); + ui->tcpServerPortTxt->setText(QString("%1").arg(prefs.tcpPort)); + + prefs.waterfallFormat = settings->value("WaterfallFormat", defPrefs.waterfallFormat).toInt(); + ui->waterfallFormatCombo->blockSignals(true); + ui->waterfallFormatCombo->setCurrentIndex(prefs.waterfallFormat); + ui->waterfallFormatCombo->blockSignals(false); + udpPrefs.ipAddress = settings->value("IPAddress", udpDefPrefs.ipAddress).toString(); ui->ipAddressTxt->setEnabled(ui->lanEnableBtn->isChecked()); ui->ipAddressTxt->setText(udpPrefs.ipAddress); @@ -1516,10 +1507,11 @@ void wfmain::loadSettings() ui->txLatencySlider->setTracking(false); // Stop it sending value on every change. ui->audioSampleRateCombo->blockSignals(true); - rxSetup.samplerate = settings->value("AudioRXSampleRate", "48000").toInt(); - txSetup.samplerate = rxSetup.samplerate; + rxSetup.sampleRate=settings->value("AudioRXSampleRate", "48000").toInt(); + txSetup.sampleRate=rxSetup.sampleRate; + ui->audioSampleRateCombo->setEnabled(ui->lanEnableBtn->isChecked()); - int audioSampleRateIndex = ui->audioSampleRateCombo->findText(QString::number(rxSetup.samplerate)); + int audioSampleRateIndex = ui->audioSampleRateCombo->findText(QString::number(rxSetup.sampleRate)); if (audioSampleRateIndex != -1) { ui->audioSampleRateCombo->setCurrentIndex(audioSampleRateIndex); } @@ -1556,39 +1548,12 @@ void wfmain::loadSettings() ui->audioTXCodecCombo->setCurrentIndex(f); ui->audioRXCodecCombo->blockSignals(false); - ui->audioOutputCombo->blockSignals(true); rxSetup.name = settings->value("AudioOutput", "").toString(); - qInfo(logGui()) << "Got Audio Output: " << rxSetup.name; - int audioOutputIndex = ui->audioOutputCombo->findText(rxSetup.name); - if (audioOutputIndex != -1) { - ui->audioOutputCombo->setCurrentIndex(audioOutputIndex); -#if defined(RTAUDIO) - rxSetup.port = ui->audioOutputCombo->itemData(audioOutputIndex).toInt(); -#elif defined(PORTAUDIO) - rxSetup.port = ui->audioOutputCombo->itemData(audioOutputIndex).toInt(); -#else - QVariant v = ui->audioOutputCombo->currentData(); - rxSetup.port = v.value(); -#endif - } - ui->audioOutputCombo->blockSignals(false); + qInfo(logGui()) << "Got Audio Output from Settings: " << rxSetup.name; - ui->audioInputCombo->blockSignals(true); txSetup.name = settings->value("AudioInput", "").toString(); - qInfo(logGui()) << "Got Audio Input: " << txSetup.name; - int audioInputIndex = ui->audioInputCombo->findText(txSetup.name); - if (audioInputIndex != -1) { - ui->audioInputCombo->setCurrentIndex(audioInputIndex); -#if defined(RTAUDIO) - txSetup.port = ui->audioInputCombo->itemData(audioInputIndex).toInt(); -#elif defined(PORTAUDIO) - txSetup.port = ui->audioInputCombo->itemData(audioInputIndex).toInt(); -#else - QVariant v = ui->audioInputCombo->currentData(); - txSetup.port = v.value(); -#endif - } - ui->audioInputCombo->blockSignals(false); + qInfo(logGui()) << "Got Audio Input from Settings: " << txSetup.name; + rxSetup.resampleQuality = settings->value("ResampleQuality", "4").toInt(); txSetup.resampleQuality = rxSetup.resampleQuality; @@ -1602,64 +1567,70 @@ void wfmain::loadSettings() serverConfig.controlPort = settings->value("ServerControlPort", 50001).toInt(); serverConfig.civPort = settings->value("ServerCivPort", 50002).toInt(); serverConfig.audioPort = settings->value("ServerAudioPort", 50003).toInt(); - int numUsers = settings->value("ServerNumUsers", 2).toInt(); + serverConfig.users.clear(); - for (int f = 0; f < numUsers; f++) - { - SERVERUSER user; - user.username = settings->value("ServerUsername_" + QString::number(f), "").toString(); - user.password = settings->value("ServerPassword_" + QString::number(f), "").toString(); - user.userType = settings->value("ServerUserType_" + QString::number(f), 0).toInt(); - serverConfig.users.append(user); + + int numUsers = settings->beginReadArray("Users"); + if (numUsers > 0) { + { + for (int f = 0; f < numUsers; f++) + { + settings->setArrayIndex(f); + SERVERUSER user; + user.username = settings->value("Username", "").toString(); + user.password = settings->value("Password", "").toString(); + user.userType = settings->value("UserType", 0).toInt(); + serverConfig.users.append(user); + } + } + settings->endArray(); } + else { + /* Support old way of storing users*/ + settings->endArray(); + numUsers = settings->value("ServerNumUsers", 2).toInt(); + for (int f = 0; f < numUsers; f++) + { + SERVERUSER user; + user.username = settings->value("ServerUsername_" + QString::number(f), "").toString(); + user.password = settings->value("ServerPassword_" + QString::number(f), "").toString(); + user.userType = settings->value("ServerUserType_" + QString::number(f), 0).toInt(); + serverConfig.users.append(user); + } + } + ui->serverEnableCheckbox->setChecked(serverConfig.enabled); ui->serverControlPortText->setText(QString::number(serverConfig.controlPort)); ui->serverCivPortText->setText(QString::number(serverConfig.civPort)); ui->serverAudioPortText->setText(QString::number(serverConfig.audioPort)); - serverRxSetup.isinput = true; - serverTxSetup.isinput = false; + RIGCONFIG* rigTemp = new RIGCONFIG(); + rigTemp->rxAudioSetup.isinput = true; + rigTemp->txAudioSetup.isinput = false; + rigTemp->rxAudioSetup.localAFgain = 255; + rigTemp->txAudioSetup.localAFgain = 255; + rigTemp->rxAudioSetup.resampleQuality = 4; + rigTemp->txAudioSetup.resampleQuality = 4; + rigTemp->rxAudioSetup.type = prefs.audioSystem; + rigTemp->txAudioSetup.type = prefs.audioSystem; - ui->serverRXAudioInputCombo->blockSignals(true); - serverRxSetup.name = settings->value("ServerAudioInput", "").toString(); - qInfo(logGui()) << "Got Server Audio Input: " << serverRxSetup.name; - int serverAudioInputIndex = ui->serverRXAudioInputCombo->findText(serverRxSetup.name); - if (serverAudioInputIndex != -1) { - ui->serverRXAudioInputCombo->setCurrentIndex(serverAudioInputIndex); -#if defined(RTAUDIO) - serverRxSetup.port = ui->serverRXAudioInputCombo->itemData(serverAudioInputIndex).toInt(); -#elif defined(PORTAUDIO) - serverRxSetup.port = ui->audioOutputCombo->itemData(serverAudioInputIndex).toInt(); -#else - QVariant v = ui->serverRXAudioInputCombo->currentData(); - serverRxSetup.port = v.value(); -#endif + rigTemp->baudRate = prefs.serialPortBaud; + rigTemp->civAddr = prefs.radioCIVAddr; + rigTemp->serialPort = prefs.serialPortBaud; + + QString guid = settings->value("GUID", "").toString(); + if (guid.isEmpty()) { + guid = QUuid::createUuid().toString(); + settings->setValue("GUID", guid); } - ui->serverRXAudioInputCombo->blockSignals(false); - - serverRxSetup.resampleQuality = settings->value("ResampleQuality", "4").toInt(); - serverRxSetup.resampleQuality = rxSetup.resampleQuality; - - ui->serverTXAudioOutputCombo->blockSignals(true); - serverTxSetup.name = settings->value("ServerAudioOutput", "").toString(); - qInfo(logGui()) << "Got Server Audio Output: " << serverTxSetup.name; - int serverAudioOutputIndex = ui->serverTXAudioOutputCombo->findText(serverTxSetup.name); - if (serverAudioOutputIndex != -1) { - ui->serverTXAudioOutputCombo->setCurrentIndex(serverAudioOutputIndex); -#if defined(RTAUDIO) - serverTxSetup.port = ui->serverTXAudioOutputCombo->itemData(serverAudioOutputIndex).toInt(); -#elif defined(PORTAUDIO) - serverTxSetup.port = ui->serverTXAudioOutputCombo->itemData(serverAudioOutputIndex).toInt(); -#else - QVariant v = ui->serverTXAudioOutputCombo->currentData(); - serverRxSetup.port = v.value(); +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + memcpy(rigTemp->guid, QUuid::fromString(guid).toRfc4122().constData(), GUIDLEN); #endif - } - ui->serverTXAudioOutputCombo->blockSignals(false); - serverTxSetup.resampleQuality = settings->value("ResampleQuality", "4").toInt(); - serverTxSetup.resampleQuality = rxSetup.resampleQuality; + rigTemp->rxAudioSetup.name = settings->value("ServerAudioInput", "").toString(); + rigTemp->txAudioSetup.name = settings->value("ServerAudioOutput", "").toString(); + serverConfig.rigs.append(rigTemp); int row = 0; ui->serverUsersTable->setRowCount(0); @@ -1784,45 +1755,55 @@ void wfmain::on_serverEnableCheckbox_clicked(bool checked) void wfmain::on_serverControlPortText_textChanged(QString text) { - serverConfig.controlPort = ui->serverControlPortText->text().toInt(); + serverConfig.controlPort = text.toInt(); } void wfmain::on_serverCivPortText_textChanged(QString text) { - serverConfig.civPort = ui->serverCivPortText->text().toInt(); + serverConfig.civPort = text.toInt(); } void wfmain::on_serverAudioPortText_textChanged(QString text) { - serverConfig.audioPort = ui->serverAudioPortText->text().toInt(); + serverConfig.audioPort = text.toInt(); } void wfmain::on_serverRXAudioInputCombo_currentIndexChanged(int value) { -#if defined(RTAUDIO) - serverRxSetup.port = ui->serverRXAudioInputCombo->itemData(value).toInt(); -#elif defined(PORTAUDIO) - serverRxSetup.port = ui->serverRXAudioInputCombo->itemData(value).toInt(); -#else - QVariant v = ui->serverRXAudioInputCombo->itemData(value); - serverRxSetup.port = v.value(); -#endif - serverRxSetup.name = ui->serverRXAudioInputCombo->itemText(value); - qDebug(logGui()) << "Changed default server audio input to:" << serverRxSetup.name; + + if (!serverConfig.rigs.isEmpty()) + { + if (prefs.audioSystem == qtAudio) { + QVariant v = ui->serverRXAudioInputCombo->itemData(value); + serverConfig.rigs.first()->rxAudioSetup.port = v.value(); + } + else { + serverConfig.rigs.first()->rxAudioSetup.portInt = ui->serverRXAudioInputCombo->itemData(value).toInt(); + } + + serverConfig.rigs.first()->rxAudioSetup.name = ui->serverRXAudioInputCombo->itemText(value); + } + + } void wfmain::on_serverTXAudioOutputCombo_currentIndexChanged(int value) { -#if defined(RTAUDIO) - serverTxSetup.port = ui->serverTXAudioOutputCombo->itemData(value).toInt(); -#elif defined(PORTAUDIO) - serverTxSetup.port = ui->serverTXAudioOutputCombo->itemData(value).toInt(); -#else - QVariant v = ui->serverTXAudioOutputCombo->itemData(value); - serverTxSetup.port = v.value(); -#endif - serverTxSetup.name = ui->serverTXAudioOutputCombo->itemText(value); - qDebug(logGui()) << "Changed default server audio output to:" << serverTxSetup.name; + + if (!serverConfig.rigs.isEmpty()) + { + if (prefs.audioSystem == qtAudio) { + QVariant v = ui->serverTXAudioOutputCombo->itemData(value); + serverConfig.rigs.first()->txAudioSetup.port = v.value(); + } + else { + serverConfig.rigs.first()->txAudioSetup.portInt = ui->serverTXAudioOutputCombo->itemData(value).toInt(); + } + + serverConfig.rigs.first()->txAudioSetup.name = ui->serverTXAudioOutputCombo->itemText(value); + } + + } void wfmain::on_serverUsersTable_cellChanged(int row, int column) @@ -1860,9 +1841,13 @@ void wfmain::saveSettings() settings->setValue("UseSystemTheme", prefs.useSystemTheme); settings->setValue("UseDarkMode", prefs.useDarkMode); settings->setValue("DrawPeaks", prefs.drawPeaks); + settings->setValue("underlayMode", prefs.underlayMode); + settings->setValue("underlayBufferSize", prefs.underlayBufferSize); settings->setValue("WFAntiAlias", prefs.wfAntiAlias); settings->setValue("WFInterpolate", prefs.wfInterpolate); settings->setValue("WFTheme", prefs.wftheme); + settings->setValue("plotFloor", prefs.plotFloor); + settings->setValue("plotCeiling", prefs.plotCeiling); settings->setValue("StylesheetPath", prefs.stylesheetPath); settings->setValue("splitter", ui->splitter->saveState()); settings->setValue("windowGeometry", saveGeometry()); @@ -1882,6 +1867,8 @@ void wfmain::saveSettings() settings->setValue("SerialPortBaud", prefs.serialPortBaud); settings->setValue("VirtualSerialPort", prefs.virtualSerialPort); settings->setValue("localAFgain", prefs.localAFgain); + settings->setValue("AudioSystem", prefs.audioSystem); + settings->endGroup(); // Misc. user settings (enable PTT, draw peaks, etc) @@ -1893,7 +1880,9 @@ void wfmain::saveSettings() settings->beginGroup("LAN"); settings->setValue("EnableLAN", prefs.enableLAN); settings->setValue("EnableRigCtlD", prefs.enableRigCtlD); + settings->setValue("TcpServerPort", prefs.tcpPort); settings->setValue("RigCtlPort", prefs.rigCtlPort); + settings->setValue("tcpServerPort", prefs.tcpPort); settings->setValue("IPAddress", udpPrefs.ipAddress); settings->setValue("ControlLANPort", udpPrefs.controlLANPort); settings->setValue("SerialLANPort", udpPrefs.serialLANPort); @@ -1902,14 +1891,16 @@ void wfmain::saveSettings() settings->setValue("Password", udpPrefs.password); settings->setValue("AudioRXLatency", rxSetup.latency); settings->setValue("AudioTXLatency", txSetup.latency); - settings->setValue("AudioRXSampleRate", rxSetup.samplerate); + settings->setValue("AudioRXSampleRate", rxSetup.sampleRate); settings->setValue("AudioRXCodec", rxSetup.codec); - settings->setValue("AudioTXSampleRate", txSetup.samplerate); + settings->setValue("AudioTXSampleRate", txSetup.sampleRate); settings->setValue("AudioTXCodec", txSetup.codec); settings->setValue("AudioOutput", rxSetup.name); settings->setValue("AudioInput", txSetup.name); settings->setValue("ResampleQuality", rxSetup.resampleQuality); settings->setValue("ClientName", udpPrefs.clientName); + settings->setValue("WaterfallFormat", prefs.waterfallFormat); + settings->endGroup(); // Memory channels @@ -1982,18 +1973,32 @@ void wfmain::saveSettings() settings->setValue("ServerControlPort", serverConfig.controlPort); settings->setValue("ServerCivPort", serverConfig.civPort); settings->setValue("ServerAudioPort", serverConfig.audioPort); - settings->setValue("ServerNumUsers", serverConfig.users.count()); - settings->setValue("ServerAudioOutput", serverTxSetup.name); - settings->setValue("ServerAudioInput", serverRxSetup.name); + settings->setValue("ServerAudioOutput", serverConfig.rigs.first()->txAudioSetup.name); + settings->setValue("ServerAudioInput", serverConfig.rigs.first()->rxAudioSetup.name); + /* Remove old format users*/ + int numUsers = settings->value("ServerNumUsers", 0).toInt(); + if (numUsers > 0) { + settings->remove("ServerNumUsers"); + for (int f = 0; f < numUsers; f++) + { + settings->remove("ServerUsername_" + QString::number(f)); + settings->remove("ServerPassword_" + QString::number(f)); + settings->remove("ServerUserType_" + QString::number(f)); + } + } + + settings->beginWriteArray("Users"); for (int f = 0; f < serverConfig.users.count(); f++) { - settings->setValue("ServerUsername_" + QString::number(f), serverConfig.users[f].username); - settings->setValue("ServerPassword_" + QString::number(f), serverConfig.users[f].password); - settings->setValue("ServerUserType_" + QString::number(f), serverConfig.users[f].userType); + settings->setArrayIndex(f); + settings->setValue("Username", serverConfig.users[f].username); + settings->setValue("Password", serverConfig.users[f].password); + settings->setValue("UserType", serverConfig.users[f].userType); } + settings->endArray(); qInfo() << "Server config stored"; settings->endGroup(); @@ -2075,6 +2080,15 @@ void wfmain::prepareWf(unsigned int wfLength) QByteArray empty((int)spectWidth, '\x01'); spectrumPeaks = QByteArray( (int)spectWidth, '\x01' ); + if(spectrumPlasmaSize == 0) + spectrumPlasmaSize = 128; + + //spectrumPlasma.resize(spectrumPlasmaSize); + for(unsigned int p=0; p < spectrumPlasmaSize; p++) + { + spectrumPlasma.append(empty); + } + //wfimage.resize(wfLengthMax); if((unsigned int)wfimage.size() < wfLengthMax) @@ -2449,7 +2463,7 @@ void wfmain:: getInitialRigState() // These are made when the program starts up // and are used to adjust the UI to match the radio settings // the polling interval is set at 200ms. Faster is possible but slower - // computers will glitch occassionally. + // computers will glitch occasionally. issueDelayedCommand(cmdGetFreq); issueDelayedCommand(cmdGetMode); @@ -3214,8 +3228,18 @@ void wfmain::receiveRigID(rigCapabilities rigCaps) this->rigCaps = rigCaps; rigName->setText(rigCaps.modelName); + if (serverConfig.enabled) { + serverConfig.rigs.first()->modelName = rigCaps.modelName; + serverConfig.rigs.first()->rigName = rigCaps.modelName; + serverConfig.rigs.first()->civAddr = rigCaps.civ; + serverConfig.rigs.first()->baudRate = rigCaps.baudRate; + } setWindowTitle(rigCaps.modelName); this->spectWidth = rigCaps.spectLenMax; // used once haveRigCaps is true. + //wfCeiling = rigCaps.spectAmpMax; + //plotCeiling = rigCaps.spectAmpMax; + ui->topLevelSlider->setMaximum(rigCaps.spectAmpMax); + haveRigCaps = true; // Added so that server receives rig capabilities. emit sendRigCaps(rigCaps); @@ -3316,7 +3340,7 @@ void wfmain::receiveRigID(rigCapabilities rigCaps) ui->antennaSelCombo->setDisabled(false); for(unsigned int i=0; i < rigCaps.antennas.size(); i++) { - inName = QString("%1").arg(rigCaps.antennas.at(i)+1, 0, 16); // adding 1 to have the combobox start with ant 1 insted of 0 + inName = QString("%1").arg(rigCaps.antennas.at(i)+1, 0, 16); // adding 1 to have the combobox start with ant 1 instead of 0 ui->antennaSelCombo->addItem(inName, rigCaps.antennas.at(i)); } } else { @@ -3335,6 +3359,8 @@ void wfmain::receiveRigID(rigCapabilities rigCaps) { ui->scopeBWCombo->addItem(rigCaps.scopeCenterSpans.at(i).name, (int)rigCaps.scopeCenterSpans.at(i).cstype); } + plot->yAxis->setRange(QCPRange(prefs.plotFloor, prefs.plotCeiling)); + colorMap->setDataRange(QCPRange(prefs.plotFloor, prefs.plotCeiling)); } else { ui->scopeBWCombo->setHidden(true); } @@ -3351,6 +3377,8 @@ void wfmain::receiveRigID(rigCapabilities rigCaps) ui->useRTSforPTTchk->blockSignals(false); ui->connectBtn->setText("Disconnect"); // We must be connected now. + ui->audioSystemCombo->setEnabled(false); + prepareWf(ui->wfLengthSlider->value()); if(usingLAN) { @@ -3447,7 +3475,7 @@ void wfmain::insertSlowPeriodicCommand(cmds cmd, unsigned char priority) { // TODO: meaningful priority // These commands are run every 20 "ticks" of the primary radio command loop - // Basically 20 times less often than the standard peridic command + // Basically 20 times less often than the standard periodic command if(priority < 10) { slowPollCmdQueue.push_front(cmd); @@ -3505,15 +3533,19 @@ void wfmain::receiveSpectrumData(QByteArray spectrum, double startFreq, double e return; } + QElapsedTimer performanceTimer; + bool updateRange = false; + if((startFreq != oldLowerFreq) || (endFreq != oldUpperFreq)) { // If the frequency changed and we were drawing peaks, now is the time to clearn them - if(drawPeaks) + if(underlayMode == underlayPeakHold) { // TODO: create non-button function to do this // This will break if the button is ever moved or renamed. on_clearPeakBtn_clicked(); } + // TODO: Add clear-out for the buffer } oldLowerFreq = startFreq; @@ -3544,7 +3576,7 @@ void wfmain::receiveSpectrumData(QByteArray spectrum, double startFreq, double e { //x[i] = (i * (endFreq-startFreq)/specLen) + startFreq; y[i] = (unsigned char)spectrum.at(i); - if(drawPeaks) + if(underlayMode == underlayPeakHold) { if((unsigned char)spectrum.at(i) > (unsigned char)spectrumPeaks.at(i)) { @@ -3552,46 +3584,101 @@ void wfmain::receiveSpectrumData(QByteArray spectrum, double startFreq, double e } y2[i] = (unsigned char)spectrumPeaks.at(i); } - } + plasmaMutex.lock(); + spectrumPlasma.push_front(spectrum); + spectrumPlasma.pop_back(); + //spectrumPlasma.resize(spectrumPlasmaSize); + plasmaMutex.unlock(); + + // HACK DO NOT CHECK IN: + drawPeaks = false; + drawPlasma = true; if(!spectrumDrawLock) { + if((plotFloor != oldPlotFloor) || (plotCeiling != oldPlotCeiling)) + updateRange = true; + //ui->qcp->addGraph(); - plot->graph(0)->setData(x,y); + plot->graph(0)->setData(x,y, true); if((freq.MHzDouble < endFreq) && (freq.MHzDouble > startFreq)) { freqIndicatorLine->start->setCoords(freq.MHzDouble,0); - freqIndicatorLine->end->setCoords(freq.MHzDouble,160); + freqIndicatorLine->end->setCoords(freq.MHzDouble,rigCaps.spectAmpMax); } - if(drawPeaks) + if(underlayMode == underlayPeakHold) { - plot->graph(1)->setData(x,y2); // peaks + plot->graph(1)->setData(x,y2, true); // peaks + } else if (underlayMode != underlayNone) { + computePlasma(); + plot->graph(1)->setData(x,spectrumPlasmaLine); + } else { + plot->graph(1)->setData(x,y2, true); // peaks, but probably cleared out } - plot->yAxis->setRange(0, rigCaps.spectAmpMax+1); + + if(updateRange) + plot->yAxis->setRange(prefs.plotFloor, prefs.plotCeiling); + plot->xAxis->setRange(startFreq, endFreq); plot->replot(); if(specLen == spectWidth) { wfimage.prepend(spectrum); - wfimage.resize(wfLengthMax); - wfimage.squeeze(); - + wfimage.pop_back(); + QByteArray wfRow; // Waterfall: for(int row = 0; row < wfLength; row++) { + wfRow = wfimage.at(row); for(int col = 0; col < spectWidth; col++) { - colorMap->data()->setCell( col, row, wfimage.at(row).at(col)); + colorMap->data()->setCell( col, row, wfRow.at(col)); } } + if(updateRange) + { + colorMap->setDataRange(QCPRange(wfFloor, wfCeiling)); + } + wf->yAxis->setRange(0,wfLength - 1); + wf->xAxis->setRange(0, spectWidth-1); + wf->replot(); + } + oldPlotFloor = plotFloor; + oldPlotCeiling = plotCeiling; + } +} - wf->yAxis->setRange(0,wfLength - 1); - wf->xAxis->setRange(0, spectWidth-1); - wf->replot(); +void wfmain::computePlasma() +{ + plasmaMutex.lock(); + spectrumPlasmaLine.clear(); + spectrumPlasmaLine.resize(spectWidth); + int specPlasmaSize = spectrumPlasma.size(); + if(underlayMode == underlayAverageBuffer) + { + for(int col=0; col < spectWidth; col++) + { + for(int pos=0; pos < specPlasmaSize; pos++) + { + spectrumPlasmaLine[col] += spectrumPlasma[pos][col]; + } + spectrumPlasmaLine[col] = spectrumPlasmaLine[col] / specPlasmaSize; + } + } else if (underlayMode == underlayPeakBuffer){ + // peak mode, running peak display + for(int col=0; col < spectWidth; col++) + { + for(int pos=0; pos < specPlasmaSize; pos++) + { + if((double)(spectrumPlasma[pos][col]) > spectrumPlasmaLine[col]) + spectrumPlasmaLine[col] = spectrumPlasma[pos][col]; + } } } + plasmaMutex.unlock(); + } void wfmain::receiveSpectrumMode(spectrumMode spectMode) @@ -4007,7 +4094,7 @@ void wfmain::changeMode(mode_kind mode, bool dataOn) void wfmain::on_modeSelectCombo_activated(int index) { // The "acticvated" signal means the user initiated a mode change. - // This function is not called if code initated the change. + // This function is not called if code initiated the change. mode_info mode; unsigned char newMode = static_cast(ui->modeSelectCombo->itemData(index).toUInt()); @@ -4110,7 +4197,7 @@ void wfmain::on_freqDial_valueChanged(int value) } // With the number of steps and direction of steps established, - // we can now adjust the frequeny: + // we can now adjust the frequency: f.Hz = roundFrequencyWithStep(freq.Hz, delta, tsKnobHz); f.MHzDouble = f.Hz / (double)1E6; @@ -4556,6 +4643,8 @@ void wfmain::on_serialEnableBtn_clicked(bool checked) ui->audioInputCombo->setEnabled(!checked); ui->baudRateCombo->setEnabled(checked); ui->serialDeviceListCombo->setEnabled(checked); + ui->serverRXAudioInputCombo->setEnabled(checked); + ui->serverTXAudioOutputCombo->setEnabled(checked); } void wfmain::on_lanEnableBtn_clicked(bool checked) @@ -4577,6 +4666,8 @@ void wfmain::on_lanEnableBtn_clicked(bool checked) ui->audioInputCombo->setEnabled(checked); ui->baudRateCombo->setEnabled(!checked); ui->serialDeviceListCombo->setEnabled(!checked); + ui->serverRXAudioInputCombo->setEnabled(!checked); + ui->serverTXAudioOutputCombo->setEnabled(!checked); if(checked) { showStatusBarText("After filling in values, press Save Settings."); @@ -4605,38 +4696,41 @@ void wfmain::on_passwordTxt_textChanged(QString text) void wfmain::on_audioOutputCombo_currentIndexChanged(int value) { -#if defined(RTAUDIO) - rxSetup.port = ui->audioOutputCombo->itemData(value).toInt(); -#elif defined(PORTAUDIO) - rxSetup.port = ui->audioOutputCombo->itemData(value).toInt(); -#else - QVariant v = ui->audioOutputCombo->itemData(value); - rxSetup.port = v.value(); -#endif + + if (prefs.audioSystem == qtAudio) { + QVariant v = ui->audioOutputCombo->itemData(value); + rxSetup.port = v.value(); + } + else { + rxSetup.portInt = ui->audioOutputCombo->itemData(value).toInt(); + } + rxSetup.name = ui->audioOutputCombo->itemText(value); - qDebug(logGui()) << "Changed default audio output to:" << rxSetup.name; + qDebug(logGui()) << "Changed audio output to:" << rxSetup.name; } void wfmain::on_audioInputCombo_currentIndexChanged(int value) { -#if defined(RTAUDIO) - txSetup.port = ui->audioInputCombo->itemData(value).toInt(); -#elif defined(PORTAUDIO) - txSetup.port = ui->audioInputCombo->itemData(value).toInt(); -#else - QVariant v = ui->audioInputCombo->itemData(value); - txSetup.port = v.value(); -#endif + + if (prefs.audioSystem == qtAudio) { + QVariant v = ui->audioInputCombo->itemData(value); + txSetup.port = v.value(); + } + else { + txSetup.portInt = ui->audioInputCombo->itemData(value).toInt(); + } + txSetup.name = ui->audioInputCombo->itemText(value); - qDebug(logGui()) << "Changed default audio input to:" << txSetup.name; + + qDebug(logGui()) << "Changed audio input to:" << txSetup.name; } void wfmain::on_audioSampleRateCombo_currentIndexChanged(QString text) { //udpPrefs.audioRXSampleRate = text.toInt(); //udpPrefs.audioTXSampleRate = text.toInt(); - rxSetup.samplerate = text.toInt(); - txSetup.samplerate = text.toInt(); + rxSetup.sampleRate=text.toInt(); + txSetup.sampleRate=text.toInt(); } void wfmain::on_audioRXCodecCombo_currentIndexChanged(int value) @@ -4683,6 +4777,7 @@ void wfmain::on_connectBtn_clicked() if (haveRigCaps) { emit sendCloseComm(); ui->connectBtn->setText("Connect"); + ui->audioSystemCombo->setEnabled(true); haveRigCaps = false; rigName->setText("NONE"); } @@ -4809,7 +4904,7 @@ void wfmain::setRadioTimeDatePrep() int msecdelay = QTime::currentTime().msecsTo( QTime::currentTime().addSecs(60-second) ); // 3: Compute time and date at one minute later - QDateTime setpoint = now.addMSecs(msecdelay); // at HMS or posibly HMS + some ms. Never under though. + QDateTime setpoint = now.addMSecs(msecdelay); // at HMS or possibly HMS + some ms. Never under though. // 4: Prepare data structs for the time at one minute later timesetpoint.hours = (unsigned char)setpoint.time().hour(); @@ -5714,6 +5809,22 @@ void wfmain::on_rigctldPortTxt_editingFinished() } } +void wfmain::on_tcpServerPortTxt_editingFinished() +{ + + bool okconvert = false; + unsigned int port = ui->tcpServerPortTxt->text().toUInt(&okconvert); + if (okconvert) + { + prefs.tcpPort = port; + } +} + +void wfmain::on_waterfallFormatCombo_activated(int index) +{ + prefs.waterfallFormat = index; +} + void wfmain::on_moreControlsBtn_clicked() { trxadj->show(); @@ -5735,15 +5846,372 @@ void wfmain::on_setClockBtn_clicked() setRadioTimeDatePrep(); } +void wfmain::radioSelection(QList radios) +{ + selRad->populate(radios); +} + +void wfmain::on_radioStatusBtn_clicked() +{ + if (selRad->isVisible()) + { + selRad->hide(); + } + else + { + selRad->show(); + } +} + +void wfmain::setAudioDevicesUI() +{ + + // Enumerate audio devices, need to do before settings are loaded, + // First clear all existing entries + ui->audioInputCombo->blockSignals(true); + ui->audioOutputCombo->blockSignals(true); + ui->serverTXAudioOutputCombo->blockSignals(true); + ui->serverRXAudioInputCombo->blockSignals(true); + + ui->audioInputCombo->clear(); + ui->audioOutputCombo->clear(); + ui->serverTXAudioOutputCombo->clear(); + ui->serverRXAudioInputCombo->clear(); + + qDebug(logSystem()) << "Finding audio devices, output=" << rxSetup.name << "input="<audioInputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); + ui->serverRXAudioInputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); + inCount++; +#ifdef Q_OS_WIN + } +#endif + } + + const auto audioOutputs = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + for (const QAudioDeviceInfo& deviceInfo : audioOutputs) { +#ifdef Q_OS_WIN + if (deviceInfo.realm() == "wasapi") { +#endif + ui->audioOutputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); + ui->serverTXAudioOutputCombo->addItem(deviceInfo.deviceName(), QVariant::fromValue(deviceInfo)); + outCount++; +#ifdef Q_OS_WIN + } +#endif + } + break; + } + case portAudio: + { + PaError err; + + err = Pa_Initialize(); + + if (err != paNoError) + { + qInfo(logAudio()) << "ERROR: Cannot initialize Portaudio"; + return; + } + + qInfo(logAudio()) << "PortAudio version: " << Pa_GetVersionInfo()->versionText; + + int numDevices = Pa_GetDeviceCount(); + qInfo(logAudio()) << "Pa_CountDevices returned" << numDevices; + + const PaDeviceInfo* info; + for (int i = 0; i < numDevices; i++) + { + info = Pa_GetDeviceInfo(i); + if (info->maxInputChannels > 0) { + qDebug(logAudio()) << (i == Pa_GetDefaultInputDevice() ? "*" : " ") << "(" << i << ") Input Device : " << info->name; + + ui->audioInputCombo->addItem(info->name, i); + ui->serverRXAudioInputCombo->addItem(info->name, i); + if (i == Pa_GetDefaultInputDevice()) { + defaultAudioInputIndex = inCount; + } + inCount++; + } + if (info->maxOutputChannels > 0) { + qDebug(logAudio()) << (i == Pa_GetDefaultOutputDevice() ? "*" : " ") << "(" << i << ") Output Device : " << info->name; + ui->audioOutputCombo->addItem(info->name, i); + ui->serverTXAudioOutputCombo->addItem(info->name, i); + if (i == Pa_GetDefaultOutputDevice()) { + defaultAudioOutputIndex = outCount; + } + outCount++; + } + } + break; + } + case rtAudio: + { + Pa_Terminate(); + +#if defined(Q_OS_LINUX) + RtAudio* audio = new RtAudio(RtAudio::Api::LINUX_ALSA); +#elif defined(Q_OS_WIN) + RtAudio* audio = new RtAudio(RtAudio::Api::WINDOWS_WASAPI); +#elif defined(Q_OS_MACX) + RtAudio* audio = new RtAudio(RtAudio::Api::MACOSX_CORE); +#endif + + + // Enumerate audio devices, need to do before settings are loaded. + std::map apiMap; + apiMap[RtAudio::MACOSX_CORE] = "OS-X Core Audio"; + apiMap[RtAudio::WINDOWS_ASIO] = "Windows ASIO"; + apiMap[RtAudio::WINDOWS_DS] = "Windows DirectSound"; + apiMap[RtAudio::WINDOWS_WASAPI] = "Windows WASAPI"; + apiMap[RtAudio::UNIX_JACK] = "Jack Client"; + apiMap[RtAudio::LINUX_ALSA] = "Linux ALSA"; + apiMap[RtAudio::LINUX_PULSE] = "Linux PulseAudio"; + apiMap[RtAudio::LINUX_OSS] = "Linux OSS"; + apiMap[RtAudio::RTAUDIO_DUMMY] = "RtAudio Dummy"; + + std::vector< RtAudio::Api > apis; + RtAudio::getCompiledApi(apis); + + qInfo(logAudio()) << "RtAudio Version " << QString::fromStdString(RtAudio::getVersion()); + + qInfo(logAudio()) << "Compiled APIs:"; + for (unsigned int i = 0; i < apis.size(); i++) { + qInfo(logAudio()) << " " << QString::fromStdString(apiMap[apis[i]]); + } + + RtAudio::DeviceInfo info; + + qInfo(logAudio()) << "Current API: " << QString::fromStdString(apiMap[audio->getCurrentApi()]); + + unsigned int devices = audio->getDeviceCount(); + qInfo(logAudio()) << "Found " << devices << " audio device(s) *=default"; + + for (unsigned int i = 1; i < devices; i++) { + info = audio->getDeviceInfo(i); + if (info.inputChannels > 0) { + qInfo(logAudio()) << (info.isDefaultInput ? "*" : " ") << "(" << i << ") Input Device : " << QString::fromStdString(info.name); + ui->audioInputCombo->addItem(QString::fromStdString(info.name), i); + ui->serverRXAudioInputCombo->addItem(QString::fromStdString(info.name), i); + if (info.isDefaultInput) { + defaultAudioInputIndex = inCount; + } + inCount++; + } + if (info.outputChannels > 0) { + qInfo(logAudio()) << (info.isDefaultOutput ? "*" : " ") << "(" << i << ") Output Device : " << QString::fromStdString(info.name); + ui->audioOutputCombo->addItem(QString::fromStdString(info.name), i); + ui->serverTXAudioOutputCombo->addItem(QString::fromStdString(info.name), i); + if (info.isDefaultOutput) { + defaultAudioOutputIndex = outCount; + } + outCount++; + } + } + + delete audio; + break; + } + + } + + + // Stop blocking signals so we can set the current values + ui->audioInputCombo->blockSignals(false); + ui->audioOutputCombo->blockSignals(false); + ui->serverTXAudioOutputCombo->blockSignals(false); + ui->serverRXAudioInputCombo->blockSignals(false); + + + rxSetup.type = prefs.audioSystem; + txSetup.type = prefs.audioSystem; + + int audioInputIndex = ui->audioInputCombo->findText(txSetup.name); + if (audioInputIndex != -1) { + ui->audioInputCombo->setCurrentIndex(audioInputIndex); + } + else { + qDebug(logSystem()) << "Audio input not found"; + ui->audioInputCombo->setCurrentIndex(defaultAudioInputIndex); + } + + int audioOutputIndex = ui->audioOutputCombo->findText(rxSetup.name); + if (audioOutputIndex != -1) { + ui->audioOutputCombo->setCurrentIndex(audioOutputIndex); + } + else { + qDebug(logSystem()) << "Audio output not found"; + ui->audioOutputCombo->setCurrentIndex(defaultAudioOutputIndex); + } + + if (!serverConfig.rigs.isEmpty()) + + { + qInfo(logGui()) << "Got Server Audio Input: " << serverConfig.rigs.first()->rxAudioSetup.name; + + serverConfig.rigs.first()->rxAudioSetup.type = prefs.audioSystem; + serverConfig.rigs.first()->txAudioSetup.type = prefs.audioSystem; + + int serverAudioInputIndex = ui->serverRXAudioInputCombo->findText(serverConfig.rigs.first()->rxAudioSetup.name); + if (serverAudioInputIndex != -1) { + ui->serverRXAudioInputCombo->setCurrentIndex(serverAudioInputIndex); + } + else { + // Set to default + ui->serverRXAudioInputCombo->setCurrentIndex(defaultAudioInputIndex); + } + + qInfo(logGui()) << "Got Server Audio Output: " << serverConfig.rigs.first()->txAudioSetup.name; + int serverAudioOutputIndex = ui->serverTXAudioOutputCombo->findText(serverConfig.rigs.first()->txAudioSetup.name); + if (serverAudioOutputIndex != -1) { + ui->serverTXAudioOutputCombo->setCurrentIndex(serverAudioOutputIndex); + } + else { + ui->serverTXAudioOutputCombo->setCurrentIndex(defaultAudioOutputIndex); + } + } + + qDebug(logSystem()) << "Audio devices done."; +} + +void wfmain::on_audioSystemCombo_currentIndexChanged(int value) +{ + prefs.audioSystem = static_cast(value); + setAudioDevicesUI(); // Force all audio devices to update +} + +void wfmain::on_topLevelSlider_valueChanged(int value) +{ + wfCeiling = value; + plotCeiling = value; + prefs.plotCeiling = value; + plot->yAxis->setRange(QCPRange(plotFloor, plotCeiling)); + colorMap->setDataRange(QCPRange(wfFloor, wfCeiling)); +} + +void wfmain::on_botLevelSlider_valueChanged(int value) +{ + wfFloor = value; + plotFloor = value; + prefs.plotFloor = value; + plot->yAxis->setRange(QCPRange(plotFloor, plotCeiling)); + colorMap->setDataRange(QCPRange(wfFloor, wfCeiling)); +} + +void wfmain::on_underlayBufferSlider_valueChanged(int value) +{ + resizePlasmaBuffer(value); + prefs.underlayBufferSize = value; +} + +void wfmain::resizePlasmaBuffer(int newSize) +{ + + QByteArray empty((int)spectWidth, '\x01'); + plasmaMutex.lock(); + + int oldSize = spectrumPlasma.size(); + + if(oldSize < newSize) + { + spectrumPlasma.resize(newSize); + for(int p=oldSize; p < newSize; p++) + { + spectrumPlasma.append(empty); + } + } else if (oldSize > newSize){ + for(int p = oldSize; p > newSize; p--) + { + spectrumPlasma.pop_back(); + } + } + + spectrumPlasma.squeeze(); + plasmaMutex.unlock(); +} + +void wfmain::on_underlayNone_toggled(bool checked) +{ + ui->underlayBufferSlider->setDisabled(checked); + if(checked) + { + underlayMode = underlayNone; + prefs.underlayMode = underlayMode; + on_clearPeakBtn_clicked(); + } +} + +void wfmain::on_underlayPeakHold_toggled(bool checked) +{ + ui->underlayBufferSlider->setDisabled(checked); + if(checked) + { + underlayMode = underlayPeakHold; + prefs.underlayMode = underlayMode; + on_clearPeakBtn_clicked(); + } + +} + +void wfmain::on_underlayPeakBuffer_toggled(bool checked) +{ + ui->underlayBufferSlider->setDisabled(!checked); + if(checked) + { + underlayMode = underlayPeakBuffer; + prefs.underlayMode = underlayMode; + } +} + +void wfmain::on_underlayAverageBuffer_toggled(bool checked) +{ + ui->underlayBufferSlider->setDisabled(!checked); + if(checked) + { + underlayMode = underlayAverageBuffer; + prefs.underlayMode = underlayMode; + } +} + // --- DEBUG FUNCTION --- void wfmain::on_debugBtn_clicked() { qInfo(logSystem()) << "Debug button pressed."; // issueDelayedCommand(cmdGetRigID); //emit getRigCIV(); - trxadj->show(); + //trxadj->show(); //setRadioTimeDatePrep(); //wf->setInteraction(QCP::iRangeZoom, true); //wf->setInteraction(QCP::iRangeDrag, true); + plot->yAxis->setRange(QCPRange(plotFloor, plotCeiling)); + colorMap->setDataRange(QCPRange(wfFloor, wfCeiling)); + +// bool ok; +// int height = QInputDialog::getInt(this, "wfview window fixed height", "number: ", 350, 1, 500, 1, &ok ); + +// this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); +// this->setMaximumSize(QSize(1025,height)); +// this->setMinimumSize(QSize(1025,height)); +// //this->setMaximumSize(QSize(929, 270)); +// //this->setMinimumSize(QSize(929, 270)); + +// resize(minimumSize()); +// adjustSize(); // main window +// adjustSize(); } diff --git a/wfmain.h b/wfmain.h index b4d4c50..4dda663 100644 --- a/wfmain.h +++ b/wfmain.h @@ -1,3 +1,7 @@ +#ifdef BUILD_WFSERVER +#include "servermain.h" +#else + #ifndef WFMAIN_H #define WFMAIN_H @@ -10,6 +14,8 @@ #include #include #include +#include +#include #include "logcategories.h" #include "commhandler.h" @@ -19,6 +25,7 @@ #include "rigidentities.h" #include "repeaterattributes.h" +#include "packettypes.h" #include "calibrationwindow.h" #include "repeatersetup.h" #include "satellitesetup.h" @@ -27,6 +34,7 @@ #include "qledlabel.h" #include "rigctld.h" #include "aboutbox.h" +#include "selectradio.h" #include #include @@ -34,6 +42,14 @@ #include #include +#include +#ifndef Q_OS_LINUX +#include "RtAudio.h" +#else +#include "rtaudio/RtAudio.h" +#endif + + namespace Ui { class wfmain; } @@ -158,8 +174,8 @@ signals: void sayFrequency(); void sayMode(); void sayAll(); - void sendCommSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp); - void sendCommSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp); + void sendCommSetup(unsigned char rigCivAddr, QString rigSerialPort, quint32 rigBaudRate,QString vsp, quint16 tcp, quint8 wf); + void sendCommSetup(unsigned char rigCivAddr, udpPreferences prefs, audioSetup rxSetup, audioSetup txSetup, QString vsp, quint16 tcp); void sendCloseComm(); void sendChangeLatency(quint16 latency); void initServer(); @@ -258,7 +274,7 @@ private slots: void receiveRigID(rigCapabilities rigCaps); void receiveFoundRigID(rigCapabilities rigCaps); void receiveSerialPortError(QString port, QString errorText); - void receiveStatusUpdate(QString errorText); + void receiveStatusUpdate(networkStatus status); void handlePlotClick(QMouseEvent *); void handlePlotDoubleClick(QMouseEvent *); void handleWFClick(QMouseEvent *); @@ -268,6 +284,7 @@ private slots: void sendRadioCommandLoop(); void showStatusBarText(QString text); void receiveBaudRate(quint32 baudrate); + void radioSelection(QList radios); void setRadioTimeDateSend(); @@ -490,11 +507,13 @@ private slots: void on_meter2selectionCombo_activated(int index); + void on_waterfallFormatCombo_activated(int index); + void on_enableRigctldChk_clicked(bool checked); void on_rigctldPortTxt_editingFinished(); - void setAudioDevicesUI(); + void on_tcpServerPortTxt_editingFinished(); void on_moreControlsBtn_clicked(); @@ -518,6 +537,24 @@ private slots: void on_useRTSforPTTchk_clicked(bool checked); + void on_radioStatusBtn_clicked(); + + void on_audioSystemCombo_currentIndexChanged(int value); + + void on_topLevelSlider_valueChanged(int value); + + void on_botLevelSlider_valueChanged(int value); + + void on_underlayBufferSlider_valueChanged(int value); + + void on_underlayNone_toggled(bool checked); + + void on_underlayPeakHold_toggled(bool checked); + + void on_underlayPeakBuffer_toggled(bool checked); + + void on_underlayAverageBuffer_toggled(bool checked); + private: Ui::wfmain *ui; void closeEvent(QCloseEvent *event); @@ -536,6 +573,7 @@ private: void setPlotTheme(QCustomPlot *plot, bool isDark); void prepareWf(); void prepareWf(unsigned int wfLength); + void computePlasma(); void showHideSpectrum(bool show); void getInitialRigState(); void setBandButtons(); @@ -625,7 +663,24 @@ private: quint16 wfLength; bool spectrumDrawLock; + enum underlay_t { underlayNone, underlayPeakHold, underlayPeakBuffer, underlayAverageBuffer }; + + QByteArray spectrumPeaks; + QVector spectrumPlasmaLine; + QVector spectrumPlasma; + unsigned int spectrumPlasmaSize = 64; + underlay_t underlayMode = underlayNone; + bool drawPlasma = true; + QMutex plasmaMutex; + void resizePlasmaBuffer(int newSize); + + double plotFloor = 0; + double plotCeiling = 160; + double wfFloor = 0; + double wfCeiling = 160; + double oldPlotFloor = -1; + double oldPlotCeiling = 999; QVector wfimage; unsigned int wfLengthMax; @@ -734,6 +789,8 @@ private: bool useDarkMode; bool useSystemTheme; bool drawPeaks; + underlay_t underlayMode = underlayNone; + int underlayBufferSize = 64; bool wfAntiAlias; bool wfInterpolate; QString stylesheetPath; @@ -752,10 +809,14 @@ private: unsigned char localAFgain; unsigned int wflength; int wftheme; + int plotFloor; + int plotCeiling; bool confirmExit; bool confirmPowerOff; meterKind meter2Type; - // plot scheme + quint16 tcpPort; + quint8 waterfallFormat; + audioType audioSystem; } prefs; preferences defPrefs; @@ -766,8 +827,6 @@ private: audioSetup rxSetup; audioSetup txSetup; - audioSetup serverRxSetup; - audioSetup serverTxSetup; colors defaultColors; @@ -838,7 +897,7 @@ private: satelliteSetup *sat; transceiverAdjustments *trxadj; aboutbox *abtBox; - + selectRadio *selRad; udpServer* udp = Q_NULLPTR; rigCtlD* rigCtl = Q_NULLPTR; @@ -882,12 +941,16 @@ Q_DECLARE_METATYPE(struct mode_info) Q_DECLARE_METATYPE(struct udpPreferences) Q_DECLARE_METATYPE(struct audioPacket) Q_DECLARE_METATYPE(struct audioSetup) +Q_DECLARE_METATYPE(struct SERVERCONFIG) Q_DECLARE_METATYPE(struct timekind) Q_DECLARE_METATYPE(struct datekind) +Q_DECLARE_METATYPE(struct networkStatus) Q_DECLARE_METATYPE(enum rigInput) Q_DECLARE_METATYPE(enum meterKind) Q_DECLARE_METATYPE(enum spectrumMode) +Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(rigstate*) #endif // WFMAIN_H +#endif diff --git a/wfmain.ui b/wfmain.ui index a938ee3..2e0d107 100644 --- a/wfmain.ui +++ b/wfmain.ui @@ -6,7 +6,7 @@ 0 0 - 946 + 1002 569 @@ -108,7 +108,7 @@ - <html><head/><body><p>Press button to convert center mode spectrum to fixed mode, preserving the range. This allows you to tune without the spectrum moving, in the same currently-visible range that you see now. </p><p><br/></p><p>The currently-selected edge slot will be overriden.</p></body></html> + <html><head/><body><p>Press button to convert center mode spectrum to fixed mode, preserving the range. This allows you to tune without the spectrum moving, in the same currently-visible range that you see now. </p><p><br/></p><p>The currently-selected edge slot will be overridden.</p></body></html> ToFixed @@ -148,7 +148,7 @@ Waterfall display color theme - Selects the theme for the color waterfall dispaly + Selects the theme for the color waterfall display @@ -846,6 +846,114 @@ + + + + 0 + + + 0 + + + + + + 0 + 70 + + + + + 16777215 + 80 + + + + Sets the ceiling for the waterfall and spectrum displays + + + Ceiling for waterfall and spectrum display + + + 1 + + + 160 + + + 160 + + + Qt::Vertical + + + + + + + + 16777215 + 15 + + + + Top + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 70 + + + + + 16777215 + 80 + + + + 160 + + + Qt::Vertical + + + + + + + + 16777215 + 15 + + + + Bot + + + + + @@ -2066,7 +2174,7 @@ - 0 + 1 @@ -2163,7 +2271,7 @@ - <html><head/><body><p>Enter the address in as hexidecimal, without any prefix, just as the radio presents the address in the menu. </p><p>Here are some common examples:</p> + <html><head/><body><p>Enter the address in as hexadecimal, without any prefix, just as the radio presents the address in the menu. </p><p>Here are some common examples:</p> <p>IC-706: 58 <br/>IC-756: 50 <br/>IC-756 Pro: 5C @@ -2633,6 +2741,32 @@ + + + + Audio System + + + + + + + + QT Audio + + + + + PortAudio + + + + + RT Audio + + + + @@ -2773,6 +2907,114 @@ + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Underlay Mode + + + + + + + None + + + true + + + underlayButtonGroup + + + + + + + Peak Hold + + + underlayButtonGroup + + + + + + + Peak + + + underlayButtonGroup + + + + + + + Average + + + underlayButtonGroup + + + + + + + Uneerlay Buffer Size: + + + + + + + + 100 + 16777215 + + + + 8 + + + 128 + + + 64 + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -3099,7 +3341,7 @@ - Contol Port + Control Port @@ -3527,6 +3769,93 @@ + + + + + + TCP Server Port + + + + + + + true + + + + 0 + 0 + + + + + + + + Enter port for TCP server, 0 = disabled (restart required if changed) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Waterfall Format + + + + + + + + Default + + + + + Single (network) + + + + + Multi (serial) + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -3634,6 +3963,13 @@ + + + + Radio Status + + + @@ -3670,8 +4006,8 @@ 0 0 - 946 - 22 + 1002 + 21 @@ -3693,5 +4029,6 @@ + diff --git a/wfserver.pro b/wfserver.pro new file mode 100644 index 0000000..779a7ed --- /dev/null +++ b/wfserver.pro @@ -0,0 +1,172 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2018-05-26T16:57:32 +# +#------------------------------------------------- + +QT += core serialport network multimedia + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport + +TARGET = wfserver +TEMPLATE = app + +CONFIG += console + +DEFINES += WFVIEW_VERSION=\\\"1.4\\\" + +DEFINES += BUILD_WFSERVER + + +CONFIG(debug, release|debug) { + # For Debug builds only: + QMAKE_CXXFLAGS += -faligned-new + win32:DESTDIR = wfview-release + win32:LIBS += -L../portaudio/msvc/Win32/Debug/ -lportaudio_x86 -ole32 +} else { + # For Release builds only: + linux:QMAKE_CXXFLAGS += -s + QMAKE_CXXFLAGS += -fvisibility=hidden + QMAKE_CXXFLAGS += -fvisibility-inlines-hidden + QMAKE_CXXFLAGS += -faligned-new + linux:QMAKE_LFLAGS += -O2 -s + win32:DESTDIR = wfview-debug + win32:LIBS += -L../portaudio/msvc/Win32/Release/ -lportaudio_x86 -lole32 +} + +# RTAudio defines +win32:DEFINES += __WINDOWS_WASAPI__ +#win32:DEFINES += __WINDOWS_DS__ # Requires DirectSound libraries +#linux:DEFINES += __LINUX_ALSA__ +#linux:DEFINES += __LINUX_OSS__ +linux:DEFINES += __LINUX_PULSE__ +macx:DEFINES += __MACOSX_CORE__ +!linux:SOURCES += ../rtaudio/RTAudio.cpp +!linux:HEADERS += ../rtaudio/RTAUdio.h +!linux:INCLUDEPATH += ../rtaudio +linux:LIBS += -lpulse -lpulse-simple -lrtaudio -lpthread + +win32:INCLUDEPATH += ../portaudio/include +!win32:LIBS += -lportaudio + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QCUSTOMPLOT_COMPILE_LIBRARY + + +# These defines are used for the resampler +equals(QT_ARCH, i386): DEFINES += USE_SSE +equals(QT_ARCH, i386): DEFINES += USE_SSE2 +equals(QT_ARCH, arm): DEFINES += USE_NEON +DEFINES += OUTSIDE_SPEEX +DEFINES += RANDOM_PREFIX=wf + +isEmpty(PREFIX) { + PREFIX = /usr/local +} + +# These defines are used for the Eigen library +DEFINES += EIGEN_MPL2_ONLY +DEFINES += EIGEN_DONT_VECTORIZE #Clear vector flags +equals(QT_ARCH, i386): win32:DEFINES += EIGEN_VECTORIZE_SSE3 +equals(QT_ARCH, x86_64): DEFINES += EIGEN_VECTORIZE_SSE3 + +DEFINES += PREFIX=\\\"$$PREFIX\\\" + +macx:INCLUDEPATH += /usr/local/include /opt/local/include +macx:LIBS += -L/usr/local/lib -L/opt/local/lib + +macx:ICON = ../wfview/resources/wfview.icns +win32:RC_ICONS = ../wfview/resources/wfview.ico +QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.13 +QMAKE_TARGET_BUNDLE_PREFIX = org.wfview +MY_ENTITLEMENTS.name = CODE_SIGN_ENTITLEMENTS +MY_ENTITLEMENTS.value = ../wfview/resources/wfview.entitlements +QMAKE_MAC_XCODE_SETTINGS += MY_ENTITLEMENTS +QMAKE_INFO_PLIST = ../wfview/resources/Info.plist + +!win32:DEFINES += HOST=\\\"`hostname`\\\" UNAME=\\\"`whoami`\\\" + +!win32:DEFINES += GITSHORT="\\\"$(shell git -C $$PWD rev-parse --short HEAD)\\\"" +win32:DEFINES += GITSHORT=\\\"$$system(git -C $$PWD rev-parse --short HEAD)\\\" + +win32:DEFINES += HOST=\\\"wfview.org\\\" +win32:DEFINES += UNAME=\\\"build\\\" + + +RESOURCES += qdarkstyle/style.qrc \ + resources/resources.qrc + +unix:target.path = $$PREFIX/bin +INSTALLS += target + +# Do not do this, it will hang on start: +# CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT + +CONFIG(debug, release|debug) { + win32:LIBS += -L../opus/win32/VS2015/Win32/Debug/ -lopus +} else { + win32:LIBS += -L../opus/win32/VS2015/Win32/Release/ -lopus +} + +linux:LIBS += -L./ -lopus +macx:LIBS += -framework CoreAudio -framework CoreFoundation -lpthread -lopus + +!linux:INCLUDEPATH += ../opus/include + +!linux:INCLUDEPATH += ../eigen +!linux:INCLUDEPATH += ../r8brain-free-src + +INCLUDEPATH += resampler + +SOURCES += main.cpp\ + servermain.cpp \ + commhandler.cpp \ + rigcommander.cpp \ + freqmemory.cpp \ + rigidentities.cpp \ + udpbase.cpp \ + udphandler.cpp \ + udpcivdata.cpp \ + udpaudio.cpp \ + logcategories.cpp \ + pahandler.cpp \ + rthandler.cpp \ + audiohandler.cpp \ + audioconverter.cpp \ + udpserver.cpp \ + pttyhandler.cpp \ + resampler/resample.c \ + rigctld.cpp \ + tcpserver.cpp \ + keyboard.cpp + +HEADERS += servermain.h \ + commhandler.h \ + rigcommander.h \ + freqmemory.h \ + rigidentities.h \ + udpbase.h \ + udphandler.h \ + udpcivdata.h \ + udpaudio.h \ + logcategories.h \ + pahandler.h \ + rthandler.h \ + audiohandler.h \ + audioconverter.h \ + udpserver.h \ + packettypes.h \ + pttyhandler.h \ + resampler/speex_resampler.h \ + resampler/arch.h \ + resampler/resample_sse.h \ + repeaterattributes.h \ + rigctld.h \ + ulaw.h \ + tcpserver.h \ + audiotaper.h \ + keyboard.h diff --git a/wfserver.vcxproj b/wfserver.vcxproj new file mode 100644 index 0000000..471461b --- /dev/null +++ b/wfserver.vcxproj @@ -0,0 +1,440 @@ + + + + + Release + Win32 + + + Debug + Win32 + + + + {00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56} + wfserver + QtVS_v304 + 10.0.19041.0 + 10.0.19041.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + v142 + wfview-release\ + false + NotSet + Application + release\ + wfserver + + + v142 + wfview-debug\ + false + NotSet + Application + debug\ + wfserver + + + + + + + + + + wfview-debug\debug\wfservertruewfview-release\release\wfservertruefalsemsvc2019core;network;gui;multimedia;widgets;serialport;printsupportmsvc2019core;network;gui;multimedia;widgets;serialport;printsupport + + + + .;..\rtaudio;..\portaudio\include;..\opus\include;..\eigen;..\r8brain-free-src;resampler;release;/include;%(AdditionalIncludeDirectories) + -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) + release\ + false + None + 4577;4467;%(DisableSpecificWarnings) + Sync + release\ + MaxSpeed + _CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2e";BUILD_WFSERVER;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX="/usr/local";GITSHORT="44f6ec2";HOST="wfview.org";UNAME="build";NDEBUG;QT_NO_DEBUG;%(PreprocessorDefinitions) + false + + MultiThreadedDLL + true + true + Level3 + true + + ..\portaudio\msvc\Win32\Release\portaudio_x86.lib;..\opus\win32\VS2015\Win32\Release\opus.lib;%(AdditionalDependencies) + ..\portaudio\msvc\Win32\Release;..\opus\win32\VS2015\Win32\Release;%(AdditionalLibraryDirectories) + "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) + true + false + true + false + true + $(OutDir)\wfserver.exe + true + Console + true + + + Unsigned + None + 0 + + + _CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2e\";BUILD_WFSERVER;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX=\"/usr/local\";GITSHORT=\"44f6ec2\";HOST=\"wfview.org\";UNAME=\"build\";NDEBUG;QT_NO_DEBUG;QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions) + + msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cpp + + + .;..\rtaudio;..\portaudio\include;..\opus\include;..\eigen;..\r8brain-free-src;resampler;debug;/include;%(AdditionalIncludeDirectories) + -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) + debug\ + false + ProgramDatabase + 4577;4467;%(DisableSpecificWarnings) + Sync + debug\ + Disabled + _CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2e";BUILD_WFSERVER;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX="/usr/local";GITSHORT="44f6ec2";HOST="wfview.org";UNAME="build";%(PreprocessorDefinitions) + false + MultiThreadedDebugDLL + true + true + Level3 + true + + ..\portaudio\msvc\Win32\Debug\portaudio_x86.lib;..\opus\win32\VS2015\Win32\Debug\opus.lib;%(AdditionalDependencies) + ..\portaudio\msvc\Win32\Debug;..\opus\win32\VS2015\Win32\Debug;%(AdditionalLibraryDirectories) + "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) + true + true + true + $(OutDir)\wfserver.exe + true + Console + true + + + Unsigned + None + 0 + + + _CONSOLE;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2e\";BUILD_WFSERVER;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX=\"/usr/local\";GITSHORT=\"44f6ec2\";HOST=\"wfview.org\";UNAME=\"build\";QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;_DEBUG;%(PreprocessorDefinitions) + + msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cpp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Document + true + $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) + cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -faligned-new -Zi -MDd -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >debug\moc_predefs.h + Generate moc_predefs.h + debug\moc_predefs.h;%(Outputs) + + + Document + $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) + cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -fvisibility=hidden -fvisibility-inlines-hidden -faligned-new -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >release\moc_predefs.h + Generate moc_predefs.h + release\moc_predefs.h;%(Outputs) + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + resourcesresources + + + + + + + + + + + + + + stylestyle + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wfserver.vcxproj.filters b/wfserver.vcxproj.filters new file mode 100644 index 0000000..b378c08 --- /dev/null +++ b/wfserver.vcxproj.filters @@ -0,0 +1,366 @@ + + + + + {71ED8ED8-ACB9-4CE9-BBE1-E00B30144E11} + cpp;c;cxx;moc;h;def;odl;idl;res; + + + {71ED8ED8-ACB9-4CE9-BBE1-E00B30144E11} + cpp;c;cxx;moc;h;def;odl;idl;res; + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {D9D6E242-F8AF-46E4-B9FD-80ECBC20BA3E} + qrc;* + false + + + {D9D6E242-F8AF-46E4-B9FD-80ECBC20BA3E} + qrc;* + false + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + + + + + + + + + + Generated Files + + + Generated Files + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + + + + \ No newline at end of file diff --git a/wfserver.vcxproj.user b/wfserver.vcxproj.user new file mode 100644 index 0000000..f4da9e5 --- /dev/null +++ b/wfserver.vcxproj.user @@ -0,0 +1,10 @@ + + + + + 2022-04-13T11:33:50.3607712Z + + + 2022-04-13T11:33:53.0745117Z + + \ No newline at end of file diff --git a/wfserver_resource.rc b/wfserver_resource.rc new file mode 100644 index 0000000..d437633 --- /dev/null +++ b/wfserver_resource.rc @@ -0,0 +1,37 @@ +#include + +IDI_ICON1 ICON DISCARDABLE "C:\\Users\\Phil\\source\\repos\\wfview\\resources\\wfview.ico" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 0,0,0,0 + PRODUCTVERSION 0,0,0,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "\0" + VALUE "FileDescription", "\0" + VALUE "FileVersion", "0.0.0.0\0" + VALUE "LegalCopyright", "\0" + VALUE "OriginalFilename", "wfserver.exe\0" + VALUE "ProductName", "wfserver\0" + VALUE "ProductVersion", "0.0.0.0\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END + END +/* End of Version info */ + diff --git a/wfview.pro b/wfview.pro index ff01979..2d8f027 100644 --- a/wfview.pro +++ b/wfview.pro @@ -11,21 +11,42 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport TARGET = wfview TEMPLATE = app -DEFINES += WFVIEW_VERSION=\\\"1.2d\\\" +DEFINES += WFVIEW_VERSION=\\\"1.4\\\" + +DEFINES += BUILD_WFVIEW CONFIG(debug, release|debug) { -# For Debug builds only: -QMAKE_CXXFLAGS += -faligned-new - + # For Debug builds only: + QMAKE_CXXFLAGS += -faligned-new + win32:DESTDIR = wfview-release + win32:LIBS += -L../portaudio/msvc/Win32/Debug/ -lportaudio_x86 -lole32 } else { -# For Release builds only: -linux:QMAKE_CXXFLAGS += -s -QMAKE_CXXFLAGS += -fvisibility=hidden -QMAKE_CXXFLAGS += -fvisibility-inlines-hidden -QMAKE_CXXFLAGS += -faligned-new -linux:QMAKE_LFLAGS += -O2 -s + # For Release builds only: + linux:QMAKE_CXXFLAGS += -s + QMAKE_CXXFLAGS += -fvisibility=hidden + QMAKE_CXXFLAGS += -fvisibility-inlines-hidden + QMAKE_CXXFLAGS += -faligned-new + linux:QMAKE_LFLAGS += -O2 -s + win32:DESTDIR = wfview-debug + win32:LIBS += -L../portaudio/msvc/Win32/Release/ -lportaudio_x86 -lole32 } +# RTAudio defines +win32:DEFINES += __WINDOWS_WASAPI__ +#win32:DEFINES += __WINDOWS_DS__ # Requires DirectSound libraries +#linux:DEFINES += __LINUX_ALSA__ +#linux:DEFINES += __LINUX_OSS__ +linux:DEFINES += __LINUX_PULSE__ +macx:DEFINES += __MACOSX_CORE__ +!linux:SOURCES += ../rtaudio/RTAudio.cpp +!linux:HEADERS += ../rtaudio/RTAUdio.h +!linux:INCLUDEPATH += ../rtaudio + +linux:LIBS += -lpulse -lpulse-simple -lrtaudio -lpthread + +win32:INCLUDEPATH += ../portaudio/include +!win32:LIBS += -lportaudio + # The following define makes your compiler emit warnings if you use # any feature of Qt which as been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the @@ -35,48 +56,28 @@ DEFINES += QCUSTOMPLOT_USE_LIBRARY # These defines are used for the resampler -equals(QT_ARCH, i386): DEFINES += USE_SSE -equals(QT_ARCH, i386): DEFINES += USE_SSE2 +equals(QT_ARCH, i386): win32:DEFINES += USE_SSE +equals(QT_ARCH, i386): win32:DEFINES += USE_SSE2 +equals(QT_ARCH, x86_64): DEFINES += USE_SSE +equals(QT_ARCH, x86_64): DEFINES += USE_SSE2 equals(QT_ARCH, arm): DEFINES += USE_NEON DEFINES += OUTSIDE_SPEEX DEFINES += RANDOM_PREFIX=wf +# These defines are used for the Eigen library +DEFINES += EIGEN_MPL2_ONLY +DEFINES += EIGEN_DONT_VECTORIZE #Clear vector flags +equals(QT_ARCH, i386): win32:DEFINES += EIGEN_VECTORIZE_SSE3 +equals(QT_ARCH, x86_64): DEFINES += EIGEN_VECTORIZE_SSE3 + + isEmpty(PREFIX) { PREFIX = /usr/local } DEFINES += PREFIX=\\\"$$PREFIX\\\" -# Choose audio system, uses QTMultimedia if both are commented out. -# DEFINES += RTAUDIO -# DEFINES += PORTAUDIO - -contains(DEFINES, RTAUDIO) { - # RTAudio defines - win32:DEFINES += __WINDOWS_WASAPI__ - #win32:DEFINES += __WINDOWS_DS__ # Requires DirectSound libraries - linux:DEFINES += __LINUX_ALSA__ - #linux:DEFINES += __LINUX_OSS__ - #linux:DEFINES += __LINUX_PULSE__ - macx:DEFINES += __MACOSX_CORE__ - win32:SOURCES += ../rtaudio/RTAudio.cpp - win32:HEADERS += ../rtaudio/RTAUdio.h - !linux:INCLUDEPATH += ../rtaudio - linux:LIBS += -lpulse -lpulse-simple -lrtaudio -lpthread -} - -contains(DEFINES, PORTAUDIO) { - CONFIG(debug, release|debug) { - win32:LIBS += -L../portaudio/msvc/Win32/Debug/ -lportaudio_x86 - - } else { - win32:LIBS += -L../portaudio/msvc/Win32/Release/ -lportaudio_x86 - } - win32:INCLUDEPATH += ../portaudio/include - !win32:LIBS += -lportaudio -} - -macx:INCLUDEPATH += /usr/local/include /opt/local/include +macx:INCLUDEPATH += /usr/local/include /opt/local/include macx:LIBS += -L/usr/local/lib -L/opt/local/lib macx:ICON = ../wfview/resources/wfview.icns @@ -104,9 +105,9 @@ unix:target.path = $$PREFIX/bin INSTALLS += target # Why doesn't this seem to do anything? -DISTFILES += resources/wfview.png \ +unix:DISTFILES += resources/wfview.png \ resources/install.sh -DISTFILES += resources/wfview.desktop +unix:DISTFILES += resources/wfview.desktop unix:applications.files = resources/wfview.desktop unix:applications.path = $$PREFIX/share/applications @@ -160,6 +161,9 @@ macx:LIBS += -framework CoreAudio -framework CoreFoundation -lpthread -lopus !linux:INCLUDEPATH += ../qcustomplot !linux:INCLUDEPATH += ../opus/include +win32:INCLUDEPATH += ../eigen +win32:INCLUDEPATH += ../r8brain-free-src + INCLUDEPATH += resampler SOURCES += main.cpp\ @@ -168,9 +172,15 @@ SOURCES += main.cpp\ rigcommander.cpp \ freqmemory.cpp \ rigidentities.cpp \ + udpbase.cpp \ udphandler.cpp \ + udpcivdata.cpp \ + udpaudio.cpp \ logcategories.cpp \ + pahandler.cpp \ + rthandler.cpp \ audiohandler.cpp \ + audioconverter.cpp \ calibrationwindow.cpp \ satellitesetup.cpp \ udpserver.cpp \ @@ -180,18 +190,25 @@ SOURCES += main.cpp\ resampler/resample.c \ repeatersetup.cpp \ rigctld.cpp \ - ring/ring.cpp \ transceiveradjustments.cpp \ - aboutbox.cpp + selectradio.cpp \ + tcpserver.cpp \ + aboutbox.cpp HEADERS += wfmain.h \ commhandler.h \ rigcommander.h \ freqmemory.h \ rigidentities.h \ + udpbase.h \ udphandler.h \ + udpcivdata.h \ + udpaudio.h \ logcategories.h \ + pahandler.h \ + rthandler.h \ audiohandler.h \ + audioconverter.h \ calibrationwindow.h \ satellitesetup.h \ udpserver.h \ @@ -206,15 +223,17 @@ HEADERS += wfmain.h \ repeaterattributes.h \ rigctld.h \ ulaw.h \ - ring/ring.h \ transceiveradjustments.h \ audiotaper.h \ + selectradio.h \ + tcpserver.h \ aboutbox.h FORMS += wfmain.ui \ calibrationwindow.ui \ satellitesetup.ui \ + selectradio.ui \ repeatersetup.ui \ transceiveradjustments.ui \ aboutbox.ui diff --git a/wfview.sln b/wfview.sln index 67d83ad..ff4fe46 100644 --- a/wfview.sln +++ b/wfview.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wfview", "wfview.vcxproj", "{326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wfserver", "wfserver.vcxproj", "{00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -19,6 +21,10 @@ Global {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x64.ActiveCfg = Release|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x86.ActiveCfg = Release|Win32 {326108AD-FA9D-3AAF-8D3E-062C4DDC34E2}.Release|x86.Build.0 = Release|Win32 + {00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56}.Debug|x86.ActiveCfg = Debug|Win32 + {00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56}.Debug|x86.Build.0 = Debug|Win32 + {00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56}.Release|x86.ActiveCfg = Release|Win32 + {00E054F8-A1D4-3ECA-A8D6-DFC8A68AFD56}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/wfview.vcxproj b/wfview.vcxproj index 2ff3097..3ca575a 100644 --- a/wfview.vcxproj +++ b/wfview.vcxproj @@ -20,7 +20,7 @@ v142 - release\ + wfview-release\ false NotSet Application @@ -29,7 +29,7 @@ v142 - debug\ + wfview-debug\ false NotSet Application @@ -44,11 +44,11 @@ - debug\debug\wfviewtruetruerelease\release\wfviewtruefalsetruemsvc2019core;network;gui;multimedia;widgets;serialport;printsupportmsvc2019core;network;gui;multimedia;widgets;serialport;printsupport + wfview-debug\debug\wfviewtruewfview-release\release\wfviewtruefalsemsvc2019core;network;gui;multimedia;widgets;serialport;printsupportmsvc2019core;network;gui;multimedia;widgets;serialport;printsupport - .;..\qcustomplot;..\opus\include;resampler;release;/include;%(AdditionalIncludeDirectories) + .;..\rtaudio;..\portaudio\include;..\qcustomplot;..\opus\include;..\eigen;..\r8brain-free-src;resampler;release;/include;%(AdditionalIncludeDirectories) -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) release\ false @@ -57,7 +57,7 @@ Sync release\ MaxSpeed - _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2d";QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_USE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX="/usr/local";GITSHORT="4203b62";HOST="wfview.org";UNAME="build";NDEBUG;QT_NO_DEBUG;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2e";BUILD_WFVIEW;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX="/usr/local";GITSHORT="1aa45dc";HOST="wfview.org";UNAME="build";NDEBUG;QT_NO_DEBUG;%(PreprocessorDefinitions) false MultiThreadedDLL @@ -66,8 +66,8 @@ Level3 true - ..\qcustomplot\win32\qcustomplot2.lib;..\opus\win32\VS2015\win32\Release\opus.lib;shell32.lib;%(AdditionalDependencies) - ..\opus\win32\VS2015\win32\Release;..\qcustomplot\win32;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) + ..\portaudio\msvc\Win32\Release\portaudio_x86.lib;..\opus\win32\VS2015\Win32\Release\opus.lib;shell32.lib;%(AdditionalDependencies) + ..\portaudio\msvc\Win32\Release;..\opus\win32\VS2015\Win32\Release;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) true false @@ -85,16 +85,12 @@ 0 - _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2d\";QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_USE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX=\"/usr/local\";GITSHORT=\"4203b62\";HOST=\"wfview.org\";UNAME=\"build\";NDEBUG;QT_NO_DEBUG;QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2e\";BUILD_WFVIEW;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX=\"/usr/local\";GITSHORT=\"1aa45dc\";HOST=\"wfview.org\";UNAME=\"build\";NDEBUG;QT_NO_DEBUG;QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions) - - copy /Y ..\qcustomplot\win32\qcustomplot2.dll release - copy /Y ..\qcustomplot\win32\qcustomplot2.dll release - msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cppUic'ing %(Identity)...$(ProjectDir)ui_%(Filename).h - .;..\qcustomplot;..\opus\include;resampler;debug;/include;%(AdditionalIncludeDirectories) + .;..\rtaudio;..\portaudio\include;..\qcustomplot;..\opus\include;..\eigen;..\r8brain-free-src;resampler;debug;/include;%(AdditionalIncludeDirectories) -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -Zc:__cplusplus -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) debug\ false @@ -103,7 +99,7 @@ Sync debug\ Disabled - _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2d";QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_USE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX="/usr/local";GITSHORT="4203b62";HOST="wfview.org";UNAME="build";%(PreprocessorDefinitions) + _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION="1.2e";BUILD_WFVIEW;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX="/usr/local";GITSHORT="1aa45dc";HOST="wfview.org";UNAME="build";%(PreprocessorDefinitions) false MultiThreadedDebugDLL true @@ -111,8 +107,8 @@ Level3 true - ..\qcustomplot\win32\qcustomplotd2.lib;..\opus\win32\VS2015\win32\Debug\opus.lib;shell32.lib;%(AdditionalDependencies) - ..\opus\win32\VS2015\win32\Debug;..\qcustomplot\win32;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) + ..\portaudio\msvc\Win32\Debug\portaudio_x86.lib;..\opus\win32\VS2015\Win32\Debug\opus.lib;shell32.lib;%(AdditionalDependencies) + ..\portaudio\msvc\Win32\Debug;..\opus\win32\VS2015\Win32\Debug;C:\opensslx86\lib;C:\Utils\my_sql\mysql-5.7.25-win32\lib;C:\Utils\postgresqlx86\pgsql\lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) true true @@ -128,15 +124,13 @@ 0 - _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2d\";QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_USE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;PREFIX=\"/usr/local\";GITSHORT=\"4203b62\";HOST=\"wfview.org\";UNAME=\"build\";QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;_DEBUG;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;_UNICODE;WIN32;_ENABLE_EXTENDED_ALIGNED_STORAGE;WFVIEW_VERSION=\"1.2e\";BUILD_WFVIEW;__WINDOWS_WASAPI__;QT_DEPRECATED_WARNINGS;QCUSTOMPLOT_COMPILE_LIBRARY;USE_SSE;USE_SSE2;OUTSIDE_SPEEX;RANDOM_PREFIX=wf;EIGEN_MPL2_ONLY;EIGEN_DONT_VECTORIZE;EIGEN_VECTORIZE_SSE3;PREFIX=\"/usr/local\";GITSHORT=\"1aa45dc\";HOST=\"wfview.org\";UNAME=\"build\";QT_MULTIMEDIA_LIB;QT_PRINTSUPPORT_LIB;QT_WIDGETS_LIB;QT_GUI_LIB;QT_SERIALPORT_LIB;QT_NETWORK_LIB;QT_CORE_LIB;_DEBUG;%(PreprocessorDefinitions) - - copy /Y ..\qcustomplot\win32\qcustomplotd2.dll debug - copy /Y ..\qcustomplot\win32\qcustomplotd2.dll debug - msvc./$(Configuration)/moc_predefs.hMoc'ing %(Identity)...output$(Configuration)moc_%(Filename).cppdefaultRcc'ing %(Identity)...$(Configuration)qrc_%(Filename).cppUic'ing %(Identity)...$(ProjectDir)ui_%(Filename).h + + @@ -144,21 +138,29 @@ + + - + + + + + + + @@ -170,6 +172,16 @@ + + + + + + + + + + @@ -214,6 +226,16 @@ + + + + + + + + + + @@ -223,6 +245,16 @@ + + + + + + + + + + @@ -267,7 +299,16 @@ - + + + + + + + + + + @@ -277,8 +318,28 @@ + + + + + + + + + + + + + + + + + + + + @@ -288,6 +349,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -332,6 +414,10 @@ + + + + Document true @@ -368,6 +454,19 @@ + + + + + + + + + + + + + @@ -423,6 +522,17 @@ + + + + + + + + + + + @@ -512,11 +622,6 @@ - - - - - diff --git a/wfview.vcxproj.filters b/wfview.vcxproj.filters index e8ff355..a8fca3d 100644 --- a/wfview.vcxproj.filters +++ b/wfview.vcxproj.filters @@ -45,21 +45,17 @@ {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx - - {B83CAF91-C7BF-462F-B76C-EA11631F866C} - * - false - - - {B83CAF91-C7BF-462F-B76C-EA11631F866C} - * - false - + + Source Files + Source Files + + Source Files + Source Files @@ -81,9 +77,15 @@ Source Files + + Source Files + Source Files + + Source Files + Source Files @@ -102,15 +104,30 @@ Source Files - + Source Files Source Files + + Source Files + + + Source Files + Source Files + + Source Files + + + Source Files + + + Source Files + Source Files @@ -122,12 +139,18 @@ + + Header Files + Header Files Header Files + + Header Files + Header Files @@ -152,9 +175,15 @@ Header Files + + Header Files + Header Files + + Header Files + Header Files @@ -176,18 +205,33 @@ Header Files - + Header Files - + Header Files + + Header Files + Header Files + + Header Files + Header Files + + Header Files + + + Header Files + + + Header Files + Header Files @@ -212,6 +256,10 @@ + + + + Generated Files @@ -238,6 +286,19 @@ + + + + + + + + + + + + + @@ -262,6 +323,9 @@ Form Files + + Form Files + Form Files @@ -400,17 +464,6 @@ Resource Files - - - Distribution Files - - - Distribution Files - - - Distribution Files - - diff --git a/wfview.vcxproj.user b/wfview.vcxproj.user index a638198..2d805e3 100644 --- a/wfview.vcxproj.user +++ b/wfview.vcxproj.user @@ -7,9 +7,9 @@ PATH=$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(QTDIR)\bin%3bC:\QT\5.15.2\MSVC2019\bin%3b$(PATH) - 2021-11-22T18:24:33.3752914Z + 2022-04-18T13:23:03.5252168Z - 2021-11-22T18:24:41.6960953Z + 2022-04-18T13:23:05.0598803Z \ No newline at end of file diff --git a/wfview_resource.aps b/wfview_resource.aps new file mode 100644 index 0000000..3e7faab Binary files /dev/null and b/wfview_resource.aps differ