diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index a9bbd9be..df8de355 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -20,13 +20,19 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install packages + - name: Install rtkit for RT threading shell: bash run: | sudo apt-get update sudo apt-get upgrade -y - sudo apt-get install codespell libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.2-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev xvfb pipewire pulseaudio-utils pipewire-pulse wireplumber metacity dbus-x11 at-spi2-core rtkit octave octave-signal libdbus-1-dev - sudo usermod -a -G rtkit $(whoami) + sudo apt-get install dbus-x11 rtkit libdbus-1-dev polkitd + sudo sed -i 's/no/yes/g' /usr/share/polkit-1/actions/org.freedesktop.RealtimeKit1.policy + sudo systemctl restart polkit + + - name: Install packages + shell: bash + run: | + sudo apt-get install codespell libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.2-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev xvfb pipewire pulseaudio-utils pipewire-pulse wireplumber metacity at-spi2-core octave octave-signal - name: Spellcheck codebase shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index a018b3c3..b41a07a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -243,6 +243,14 @@ if(NOT BOOTSTRAP_WXWIDGETS) if(WX_VERSION VERSION_EQUAL ${WX_VERSION_MIN} OR WX_VERSION VERSION_GREATER ${WX_VERSION_MIN}) message(STATUS "wxWidgets version: ${WX_VERSION}") + if(WIN32 OR APPLE) + message(WARNING + "On Windows and macOS, FreeDV patches wxWidgets to ensure consistent UI behavior across platforms." + "It is highly recommended to allow FreeDV to build its own wxWidgets with these patches by setting " + "-DBOOTSTRAP_WXWIDGETS=TRUE instead of using any version of wxWidgets already on the system. " + "Alternatively, you can apply the patch in cmake/wxWidgets-Direct2D-color-font.patch to the wxWidgets " + "source tree and build it yourself.") + endif(WIN32 OR APPLE) else() set(BOOTSTRAP_WXWIDGETS TRUE) endif() diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 97befde3..79ec25dc 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -916,7 +916,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes * Shorten PulseAudio/pipewire app name. (PR #843) 3. Build system: * Allow overriding the version tag when building. (PR #727) - * Update wxWidgets to 3.2.6. (PR #748) + * Update wxWidgets to 3.2.8. (PR #861) * Update Hamlib to 4.6.2. (PR #834) * Use optimal number of parallel builds during build process. (PR #842) 4. Miscellaneous: diff --git a/cmake/BuildWxWidgets.cmake b/cmake/BuildWxWidgets.cmake index 5b991191..def08f22 100644 --- a/cmake/BuildWxWidgets.cmake +++ b/cmake/BuildWxWidgets.cmake @@ -1,4 +1,4 @@ -set(WXWIDGETS_VERSION "3.2.7") +set(WXWIDGETS_VERSION "3.2.8") # Ensure that the wxWidgets library is staticly built. set(wxBUILD_SHARED OFF CACHE BOOL "Build wx libraries as shared libs") @@ -19,14 +19,19 @@ set(wxUSE_LIBSDL OFF CACHE STRING "use SDL for audio on Unix") set(wxUSE_LIBMSPACK OFF CACHE STRING "use libmspack (CHM help files loading)") set(wxUSE_LIBICONV OFF CACHE STRING "disable use of libiconv") +if(WIN32) +set(wxUSE_GRAPHICS_DIRECT2D ON CACHE STRING "use Direct2D graphics context") +endif(WIN32) + include(FetchContent) FetchContent_Declare( wxWidgets GIT_REPOSITORY https://github.com/wxWidgets/wxWidgets.git GIT_SHALLOW TRUE GIT_PROGRESS TRUE - #GIT_TAG v${WXWIDGETS_VERSION} - GIT_TAG 3.2 + GIT_TAG v${WXWIDGETS_VERSION} + PATCH_COMMAND git apply ${CMAKE_SOURCE_DIR}/cmake/wxWidgets-Direct2D-color-font.patch + UPDATE_DISCONNECTED 1 ) FetchContent_GetProperties(wxWidgets) diff --git a/cmake/wxWidgets-Direct2D-color-font.patch b/cmake/wxWidgets-Direct2D-color-font.patch new file mode 100644 index 00000000..514159ee --- /dev/null +++ b/cmake/wxWidgets-Direct2D-color-font.patch @@ -0,0 +1,129 @@ +diff --git a/include/wx/msw/setup.h b/include/wx/msw/setup.h +index b1d20d927..4f834c941 100644 +--- a/include/wx/msw/setup.h ++++ b/include/wx/msw/setup.h +@@ -1648,7 +1648,7 @@ + #if defined(_MSC_VER) && _MSC_VER >= 1600 + #define wxUSE_GRAPHICS_DIRECT2D wxUSE_GRAPHICS_CONTEXT + #else +- #define wxUSE_GRAPHICS_DIRECT2D 0 ++ #define wxUSE_GRAPHICS_DIRECT2D wxUSE_GRAPHICS_CONTEXT + #endif + + // wxWebRequest backend based on WinHTTP. +diff --git a/src/generic/tipwin.cpp b/src/generic/tipwin.cpp +index f8ac37f5f..c95371d06 100644 +--- a/src/generic/tipwin.cpp ++++ b/src/generic/tipwin.cpp +@@ -33,6 +33,10 @@ + #include "wx/display.h" + #include "wx/vector.h" + ++#if defined(WIN32) ++#include "wx/graphics.h" ++#endif // defined(WIN32) ++ + // ---------------------------------------------------------------------------- + // constants + // ---------------------------------------------------------------------------- +@@ -298,15 +302,38 @@ void wxTipWindowView::OnPaint(wxPaintEvent& WXUNUSED(event)) + rect.width = size.x; + rect.height = size.y; + +- // first filll the background +- dc.SetBrush(wxBrush(GetBackgroundColour(), wxBRUSHSTYLE_SOLID)); +- dc.SetPen(wxPen(GetForegroundColour(), 1, wxPENSTYLE_SOLID)); +- dc.DrawRectangle(rect); ++#if defined(WIN32) ++ // Tooltips should be rendered with Direct2D if at all possible. ++ wxGraphicsRenderer* renderer = wxGraphicsRenderer::GetDirect2DRenderer(); ++ wxGraphicsContext* context = nullptr; ++ if (renderer != nullptr) ++ { ++ context = renderer->CreateContextFromUnknownDC(dc); ++ if (context != nullptr) ++ { ++ // first fill the background ++ context->SetBrush(wxBrush(GetBackgroundColour(), wxBRUSHSTYLE_SOLID)); ++ context->SetPen(wxPen(GetForegroundColour(), 1, wxPENSTYLE_SOLID)); ++ context->DrawRectangle(0, 0, rect.width - 1, rect.height - 1); + +- // and then draw the text line by line +- dc.SetTextBackground(GetBackgroundColour()); +- dc.SetTextForeground(GetForegroundColour()); +- dc.SetFont(GetFont()); ++ // and then draw the text line by line ++ context->SetFont(GetFont(), GetForegroundColour()); ++ } ++ } ++ ++ if (context == nullptr) ++#endif // defined(WIN32) ++ { ++ // first filll the background ++ dc.SetBrush(wxBrush(GetBackgroundColour(), wxBRUSHSTYLE_SOLID)); ++ dc.SetPen(wxPen(GetForegroundColour(), 1, wxPENSTYLE_SOLID)); ++ dc.DrawRectangle(rect); ++ ++ // and then draw the text line by line ++ dc.SetTextBackground(GetBackgroundColour()); ++ dc.SetTextForeground(GetForegroundColour()); ++ dc.SetFont(GetFont()); ++ } + + wxPoint pt; + pt.x = TEXT_MARGIN_X; +@@ -314,10 +341,26 @@ void wxTipWindowView::OnPaint(wxPaintEvent& WXUNUSED(event)) + const size_t count = m_textLines.size(); + for ( size_t n = 0; n < count; n++ ) + { +- dc.DrawText(m_textLines[n], pt); ++#if defined(WIN32) ++ if (context != nullptr) ++ { ++ context->DrawText(m_textLines[n], pt.x, pt.y); ++ } ++ else ++#endif // defined(WIN32) ++ { ++ dc.DrawText(m_textLines[n], pt); ++ } + + pt.y += m_heightLine; + } ++ ++#if defined(WIN32) ++ if (context != nullptr) ++ { ++ delete context; ++ } ++#endif // defined(WIN32) + } + + void wxTipWindowView::OnMouseClick(wxMouseEvent& WXUNUSED(event)) +diff --git a/src/msw/graphicsd2d.cpp b/src/msw/graphicsd2d.cpp +index 89b74102a..e2a96a760 100644 +--- a/src/msw/graphicsd2d.cpp ++++ b/src/msw/graphicsd2d.cpp +@@ -4767,7 +4767,8 @@ void wxD2DContext::DoDrawText(const wxString& str, wxDouble x, wxDouble y) + GetRenderTarget()->DrawTextLayout( + D2D1::Point2F(x, y), + textLayout, +- fontData->GetBrushData().GetBrush()); ++ fontData->GetBrushData().GetBrush(), ++ D2D1_DRAW_TEXT_OPTIONS::D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); + } + + void wxD2DContext::EnsureInitialized() +diff --git a/src/osx/cocoa/dataview.mm b/src/osx/cocoa/dataview.mm +index 93554f1c7..3e879b3c1 100644 +--- a/src/osx/cocoa/dataview.mm ++++ b/src/osx/cocoa/dataview.mm +@@ -1573,6 +1573,7 @@ outlineView:(NSOutlineView*)outlineView + [self setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; + [self setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; + [self setTarget:self]; ++ self.intercellSpacing = NSZeroSize; + } + return self; + } diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index 51840c0f..725b68ee 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -68,6 +68,10 @@ public: // Reverts real-time priority for current thread. virtual void clearHelperRealTime() override { /* empty */ } + // Returns true if real-time thread MUST sleep ASAP. Failure to do so + // may result in SIGKILL being sent to the process by the kernel. + virtual bool mustStopWork() override { return false; } + // Sets user friendly description of device. Not used by all engines. void setDescription(std::string desc); diff --git a/src/audio/MacAudioDevice.mm b/src/audio/MacAudioDevice.mm index 973ba7e9..fed6232e 100644 --- a/src/audio/MacAudioDevice.mm +++ b/src/audio/MacAudioDevice.mm @@ -136,6 +136,8 @@ void MacAudioDevice::start() kAudioDevicePropertyScopeOutput; Float64 sampleRateAsFloat = sampleRate_; + + log_info("Attempting to set sample rate to %f for device %d", sampleRateAsFloat, coreAudioId_); OSStatus error = AudioObjectSetPropertyData( coreAudioId_, &propertyAddress, @@ -255,9 +257,10 @@ void MacAudioDevice::start() return OSStatus(noErr); }; + AVAudioFormat* inputFormat = [inNode inputFormatForBus:0]; AVAudioSinkNode* sinkNode = [[AVAudioSinkNode alloc] initWithReceiverBlock:block]; [engine attachNode:sinkNode]; - [engine connect:[engine inputNode] to:sinkNode format:nil]; + [engine connect:inNode to:sinkNode format:inputFormat]; } else { @@ -301,10 +304,12 @@ void MacAudioDevice::start() [engine prepare]; if (![engine startAndReturnError:&nse]) { + NSString* errorDesc = [nse localizedDescription]; + std::string err = std::string("Could not start AVAudioEngine: ") + [errorDesc cStringUsingEncoding:NSUTF8StringEncoding]; + log_error(err.c_str()); + if (onAudioErrorFunction) { - NSString* errorDesc = [nse localizedDescription]; - std::string err = std::string("Could not start AVAudioEngine: ") + [errorDesc cStringUsingEncoding:NSUTF8StringEncoding]; onAudioErrorFunction(*this, err, onAudioErrorState); } [engine release]; diff --git a/src/audio/MacAudioEngine.cpp b/src/audio/MacAudioEngine.cpp index 6653643e..b20459eb 100644 --- a/src/audio/MacAudioEngine.cpp +++ b/src/audio/MacAudioEngine.cpp @@ -26,6 +26,8 @@ #include +static const int kAdmMaxDeviceNameSize = 128; + void MacAudioEngine::start() { // empty - no initialization needed. @@ -274,9 +276,11 @@ AudioDeviceSpecification MacAudioEngine::getAudioSpecification_(int coreAudioId, OSStatus status = noErr; // Get device name - propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString; - propertySize = sizeof(CFStringRef); - CFStringRef name = nullptr; + propertyAddress.mSelector = kAudioDevicePropertyDeviceName; + + char name[kAdmMaxDeviceNameSize]; + propertySize = sizeof(name); + status = AudioObjectGetPropertyData( coreAudioId, &propertyAddress, @@ -294,8 +298,7 @@ AudioDeviceSpecification MacAudioEngine::getAudioSpecification_(int coreAudioId, return AudioDeviceSpecification::GetInvalidDevice(); } - std::string deviceName = cfStringToStdString_(name); - CFRelease(name); + std::string deviceName = name; // Get HW sample rate double sampleRate = 0; @@ -392,34 +395,4 @@ int MacAudioEngine::getNumChannels_(int coreAudioId, AudioDirection direction) } return numChannels; -} - -/** - * Converts a CFString to a UTF-8 std::string if possible. - * - * @param input A reference to the CFString to convert. - * @return Returns a std::string containing the contents of CFString converted to UTF-8. Returns - * an empty string if the input reference is null or conversion is not possible. - */ -std::string MacAudioEngine::cfStringToStdString_(CFStringRef input) -{ - if (!input) - return {}; - - // Attempt to access the underlying buffer directly. This only works if no conversion or - // internal allocation is required. - auto originalBuffer{ CFStringGetCStringPtr(input, kCFStringEncodingUTF8) }; - if (originalBuffer) - return originalBuffer; - - // Copy the data out to a local buffer. - auto lengthInUtf16{ CFStringGetLength(input) }; - auto maxLengthInUtf8{ CFStringGetMaximumSizeForEncoding(lengthInUtf16, - kCFStringEncodingUTF8) + 1 }; // <-- leave room for null terminator - std::vector localBuffer(maxLengthInUtf8); - - if (CFStringGetCString(input, localBuffer.data(), maxLengthInUtf8, maxLengthInUtf8)) - return localBuffer.data(); - - return {}; -} +} \ No newline at end of file diff --git a/src/audio/MacAudioEngine.h b/src/audio/MacAudioEngine.h index 4ef21196..0a7dcb10 100644 --- a/src/audio/MacAudioEngine.h +++ b/src/audio/MacAudioEngine.h @@ -45,9 +45,7 @@ public: virtual std::shared_ptr getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels) override; virtual std::vector getSupportedSampleRates(wxString deviceName, AudioDirection direction) override; -private: - std::string cfStringToStdString_(CFStringRef input); - +private: AudioDeviceSpecification getAudioSpecification_(int coreAudioId, AudioDirection direction); int getNumChannels_(int coreAudioId, AudioDirection direction); }; diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index ec793d1b..eb606e71 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "PulseAudioDevice.h" @@ -36,23 +37,17 @@ using namespace std::chrono_literals; -// Optimal settings based on ones used for PortAudio. -#define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 20000 +// Target latency. This controls e.g. how long it takes for +// TX audio to reach the radio. +#define PULSE_TARGET_LATENCY_US 10000 + +thread_local std::chrono::high_resolution_clock::time_point PulseAudioDevice::StartTime_; +thread_local bool PulseAudioDevice::MustStopWork_ = false; PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) , mainloop_(mainloop) , stream_(nullptr) - , outputPending_(nullptr) - , outputPendingLength_(0) - , outputPendingThreadActive_(false) - , outputPendingThread_(nullptr) - , targetOutputPendingLength_(PULSE_FPB * numChannels * 2) - , inputPending_(nullptr) - , inputPendingLength_(0) - , inputPendingThreadActive_(false) - , inputPendingThread_(nullptr) , devName_(devName) , direction_(direction) , sampleRate_(sampleRate) @@ -104,8 +99,8 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; - buffer_attr.maxlength = (uint32_t)-1; buffer_attr.tlength = pa_usec_to_bytes(PULSE_TARGET_LATENCY_US, &sample_specification); + buffer_attr.maxlength = (uint32_t)-1; buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; buffer_attr.fragsize = buffer_attr.tlength; @@ -145,136 +140,11 @@ void PulseAudioDevice::start() } else { - // Set up semaphore for signaling workers + // Set up semaphore for signaling workers if (sem_init(&sem_, 0, 0) < 0) - { + { log_warn("Could not set up semaphore (errno = %d)", errno); } - - // Start data collection thread. This thread - // is necessary in order to ensure that we can - // provide data to PulseAudio at a rate expected - // for the actual latency of the sound device. - outputPending_ = nullptr; - outputPendingLength_ = 0; - targetOutputPendingLength_ = PULSE_FPB * getNumChannels() * 2; - outputPendingThreadActive_ = true; - inputPending_ = nullptr; - inputPendingLength_ = 0; - if (direction_ == IAudioEngine::AUDIO_ENGINE_IN) - { - inputPendingThreadActive_ = true; - inputPendingThread_ = new std::thread([&]() { -#if defined(__linux__) - pthread_setname_np(pthread_self(), "FreeDV PAIn"); -#endif // defined(__linux__) - - while(inputPendingThreadActive_) - { - auto currentTime = std::chrono::steady_clock::now(); - int currentLength = 0; - - { - std::unique_lock lk(inputPendingMutex_); - currentLength = inputPendingLength_; - } - - currentLength = std::min(currentLength, PULSE_FPB * getNumChannels()); - if (currentLength > 0) - { - short data[currentLength]; - { - std::unique_lock lk(inputPendingMutex_); - memcpy(data, inputPending_, currentLength * sizeof(short)); - - short* newInputPending = nullptr; - if (inputPendingLength_ > currentLength) - { - newInputPending = new short[inputPendingLength_ - currentLength]; - assert(newInputPending != nullptr); - memcpy(newInputPending, inputPending_ + currentLength, (inputPendingLength_ - currentLength) * sizeof(short)); - } - delete[] inputPending_; - inputPending_ = newInputPending; - inputPendingLength_ = inputPendingLength_ - currentLength; - } - - if (onAudioDataFunction) - { - onAudioDataFunction(*this, data, currentLength / getNumChannels(), onAudioDataState); - } - sem_post(&sem_); - - // Sleep up to the number of milliseconds corresponding to the data received - int numMilliseconds = 1000.0 * ((double)currentLength / getNumChannels()) / (double)getSampleRate(); - std::this_thread::sleep_until(currentTime + std::chrono::milliseconds(numMilliseconds)); - } - else - { - // Sleep up to 20ms by default if there's no data available. - std::this_thread::sleep_until(currentTime + 20ms); - } - } - }); - assert(inputPendingThread_ != nullptr); - } -#if 0 - else if (direction_ == IAudioEngine::AUDIO_ENGINE_OUT) - { - outputPendingThread_ = new std::thread([&]() { -#if defined(__linux__) - pthread_setname_np(pthread_self(), "FreeDV PAOut"); -#endif // defined(__linux__) - - while(outputPendingThreadActive_) - { - auto currentTime = std::chrono::steady_clock::now(); - int currentLength = 0; - - { - std::unique_lock lk(outputPendingMutex_); - currentLength = outputPendingLength_; - } - - if (currentLength < targetOutputPendingLength_) - { - short data[PULSE_FPB * getNumChannels()]; - memset(data, 0, sizeof(data)); - - if (onAudioDataFunction) - { - onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); - } - - { - std::unique_lock lk(outputPendingMutex_); - short* temp = new short[outputPendingLength_ + PULSE_FPB * getNumChannels()]; - assert(temp != nullptr); - - if (outputPendingLength_ > 0) - { - memcpy(temp, outputPending_, outputPendingLength_ * sizeof(short)); - - delete[] outputPending_; - outputPending_ = nullptr; - } - memcpy(temp + outputPendingLength_, data, sizeof(data)); - - outputPending_ = temp; - outputPendingLength_ += PULSE_FPB * getNumChannels(); - } - } - - // Sleep the required amount of time to ensure we call onAudioDataFunction - // every PULSE_FPB samples. - int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - std::this_thread::sleep_until(currentTime + - std::chrono::milliseconds(sleepTimeMilliseconds)); - } - }); - assert(outputPendingThread_ != nullptr); - } -#endif } pa_threaded_mainloop_unlock(mainloop_); @@ -295,54 +165,38 @@ void PulseAudioDevice::stop() pa_stream_unref(stream_); stream_ = nullptr; - - outputPendingThreadActive_ = false; - if (outputPendingThread_ != nullptr) - { - outputPendingThread_->join(); - - delete[] outputPending_; - outputPending_ = nullptr; - outputPendingLength_ = 0; - - delete outputPendingThread_; - outputPendingThread_ = nullptr; - } - inputPendingThreadActive_ = false; - if (inputPendingThread_ != nullptr) - { - inputPendingThread_->join(); - - delete[] inputPending_; - inputPending_ = nullptr; - inputPendingLength_ = 0; - - delete inputPendingThread_; - inputPendingThread_ = nullptr; - } - sem_destroy(&sem_); } } int PulseAudioDevice::getLatencyInMicroseconds() { + pa_threaded_mainloop_lock(mainloop_); pa_usec_t latency = 0; if (stream_ != nullptr) { int neg = 0; pa_stream_get_latency(stream_, &latency, &neg); // ignore error and assume 0 } + pa_threaded_mainloop_unlock(mainloop_); return (int)latency; } void PulseAudioDevice::setHelperRealTime() { + // XXX: We can't currently enable RT scheduling on Linux + // due to unreliable behavior surrounding how long it takes to + // go through a single RX or TX cycle. This unreliability is + // likely due to the use of Python for some parts of RADE. Since + // timing is so unreliable and due to the fact that Linux actually + // kills processes that it deems as using "too much" CPU while in + // real-time, it's better just to use normal scheduling for now. +#if 0 // Set RLIMIT_RTTIME, required for rtkit struct rlimit rlim; memset(&rlim, 0, sizeof(rlim)); - rlim.rlim_cur = 100000ULL; // 100ms - rlim.rlim_max = rlim.rlim_cur; + rlim.rlim_cur = 10000ULL; // 10ms + rlim.rlim_max = 200000ULL; // 200ms if ((setrlimit(RLIMIT_RTTIME, &rlim) < 0)) { @@ -358,49 +212,89 @@ void PulseAudioDevice::setHelperRealTime() { #if defined(USE_RTKIT) DBusError error; - DBusConnection* bus = nullptr; + DBusConnection* bus = nullptr; int result = 0; dbus_error_init(&error); - if (!(bus = dbus_bus_get(DBUS_BUS_SYSTEM, &error))) - { + if (!(bus = dbus_bus_get(DBUS_BUS_SYSTEM, &error))) + { log_warn("Could not connect to system bus: %s", error.message); - } - else if ((result = rtkit_make_realtime(bus, 0, p.sched_priority)) < 0) - { - log_warn("rtkit could not make real-time: %s", strerror(-result)); + } + else if ((result = rtkit_make_realtime(bus, 0, p.sched_priority)) < 0) + { + log_warn("rtkit could not make real-time: %s", strerror(-result)); } - if (bus != nullptr) - { + if (bus != nullptr) + { dbus_connection_unref(bus); - } + } #else log_warn("No permission to make real-time"); #endif // defined(USE_RTKIT) } + + // Set up signal handling for SIGXCPU + struct sigaction action; + action.sa_flags = SA_SIGINFO; + action.sa_sigaction = HandleXCPU_; + sigaction(SIGXCPU, &action, NULL); + + sigset_t signal_set; + sigemptyset(&signal_set); + sigaddset(&signal_set, SIGXCPU); + sigprocmask(SIG_UNBLOCK, &signal_set, NULL); +#endif // 0 } +void PulseAudioDevice::startRealTimeWork() +{ + StartTime_ = std::chrono::high_resolution_clock::now(); + + sleepFallback_ = false; + if (clock_gettime(CLOCK_REALTIME, &ts_) == -1) + { + sleepFallback_ = true; + } +} void PulseAudioDevice::stopRealTimeWork() { - struct timespec ts; - - if (clock_gettime(CLOCK_REALTIME, &ts) == -1) + if (sleepFallback_) { // Fallback to simple sleep. IAudioDevice::stopRealTimeWork(); return; } - - ts.tv_nsec += 10000000; - ts.tv_sec += (ts.tv_nsec / 1000000000); - ts.tv_nsec = ts.tv_nsec % 1000000000; - if (sem_timedwait(&sem_, &ts) < 0 && errno != ETIMEDOUT) + auto latency = getLatencyInMicroseconds(); + if (latency == 0) + { + latency = PULSE_TARGET_LATENCY_US; + } + + ts_.tv_nsec += latency * 1000; + if (ts_.tv_nsec >= 1000000000) + { + ts_.tv_sec++; + ts_.tv_nsec -= 1000000000; + } + + if (sem_timedwait(&sem_, &ts_) < 0 && errno != ETIMEDOUT) { // Fallback to simple sleep. IAudioDevice::stopRealTimeWork(); } + else if (errno == ETIMEDOUT) + { + auto endTime = std::chrono::high_resolution_clock::now(); + if ((endTime - StartTime_) >= std::chrono::microseconds(PULSE_TARGET_LATENCY_US * 10)) + { + // Took a lot longer than expected. Force a sleep so we don't get killed by rtkit. + std::this_thread::sleep_for(std::chrono::microseconds(PULSE_TARGET_LATENCY_US)); + } + } + + MustStopWork_ = false; } void PulseAudioDevice::clearHelperRealTime() @@ -408,6 +302,11 @@ void PulseAudioDevice::clearHelperRealTime() IAudioDevice::clearHelperRealTime(); } +bool PulseAudioDevice::mustStopWork() +{ + return MustStopWork_; +} + void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *userdata) { const void* data = nullptr; @@ -418,26 +317,14 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us pa_stream_peek(s, &data, &length); if (!data || length == 0) { - break; + return; //break; } - // Append received audio to pending block + if (thisObj->onAudioDataFunction) { - std::unique_lock lk(thisObj->inputPendingMutex_); - short* temp = new short[thisObj->inputPendingLength_ + length / sizeof(short)]; - assert(temp != nullptr); - - if (thisObj->inputPendingLength_ > 0) - { - memcpy(temp, thisObj->inputPending_, thisObj->inputPendingLength_ * sizeof(short)); - delete[] thisObj->inputPending_; - thisObj->inputPending_ = nullptr; - } - memcpy(temp + thisObj->inputPendingLength_, data, length); - thisObj->inputPending_ = temp; - thisObj->inputPendingLength_ += length / sizeof(short); + thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / thisObj->getNumChannels() / sizeof(short), thisObj->onAudioDataState); } - + sem_post(&thisObj->sem_); pa_stream_drop(s); } while (pa_stream_readable_size(s) > 0); } @@ -452,26 +339,6 @@ void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *u memset(data, 0, sizeof(data)); PulseAudioDevice* thisObj = static_cast(userdata); -#if 0 - { - std::unique_lock lk(thisObj->outputPendingMutex_); - if (thisObj->outputPendingLength_ >= numSamples) - { - memcpy(data, thisObj->outputPending_, sizeof(data)); - - short* tmp = new short[thisObj->outputPendingLength_ - numSamples]; - assert(tmp != nullptr); - - thisObj->outputPendingLength_ -= numSamples; - memcpy(tmp, thisObj->outputPending_ + numSamples, sizeof(short) * thisObj->outputPendingLength_); - - delete[] thisObj->outputPending_; - thisObj->outputPending_ = tmp; - } - - thisObj->targetOutputPendingLength_ = std::max(thisObj->targetOutputPendingLength_, 2 * numSamples); - } -#endif if (thisObj->onAudioDataFunction) { @@ -541,3 +408,10 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) fprintf(stderr, "Current target buffer size for %s: %d\n", (const char*)thisObj->devName_.ToUTF8(), thisObj->targetOutputPendingLength_); } #endif // 0 + +void PulseAudioDevice::HandleXCPU_(int signum, siginfo_t *info, void *extra) +{ + // Notify thread that it has to stop work immediately and sleep. + log_warn("Taking too much CPU handling real-time tasks, pausing for a bit"); + MustStopWork_ = true; +} diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 4f115026..5d1c9e59 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -52,6 +52,9 @@ public: // called from the thread that will be operating on received audio. virtual void setHelperRealTime() override; + // Lets audio system know that we're starting work on received audio. + virtual void startRealTimeWork() override; + // Lets audio system know that we're done with the work on the received // audio. virtual void stopRealTimeWork() override; @@ -59,6 +62,10 @@ public: // Reverts real-time priority for current thread. virtual void clearHelperRealTime() override; + // Returns true if real-time thread MUST sleep ASAP. Failure to do so + // may result in SIGKILL being sent to the process by the kernel. + virtual bool mustStopWork() override; + protected: // PulseAudioDevice cannot be created directly, only via PulseAudioEngine. friend class PulseAudioEngine; @@ -70,27 +77,19 @@ private: pa_threaded_mainloop* mainloop_; pa_stream* stream_; - short* outputPending_; - int outputPendingLength_; - bool outputPendingThreadActive_; - std::mutex outputPendingMutex_; - std::thread* outputPendingThread_; - int targetOutputPendingLength_; - - short* inputPending_; - int inputPendingLength_; - bool inputPendingThreadActive_; - std::mutex inputPendingMutex_; - std::thread* inputPendingThread_; - wxString devName_; IAudioEngine::AudioDirection direction_; int sampleRate_; int numChannels_; std::mutex streamStateMutex_; std::condition_variable streamStateCondVar_; + + thread_local static std::chrono::high_resolution_clock::time_point StartTime_; + thread_local static bool MustStopWork_; sem_t sem_; + struct timespec ts_; + bool sleepFallback_; static void StreamReadCallback_(pa_stream *s, size_t length, void *userdata); static void StreamWriteCallback_(pa_stream *s, size_t length, void *userdata); @@ -101,6 +100,8 @@ private: #if 0 static void StreamLatencyCallback_(pa_stream *p, void *userdata); #endif // 0 + + static void HandleXCPU_(int signum, siginfo_t *info, void *extra); }; #endif // PULSE_AUDIO_DEVICE_H diff --git a/src/gui/controls/CMakeLists.txt b/src/gui/controls/CMakeLists.txt index 182da03b..dcf8d267 100644 --- a/src/gui/controls/CMakeLists.txt +++ b/src/gui/controls/CMakeLists.txt @@ -3,7 +3,8 @@ add_library(fdv_gui_controls STATIC plot_scalar.cpp plot_scatter.cpp plot_spectrum.cpp - plot_waterfall.cpp) + plot_waterfall.cpp + ReportMessageRenderer.cpp) target_include_directories(fdv_gui_controls PRIVATE ${CODEC2_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/../.. ${CMAKE_CURRENT_BINARY_DIR}/../..) diff --git a/src/gui/controls/ReportMessageRenderer.cpp b/src/gui/controls/ReportMessageRenderer.cpp new file mode 100644 index 00000000..d504ff46 --- /dev/null +++ b/src/gui/controls/ReportMessageRenderer.cpp @@ -0,0 +1,77 @@ +//========================================================================== +// Name: ReportMessageRenderer.cpp +// Purpose: Renderer for wxDataViewCtrl that helps data render properly +// on all platforms. +// Created: April 14, 2024 +// Authors: Mooneer Salem +// +// License: +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see . +// +//========================================================================== + +#include "ReportMessageRenderer.h" + +#include +#include + +ReportMessageRenderer::ReportMessageRenderer() + : wxDataViewCustomRenderer("string", wxDATAVIEW_CELL_INERT, wxALIGN_LEFT) { } + +bool ReportMessageRenderer::Render(wxRect cell, wxDC *dc, int state) +{ +#if defined(WIN32) + wxGraphicsRenderer* renderer = wxGraphicsRenderer::GetDirect2DRenderer(); + if (renderer != nullptr) + { + wxGraphicsContext* context = renderer->CreateContextFromUnknownDC(*dc); + if (context != nullptr) + { + wxColour color = dc->GetTextForeground(); + if (state & wxDATAVIEW_CELL_SELECTED) + { + color = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT); + } + const wxString paintText = + wxControl::Ellipsize(m_value, *dc, GetEllipsizeMode(), + cell.GetWidth(), wxELLIPSIZE_FLAGS_NONE); + context->SetFont(dc->GetFont(), color); + context->DrawText(paintText, cell.x, cell.y); + delete context; + return true; + } + } +#endif // defined(WIN32) + + RenderBackground(dc, cell); + RenderText(m_value, 0, cell, dc, state); + + return true; +} + +bool ReportMessageRenderer::SetValue( const wxVariant &value ) +{ + m_value = value.GetString(); + return true; +} + +bool ReportMessageRenderer::GetValue( wxVariant &WXUNUSED(value) ) const +{ + return true; +} + +wxSize ReportMessageRenderer::GetSize() const +{ + // Basically just renders text. + return GetTextExtent(m_value); +} diff --git a/src/gui/controls/ReportMessageRenderer.h b/src/gui/controls/ReportMessageRenderer.h new file mode 100644 index 00000000..4f68e6f7 --- /dev/null +++ b/src/gui/controls/ReportMessageRenderer.h @@ -0,0 +1,44 @@ +//========================================================================== +// Name: ReportMessageRenderer.h +// Purpose: Renderer for wxDataViewCtrl that helps data render properly +// on all platforms. +// Created: April 14, 2024 +// Authors: Mooneer Salem +// +// License: +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see . +// +//========================================================================== + +#ifndef REPORT_MESSAGE_RENDERER_H +#define REPORT_MESSAGE_RENDERER_H + +#include + +class ReportMessageRenderer : public wxDataViewCustomRenderer +{ +public: + ReportMessageRenderer(); + virtual ~ReportMessageRenderer() = default; + + // Overrides from wxDataViewCustomRenderer + virtual bool Render (wxRect cell, wxDC *dc, int state) override; + virtual bool SetValue( const wxVariant &value ) override; + virtual bool GetValue( wxVariant &WXUNUSED(value) ) const override; + virtual wxSize GetSize() const override; + +private: + wxString m_value; +}; + +#endif // REPORT_MESSAGE_RENDERER_H \ No newline at end of file diff --git a/src/gui/dialogs/freedv_reporter.cpp b/src/gui/dialogs/freedv_reporter.cpp index 077cc0d6..c448be3b 100644 --- a/src/gui/dialogs/freedv_reporter.cpp +++ b/src/gui/dialogs/freedv_reporter.cpp @@ -45,11 +45,7 @@ extern FreeDVInterface freedvInterface; #define LAST_UPDATE_DATE_COL (13) #define UNKNOWN_STR "" -#if defined(WIN32) -#define NUM_COLS (LAST_UPDATE_DATE_COL + 2) /* Note: need empty column 0 to work around callsign truncation issue. */ -#else #define NUM_COLS (LAST_UPDATE_DATE_COL + 1) -#endif // defined(WIN32) #define RX_ONLY_STATUS "RX Only" #define RX_COLORING_LONG_TIMEOUT_SEC (20) #define RX_COLORING_SHORT_TIMEOUT_SEC (5) @@ -59,54 +55,15 @@ extern FreeDVInterface freedvInterface; using namespace std::placeholders; -static int DefaultColumnWidths_[] = { -#if defined(WIN32) - 1, -#endif // defined(WIN32) - 70, - 65, - 60, - 60, - 70, - 60, - 65, - 60, - 130, - 60, - 65, - 60, - 60, - 100, - 1 -}; - FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style) : wxFrame(parent, id, title, pos, size, style) , tipWindow_(nullptr) - , reporter_(nullptr) - , currentBandFilter_(FreeDVReporterDialog::BAND_ALL) - , currentSortColumn_(-1) - , sortAscending_(false) - , isConnected_(false) - , filterSelfMessageUpdates_(false) - , filteredFrequency_(0) + , sortRequired_(false) { if (wxGetApp().customConfigFileName != "") { SetTitle(wxString::Format("%s (%s)", _("FreeDV Reporter"), wxGetApp().customConfigFileName)); } - - for (int col = 0; col < NUM_COLS; col++) - { - columnLengths_[col] = DefaultColumnWidths_[col]; - } - - int userMsgDefaultColWidth = wxGetApp().appConfiguration.reportingUserMsgColWidth; - int userColNum = USER_MESSAGE_COL; -#if defined(WIN32) - userColNum++; -#endif // defined(WIN32) - DefaultColumnWidths_[userColNum] = userMsgDefaultColWidth; // Create top-level of control hierarchy. wxFlexGridSizer* sectionSizer = new wxFlexGridSizer(2, 1, 0, 0); @@ -116,66 +73,149 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons // Main list box // ============================= int col = 0; - m_listSpots = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_SINGLE_SEL | wxLC_REPORT | wxLC_HRULES); + m_listSpots = new wxDataViewCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxDV_SINGLE); + + // Associate data model. + spotsDataModel_ = new FreeDVReporterDataModel(this); + m_listSpots->AssociateModel(spotsDataModel_.get()); + + auto colObj = m_listSpots->AppendTextColumn(wxT("Callsign"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->GetRenderer()->DisableEllipsize(); + colObj->SetMinWidth(70); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Locator"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(65); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("km"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_RIGHT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Hdg"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_RIGHT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Version"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(70); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn( + wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz ? wxT("kHz") : wxT("MHz"), + col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_RIGHT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Mode"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(65); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Status"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + #if defined(WIN32) - // Create "hidden" column at the beginning. The column logic in wxWidgets - // seems to want to add an image to column 0, which affects - // autosizing. - m_listSpots->InsertColumn(col, wxT(""), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; + // Use ReportMessageRenderer only on Windows so that we can render emojis in color. + colObj = new wxDataViewColumn(wxT("Msg"), new ReportMessageRenderer(), col++, wxCOL_WIDTH_DEFAULT, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + m_listSpots->AppendColumn(colObj); +#else + colObj = m_listSpots->AppendTextColumn(wxT("Msg"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_DEFAULT, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); #endif // defined(WIN32) - - m_listSpots->InsertColumn(col, wxT("Callsign"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Locator"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("km"), wxLIST_FORMAT_RIGHT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Hdg"), wxLIST_FORMAT_RIGHT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Version"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz ? wxT("kHz") : wxT("MHz"), wxLIST_FORMAT_RIGHT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Mode"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Status"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Msg"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Last TX"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("RX Call"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Mode"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("SNR"), wxLIST_FORMAT_RIGHT, DefaultColumnWidths_[col]); - col++; - m_listSpots->InsertColumn(col, wxT("Last Update"), wxLIST_FORMAT_LEFT, DefaultColumnWidths_[col]); - col++; - - // On Windows, the last column will end up taking a lot more space than desired regardless - // of the space we actually need. Create a "dummy" column to take that space instead. - m_listSpots->InsertColumn(col, wxT(""), wxLIST_FORMAT_LEFT, 1); - col++; + colObj->GetRenderer()->EnableEllipsize(wxELLIPSIZE_END); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(130); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + colObj->SetWidth(wxGetApp().appConfiguration.reportingUserMsgColWidth); + + colObj = m_listSpots->AppendTextColumn(wxT("Last TX"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("RX Call"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(65); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Mode"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("SNR"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(60); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } + + colObj = m_listSpots->AppendTextColumn(wxT("Last Update"), col++, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_CENTER, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); + colObj->GetRenderer()->DisableEllipsize(); + colObj->GetRenderer()->SetAlignment(wxALIGN_LEFT); + colObj->SetMinWidth(100); + if ((col - 1) == wxGetApp().appConfiguration.reporterWindowCurrentSort) + { + colObj->SetSortOrder(wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); + } sectionSizer->Add(m_listSpots, 0, wxALL | wxEXPAND, 2); - - // Add sorting up/down arrows. - wxArtProvider provider; - m_sortIcons = new wxImageList(16, 16, true, 2); - auto upIcon = provider.GetBitmap("wxART_GO_UP"); - assert(upIcon.IsOk()); - upIconIndex_ = m_sortIcons->Add(upIcon); - assert(upIconIndex_ >= 0); - - auto downIcon = provider.GetBitmap("wxART_GO_DOWN"); - assert(downIcon.IsOk()); - downIconIndex_ = m_sortIcons->Add(downIcon); - assert(downIconIndex_ >= 0); - - m_listSpots->AssignImageList(m_sortIcons, wxIMAGE_LIST_SMALL); // Bottom buttons // ============================= @@ -297,7 +337,7 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons // Set up highlight clear timer m_highlightClearTimer = new wxTimer(this); - m_highlightClearTimer->Start(1000); + m_highlightClearTimer->Start(100); // Create Set popup menu setPopupMenu_ = new wxMenu(); @@ -356,15 +396,14 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons this->Connect(wxEVT_MOVE, wxMoveEventHandler(FreeDVReporterDialog::OnMove)); this->Connect(wxEVT_SHOW, wxShowEventHandler(FreeDVReporterDialog::OnShow)); this->Connect(wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler(FreeDVReporterDialog::OnSystemColorChanged)); + this->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(FreeDVReporterDialog::DeselectItem), NULL, this); - m_listSpots->Connect(wxEVT_LIST_ITEM_SELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemSelected), NULL, this); - m_listSpots->Connect(wxEVT_LIST_ITEM_DESELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemDeselected), NULL, this); - m_listSpots->Connect(wxEVT_LIST_COL_CLICK, wxListEventHandler(FreeDVReporterDialog::OnSortColumn), NULL, this); - m_listSpots->Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(FreeDVReporterDialog::OnDoubleClick), NULL, this); + m_listSpots->Connect(wxEVT_DATAVIEW_SELECTION_CHANGED, wxDataViewEventHandler(FreeDVReporterDialog::OnItemSelectionChanged), NULL, this); + m_listSpots->Connect(wxEVT_DATAVIEW_ITEM_ACTIVATED, wxDataViewEventHandler(FreeDVReporterDialog::OnItemDoubleClick), NULL, this); m_listSpots->Connect(wxEVT_MOTION, wxMouseEventHandler(FreeDVReporterDialog::AdjustToolTip), NULL, this); - m_listSpots->Connect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(FreeDVReporterDialog::OnRightClickSpotsList), NULL, this); - m_listSpots->Connect(wxEVT_LIST_COL_DRAGGING, wxListEventHandler(FreeDVReporterDialog::AdjustMsgColWidth), NULL, this); - + m_listSpots->Connect(wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, wxDataViewEventHandler(FreeDVReporterDialog::OnItemRightClick), NULL, this); + m_listSpots->Connect(wxEVT_DATAVIEW_COLUMN_HEADER_CLICK, wxDataViewEventHandler(FreeDVReporterDialog::OnColumnClick), NULL, this); + m_statusMessage->Connect(wxEVT_TEXT, wxCommandEventHandler(FreeDVReporterDialog::OnStatusTextChange), NULL, this); m_buttonSend->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnStatusTextSend), NULL, this); m_buttonSend->Connect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(FreeDVReporterDialog::OnStatusTextSendContextMenu), NULL, this); @@ -380,9 +419,6 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons m_trackFreqBand->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(FreeDVReporterDialog::OnFilterTrackingEnable), NULL, this); m_trackExactFreq->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(FreeDVReporterDialog::OnFilterTrackingEnable), NULL, this); - // Trigger sorting on last sorted column - sortColumn_(wxGetApp().appConfiguration.reporterWindowCurrentSort, wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); - // Update status message and MRU list m_statusMessage->SetValue(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterStatusText); for (auto& msg : wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRecentStatusTexts.get()) @@ -420,14 +456,13 @@ FreeDVReporterDialog::~FreeDVReporterDialog() this->Disconnect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(FreeDVReporterDialog::OnClose)); this->Disconnect(wxEVT_MOVE, wxMoveEventHandler(FreeDVReporterDialog::OnMove)); this->Disconnect(wxEVT_SHOW, wxShowEventHandler(FreeDVReporterDialog::OnShow)); + this->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(FreeDVReporterDialog::DeselectItem), NULL, this); - m_listSpots->Disconnect(wxEVT_LIST_ITEM_SELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemSelected), NULL, this); - m_listSpots->Disconnect(wxEVT_LIST_ITEM_DESELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemDeselected), NULL, this); - m_listSpots->Disconnect(wxEVT_LIST_COL_CLICK, wxListEventHandler(FreeDVReporterDialog::OnSortColumn), NULL, this); - m_listSpots->Disconnect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(FreeDVReporterDialog::OnDoubleClick), NULL, this); + m_listSpots->Disconnect(wxEVT_DATAVIEW_SELECTION_CHANGED, wxDataViewEventHandler(FreeDVReporterDialog::OnItemSelectionChanged), NULL, this); + m_listSpots->Disconnect(wxEVT_DATAVIEW_ITEM_ACTIVATED, wxDataViewEventHandler(FreeDVReporterDialog::OnItemDoubleClick), NULL, this); m_listSpots->Disconnect(wxEVT_MOTION, wxMouseEventHandler(FreeDVReporterDialog::AdjustToolTip), NULL, this); - m_listSpots->Disconnect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(FreeDVReporterDialog::OnRightClickSpotsList), NULL, this); - m_listSpots->Disconnect(wxEVT_LIST_COL_DRAGGING, wxListEventHandler(FreeDVReporterDialog::AdjustMsgColWidth), NULL, this); + m_listSpots->Disconnect(wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, wxDataViewEventHandler(FreeDVReporterDialog::OnItemRightClick), NULL, this); + m_listSpots->Disconnect(wxEVT_DATAVIEW_COLUMN_HEADER_CLICK, wxDataViewEventHandler(FreeDVReporterDialog::OnColumnClick), NULL, this); m_statusMessage->Disconnect(wxEVT_TEXT, wxCommandEventHandler(FreeDVReporterDialog::OnStatusTextChange), NULL, this); m_buttonSend->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnStatusTextSend), NULL, this); @@ -449,87 +484,44 @@ bool FreeDVReporterDialog::isTextMessageFieldInFocus() void FreeDVReporterDialog::refreshLayout() { - int colOffset = 0; - -#if defined(WIN32) - // Column 0 is hidden, so everything is shifted by 1 column. - colOffset++; -#endif // defined(WIN32) - - wxListItem item; - m_listSpots->GetColumn(DISTANCE_COL + colOffset, item); + wxDataViewColumn* item = m_listSpots->GetColumn(DISTANCE_COL); if (wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances) { - item.SetText("km "); + item->SetTitle("km "); } else { - item.SetText("Miles"); + item->SetTitle("Miles"); } - - m_listSpots->SetColumn(DISTANCE_COL + colOffset, item); // Refresh frequency units as appropriate. - m_listSpots->GetColumn(FREQUENCY_COL + colOffset, item); + item = m_listSpots->GetColumn(FREQUENCY_COL); if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { - item.SetText("kHz"); + item->SetTitle("kHz"); } else { - item.SetText("MHz"); + item->SetTitle("MHz"); } - m_listSpots->SetColumn(FREQUENCY_COL + colOffset, item); // Change direction/heading column label based on preferences - m_listSpots->GetColumn(HEADING_COL + colOffset, item); + item = m_listSpots->GetColumn(HEADING_COL); if (wxGetApp().appConfiguration.reportingConfiguration.reportingDirectionAsCardinal) { - item.SetText("Dir"); - item.SetAlign(wxLIST_FORMAT_LEFT); + item->SetTitle("Dir"); + item->SetAlignment(wxALIGN_LEFT); } else { - item.SetText("Hdg"); - item.SetAlign(wxLIST_FORMAT_RIGHT); + item->SetTitle("Hdg"); + item->SetAlignment(wxALIGN_RIGHT); } - m_listSpots->SetColumn(HEADING_COL + colOffset, item); - std::map colResizeList; - for (auto& kvp : allReporterData_) - { - double frequencyUserReadable = kvp.second->frequency / 1000.0; - wxString frequencyString; - - if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) - { - frequencyString = wxString::Format(_("%.01f"), frequencyUserReadable); - } - else - { - frequencyUserReadable /= 1000.0; - frequencyString = wxString::Format(_("%.04f"), frequencyUserReadable); - } - - kvp.second->freqString = frequencyString; - - // Refresh cardinal vs. degree directions. - if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare != kvp.second->gridSquare) - { - if (wxGetApp().appConfiguration.reportingConfiguration.reportingDirectionAsCardinal) - { - kvp.second->heading = GetCardinalDirection_(kvp.second->headingVal); - } - else - { - kvp.second->heading = wxString::Format("%.0f", kvp.second->headingVal); - } - } - - addOrUpdateListIfNotFiltered_(kvp.second, colResizeList); - } - resizeChangedColumns_(colResizeList); + // Refresh all data based on current settings and filters. + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->refreshAllRows(); // Update status controls. wxCommandEvent tmpEvent; @@ -538,47 +530,30 @@ void FreeDVReporterDialog::refreshLayout() void FreeDVReporterDialog::setReporter(std::shared_ptr reporter) { - if (reporter_) + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->setReporter(reporter); + + if (reporter) { - reporter_->setOnReporterConnectFn(FreeDVReporter::ReporterConnectionFn()); - reporter_->setOnReporterDisconnectFn(FreeDVReporter::ReporterConnectionFn()); - - reporter_->setOnUserConnectFn(FreeDVReporter::ConnectionDataFn()); - reporter_->setOnUserDisconnectFn(FreeDVReporter::ConnectionDataFn()); - reporter_->setOnFrequencyChangeFn(FreeDVReporter::FrequencyChangeFn()); - reporter_->setOnTransmitUpdateFn(FreeDVReporter::TxUpdateFn()); - reporter_->setOnReceiveUpdateFn(FreeDVReporter::RxUpdateFn()); - - reporter_->setMessageUpdateFn(FreeDVReporter::MessageUpdateFn()); - reporter_->setConnectionSuccessfulFn(FreeDVReporter::ConnectionSuccessfulFn()); - reporter_->setAboutToShowSelfFn(FreeDVReporter::AboutToShowSelfFn()); - } - - reporter_ = reporter; - - if (reporter_) - { - reporter_->setOnReporterConnectFn(std::bind(&FreeDVReporterDialog::onReporterConnect_, this)); - reporter_->setOnReporterDisconnectFn(std::bind(&FreeDVReporterDialog::onReporterDisconnect_, this)); - - reporter_->setOnUserConnectFn(std::bind(&FreeDVReporterDialog::onUserConnectFn_, this, _1, _2, _3, _4, _5, _6)); - reporter_->setOnUserDisconnectFn(std::bind(&FreeDVReporterDialog::onUserDisconnectFn_, this, _1, _2, _3, _4, _5, _6)); - reporter_->setOnFrequencyChangeFn(std::bind(&FreeDVReporterDialog::onFrequencyChangeFn_, this, _1, _2, _3, _4, _5)); - reporter_->setOnTransmitUpdateFn(std::bind(&FreeDVReporterDialog::onTransmitUpdateFn_, this, _1, _2, _3, _4, _5, _6, _7)); - reporter_->setOnReceiveUpdateFn(std::bind(&FreeDVReporterDialog::onReceiveUpdateFn_, this, _1, _2, _3, _4, _5, _6, _7)); - - reporter_->setMessageUpdateFn(std::bind(&FreeDVReporterDialog::onMessageUpdateFn_, this, _1, _2, _3)); - reporter_->setConnectionSuccessfulFn(std::bind(&FreeDVReporterDialog::onConnectionSuccessfulFn_, this)); - reporter_->setAboutToShowSelfFn(std::bind(&FreeDVReporterDialog::onAboutToShowSelfFn_, this)); - // Update status message auto statusMsg = m_statusMessage->GetValue(); - reporter_->updateMessage(statusMsg.utf8_string()); + reporter->updateMessage(statusMsg.utf8_string()); } - else +} + +void FreeDVReporterDialog::DeselectItem(wxMouseEvent& event) +{ + DeselectItem(); + event.Skip(); +} + + +void FreeDVReporterDialog::DeselectItem() +{ + wxDataViewItem item = m_listSpots->GetSelection(); + if (item.IsOk()) { - // Spot list no longer valid, delete the items currently on there - clearAllEntries_(true); + m_listSpots->Unselect(item); } } @@ -588,10 +563,13 @@ void FreeDVReporterDialog::OnSystemColorChanged(wxSysColourChangedEvent& event) // when the user switches between light and dark mode. wxColour currentControlBackground = wxTransparentColour; + // TBD - see if this workaround is still necessary +#if 0 m_listSpots->SetBackgroundColour(currentControlBackground); #if !defined(WIN32) ((wxWindow*)m_listSpots->m_headerWin)->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); #endif //!defined(WIN32) +#endif event.Skip(); } @@ -604,6 +582,12 @@ void FreeDVReporterDialog::OnInitDialog(wxInitDialogEvent& event) void FreeDVReporterDialog::OnShow(wxShowEvent& event) { wxGetApp().appConfiguration.reporterWindowVisible = true; + + auto selected = m_listSpots->GetSelection(); + if (selected.IsOk()) + { + m_listSpots->Unselect(selected); + } } void FreeDVReporterDialog::OnSize(wxSizeEvent& event) @@ -626,24 +610,38 @@ void FreeDVReporterDialog::OnMove(wxMoveEvent& event) void FreeDVReporterDialog::OnOK(wxCommandEvent& event) { + // Preserve sort column/ordering + for (unsigned int index = 0; index < m_listSpots->GetColumnCount(); index++) + { + auto colObj = m_listSpots->GetColumn(index); + if (colObj != nullptr && colObj->IsSortKey()) + { + wxGetApp().appConfiguration.reporterWindowCurrentSort = index; + wxGetApp().appConfiguration.reporterWindowCurrentSortDirection = colObj->IsSortOrderAscending(); + break; + } + } + + // Preserve Msg column width + auto userMsgCol = m_listSpots->GetColumn(USER_MESSAGE_COL); + wxGetApp().appConfiguration.reportingUserMsgColWidth = userMsgCol->GetWidth(); + wxGetApp().appConfiguration.reporterWindowVisible = false; Hide(); } void FreeDVReporterDialog::OnSendQSY(wxCommandEvent& event) { - auto selectedIndex = m_listSpots->GetFirstSelected(); - if (selectedIndex >= 0) + auto selected = m_listSpots->GetSelection(); + if (selected.IsOk()) { - auto selectedCallsign = m_listSpots->GetItemText(selectedIndex); - - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(selectedIndex); - std::string sid = *sidPtr; - - reporter_->requestQSY(sid, wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency, ""); // Custom message TBD. - - wxString fullMessage = wxString::Format(_("QSY request sent to %s"), selectedCallsign); + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->requestQSY(selected, wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency, ""); // Custom message TBD + + wxString fullMessage = wxString::Format(_("QSY request sent to %s"), model->getCallsign(selected)); wxMessageBox(fullMessage, wxT("FreeDV Reporter"), wxOK | wxICON_INFORMATION, this); + + m_listSpots->Unselect(selected); } } @@ -651,93 +649,59 @@ void FreeDVReporterDialog::OnOpenWebsite(wxCommandEvent& event) { std::string url = "https://" + wxGetApp().appConfiguration.reportingConfiguration.freedvReporterHostname->utf8_string() + "/"; wxLaunchDefaultBrowser(url); + DeselectItem(); } void FreeDVReporterDialog::OnClose(wxCloseEvent& event) { + // Preserve sort column/ordering + bool found = false; + for (unsigned int index = 0; index < m_listSpots->GetColumnCount(); index++) + { + auto colObj = m_listSpots->GetColumn(index); + if (colObj != nullptr && colObj->IsSortKey()) + { + found = true; + wxGetApp().appConfiguration.reporterWindowCurrentSort = index; + wxGetApp().appConfiguration.reporterWindowCurrentSortDirection = colObj->IsSortOrderAscending(); + break; + } + } + + if (!found) + { + wxGetApp().appConfiguration.reporterWindowCurrentSort = -1; + wxGetApp().appConfiguration.reporterWindowCurrentSortDirection = true; + } + + // Preserve Msg column width + auto userMsgCol = m_listSpots->GetColumn(USER_MESSAGE_COL); + wxGetApp().appConfiguration.reportingUserMsgColWidth = userMsgCol->GetWidth(); + wxGetApp().appConfiguration.reporterWindowVisible = false; Hide(); } -void FreeDVReporterDialog::OnItemSelected(wxListEvent& event) +void FreeDVReporterDialog::OnItemSelectionChanged(wxDataViewEvent& event) { - refreshQSYButtonState(); - - // Bring up tooltip for longer reporting messages if the user happened to click on that column. - wxMouseEvent dummyEvent; - AdjustToolTip(dummyEvent); -} - -void FreeDVReporterDialog::OnItemDeselected(wxListEvent& event) -{ - m_buttonSendQSY->Enable(false); -} - -void FreeDVReporterDialog::AdjustMsgColWidth(wxListEvent& event) -{ - int col = event.GetColumn(); - int desiredCol = USER_MESSAGE_COL; -#if defined(WIN32) - desiredCol++; -#endif // defined(WIN32) - - if (col != desiredCol) + if (event.GetItem().IsOk()) { - return; + refreshQSYButtonState(); + + // Bring up tooltip for longer reporting messages if the user happened to click on that column. + wxMouseEvent dummyEvent; + AdjustToolTip(dummyEvent); } - - int currentColWidth = m_listSpots->GetColumnWidth(desiredCol); - int textWidth = 0; - - wxGetApp().appConfiguration.reportingUserMsgColWidth = currentColWidth; - - // Iterate through and re-truncate column as required. - for (int index = 0; index < m_listSpots->GetItemCount(); index++) + else { - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); - wxString tempUserMessage = _(" ") + allReporterData_[*sidPtr]->userMessage; // note: extra space at beginning is to provide extra space from previous col - - textWidth = getSizeForTableCellString_(tempUserMessage); - int tmpLength = allReporterData_[*sidPtr]->userMessage.Length() - 1; - while (textWidth > currentColWidth && tmpLength >= 0) - { - tempUserMessage = allReporterData_[*sidPtr]->userMessage.SubString(0, tmpLength--) + _("..."); - textWidth = getSizeForTableCellString_(tempUserMessage); - } - - if (tmpLength > 0 && tmpLength < (allReporterData_[*sidPtr]->userMessage.Length() - 1)) - { - tempUserMessage = allReporterData_[*sidPtr]->userMessage.SubString(0, tmpLength) + _("..."); - } - - m_listSpots->SetItem(index, desiredCol, tempUserMessage); + m_buttonSendQSY->Enable(false); } } -void FreeDVReporterDialog::OnSortColumn(wxListEvent& event) -{ - int col = event.GetColumn(); - -#if defined(WIN32) - // The "hidden" column 0 is new as of 1.9.7. Automatically - // assume the user is sorting by callsign. - if (col == 0) - { - col = 1; - } -#endif // defined(WIN32) - - if (col > (NUM_COLS - 1)) - { - // Don't allow sorting by "fake" columns. - col = -1; - } - - sortColumn_(col); -} - void FreeDVReporterDialog::OnBandFilterChange(wxCommandEvent& event) { + DeselectItem(); + wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilter = m_bandFilter->GetSelection(); @@ -746,16 +710,19 @@ void FreeDVReporterDialog::OnBandFilterChange(wxCommandEvent& event) setBandFilter(freq); } -void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) +void FreeDVReporterDialog::FreeDVReporterDataModel::updateHighlights() { + std::unique_lock lk(dataMtx_); + // Iterate across all visible rows. If a row is currently highlighted // green and it's been more than >20 seconds, clear coloring. auto curDate = wxDateTime::Now().ToUTC(); - for (auto index = 0; index < m_listSpots->GetItemCount(); index++) + + wxDataViewItemArray itemsChanged; + for (auto& item : allReporterData_) { - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); - auto reportData = allReporterData_[*sidPtr]; - + auto reportData = item.second; + bool isTransmitting = reportData->transmitting; bool isReceivingValidCallsign = reportData->lastRxDate.IsValid() && @@ -768,7 +735,7 @@ void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) bool isMessaging = reportData->lastUpdateUserMessage.IsValid() && reportData->lastUpdateUserMessage.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, MSG_COLORING_TIMEOUT_SEC)); - + // Messaging notifications take highest priority. wxColour backgroundColor; wxColour foregroundColor; @@ -788,19 +755,47 @@ void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowBackgroundColor); foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowForegroundColor); } +#if defined(__APPLE__) else { - backgroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX); - foregroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT); + // To ensure that the columns don't have a different color than the rest of the control. + // Needed mainly for macOS. + backgroundColor = wxColour(wxTransparentColour); } +#endif // defined(__APPLE__) - m_listSpots->SetItemBackgroundColour(index, backgroundColor); - m_listSpots->SetItemTextColour(index, foregroundColor); + bool isHighlightUpdated = + backgroundColor != reportData->backgroundColor || + foregroundColor != reportData->foregroundColor; + if (isHighlightUpdated) + { + reportData->backgroundColor = backgroundColor; + reportData->foregroundColor = foregroundColor; + + wxDataViewItem dvi(reportData); + itemsChanged.Add(dvi); + } + } + + ItemsChanged(itemsChanged); +} + +void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) +{ + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->updateHighlights(); + + if (sortRequired_) + { + model->Resort(); + sortRequired_ = false; } } void FreeDVReporterDialog::OnFilterTrackingEnable(wxCommandEvent& event) { + DeselectItem(); + wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksFrequency = m_trackFrequency->GetValue(); m_bandFilter->Enable( @@ -826,23 +821,22 @@ void FreeDVReporterDialog::OnFilterTrackingEnable(wxCommandEvent& event) (FilterFrequency)wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilter.get(); } - // Force refresh of filters since the user expects this to happen immediately after changing a - // filter setting. - filteredFrequency_ = 0; - currentBandFilter_ = BAND_ALL; - setBandFilter(freq); } -void FreeDVReporterDialog::OnDoubleClick(wxMouseEvent& event) +void FreeDVReporterDialog::OnItemDoubleClick(wxDataViewEvent& event) { - auto selectedIndex = m_listSpots->GetFirstSelected(); - if (selectedIndex >= 0 && wxGetApp().rigFrequencyController && - (wxGetApp().appConfiguration.rigControlConfiguration.hamlibEnableFreqModeChanges || wxGetApp().appConfiguration.rigControlConfiguration.hamlibEnableFreqChangesOnly)) + if (event.GetItem().IsOk()) { - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(selectedIndex); - - wxGetApp().rigFrequencyController->setFrequency(allReporterData_[*sidPtr]->frequency); + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + auto frequency = model->getFrequency(event.GetItem()); + if (wxGetApp().rigFrequencyController && + (wxGetApp().appConfiguration.rigControlConfiguration.hamlibEnableFreqModeChanges || + wxGetApp().appConfiguration.rigControlConfiguration.hamlibEnableFreqChangesOnly)) + { + wxGetApp().rigFrequencyController->setFrequency(frequency); + } + DeselectItem(); } } @@ -853,34 +847,31 @@ void FreeDVReporterDialog::AdjustToolTip(wxMouseEvent& event) int mouseY = pt.y - m_listSpots->GetScreenPosition().y; wxRect rect; - int desiredCol = USER_MESSAGE_COL; -#if defined(WIN32) - desiredCol++; -#endif // defined(WIN32) + unsigned int desiredCol = USER_MESSAGE_COL; - for (auto index = 0; index < m_listSpots->GetItemCount(); index++) + wxDataViewItem item; + wxDataViewColumn* col; + m_listSpots->HitTest(wxPoint(mouseX, mouseY), item, col); + if (item.IsOk()) { - bool gotUserMessageColBounds = m_listSpots->GetSubItemRect(index, desiredCol, rect); - bool mouseInBounds = gotUserMessageColBounds && rect.Contains(mouseX, mouseY); + // Show popup corresponding to the full message. + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + tempUserMessage_ = model->getUserMessage(item); - if (gotUserMessageColBounds && mouseInBounds) + if (col->GetModelColumn() == desiredCol) { - // Show popup corresponding to the full message. - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); - tempUserMessage_ = allReporterData_[*sidPtr]->userMessage; - wxString userMessageTruncated = m_listSpots->GetItemText(index, desiredCol); - userMessageTruncated = userMessageTruncated.SubString(1, userMessageTruncated.size() - 1); - - if (tipWindow_ == nullptr && tempUserMessage_ != userMessageTruncated) + auto textSize = m_listSpots->GetTextExtent(tempUserMessage_); + rect = m_listSpots->GetItemRect(item, col); + if (tipWindow_ == nullptr && textSize.GetWidth() > col->GetWidth()) { // Use screen coordinates to determine bounds. auto pos = rect.GetPosition(); rect.SetPosition(ClientToScreen(pos)); - + tipWindow_ = new wxTipWindow(m_listSpots, tempUserMessage_, 1000, &tipWindow_, &rect); tipWindow_->Connect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(FreeDVReporterDialog::OnRightClickSpotsList), NULL, this); tipWindow_->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(FreeDVReporterDialog::SkipMouseEvent), NULL, this); - + // Make sure we actually override behavior of needed events inside the tooltip. for (auto& child : tipWindow_->GetChildren()) { @@ -888,24 +879,62 @@ void FreeDVReporterDialog::AdjustToolTip(wxMouseEvent& event) child->Connect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(FreeDVReporterDialog::OnRightClickSpotsList), NULL, this); } } - - break; } else { tempUserMessage_ = _(""); } } + else + { + tempUserMessage_ = _(""); + } +} + +void FreeDVReporterDialog::OnRightClickSpotsList(wxContextMenuEvent& event) +{ + wxDataViewEvent contextEvent; + OnItemRightClick(contextEvent); } void FreeDVReporterDialog::SkipMouseEvent(wxMouseEvent& event) { - wxContextMenuEvent contextEvent; - OnRightClickSpotsList(contextEvent); + wxDataViewEvent contextEvent; + OnItemRightClick(contextEvent); } -void FreeDVReporterDialog::OnRightClickSpotsList( wxContextMenuEvent& event ) +void FreeDVReporterDialog::OnColumnClick(wxDataViewEvent& event) { + DeselectItem(); + event.Skip(); + +#if 0 + auto col = event.GetDataViewColumn(); + if (col != nullptr) + { + if (col->IsSortKey() && !col->IsSortOrderAscending()) + { + col->UnsetAsSortKey(); + } + else if (!col->IsSortKey()) + { + col->SetSortOrder(true); + } + else + { + col->SetSortOrder(false); + } + event.StopPropagation(); + } +#endif // 0 +} + +void FreeDVReporterDialog::OnItemRightClick(wxDataViewEvent& event) +{ + // Make sure item's deselected as it should only be selected on + // left-click. + DeselectItem(); + if (tipWindow_ != nullptr) { tipWindow_->Close(); @@ -936,6 +965,7 @@ void FreeDVReporterDialog::OnCopyUserMessage(wxCommandEvent& event) wxTheClipboard->SetData( new wxTextDataObject(tempUserMessage_) ); wxTheClipboard->Close(); } + DeselectItem(); } void FreeDVReporterDialog::OnStatusTextChange(wxCommandEvent& event) @@ -953,10 +983,8 @@ void FreeDVReporterDialog::OnStatusTextSend(wxCommandEvent& event) { auto statusMsg = m_statusMessage->GetValue(); - if (reporter_) - { - reporter_->updateMessage(statusMsg.utf8_string()); - } + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->updateMessage(statusMsg); wxGetApp().appConfiguration.reportingConfiguration.freedvReporterStatusText = statusMsg; @@ -976,11 +1004,14 @@ void FreeDVReporterDialog::OnStatusTextSend(wxCommandEvent& event) { wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRecentStatusTexts->push_back(m_statusMessage->GetString(index)); } - } + } + + DeselectItem(); } void FreeDVReporterDialog::OnStatusTextSendContextMenu(wxContextMenuEvent& event) { + DeselectItem(); m_buttonSend->PopupMenu(setPopupMenu_); } @@ -1024,13 +1055,13 @@ void FreeDVReporterDialog::OnStatusTextSaveMessage(wxCommandEvent& event) void FreeDVReporterDialog::OnStatusTextClear(wxCommandEvent& event) { - if (reporter_) - { - reporter_->updateMessage(""); - } + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->updateMessage(""); wxGetApp().appConfiguration.reportingConfiguration.freedvReporterStatusText = ""; m_statusMessage->SetValue(""); + + DeselectItem(); } void FreeDVReporterDialog::OnStatusTextClearContextMenu(wxContextMenuEvent& event) @@ -1067,40 +1098,32 @@ void FreeDVReporterDialog::OnStatusTextClearAll(wxCommandEvent& event) void FreeDVReporterDialog::refreshQSYButtonState() { + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + // Update filter if the frequency's changed. if (wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksFrequency) { FilterFrequency freq = getFilterForFrequency_(wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency); - if (currentBandFilter_ != freq || wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksExactFreq) + if (model->getCurrentBandFilter() != freq || wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksExactFreq) { setBandFilter(freq); } } bool enabled = false; - auto selectedIndex = m_listSpots->GetFirstSelected(); - if (selectedIndex >= 0) + auto selected = m_listSpots->GetSelection(); + if (selected.IsOk()) { - auto selectedCallsign = m_listSpots->GetItemText(selectedIndex); + auto selectedCallsign = model->getCallsign(selected); - if (reporter_->isValidForReporting() && + if (model->isValidForReporting() && selectedCallsign != wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign && wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency > 0 && freedvInterface.isRunning()) { - wxString theirFreqString = m_listSpots->GetItemText(selectedIndex, 5); - - uint64_t theirFreq = 0; - if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) - { - theirFreq = wxAtof(theirFreqString) * 1000; - } - else - { - theirFreq = wxAtof(theirFreqString) * 1000 * 1000; - } + uint64_t theirFreq = model->getFrequency(selected); enabled = theirFreq != wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency; } } @@ -1109,6 +1132,12 @@ void FreeDVReporterDialog::refreshQSYButtonState() } void FreeDVReporterDialog::setBandFilter(FilterFrequency freq) +{ + FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get(); + model->setBandFilter(freq); +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::setBandFilter(FilterFrequency freq) { if (filteredFrequency_ != wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency || currentBandFilter_ != freq) @@ -1116,116 +1145,85 @@ void FreeDVReporterDialog::setBandFilter(FilterFrequency freq) filteredFrequency_ = wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency; currentBandFilter_ = freq; - // Update displayed list based on new filter criteria. - clearAllEntries_(false); - - std::map colResizeList; - for (auto& kvp : allReporterData_) - { - addOrUpdateListIfNotFiltered_(kvp.second, colResizeList); - } - resizeChangedColumns_(colResizeList); + refreshAllRows(); } } -void FreeDVReporterDialog::sortColumn_(int col) -{ - bool direction = true; +wxString FreeDVReporterDialog::FreeDVReporterDataModel::makeValidTime_(std::string timeStr, wxDateTime& timeObj) +{ + wxRegEx millisecondsRemoval(_("\\.[^+-]+")); + wxString tmp = timeStr; + millisecondsRemoval.Replace(&tmp, _("")); - if (currentSortColumn_ != col) + wxRegEx timezoneRgx(_("([+-])([0-9]+):([0-9]+)$")); + wxDateTime::TimeZone timeZone(0); // assume UTC by default + if (timezoneRgx.Matches(tmp)) { - direction = true; + auto tzOffset = timezoneRgx.GetMatch(tmp, 1); + auto hours = timezoneRgx.GetMatch(tmp, 2); + auto minutes = timezoneRgx.GetMatch(tmp, 3); + + int tzMinutes = wxAtoi(hours) * 60; + tzMinutes += wxAtoi(minutes); + + if (tzOffset == "-") + { + tzMinutes = -tzMinutes; + } + + timezoneRgx.Replace(&tmp, _("")); + + timeZone = wxDateTime::TimeZone(tzMinutes); } - else if (sortAscending_) + + wxDateTime tmpDate; + if (tmpDate.ParseISOCombined(tmp)) { - direction = false; + tmpDate.MakeFromTimezone(timeZone); + timeObj = tmpDate; + if (wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting) + { + timeZone = wxDateTime::TimeZone(wxDateTime::TZ::UTC); + } + else + { + timeZone = wxDateTime::TimeZone(wxDateTime::TZ::Local); + } + + wxString formatStr = "%x %X"; + +#if __APPLE__ + // Workaround for weird macOS bug preventing .Format from working properly when double-clicking + // on the .app in Finder. Running the app from Terminal seems to work fine with .Format for + // some reason. O_o + struct tm tmpTm; + auto tmpDateTm = tmpDate.GetTm(timeZone); + + tmpTm.tm_sec = tmpDateTm.sec; + tmpTm.tm_min = tmpDateTm.min; + tmpTm.tm_hour = tmpDateTm.hour; + tmpTm.tm_mday = tmpDateTm.mday; + tmpTm.tm_mon = tmpDateTm.mon; + tmpTm.tm_year = tmpDateTm.year - 1900; + tmpTm.tm_wday = tmpDateTm.GetWeekDay(); + tmpTm.tm_yday = tmpDateTm.yday; + tmpTm.tm_isdst = -1; + + char buf[4096]; + strftime(buf, sizeof(buf), (const char*)formatStr.ToUTF8(), &tmpTm); + return buf; +#else + return tmpDate.Format(formatStr, timeZone); +#endif // __APPLE__ } else { - col = -1; - } - - sortColumn_(col, direction); - wxGetApp().appConfiguration.reporterWindowCurrentSort = col; - wxGetApp().appConfiguration.reporterWindowCurrentSortDirection = direction; -} - -void FreeDVReporterDialog::sortColumn_(int col, bool direction) -{ -#if defined(WIN32) - // The hidden column 0 is new in 1.9.7. Assume sort by the "old" column 0 - // (callsign). - if (col == 0) - { - col = 1; - } -#endif // defined(WIN32) - - if (currentSortColumn_ != -1) - { - m_listSpots->ClearColumnImage(currentSortColumn_); - } - - sortAscending_ = direction; - currentSortColumn_ = col; - - if (currentSortColumn_ != -1) - { - m_listSpots->SetColumnImage(currentSortColumn_, direction ? upIconIndex_ : downIconIndex_); - m_listSpots->SortItems(&FreeDVReporterDialog::ListCompareFn_, (wxIntPtr)this); + timeObj = wxDateTime(); + return _(UNKNOWN_STR); } } -void FreeDVReporterDialog::clearAllEntries_(bool clearForAllBands) -{ - if (clearForAllBands) - { - for (auto& kvp : allReporterData_) - { - delete kvp.second; - } - allReporterData_.clear(); - } - - std::vector stringItemsToDelete; - for (auto index = m_listSpots->GetItemCount() - 1; index >= 0; index--) - { - stringItemsToDelete.push_back((std::string*)m_listSpots->GetItemData(index)); - } - m_listSpots->DeleteAllItems(); - - for (auto& ptr : stringItemsToDelete) - { - delete ptr; - } - - // Reset lengths to force auto-resize on (re)connect. - int userMsgCol = USER_MESSAGE_COL; -#if defined(WIN32) - userMsgCol++; -#endif // defined(WIN32) - - for (int col = 0; col < NUM_COLS; col++) - { -#if defined(WIN32) - // First column is hidden, so don't auto-size. - if (col == 0) - { - continue; - } -#endif // defined(WIN32) - - if (col != userMsgCol) - { - columnLengths_[col] = DefaultColumnWidths_[col]; - m_listSpots->SetColumnWidth(col, columnLengths_[col]); - } - } - - m_listSpots->Update(); -} - -double FreeDVReporterDialog::calculateDistance_(wxString gridSquare1, wxString gridSquare2) +double FreeDVReporterDialog::FreeDVReporterDataModel::calculateDistance_(wxString gridSquare1, wxString gridSquare2) { double lat1 = 0; double lon1 = 0; @@ -1249,7 +1247,7 @@ double FreeDVReporterDialog::calculateDistance_(wxString gridSquare1, wxString g return EARTH_RADIUS * c; } -void FreeDVReporterDialog::calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon) +void FreeDVReporterDialog::FreeDVReporterDataModel::calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon) { char charA = 'A'; char char0 = '0'; @@ -1289,7 +1287,7 @@ void FreeDVReporterDialog::calculateLatLonFromGridSquare_(wxString gridSquare, d } } -double FreeDVReporterDialog::calculateBearingInDegrees_(wxString gridSquare1, wxString gridSquare2) +double FreeDVReporterDialog::FreeDVReporterDataModel::calculateBearingInDegrees_(wxString gridSquare1, wxString gridSquare2) { double lat1 = 0; double lon1 = 0; @@ -1314,32 +1312,209 @@ double FreeDVReporterDialog::calculateBearingInDegrees_(wxString gridSquare1, wx return RadiansToDegrees_(radians); } -double FreeDVReporterDialog::DegreesToRadians_(double degrees) +double FreeDVReporterDialog::FreeDVReporterDataModel::DegreesToRadians_(double degrees) { return degrees * (M_PI / 180.0); } -double FreeDVReporterDialog::RadiansToDegrees_(double radians) +double FreeDVReporterDialog::FreeDVReporterDataModel::RadiansToDegrees_(double radians) { auto result = (radians > 0 ? radians : (2*M_PI + radians)) * 360 / (2*M_PI); return (result == 360) ? 0 : result; } -int FreeDVReporterDialog::ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) +void FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_() { - FreeDVReporterDialog* thisPtr = (FreeDVReporterDialog*)sortData; - std::string* leftSid = (std::string*)item1; - std::string* rightSid = (std::string*)item2; - auto leftData = thisPtr->allReporterData_[*leftSid]; - auto rightData = thisPtr->allReporterData_[*rightSid]; + // This ensures that we handle server events in the order they're received. + std::unique_lock lk(fnQueueMtx_); + fnQueue_[0](); + fnQueue_.erase(fnQueue_.begin()); +} + +FreeDVReporterDialog::FilterFrequency FreeDVReporterDialog::getFilterForFrequency_(uint64_t freq) +{ + auto bandForFreq = FilterFrequency::BAND_OTHER; + + if (freq >= 1800000 && freq <= 2000000) + { + bandForFreq = FilterFrequency::BAND_160M; + } + else if (freq >= 3500000 && freq <= 4000000) + { + bandForFreq = FilterFrequency::BAND_80M; + } + else if (freq >= 5250000 && freq <= 5450000) + { + bandForFreq = FilterFrequency::BAND_60M; + } + else if (freq >= 7000000 && freq <= 7300000) + { + bandForFreq = FilterFrequency::BAND_40M; + } + else if (freq >= 10100000 && freq <= 10150000) + { + bandForFreq = FilterFrequency::BAND_30M; + } + else if (freq >= 14000000 && freq <= 14350000) + { + bandForFreq = FilterFrequency::BAND_20M; + } + else if (freq >= 18068000 && freq <= 18168000) + { + bandForFreq = FilterFrequency::BAND_17M; + } + else if (freq >= 21000000 && freq <= 21450000) + { + bandForFreq = FilterFrequency::BAND_15M; + } + else if (freq >= 24890000 && freq <= 24990000) + { + bandForFreq = FilterFrequency::BAND_12M; + } + else if (freq >= 28000000 && freq <= 29700000) + { + bandForFreq = FilterFrequency::BAND_10M; + } + else if (freq >= 50000000) + { + bandForFreq = FilterFrequency::BAND_VHF_UHF; + } + + return bandForFreq; +} + +bool FreeDVReporterDialog::FreeDVReporterDataModel::isFiltered_(uint64_t freq) +{ + auto bandForFreq = parent_->getFilterForFrequency_(freq); + + if (currentBandFilter_ == FilterFrequency::BAND_ALL) + { + return false; + } + else + { + return + (bandForFreq != currentBandFilter_) || + (wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksFrequency && + wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksExactFreq && + freq != filteredFrequency_); + } +} + +wxString FreeDVReporterDialog::FreeDVReporterDataModel::GetCardinalDirection_(int degrees) +{ + int cardinalDirectionNumber( static_cast( ( ( degrees / 360.0 ) * 16 ) + 0.5 ) % 16 ); + const char* const cardinalDirectionTexts[] = { "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" }; + return cardinalDirectionTexts[cardinalDirectionNumber]; +} + +FreeDVReporterDialog::FreeDVReporterDataModel::FreeDVReporterDataModel(FreeDVReporterDialog* parent) + : isConnected_(false) + , parent_(parent) + , currentBandFilter_(FreeDVReporterDialog::BAND_ALL) + , filterSelfMessageUpdates_(false) + , filteredFrequency_(0) +{ + // empty +} + +FreeDVReporterDialog::FreeDVReporterDataModel::~FreeDVReporterDataModel() +{ + setReporter(nullptr); +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::setReporter(std::shared_ptr reporter) +{ + if (reporter_) + { + reporter_->setOnReporterConnectFn(FreeDVReporter::ReporterConnectionFn()); + reporter_->setOnReporterDisconnectFn(FreeDVReporter::ReporterConnectionFn()); + + reporter_->setOnUserConnectFn(FreeDVReporter::ConnectionDataFn()); + reporter_->setOnUserDisconnectFn(FreeDVReporter::ConnectionDataFn()); + reporter_->setOnFrequencyChangeFn(FreeDVReporter::FrequencyChangeFn()); + reporter_->setOnTransmitUpdateFn(FreeDVReporter::TxUpdateFn()); + reporter_->setOnReceiveUpdateFn(FreeDVReporter::RxUpdateFn()); + + reporter_->setMessageUpdateFn(FreeDVReporter::MessageUpdateFn()); + reporter_->setConnectionSuccessfulFn(FreeDVReporter::ConnectionSuccessfulFn()); + reporter_->setAboutToShowSelfFn(FreeDVReporter::AboutToShowSelfFn()); + } + + reporter_ = reporter; + + if (reporter_) + { + reporter_->setOnReporterConnectFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onReporterConnect_, this)); + reporter_->setOnReporterDisconnectFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onReporterDisconnect_, this)); + + reporter_->setOnUserConnectFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onUserConnectFn_, this, _1, _2, _3, _4, _5, _6)); + reporter_->setOnUserDisconnectFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onUserDisconnectFn_, this, _1, _2, _3, _4, _5, _6)); + reporter_->setOnFrequencyChangeFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onFrequencyChangeFn_, this, _1, _2, _3, _4, _5)); + reporter_->setOnTransmitUpdateFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onTransmitUpdateFn_, this, _1, _2, _3, _4, _5, _6, _7)); + reporter_->setOnReceiveUpdateFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onReceiveUpdateFn_, this, _1, _2, _3, _4, _5, _6, _7)); + + reporter_->setMessageUpdateFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onMessageUpdateFn_, this, _1, _2, _3)); + reporter_->setConnectionSuccessfulFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onConnectionSuccessfulFn_, this)); + reporter_->setAboutToShowSelfFn(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::onAboutToShowSelfFn_, this)); + } + else + { + // Spot list no longer valid, delete the items currently on there + clearAllEntries_(); + } +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::clearAllEntries_() +{ + std::unique_lock lk(dataMtx_); + assert(wxThread::IsMain()); + + for (auto& row : allReporterData_) + { + if (row.second->isVisible) + { + row.second->isVisible = false; + } + + delete row.second; + } + allReporterData_.clear(); + Cleared(); +} + +bool FreeDVReporterDialog::FreeDVReporterDataModel::HasDefaultCompare() const +{ + // Will compare by connect time if nothing is selected for sorting + return true; +} + +int FreeDVReporterDialog::FreeDVReporterDataModel::Compare (const wxDataViewItem &item1, const wxDataViewItem &item2, unsigned int column, bool ascending) const +{ + std::unique_lock lk(const_cast(dataMtx_)); + assert(wxThread::IsMain()); + + if (!item1.IsOk() || !item2.IsOk()) + { + int result = 0; + if (!item1.IsOk()) + { + result = 1; + } + else + { + result = -1; + } + + result *= ascending ? 1 : -1; + return result; + + } + auto leftData = (ReporterData*)item1.GetID(); + auto rightData = (ReporterData*)item2.GetID(); int result = 0; - -#if defined(WIN32) - switch(thisPtr->currentSortColumn_ - 1) -#else - switch(thisPtr->currentSortColumn_) -#endif //defined(WIN32) + switch(column) { case CALLSIGN_COL: result = leftData->callsign.CmpNoCase(rightData->callsign); @@ -1435,12 +1610,26 @@ int FreeDVReporterDialog::ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPt result = 0; } break; + case (unsigned)-1: + if (leftData->connectTime.IsEarlierThan(rightData->connectTime)) + { + result = -1; + } + else if (leftData->connectTime.IsLaterThan(rightData->connectTime)) + { + result = 1; + } + else + { + result = 0; + } + break; default: assert(false); break; } - if (!thisPtr->sortAscending_) + if (!ascending) { result = -result; } @@ -1448,12 +1637,202 @@ int FreeDVReporterDialog::ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPt return result; } -void FreeDVReporterDialog::execQueuedAction_() +bool FreeDVReporterDialog::FreeDVReporterDataModel::GetAttr (const wxDataViewItem &item, unsigned int col, wxDataViewItemAttr &attr) const { - // This ensures that we handle server events in the order they're received. - std::unique_lock lk(fnQueueMtx_); - fnQueue_[0](); - fnQueue_.erase(fnQueue_.begin()); + std::unique_lock lk(const_cast(dataMtx_)); + bool result = false; + assert(wxThread::IsMain()); + + if (item.IsOk()) + { + auto row = (ReporterData*)item.GetID(); + if (row->backgroundColor.IsOk()) + { + attr.SetBackgroundColour(row->backgroundColor); + result = true; + } + if (row->foregroundColor.IsOk()) + { + attr.SetColour(row->foregroundColor); + result = true; + } + } + + return result; +} + +unsigned int FreeDVReporterDialog::FreeDVReporterDataModel::GetChildren (const wxDataViewItem &item, wxDataViewItemArray &children) const +{ + assert(wxThread::IsMain()); + if (item.IsOk()) + { + // No children. + return 0; + } + else + { + std::unique_lock lk(const_cast(dataMtx_)); + int count = 0; + for (auto& row : allReporterData_) + { + if (row.second->isVisible) + { + count++; + + wxDataViewItem newItem(row.second); + children.Add(newItem); + } + } + + return count; + } +} + +wxDataViewItem FreeDVReporterDialog::FreeDVReporterDataModel::GetParent (const wxDataViewItem &item) const +{ + // Return root item + return wxDataViewItem(nullptr); +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::GetValue (wxVariant &variant, const wxDataViewItem &item, unsigned int col) const +{ + std::unique_lock lk(const_cast(dataMtx_)); + assert(wxThread::IsMain()); + if (item.IsOk()) + { + auto row = (ReporterData*)item.GetID(); + switch (col) + { + case CALLSIGN_COL: + variant = wxVariant(row->callsign); + break; + case GRID_SQUARE_COL: + variant = wxVariant(row->gridSquare); + break; + case DISTANCE_COL: + variant = wxVariant(row->distance); + break; + case HEADING_COL: + variant = wxVariant(row->heading); + break; + case VERSION_COL: + variant = wxVariant(row->version); + break; + case FREQUENCY_COL: + variant = wxVariant(row->freqString); + break; + case STATUS_COL: + variant = wxVariant(row->status); + break; + case USER_MESSAGE_COL: + variant = wxVariant(row->userMessage); + break; + case LAST_TX_DATE_COL: + variant = wxVariant(row->lastTx); + break; + case LAST_RX_CALLSIGN_COL: + variant = wxVariant(row->lastRxCallsign); + break; + case LAST_RX_MODE_COL: + variant = wxVariant(row->lastRxMode); + break; + case SNR_COL: + variant = wxVariant(row->snr); + break; + case LAST_UPDATE_DATE_COL: + variant = wxVariant(row->lastUpdate); + break; + case TX_MODE_COL: + variant = wxVariant(row->txMode); + break; + default: + variant = wxVariant("NOT VALID"); + break; + } + } +} + +bool FreeDVReporterDialog::FreeDVReporterDataModel::IsContainer (const wxDataViewItem &item) const +{ + // Single-level (i.e. no children) + return false; +} + +bool FreeDVReporterDialog::FreeDVReporterDataModel::SetValue (const wxVariant &variant, const wxDataViewItem &item, unsigned int col) +{ + // I think this can just return false without changing anything as this is read-only (other than what comes from the server). + return false; +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::refreshAllRows() +{ + std::unique_lock lk(dataMtx_); + + for (auto& kvp : allReporterData_) + { + bool updated = false; + double frequencyUserReadable = kvp.second->frequency / 1000.0; + wxString frequencyString; + + if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) + { + frequencyString = wxString::Format(_("%.01f"), frequencyUserReadable); + } + else + { + frequencyUserReadable /= 1000.0; + frequencyString = wxString::Format(_("%.04f"), frequencyUserReadable); + } + + updated |= kvp.second->freqString != frequencyString; + kvp.second->freqString = frequencyString; + + // Refresh cardinal vs. degree directions. + if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare != kvp.second->gridSquare) + { + wxString newHeading; + if (wxGetApp().appConfiguration.reportingConfiguration.reportingDirectionAsCardinal) + { + newHeading = GetCardinalDirection_(kvp.second->headingVal); + } + else + { + newHeading = wxString::Format("%.0f", kvp.second->headingVal); + } + + updated |= kvp.second->heading != newHeading; + kvp.second->heading = newHeading; + } + + // Refresh filter state + bool newVisibility = !isFiltered_(kvp.second->frequency); + if (newVisibility != kvp.second->isVisible) + { + kvp.second->isVisible = newVisibility; + if (newVisibility) + { + ItemAdded(wxDataViewItem(nullptr), wxDataViewItem(kvp.second)); + } + else + { + ItemDeleted(wxDataViewItem(nullptr), wxDataViewItem(kvp.second)); + } + } + else if (updated) + { + ItemChanged(wxDataViewItem(kvp.second)); + parent_->sortRequired_ = true; + } + } +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::requestQSY(wxDataViewItem selectedItem, uint64_t frequency, wxString customText) +{ + if (reporter_ && selectedItem.IsOk()) + { + auto row = (ReporterData*)selectedItem.GetID(); + reporter_->requestQSY(row->sid, wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency, (const char*)customText.ToUTF8()); + } } // ================================================================================= @@ -1461,724 +1840,457 @@ void FreeDVReporterDialog::execQueuedAction_() // UI actions happen there. // ================================================================================= -void FreeDVReporterDialog::onReporterConnect_() +void FreeDVReporterDialog::FreeDVReporterDataModel::onReporterConnect_() { - std::unique_lock lk(fnQueueMtx_); + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); - fnQueue_.push_back([&]() { - filterSelfMessageUpdates_ = false; - clearAllEntries_(true); - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom]() { + log_debug("Connected to server"); + filterSelfMessageUpdates_ = false; + clearAllEntries_(); + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -void FreeDVReporterDialog::onReporterDisconnect_() +void FreeDVReporterDialog::FreeDVReporterDataModel::onReporterDisconnect_() { - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&]() { - isConnected_ = false; - filterSelfMessageUpdates_ = false; - clearAllEntries_(true); - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom]() { + log_debug("Disconnected from server"); + isConnected_ = false; + filterSelfMessageUpdates_ = false; + clearAllEntries_(); + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -void FreeDVReporterDialog::onUserConnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly) +void FreeDVReporterDialog::FreeDVReporterDataModel::onUserConnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly) { - std::unique_lock lk(fnQueueMtx_); + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); - fnQueue_.push_back([&, sid, lastUpdate, callsign, gridSquare, version, rxOnly]() { - // Initially populate stored report data, but don't add to the viewable list just yet. - // We only add on frequency update and only if the filters check out. - ReporterData* temp = new ReporterData; - assert(temp != nullptr); - - // Limit grid square display to six characters. - wxString gridSquareWxString = wxString(gridSquare).Left(6); - - temp->sid = sid; - temp->callsign = wxString(callsign).Upper(); - temp->gridSquare = gridSquareWxString.Left(2).Upper() + gridSquareWxString.Mid(2, 2); + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid, lastUpdate, callsign, gridSquare, version, rxOnly]() { + std::unique_lock lk(const_cast(dataMtx_)); + assert(wxThread::IsMain()); - // Lowercase final letters of grid square per standard. - if (gridSquareWxString.Length() >= 6) - { - temp->gridSquare += gridSquareWxString.Mid(4, 2).Lower(); - } + log_debug("User connected: %s (%s) with SID %s", callsign.c_str(), gridSquare.c_str(), sid.c_str()); - wxRegEx gridSquareRegex(_("^[A-Za-z]{2}[0-9]{2}")); - bool validCharactersInGridSquare = gridSquareRegex.Matches(temp->gridSquare); - - if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare == "" || - !validCharactersInGridSquare) - { - // Invalid grid square means we can't calculate a distance. - temp->distance = UNKNOWN_STR; - temp->distanceVal = 0; - temp->heading = UNKNOWN_STR; - temp->headingVal = 0; - } - else - { - temp->distanceVal = calculateDistance_(wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare, gridSquareWxString); - - if (!wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances) + // Initially populate stored report data, but don't add to the viewable list just yet. + // We only add on frequency update and only if the filters check out. + ReporterData* temp = new ReporterData; + assert(temp != nullptr); + + // Limit grid square display to six characters. + wxString gridSquareWxString = wxString(gridSquare).Left(6); + + temp->sid = sid; + temp->callsign = wxString(callsign).Upper(); + temp->gridSquare = gridSquareWxString.Left(2).Upper() + gridSquareWxString.Mid(2, 2); + + // Lowercase final letters of grid square per standard. + if (gridSquareWxString.Length() >= 6) { - // Convert to miles for those who prefer it - // (calculateDistance_() returns distance in km). - temp->distanceVal *= 0.621371; + temp->gridSquare += gridSquareWxString.Mid(4, 2).Lower(); } - - temp->distance = wxString::Format("%.0f", temp->distanceVal); - - if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare == gridSquareWxString) + + wxRegEx gridSquareRegex(_("^[A-Za-z]{2}[0-9]{2}")); + bool validCharactersInGridSquare = gridSquareRegex.Matches(temp->gridSquare); + + if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare == "" || + !validCharactersInGridSquare) { - temp->headingVal = 0; + // Invalid grid square means we can't calculate a distance. + temp->distance = UNKNOWN_STR; + temp->distanceVal = 0; temp->heading = UNKNOWN_STR; + temp->headingVal = 0; } else { - temp->headingVal = calculateBearingInDegrees_(wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare, gridSquareWxString); - if (wxGetApp().appConfiguration.reportingConfiguration.reportingDirectionAsCardinal) + temp->distanceVal = calculateDistance_(wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare, gridSquareWxString); + + if (!wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances) { - temp->heading = GetCardinalDirection_(temp->headingVal); + // Convert to miles for those who prefer it + // (calculateDistance_() returns distance in km). + temp->distanceVal *= 0.621371; + } + + temp->distance = wxString::Format("%.0f", temp->distanceVal); + + if (wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare == gridSquareWxString) + { + temp->headingVal = 0; + temp->heading = UNKNOWN_STR; } else { - temp->heading = wxString::Format("%.0f", temp->headingVal); + temp->headingVal = calculateBearingInDegrees_(wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare, gridSquareWxString); + if (wxGetApp().appConfiguration.reportingConfiguration.reportingDirectionAsCardinal) + { + temp->heading = GetCardinalDirection_(temp->headingVal); + } + else + { + temp->heading = wxString::Format("%.0f", temp->headingVal); + } } } - } - - temp->version = version; - temp->freqString = UNKNOWN_STR; - temp->userMessage = UNKNOWN_STR; - temp->transmitting = false; - - if (rxOnly) - { - temp->status = RX_ONLY_STATUS; - temp->txMode = UNKNOWN_STR; - temp->lastTx = UNKNOWN_STR; - } - else - { - temp->status = UNKNOWN_STR; - temp->txMode = UNKNOWN_STR; - temp->lastTx = UNKNOWN_STR; - } - - temp->lastRxCallsign = UNKNOWN_STR; - temp->lastRxMode = UNKNOWN_STR; - temp->snr = UNKNOWN_STR; - - auto lastUpdateTime = makeValidTime_(lastUpdate, temp->lastUpdateDate); - temp->lastUpdate = lastUpdateTime; - - allReporterData_[sid] = temp; - }); - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); -} - -void FreeDVReporterDialog::onConnectionSuccessfulFn_() -{ - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&]() { - // Enable highlighting now that we're fully connected. - isConnected_ = true; - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); -} - -void FreeDVReporterDialog::resizeAllColumns_() -{ - int userMsgCol = USER_MESSAGE_COL; -#if defined(WIN32) - userMsgCol++; -#endif // defined(WIN32) - - std::map newMaxSizes; - std::map colResizeList; - for (auto index = 0; index < m_listSpots->GetItemCount(); index++) - { - for (int i = 0; i < NUM_COLS; i++) - { - wxString colText = m_listSpots->GetItemText(index, i); - auto newSize = std::max(getSizeForTableCellString_(colText), DefaultColumnWidths_[i]); - if (i != userMsgCol) + temp->version = version; + temp->freqString = UNKNOWN_STR; + temp->userMessage = UNKNOWN_STR; + temp->transmitting = false; + + if (rxOnly) { - if (newSize > newMaxSizes[i]) + temp->status = RX_ONLY_STATUS; + temp->txMode = UNKNOWN_STR; + temp->lastTx = UNKNOWN_STR; + } + else + { + temp->status = UNKNOWN_STR; + temp->txMode = UNKNOWN_STR; + temp->lastTx = UNKNOWN_STR; + } + + temp->lastRxCallsign = UNKNOWN_STR; + temp->lastRxMode = UNKNOWN_STR; + temp->snr = UNKNOWN_STR; + + auto lastUpdateTime = makeValidTime_(lastUpdate, temp->lastUpdateDate); + temp->lastUpdate = lastUpdateTime; + temp->connectTime = temp->lastUpdateDate; + temp->isVisible = !isFiltered_(temp->frequency); + + if (allReporterData_.find(sid) != allReporterData_.end() && !isConnected_) + { + log_warn("Received duplicate user during connection process"); + prom->set_value(); + return; + } + + allReporterData_[sid] = temp; + + if (temp->isVisible) + { + ItemAdded(wxDataViewItem(nullptr), wxDataViewItem(temp)); + } + + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::onConnectionSuccessfulFn_() +{ + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom]() { + std::unique_lock lk(const_cast(dataMtx_)); + + log_debug("Fully connected to server"); + + // Enable highlighting now that we're fully connected. + isConnected_ = true; + + // NOW we can display what we received + + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); +} + +void FreeDVReporterDialog::FreeDVReporterDataModel::onUserDisconnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly) +{ + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid]() { + std::unique_lock lk(const_cast(dataMtx_)); + assert(wxThread::IsMain()); + + log_debug("User with SID %s disconnected", sid.c_str()); + + auto iter = allReporterData_.find(sid); + if (iter != allReporterData_.end()) + { + auto item = iter->second; + wxDataViewItem dvi(item); + if (item->isVisible) { - newMaxSizes[i] = newSize; + item->isVisible = false; + parent_->Unselect(dvi); + ItemDeleted(wxDataViewItem(nullptr), dvi); } - - colResizeList[i] = 1; + + delete item; + allReporterData_.erase(iter); } - } + + prom->set_value(); + }); } - columnLengths_ = newMaxSizes; - resizeChangedColumns_(colResizeList); + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -void FreeDVReporterDialog::onUserDisconnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly) +void FreeDVReporterDialog::FreeDVReporterDataModel::onFrequencyChangeFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, uint64_t frequencyHz) { - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&, sid]() { - int indexToDelete = -1; - for (auto index = 0; index < m_listSpots->GetItemCount(); index++) - { - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); - if (sid == *sidPtr) - { - indexToDelete = index; - break; - } - } - - if (indexToDelete >= 0) - { - delete (std::string*)m_listSpots->GetItemData(indexToDelete); - m_listSpots->DeleteItem(indexToDelete); + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); - resizeAllColumns_(); - } - - auto iter = allReporterData_.find(sid); - if (iter != allReporterData_.end()) - { - delete allReporterData_[sid]; - allReporterData_.erase(iter); - } - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); -} - -void FreeDVReporterDialog::onFrequencyChangeFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, uint64_t frequencyHz) -{ - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&, sid, frequencyHz, lastUpdate]() { - auto iter = allReporterData_.find(sid); - if (iter != allReporterData_.end()) - { - double frequencyUserReadable = frequencyHz / 1000.0; - wxString frequencyString; + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid, frequencyHz, lastUpdate]() { + std::unique_lock lk(const_cast(dataMtx_)); - if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) + auto iter = allReporterData_.find(sid); + if (iter != allReporterData_.end()) { - frequencyString = wxString::Format(_("%.01f"), frequencyUserReadable); - } - else - { - frequencyUserReadable /= 1000.0; - frequencyString = wxString::Format(_("%.04f"), frequencyUserReadable); - } - auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); - - iter->second->frequency = frequencyHz; - iter->second->freqString = frequencyString; - iter->second->lastUpdate = lastUpdateTime; - - std::map colResizeList; - addOrUpdateListIfNotFiltered_(iter->second, colResizeList); - resizeChangedColumns_(colResizeList); - } - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); -} - -void FreeDVReporterDialog::onTransmitUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string txMode, bool transmitting, std::string lastTxDate) -{ - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&, sid, txMode, transmitting, lastTxDate, lastUpdate]() { - auto iter = allReporterData_.find(sid); - if (iter != allReporterData_.end()) - { - iter->second->transmitting = transmitting; - - std::string txStatus = "RX"; - if (transmitting) - { - txStatus = "TX"; - } - - if (iter->second->status != _(RX_ONLY_STATUS)) - { - iter->second->status = txStatus; - iter->second->txMode = txMode; + double frequencyUserReadable = frequencyHz / 1000.0; + wxString frequencyString; - auto lastTxTime = makeValidTime_(lastTxDate, iter->second->lastTxDate); - iter->second->lastTx = lastTxTime; - } - - auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); - iter->second->lastUpdate = lastUpdateTime; - - std::map colResizeList; - addOrUpdateListIfNotFiltered_(iter->second, colResizeList); - resizeChangedColumns_(colResizeList); - } - }); + if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) + { + frequencyString = wxString::Format(_("%.01f"), frequencyUserReadable); + } + else + { + frequencyUserReadable /= 1000.0; + frequencyString = wxString::Format(_("%.04f"), frequencyUserReadable); + } + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); + + iter->second->frequency = frequencyHz; + iter->second->freqString = frequencyString; + iter->second->lastUpdate = lastUpdateTime; - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); + wxDataViewItem dvi(iter->second); + bool newVisibility = !isFiltered_(iter->second->frequency); + if (newVisibility != iter->second->isVisible) + { + iter->second->isVisible = newVisibility; + if (newVisibility) + { + ItemAdded(wxDataViewItem(nullptr), dvi); + } + else + { + ItemDeleted(wxDataViewItem(nullptr), dvi); + } + } + else + { + ItemChanged(dvi); + parent_->sortRequired_ = + parent_->m_listSpots->GetColumn(FREQUENCY_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(LAST_UPDATE_DATE_COL)->IsSortKey(); + } + } + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -void FreeDVReporterDialog::onReceiveUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string receivedCallsign, float snr, std::string rxMode) +void FreeDVReporterDialog::FreeDVReporterDataModel::onTransmitUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string txMode, bool transmitting, std::string lastTxDate) { - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&, sid, lastUpdate, receivedCallsign, snr, rxMode]() { - auto iter = allReporterData_.find(sid); - if (iter != allReporterData_.end()) - { - iter->second->lastRxCallsign = receivedCallsign; - iter->second->lastRxMode = rxMode; - - auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); - iter->second->lastUpdate = lastUpdateTime; - - wxString snrString = wxString::Format(_("%.01f"), snr); - if (receivedCallsign == "" && rxMode == "") + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid, txMode, transmitting, lastTxDate, lastUpdate]() { + std::unique_lock lk(const_cast(dataMtx_)); + + auto iter = allReporterData_.find(sid); + if (iter != allReporterData_.end()) { - // Frequency change--blank out SNR too. - iter->second->lastRxCallsign = UNKNOWN_STR; - iter->second->lastRxMode = UNKNOWN_STR; - iter->second->snr = UNKNOWN_STR; - iter->second->lastRxDate = wxDateTime(); - } - else - { - iter->second->snr = snrString; - iter->second->lastRxDate = iter->second->lastUpdateDate; - } + iter->second->transmitting = transmitting; - std::map colResizeList; - addOrUpdateListIfNotFiltered_(iter->second, colResizeList); - resizeChangedColumns_(colResizeList); - } - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); -} - -void FreeDVReporterDialog::onMessageUpdateFn_(std::string sid, std::string lastUpdate, std::string message) -{ - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&, sid, lastUpdate, message]() { - auto iter = allReporterData_.find(sid); - if (iter != allReporterData_.end()) - { - if (message.size() == 0) - { - iter->second->userMessage = UNKNOWN_STR; - } - else - { - iter->second->userMessage = wxString::FromUTF8(message); - } + std::string txStatus = "RX"; + if (transmitting) + { + txStatus = "TX"; + } - auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); - iter->second->lastUpdate = lastUpdateTime; - - // Only highlight on non-empty messages. - bool ourCallsign = iter->second->callsign == wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign; - bool filteringSelf = ourCallsign && filterSelfMessageUpdates_; + if (iter->second->status != _(RX_ONLY_STATUS)) + { + iter->second->status = txStatus; + iter->second->txMode = txMode; + + auto lastTxTime = makeValidTime_(lastTxDate, iter->second->lastTxDate); + iter->second->lastTx = lastTxTime; + } - if (message.size() > 0 && isConnected_ && !filteringSelf) - { - iter->second->lastUpdateUserMessage = iter->second->lastUpdateDate; - } - else if (ourCallsign && filteringSelf) - { - // Filter only until we show up again, then return to normal behavior. - filterSelfMessageUpdates_ = false; - } + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); + iter->second->lastUpdate = lastUpdateTime; - std::map colResizeList; - addOrUpdateListIfNotFiltered_(iter->second, colResizeList); - resizeChangedColumns_(colResizeList); - } - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); + wxDataViewItem dvi(iter->second); + ItemChanged(dvi); + parent_->sortRequired_ = + parent_->m_listSpots->GetColumn(STATUS_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(TX_MODE_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(LAST_UPDATE_DATE_COL)->IsSortKey(); + } + + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -void FreeDVReporterDialog::onAboutToShowSelfFn_() +void FreeDVReporterDialog::FreeDVReporterDataModel::onReceiveUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string receivedCallsign, float snr, std::string rxMode) { - std::unique_lock lk(fnQueueMtx_); - - fnQueue_.push_back([&]() { - filterSelfMessageUpdates_ = true; - }); - - CallAfter(std::bind(&FreeDVReporterDialog::execQueuedAction_, this)); + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + + { + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid, lastUpdate, receivedCallsign, snr, rxMode]() { + std::unique_lock lk(const_cast(dataMtx_)); + + auto iter = allReporterData_.find(sid); + if (iter != allReporterData_.end()) + { + iter->second->lastRxCallsign = receivedCallsign; + iter->second->lastRxMode = rxMode; + + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); + iter->second->lastUpdate = lastUpdateTime; + + wxString snrString = wxString::Format(_("%.01f"), snr); + if (receivedCallsign == "" && rxMode == "") + { + // Frequency change--blank out SNR too. + iter->second->lastRxCallsign = UNKNOWN_STR; + iter->second->lastRxMode = UNKNOWN_STR; + iter->second->snr = UNKNOWN_STR; + iter->second->lastRxDate = wxDateTime(); + } + else + { + iter->second->snr = snrString; + iter->second->lastRxDate = iter->second->lastUpdateDate; + } + + wxDataViewItem dvi(iter->second); + ItemChanged(dvi); + parent_->sortRequired_ = + parent_->m_listSpots->GetColumn(LAST_RX_CALLSIGN_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(LAST_RX_MODE_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(SNR_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(LAST_UPDATE_DATE_COL)->IsSortKey(); + } + + prom->set_value(); + }); + } + + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -wxString FreeDVReporterDialog::makeValidTime_(std::string timeStr, wxDateTime& timeObj) -{ - wxRegEx millisecondsRemoval(_("\\.[^+-]+")); - wxString tmp = timeStr; - millisecondsRemoval.Replace(&tmp, _("")); - - wxRegEx timezoneRgx(_("([+-])([0-9]+):([0-9]+)$")); - wxDateTime::TimeZone timeZone(0); // assume UTC by default - if (timezoneRgx.Matches(tmp)) - { - auto tzOffset = timezoneRgx.GetMatch(tmp, 1); - auto hours = timezoneRgx.GetMatch(tmp, 2); - auto minutes = timezoneRgx.GetMatch(tmp, 3); - - int tzMinutes = wxAtoi(hours) * 60; - tzMinutes += wxAtoi(minutes); - - if (tzOffset == "-") - { - tzMinutes = -tzMinutes; - } - - timezoneRgx.Replace(&tmp, _("")); - - timeZone = wxDateTime::TimeZone(tzMinutes); - } - - wxDateTime tmpDate; - if (tmpDate.ParseISOCombined(tmp)) - { - tmpDate.MakeFromTimezone(timeZone); - timeObj = tmpDate; - if (wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting) - { - timeZone = wxDateTime::TimeZone(wxDateTime::TZ::UTC); - } - else - { - timeZone = wxDateTime::TimeZone(wxDateTime::TZ::Local); - } - - wxString formatStr = "%x %X"; - -#if __APPLE__ - // Workaround for weird macOS bug preventing .Format from working properly when double-clicking - // on the .app in Finder. Running the app from Terminal seems to work fine with .Format for - // some reason. O_o - struct tm tmpTm; - auto tmpDateTm = tmpDate.GetTm(timeZone); - - tmpTm.tm_sec = tmpDateTm.sec; - tmpTm.tm_min = tmpDateTm.min; - tmpTm.tm_hour = tmpDateTm.hour; - tmpTm.tm_mday = tmpDateTm.mday; - tmpTm.tm_mon = tmpDateTm.mon; - tmpTm.tm_year = tmpDateTm.year - 1900; - tmpTm.tm_wday = tmpDateTm.GetWeekDay(); - tmpTm.tm_yday = tmpDateTm.yday; - tmpTm.tm_isdst = -1; - - char buf[4096]; - strftime(buf, sizeof(buf), (const char*)formatStr.ToUTF8(), &tmpTm); - return buf; -#else - return tmpDate.Format(formatStr, timeZone); -#endif // __APPLE__ - } - else - { - timeObj = wxDateTime(); - return _(UNKNOWN_STR); - } -} - -void FreeDVReporterDialog::resizeChangedColumns_(std::map& colResizeList) +void FreeDVReporterDialog::FreeDVReporterDataModel::onMessageUpdateFn_(std::string sid, std::string lastUpdate, std::string message) { - for (auto& kvp : colResizeList) + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); + { -#if defined(WIN32) - // The first column on Windows is hidden, so don't resize. - if (kvp.first == 0) - { - continue; - } -#endif // defined(WIN32) - - m_listSpots->SetColumnWidth(kvp.first, columnLengths_[kvp.first]); - } - - // Call Update() to force immediate redraw of the list. This is needed - // to work around a wxWidgets issue where the column headers don't resize, - // but the widths of the column data do. - m_listSpots->Update(); -} - -void FreeDVReporterDialog::addOrUpdateListIfNotFiltered_(ReporterData* data, std::map& colResizeList) -{ - bool filtered = isFiltered_(data->frequency); - int itemIndex = -1; - - for (auto index = 0; index < m_listSpots->GetItemCount(); index++) - { - std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); - if (data->sid == *sidPtr) - { - itemIndex = index; - break; - } - } - - bool needResort = false; - - if (itemIndex >= 0 && filtered) - { - // Remove as it has been filtered out. - delete (std::string*)m_listSpots->GetItemData(itemIndex); - m_listSpots->DeleteItem(itemIndex); + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom, sid, lastUpdate, message]() { + std::unique_lock lk(const_cast(dataMtx_)); - // Force resizing of all columns. This should reduce space if needed. - resizeAllColumns_(); - - return; - } - else if (itemIndex == -1 && !filtered) - { - itemIndex = m_listSpots->InsertItem(m_listSpots->GetItemCount(), _("")); - m_listSpots->SetItemPtrData(itemIndex, (wxUIntPtr)new std::string(data->sid)); - } - else if (filtered) - { - // Don't add for now as it's not supposed to display. - return; + auto iter = allReporterData_.find(sid); + if (iter != allReporterData_.end()) + { + if (message.size() == 0) + { + iter->second->userMessage = UNKNOWN_STR; + } + else + { + iter->second->userMessage = wxString::FromUTF8(message); + } + + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); + iter->second->lastUpdate = lastUpdateTime; + + // Only highlight on non-empty messages. + bool ourCallsign = iter->second->callsign == wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign; + bool filteringSelf = ourCallsign && filterSelfMessageUpdates_; + + if (message.size() > 0 && !filteringSelf) + { + iter->second->lastUpdateUserMessage = iter->second->lastUpdateDate; + } + else if (ourCallsign && filteringSelf) + { + // Filter only until we show up again, then return to normal behavior. + filterSelfMessageUpdates_ = false; + } + + wxDataViewItem dvi(iter->second); + ItemChanged(dvi); + parent_->sortRequired_ = + parent_->m_listSpots->GetColumn(USER_MESSAGE_COL)->IsSortKey() || + parent_->m_listSpots->GetColumn(LAST_UPDATE_DATE_COL)->IsSortKey(); + } + prom->set_value(); + }); } - int col = 0; -#if defined(WIN32) - // Column 0 is "hidden" to avoid column autosize issue. Callsign should be in column 1 instead. - // Also, clear the item image for good measure as wxWidgets for Windows will set one for some - // reason. - m_listSpots->SetItemColumnImage(itemIndex, 0, -1); - col = 1; -#endif // defined(WIN32) - - bool changed = setColumnForRow_(itemIndex, col++, " "+data->callsign, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->gridSquare, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, data->distance, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, data->heading, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->version, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, data->freqString, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->txMode, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->status, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - int textWidth = 0; - int currentColWidth = m_listSpots->GetColumnWidth(col); - - wxString userMessageTruncated = _(" ") + data->userMessage; // note: extra space at beginning is to provide extra space from previous col - textWidth = getSizeForTableCellString_(userMessageTruncated); - int tmpLength = data->userMessage.Length() - 1; - while (textWidth > currentColWidth && tmpLength > 0) - { - userMessageTruncated = data->userMessage.SubString(0, tmpLength--) + _("..."); - textWidth = getSizeForTableCellString_(userMessageTruncated); - } - - if (tmpLength > 0 && tmpLength < (data->userMessage.Length() - 1)) - { - userMessageTruncated = data->userMessage.SubString(0, tmpLength) + _("..."); - } - - changed = setColumnForRow_(itemIndex, col++, userMessageTruncated, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->lastTx, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->lastRxCallsign, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->lastRxMode, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, data->snr, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - changed = setColumnForRow_(itemIndex, col++, " "+data->lastUpdate, colResizeList); - needResort |= changed && currentSortColumn_ == (col - 1); - - // Messaging updates take highest priority. - auto curDate = wxDateTime::Now().ToUTC(); - wxColour backgroundColor; - wxColour foregroundColor; - if (data->lastUpdateUserMessage.IsValid() && data->lastUpdateUserMessage.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, MSG_COLORING_TIMEOUT_SEC))) - { - backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterMsgRowBackgroundColor); - foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterMsgRowForegroundColor); - } - else if (data->transmitting) - { - backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowBackgroundColor); - foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowForegroundColor); - } - else if (data->lastRxDate.IsValid() && - ((data->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_SHORT_TIMEOUT_SEC)) && data->lastRxCallsign == "") || - (data->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_LONG_TIMEOUT_SEC)) && data->lastRxCallsign != ""))) - { - backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowBackgroundColor); - foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowForegroundColor); - } - else - { - backgroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX); - foregroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT); - } - - m_listSpots->SetItemBackgroundColour(itemIndex, backgroundColor); - m_listSpots->SetItemTextColour(itemIndex, foregroundColor); - - if (needResort) - { - m_listSpots->SortItems(&FreeDVReporterDialog::ListCompareFn_, (wxIntPtr)this); - } + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait(); } -int FreeDVReporterDialog::getSizeForTableCellString_(wxString str) +void FreeDVReporterDialog::FreeDVReporterDataModel::onAboutToShowSelfFn_() { - int textWidth = 0; - int textHeight = 0; // note: unused + std::shared_ptr> prom = std::make_shared>(); + auto fut = prom->get_future(); - m_listSpots->GetTextExtent(str, &textWidth, &textHeight); - - // Add buffer for sort indicator and to ensure wxWidgets doesn't truncate anything almost exactly - // fitting the new column size. - textWidth += m_sortIcons->GetIcon(upIconIndex_).GetSize().GetWidth(); - - return textWidth; -} - -bool FreeDVReporterDialog::setColumnForRow_(int row, int col, wxString val, std::map& colResizeList) -{ - int userMsgCol = USER_MESSAGE_COL; -#if defined(WIN32) - userMsgCol++; -#endif // defined(WIN32) - - bool result = false; - auto oldText = m_listSpots->GetItemText(row, col); - - if (oldText != val) { - result = true; - m_listSpots->SetItem(row, col, val); - - int textWidth = std::max(getSizeForTableCellString_(val), DefaultColumnWidths_[col]); - - // Resize column if not big enough. - if (textWidth > columnLengths_[col] && col != userMsgCol) - { - columnLengths_[col] = textWidth; - colResizeList[col]++; - } + std::unique_lock lk(fnQueueMtx_); + fnQueue_.push_back([&, prom]() { + filterSelfMessageUpdates_ = true; + prom->set_value(); + }); } - return result; -} - -FreeDVReporterDialog::FilterFrequency FreeDVReporterDialog::getFilterForFrequency_(uint64_t freq) -{ - auto bandForFreq = FilterFrequency::BAND_OTHER; - - if (freq >= 1800000 && freq <= 2000000) - { - bandForFreq = FilterFrequency::BAND_160M; - } - else if (freq >= 3500000 && freq <= 4000000) - { - bandForFreq = FilterFrequency::BAND_80M; - } - else if (freq >= 5250000 && freq <= 5450000) - { - bandForFreq = FilterFrequency::BAND_60M; - } - else if (freq >= 7000000 && freq <= 7300000) - { - bandForFreq = FilterFrequency::BAND_40M; - } - else if (freq >= 10100000 && freq <= 10150000) - { - bandForFreq = FilterFrequency::BAND_30M; - } - else if (freq >= 14000000 && freq <= 14350000) - { - bandForFreq = FilterFrequency::BAND_20M; - } - else if (freq >= 18068000 && freq <= 18168000) - { - bandForFreq = FilterFrequency::BAND_17M; - } - else if (freq >= 21000000 && freq <= 21450000) - { - bandForFreq = FilterFrequency::BAND_15M; - } - else if (freq >= 24890000 && freq <= 24990000) - { - bandForFreq = FilterFrequency::BAND_12M; - } - else if (freq >= 28000000 && freq <= 29700000) - { - bandForFreq = FilterFrequency::BAND_10M; - } - else if (freq >= 50000000) - { - bandForFreq = FilterFrequency::BAND_VHF_UHF; - } - - return bandForFreq; -} - -bool FreeDVReporterDialog::isFiltered_(uint64_t freq) -{ - auto bandForFreq = getFilterForFrequency_(freq); - - if (currentBandFilter_ == FilterFrequency::BAND_ALL) - { - return false; - } - else - { - return - (bandForFreq != currentBandFilter_) || - (wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksFrequency && - wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilterTracksExactFreq && - freq != filteredFrequency_); - } -} - -wxString FreeDVReporterDialog::GetCardinalDirection_(int degrees) -{ - int cardinalDirectionNumber( static_cast( ( ( degrees / 360.0 ) * 16 ) + 0.5 ) % 16 ); - const char* const cardinalDirectionTexts[] = { "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" }; - return cardinalDirectionTexts[cardinalDirectionNumber]; -} + parent_->CallAfter(std::bind(&FreeDVReporterDialog::FreeDVReporterDataModel::execQueuedAction_, this)); + fut.wait();} diff --git a/src/gui/dialogs/freedv_reporter.h b/src/gui/dialogs/freedv_reporter.h index 7ff994d4..31b05877 100644 --- a/src/gui/dialogs/freedv_reporter.h +++ b/src/gui/dialogs/freedv_reporter.h @@ -25,13 +25,15 @@ #include #include #include +#include -#include #include +#include #include "main.h" #include "defines.h" #include "reporting/FreeDVReporter.h" +#include "../controls/ReportMessageRenderer.h" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= // Class FreeDVReporterDialog @@ -70,7 +72,9 @@ class FreeDVReporterDialog : public wxFrame void setBandFilter(FilterFrequency freq); bool isTextMessageFieldInFocus(); - + + void Unselect(wxDataViewItem& dvi) { m_listSpots->Unselect(dvi); } + protected: // Handlers for events. @@ -94,24 +98,24 @@ class FreeDVReporterDialog : public wxFrame void OnStatusTextChange(wxCommandEvent& event); void OnSystemColorChanged(wxSysColourChangedEvent& event); - void OnItemSelected(wxListEvent& event); - void OnItemDeselected(wxListEvent& event); - void OnSortColumn(wxListEvent& event); + void OnItemSelectionChanged(wxDataViewEvent& event); + void OnColumnClick(wxDataViewEvent& event); + void OnItemDoubleClick(wxDataViewEvent& event); + void OnItemRightClick(wxDataViewEvent& event); + void OnTimer(wxTimerEvent& event); + void DeselectItem(); + void DeselectItem(wxMouseEvent& event); void AdjustToolTip(wxMouseEvent& event); void OnFilterTrackingEnable(wxCommandEvent& event); - void OnRightClickSpotsList(wxContextMenuEvent& event); void OnCopyUserMessage(wxCommandEvent& event); void SkipMouseEvent(wxMouseEvent& event); void AdjustMsgColWidth(wxListEvent& event); - - void OnDoubleClick(wxMouseEvent& event); - + void OnRightClickSpotsList(wxContextMenuEvent& event); + // Main list box that shows spots - wxListView* m_listSpots; - wxImageList* m_sortIcons; - int upIconIndex_; - int downIconIndex_; + wxDataViewCtrl* m_listSpots; + wxObjectDataPtr spotsDataModel_; wxMenu* spotsPopupMenu_; wxString tempUserMessage_; // to store the currently hovering message prior to going on the clipboard @@ -138,87 +142,154 @@ class FreeDVReporterDialog : public wxFrame // Timer to unhighlight RX rows after 10s (like with web-based Reporter) wxTimer* m_highlightClearTimer; - - std::vector > fnQueue_; - std::mutex fnQueueMtx_; wxTipWindow* tipWindow_; private: - struct ReporterData + class FreeDVReporterDataModel : public wxDataViewModel { - std::string sid; - wxString callsign; - wxString gridSquare; - double distanceVal; - wxString distance; - double headingVal; - wxString heading; - wxString version; - uint64_t frequency; - wxString freqString; - wxString status; - wxString txMode; - bool transmitting; - wxString lastTx; - wxDateTime lastTxDate; - wxDateTime lastRxDate; - wxString lastRxCallsign; - wxString lastRxMode; - wxString snr; - wxString lastUpdate; - wxDateTime lastUpdateDate; - wxString userMessage; - wxDateTime lastUpdateUserMessage; - }; - - std::shared_ptr reporter_; - std::map columnLengths_; - std::map allReporterData_; - FilterFrequency currentBandFilter_; - int currentSortColumn_; - bool sortAscending_; - bool isConnected_; - bool filterSelfMessageUpdates_; - uint64_t filteredFrequency_; - - void clearAllEntries_(bool clearForAllBands); - void onReporterConnect_(); - void onReporterDisconnect_(); - void onUserConnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly); - void onUserDisconnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly); - void onFrequencyChangeFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, uint64_t frequencyHz); - void onTransmitUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string txMode, bool transmitting, std::string lastTxDate); - void onReceiveUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string receivedCallsign, float snr, std::string rxMode); - void onMessageUpdateFn_(std::string sid, std::string lastUpdate, std::string message); - void onConnectionSuccessfulFn_(); - void onAboutToShowSelfFn_(); + public: + FreeDVReporterDataModel(FreeDVReporterDialog* parent); + virtual ~FreeDVReporterDataModel(); - wxString makeValidTime_(std::string timeStr, wxDateTime& timeObj); - - void addOrUpdateListIfNotFiltered_(ReporterData* data, std::map& colResizeList); - FilterFrequency getFilterForFrequency_(uint64_t freq); - bool isFiltered_(uint64_t freq); - - bool setColumnForRow_(int row, int col, wxString val, std::map& colResizeList); - void resizeChangedColumns_(std::map& colResizeList); + void setReporter(std::shared_ptr reporter); + void setBandFilter(FilterFrequency freq); + void refreshAllRows(); + void requestQSY(wxDataViewItem selectedItem, uint64_t frequency, wxString customText); + void updateHighlights(); + void updateMessage(wxString statusMsg) + { + if (reporter_) + { + reporter_->updateMessage(statusMsg.utf8_string()); + } + } - void sortColumn_(int col); - void sortColumn_(int col, bool direction); - - double calculateDistance_(wxString gridSquare1, wxString gridSquare2); - double calculateBearingInDegrees_(wxString gridSquare1, wxString gridSquare2); - void calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon); - - void execQueuedAction_(); + uint64_t getFrequency(wxDataViewItem item) + { + if (item.IsOk()) + { + auto data = (ReporterData*)item.GetID(); + return data->frequency; + } - void resizeAllColumns_(); - int getSizeForTableCellString_(wxString string); + return 0; + } - static wxCALLBACK int ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData); - static double DegreesToRadians_(double degrees); - static double RadiansToDegrees_(double radians); - static wxString GetCardinalDirection_(int degrees); + FilterFrequency getCurrentBandFilter() const { return currentBandFilter_; } + wxString getCallsign(wxDataViewItem& item) + { + if (item.IsOk()) + { + auto data = (ReporterData*)item.GetID(); + return data->callsign; + } + return ""; + } + + wxString getUserMessage(wxDataViewItem& item) + { + if (item.IsOk()) + { + auto data = (ReporterData*)item.GetID(); + return data->userMessage; + } + return ""; + } + + bool isValidForReporting() + { + return reporter_ && reporter_->isValidForReporting(); + } + + // Required overrides to implement functionality + virtual bool HasDefaultCompare() const override; + virtual int Compare (const wxDataViewItem &item1, const wxDataViewItem &item2, unsigned int column, bool ascending) const override; + virtual bool GetAttr (const wxDataViewItem &item, unsigned int col, wxDataViewItemAttr &attr) const override; + virtual unsigned int GetChildren (const wxDataViewItem &item, wxDataViewItemArray &children) const override; + virtual wxDataViewItem GetParent (const wxDataViewItem &item) const override; + virtual void GetValue (wxVariant &variant, const wxDataViewItem &item, unsigned int col) const override; + virtual bool IsContainer (const wxDataViewItem &item) const override; + virtual bool SetValue (const wxVariant &variant, const wxDataViewItem &item, unsigned int col) override; + + private: + struct ReporterData + { + std::string sid; + wxString callsign; + wxString gridSquare; + double distanceVal; + wxString distance; + double headingVal; + wxString heading; + wxString version; + uint64_t frequency; + wxString freqString; + wxString status; + wxString txMode; + bool transmitting; + wxString lastTx; + wxDateTime lastTxDate; + wxDateTime lastRxDate; + wxString lastRxCallsign; + wxString lastRxMode; + wxString snr; + wxString lastUpdate; + wxDateTime lastUpdateDate; + wxString userMessage; + wxDateTime lastUpdateUserMessage; + wxDateTime connectTime; + + // Controls whether this row has been filtered + bool isVisible; + + // Controls the current highlight color + wxColour foregroundColor; + wxColour backgroundColor; + }; + + std::shared_ptr reporter_; + std::map allReporterData_; + std::vector > fnQueue_; + std::mutex fnQueueMtx_; + std::recursive_mutex dataMtx_; + bool isConnected_; + FreeDVReporterDialog* parent_; + + FilterFrequency currentBandFilter_; + bool filterSelfMessageUpdates_; + uint64_t filteredFrequency_; + + bool isFiltered_(uint64_t freq); + + void clearAllEntries_(); + + void onReporterConnect_(); + void onReporterDisconnect_(); + void onUserConnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly); + void onUserDisconnectFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string version, bool rxOnly); + void onFrequencyChangeFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, uint64_t frequencyHz); + void onTransmitUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string txMode, bool transmitting, std::string lastTxDate); + void onReceiveUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string receivedCallsign, float snr, std::string rxMode); + void onMessageUpdateFn_(std::string sid, std::string lastUpdate, std::string message); + + void onConnectionSuccessfulFn_(); + void onAboutToShowSelfFn_(); + + void execQueuedAction_(); + + wxString makeValidTime_(std::string timeStr, wxDateTime& timeObj); + double calculateDistance_(wxString gridSquare1, wxString gridSquare2); + double calculateBearingInDegrees_(wxString gridSquare1, wxString gridSquare2); + void calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon); + + static double DegreesToRadians_(double degrees); + static double RadiansToDegrees_(double radians); + static wxString GetCardinalDirection_(int degrees); + }; + + FilterFrequency getFilterForFrequency_(uint64_t freq); + bool sortRequired_; }; #endif // __FREEDV_REPORTER_DIALOG__ diff --git a/src/main.cpp b/src/main.cpp index 6919152b..1d164388 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -484,19 +484,19 @@ bool MainApp::OnCmdLineParsed(wxCmdLineParser& parser) { log_info("Will transmit for %d seconds", utTxTimeSeconds); } - else - { + else + { utTxTimeSeconds = 60; - } + } if (parser.Found("txattempts", (long*)&utTxAttempts)) { log_info("Will transmit %d time(s)", utTxAttempts); } - else - { + else + { utTxAttempts = 1; - } + } } if (parser.Found("rxfeaturefile", &utRxFeatureFile)) @@ -1002,7 +1002,7 @@ setDefaultMode: } // Initialize FreeDV Reporter as required - initializeFreeDVReporter_(); + CallAfter([&]() { initializeFreeDVReporter_(); }); // If the FreeDV Reporter window was open on last execution, reopen it now. CallAfter([&]() { @@ -1106,17 +1106,17 @@ MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent, wxID_ANY, _("FreeDV ") // Add Demod Input window m_panelDemodIn = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelDemodIn, _("Frm Radio"), true, wxNullBitmap); - g_plotDemodInFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); + g_plotDemodInFifo = codec2_fifo_create(10*WAVEFORM_PLOT_FS); // Add Speech Input window m_panelSpeechIn = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelSpeechIn, _("Frm Mic"), true, wxNullBitmap); - g_plotSpeechInFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); + g_plotSpeechInFifo = codec2_fifo_create(10*WAVEFORM_PLOT_FS); // Add Speech Output window m_panelSpeechOut = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelSpeechOut, _("To Spkr/Hdphns"), true, wxNullBitmap); - g_plotSpeechOutFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); + g_plotSpeechOutFifo = codec2_fifo_create(10*WAVEFORM_PLOT_FS); // Add Timing Offset window m_panelTimeOffset = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, 5.0, DT, -0.5, 0.5, 1, 0.1, "%2.1f", 0); @@ -1703,7 +1703,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) float snr_limited; // some APIs pass us invalid values, so lets trap it rather than bombing float snrEstimate = freedvInterface.getSNREstimate(); - if (!(isnan(snrEstimate) || isinf(snrEstimate))) { + if (!(isnan(snrEstimate) || isinf(snrEstimate)) && freedvInterface.getSync()) { g_snr = m_snrBeta*g_snr + (1.0 - m_snrBeta)*snrEstimate; } snr_limited = g_snr; @@ -1814,7 +1814,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) if (oldColor != newColor) { m_textSync->SetForegroundColour(newColor); - m_textSync->SetLabel("Modem"); + m_textSync->SetLabel("Modem"); m_textSync->Refresh(); } } @@ -3125,9 +3125,16 @@ void MainFrame::startRxStream() // (depending on platform/audio library). Sample rate conversion, // stats for spectral plots, and transmit processng are all performed // in the tx/rxProcessing loop. - + // + // Note that soundCard1InFifoSizeSamples is significantly larger than + // the other FIFO sizes. This is to better handle PulseAudio/pipewire + // behavior on some devices, where the system sends multiple *seconds* + // of audio samples at once followed by long periods with no samples at + // all. Without a very large FIFO size (or a way to dynamically change + // FIFO sizes, which isn't recommended for real-time operation), we will + // definitely lose audio. int m_fifoSize_ms = wxGetApp().appConfiguration.fifoSizeMs; - int soundCard1InFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate / 1000; + int soundCard1InFifoSizeSamples = 10 * wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate; int soundCard1OutFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate / 1000; if (txInSoundDevice && txOutSoundDevice) @@ -3266,26 +3273,22 @@ void MainFrame::startRxStream() rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); - short* outdata = new short[size]; - assert(outdata != nullptr); - - int result = codec2_fifo_read(cbData->outfifo2, outdata, size); - if (result == 0) - { - for (size_t i = 0; i < size; i++) - { - for (int j = 0; j < dev.getNumChannels(); j++) - { - *audioData++ = outdata[i]; - } - } - } - else + short outdata = 0; + + if ((size_t)codec2_fifo_used(cbData->outfifo2) < size) { g_outfifo2_empty++; + return; } - delete[] outdata; + for (; size > 0; size--) + { + codec2_fifo_read(cbData->outfifo2, &outdata, 1); + for (int j = 0; j < dev.getNumChannels(); j++) + { + *audioData++ = outdata; + } + } }, g_rxUserdata); rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) @@ -3329,47 +3332,34 @@ void MainFrame::startRxStream() txOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); - short* outdata = new short[size]; - assert(outdata != nullptr); - - unsigned int available = std::min(codec2_fifo_used(cbData->outfifo1), (int)size); - - int result = codec2_fifo_read(cbData->outfifo1, outdata, available); - if (result == 0) + short outdata = 0; + + if ((size_t)codec2_fifo_used(cbData->outfifo1) < size) { + g_outfifo1_empty++; + return; + } + + for (; size > 0; size--, audioData += dev.getNumChannels()) + { + codec2_fifo_read(cbData->outfifo1, &outdata, 1); + // write signal to all channels to start. This is so that // the compiler can optimize for the most common case. - for(size_t i = 0; i < available; i++, audioData += dev.getNumChannels()) + for (auto j = 0; j < dev.getNumChannels(); j++) { - for (auto j = 0; j < dev.getNumChannels(); j++) - { - audioData[j] = outdata[i]; - } + audioData[j] = outdata; } // If VOX tone is enabled, go back through and add the VOX tone // on the left channel. if (cbData->leftChannelVoxTone) { - for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) - { - cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate; - cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI)); - audioData[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase); - } - } - - if (size != available) - { - g_outfifo1_empty++; + cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate; + cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI)); + audioData[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase); } } - else - { - g_outfifo1_empty++; - } - - delete[] outdata; }, g_rxUserdata); txOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) @@ -3390,27 +3380,22 @@ void MainFrame::startRxStream() rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); + short outdata = 0; - short* outdata = new short[size]; - assert(outdata != nullptr); - - int result = codec2_fifo_read(cbData->outfifo1, outdata, size); - if (result == 0) - { - for (size_t i = 0; i < size; i++) - { - for (int j = 0; j < dev.getNumChannels(); j++) - { - *audioData++ = outdata[i]; - } - } - } - else + if ((size_t)codec2_fifo_used(cbData->outfifo1) < size) { g_outfifo1_empty++; + return; } - delete[] outdata; + for (; size > 0; size--) + { + codec2_fifo_read(cbData->outfifo1, &outdata, 1); + for (int j = 0; j < dev.getNumChannels(); j++) + { + *audioData++ = outdata; + } + } }, g_rxUserdata); rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) diff --git a/src/ongui.cpp b/src/ongui.cpp index ba9beaf8..894fdfeb 100644 --- a/src/ongui.cpp +++ b/src/ongui.cpp @@ -858,6 +858,8 @@ void MainFrame::togglePTT(void) { { log_info("Waiting for EOO to be queued"); endingTx = true; + + auto beginTime = std::chrono::high_resolution_clock::now(); while(true) { if (g_eoo_enqueued) @@ -868,6 +870,13 @@ void MainFrame::togglePTT(void) { wxThread::Sleep(1); wxGetApp().Yield(true); + + auto endTime = std::chrono::high_resolution_clock::now(); + if ((endTime - beginTime) >= std::chrono::seconds(2)) + { + log_warn("Timed out waiting for EOO to be enqueued"); + break; + } } } diff --git a/src/pipeline/TapStep.cpp b/src/pipeline/TapStep.cpp index 53756f24..f8270a85 100644 --- a/src/pipeline/TapStep.cpp +++ b/src/pipeline/TapStep.cpp @@ -23,6 +23,7 @@ #include "TapStep.h" #include +#include TapStep::TapStep(int sampleRate, IPipelineStep* tapStep, bool operateBackground) : tapStep_(tapStep) @@ -34,7 +35,15 @@ TapStep::TapStep(int sampleRate, IPipelineStep* tapStep, bool operateBackground) TapStep::~TapStep() { - // empty + // Make sure we clear everything remaining in queue before + // deallocating. This isn't done in the base class as tapStep_ + // could be deallocated by the time we call that class' destructor. + auto prom = std::make_shared>(); + auto fut = prom->get_future(); + enqueue_([&]() { + prom->set_value(); + }); + fut.wait(); } int TapStep::getInputSampleRate() const diff --git a/src/pipeline/TxRxThread.cpp b/src/pipeline/TxRxThread.cpp index 702d5246..467cb7b5 100644 --- a/src/pipeline/TxRxThread.cpp +++ b/src/pipeline/TxRxThread.cpp @@ -624,7 +624,7 @@ void TxRxThread::txProcessing_() int nout; - while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { + while(!helper_->mustStopWork() && (unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { // OK to generate a frame of modem output samples we need // an input frame of speech samples from the microphone. @@ -742,15 +742,16 @@ void TxRxThread::rxProcessing_() { clearFifos_(); } - - // while we have enough input samples available ... - while (codec2_fifo_read(cbData->infifo1, inputSamples_.get(), nsam) == 0 && processInputFifo) { + int nsam_one_speech_frame = freedvInterface.getRxNumSpeechSamples() * ((float)outputSampleRate_ / (float)freedvInterface.getRxSpeechSampleRate()); + auto outFifo = (g_nSoundCards == 1) ? cbData->outfifo1 : cbData->outfifo2; + + // while we have enough input samples available and enough space in the output FIFO ... + while (!helper_->mustStopWork() && codec2_fifo_free(outFifo) >= nsam_one_speech_frame && codec2_fifo_read(cbData->infifo1, inputSamples_.get(), nsam) == 0 && processInputFifo) { // send latest squelch level to FreeDV API, as it handles squelch internally freedvInterface.setSquelch(g_SquelchActive, g_SquelchLevel); auto outputSamples = pipeline_->execute(inputSamples_, nsam, &nout); - auto outFifo = (g_nSoundCards == 1) ? cbData->outfifo1 : cbData->outfifo2; if (nout > 0 && outputSamples.get() != nullptr) { diff --git a/src/util/IRealtimeHelper.h b/src/util/IRealtimeHelper.h index c7bd3b24..4975ad77 100644 --- a/src/util/IRealtimeHelper.h +++ b/src/util/IRealtimeHelper.h @@ -41,6 +41,10 @@ public: // Reverts real-time priority for current thread. virtual void clearHelperRealTime() = 0; + + // Returns true if real-time thread MUST sleep ASAP. Failure to do so + // may result in SIGKILL being sent to the process by the kernel. + virtual bool mustStopWork() = 0; }; -#endif \ No newline at end of file +#endif