diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6efc38a6..102f2aab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ jobs:
             msvc_version: 2022
             qt_version: 6.6.2
             qt_arch: win64_msvc2019_64
-            qt_modules: qtimageformats qtmultimedia qtpositioning
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
             qt_tools: ''
             conan_arch: x86_64
             conan_compiler: Visual Studio
@@ -46,7 +46,7 @@ jobs:
             compiler: gcc
             qt_version: 6.6.2
             qt_arch: gcc_64
-            qt_modules: qtimageformats qtmultimedia qtpositioning
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
             qt_tools: ''
             conan_arch: x86_64
             conan_compiler: gcc
diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md
index 4b5474f2..154d0f6c 100644
--- a/ACKNOWLEDGEMENTS.md
+++ b/ACKNOWLEDGEMENTS.md
@@ -34,7 +34,7 @@ Supercell Wx uses code from the following dependencies:
 | [MapLibre Native](https://maplibre.org/projects/maplibre-native/) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) |
 | [nunicode](https://bitbucket.org/alekseyt/nunicode/src/master/) | [MIT License](https://spdx.org/licenses/MIT.html) | Modified for MapLibre Native |
 | [OpenSSL](https://www.openssl.org/) | [OpenSSL License](https://spdx.org/licenses/OpenSSL.html) |
-| [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html |
+| [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html |
 | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) |
 | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [SQLite](https://www.sqlite.org/) | Public Domain |
diff --git a/scwx-qt/res/icons/font-awesome-6/copy-regular.svg b/scwx-qt/res/icons/font-awesome-6/copy-regular.svg
new file mode 100644
index 00000000..72d18b16
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/copy-regular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index cda6c7f2..156b622e 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -28,6 +28,7 @@ find_package(QT NAMES Qt6
                         OpenGL
                         OpenGLWidgets
                         Positioning
+                        SerialPort
                         Svg
                         Widgets REQUIRED)
 
@@ -39,6 +40,7 @@ find_package(Qt${QT_VERSION_MAJOR}
                         OpenGL
                         OpenGLWidgets
                         Positioning
+                        SerialPort
                         Svg
                         Widgets
              REQUIRED)
@@ -97,6 +99,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
                 source/scwx/qt/manager/resource_manager.hpp
                 source/scwx/qt/manager/settings_manager.hpp
                 source/scwx/qt/manager/text_event_manager.hpp
+                source/scwx/qt/manager/thread_manager.hpp
                 source/scwx/qt/manager/timeline_manager.hpp
                 source/scwx/qt/manager/update_manager.hpp)
 set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
@@ -111,6 +114,7 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
                 source/scwx/qt/manager/resource_manager.cpp
                 source/scwx/qt/manager/settings_manager.cpp
                 source/scwx/qt/manager/text_event_manager.cpp
+                source/scwx/qt/manager/thread_manager.cpp
                 source/scwx/qt/manager/timeline_manager.cpp
                 source/scwx/qt/manager/update_manager.cpp)
 set(HDR_MAP source/scwx/qt/map/alert_layer.hpp
@@ -235,6 +239,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/county_dialog.hpp
            source/scwx/qt/ui/download_dialog.hpp
            source/scwx/qt/ui/flow_layout.hpp
+           source/scwx/qt/ui/gps_info_dialog.hpp
            source/scwx/qt/ui/hotkey_edit.hpp
            source/scwx/qt/ui/imgui_debug_dialog.hpp
            source/scwx/qt/ui/imgui_debug_widget.hpp
@@ -248,6 +253,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/placefile_settings_widget.hpp
            source/scwx/qt/ui/progress_dialog.hpp
            source/scwx/qt/ui/radar_site_dialog.hpp
+           source/scwx/qt/ui/serial_port_dialog.hpp
            source/scwx/qt/ui/settings_dialog.hpp
            source/scwx/qt/ui/update_dialog.hpp)
 set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
@@ -258,6 +264,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
            source/scwx/qt/ui/county_dialog.cpp
            source/scwx/qt/ui/download_dialog.cpp
            source/scwx/qt/ui/flow_layout.cpp
+           source/scwx/qt/ui/gps_info_dialog.cpp
            source/scwx/qt/ui/hotkey_edit.cpp
            source/scwx/qt/ui/imgui_debug_dialog.cpp
            source/scwx/qt/ui/imgui_debug_widget.cpp
@@ -272,6 +279,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
            source/scwx/qt/ui/progress_dialog.cpp
            source/scwx/qt/ui/radar_site_dialog.cpp
            source/scwx/qt/ui/settings_dialog.cpp
+           source/scwx/qt/ui/serial_port_dialog.cpp
            source/scwx/qt/ui/update_dialog.cpp)
 set(UI_UI  source/scwx/qt/ui/about_dialog.ui
            source/scwx/qt/ui/alert_dialog.ui
@@ -279,6 +287,7 @@ set(UI_UI  source/scwx/qt/ui/about_dialog.ui
            source/scwx/qt/ui/animation_dock_widget.ui
            source/scwx/qt/ui/collapsible_group.ui
            source/scwx/qt/ui/county_dialog.ui
+           source/scwx/qt/ui/gps_info_dialog.ui
            source/scwx/qt/ui/imgui_debug_dialog.ui
            source/scwx/qt/ui/layer_dialog.ui
            source/scwx/qt/ui/open_url_dialog.ui
@@ -287,6 +296,7 @@ set(UI_UI  source/scwx/qt/ui/about_dialog.ui
            source/scwx/qt/ui/progress_dialog.ui
            source/scwx/qt/ui/radar_site_dialog.ui
            source/scwx/qt/ui/settings_dialog.ui
+           source/scwx/qt/ui/serial_port_dialog.ui
            source/scwx/qt/ui/update_dialog.ui)
 set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
                     source/scwx/qt/ui/settings/settings_page_widget.hpp
@@ -597,11 +607,13 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
                                      Qt${QT_VERSION_MAJOR}::OpenGLWidgets
                                      Qt${QT_VERSION_MAJOR}::Multimedia
                                      Qt${QT_VERSION_MAJOR}::Positioning
+                                     Qt${QT_VERSION_MAJOR}::SerialPort
                                      Qt${QT_VERSION_MAJOR}::Svg
                                      Boost::json
                                      Boost::timer
                                      QMapLibre::Core
                                      $<$:opengl32>
+                                     $<$:SetupAPI>
                                      Fontconfig::Fontconfig
                                      GeographicLib::GeographicLib
                                      GEOS::geos
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index 6817307a..c9e00337 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -32,6 +32,7 @@
         res/icons/font-awesome-6/angles-up-solid.svg
         res/icons/font-awesome-6/backward-step-solid.svg
         res/icons/font-awesome-6/book-solid.svg
+        res/icons/font-awesome-6/copy-regular.svg
         res/icons/font-awesome-6/discord.svg
         res/icons/font-awesome-6/earth-americas-solid.svg
         res/icons/font-awesome-6/font-solid.svg
diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp
index 2d8073da..b6dcf5f0 100644
--- a/scwx-qt/source/scwx/qt/main/main.cpp
+++ b/scwx-qt/source/scwx/qt/main/main.cpp
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -129,6 +130,9 @@ int main(int argc, char* argv[])
    // Deinitialize application
    scwx::qt::manager::RadarProductManager::Cleanup();
 
+   // Stop Qt Threads
+   scwx::qt::manager::ThreadManager::Instance().StopThreads();
+
    // Gracefully stop the io_context main loop
    work.reset();
    threadPool.join();
diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp
index a74533f2..a0a44fe4 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.cpp
+++ b/scwx-qt/source/scwx/qt/main/main_window.cpp
@@ -23,6 +23,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -85,6 +86,7 @@ public:
        alertDockWidget_ {nullptr},
        animationDockWidget_ {nullptr},
        aboutDialog_ {nullptr},
+       gpsInfoDialog_ {nullptr},
        imGuiDebugDialog_ {nullptr},
        layerDialog_ {nullptr},
        placefileDialog_ {nullptr},
@@ -190,6 +192,7 @@ public:
    ui::AlertDockWidget*     alertDockWidget_;
    ui::AnimationDockWidget* animationDockWidget_;
    ui::AboutDialog*         aboutDialog_;
+   ui::GpsInfoDialog*       gpsInfoDialog_;
    ui::ImGuiDebugDialog*    imGuiDebugDialog_;
    ui::LayerDialog*         layerDialog_;
    ui::PlacefileDialog*     placefileDialog_;
@@ -264,6 +267,9 @@ MainWindow::MainWindow(QWidget* parent) :
    p->alertDockWidget_->setVisible(false);
    addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_);
 
+   // GPS Info Dialog
+   p->gpsInfoDialog_ = new ui::GpsInfoDialog(this);
+
    // Configure Menu
    ui->menuView->insertAction(ui->actionRadarToolbox,
                               ui->radarToolboxDock->toggleViewAction());
@@ -535,6 +541,11 @@ void MainWindow::on_actionExit_triggered()
    close();
 }
 
+void MainWindow::on_actionGpsInfo_triggered()
+{
+   p->gpsInfoDialog_->show();
+}
+
 void MainWindow::on_actionColorTable_triggered(bool checked)
 {
    p->layerModel_->SetLayerDisplayed(types::LayerType::Information,
diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp
index d0adf225..33043308 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.hpp
+++ b/scwx-qt/source/scwx/qt/main/main_window.hpp
@@ -38,6 +38,7 @@ private slots:
    void on_actionOpenTextEvent_triggered();
    void on_actionSettings_triggered();
    void on_actionExit_triggered();
+   void on_actionGpsInfo_triggered();
    void on_actionColorTable_triggered(bool checked);
    void on_actionRadarRange_triggered(bool checked);
    void on_actionRadarSites_triggered(bool checked);
diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui
index aeb4517f..9fab1adf 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.ui
+++ b/scwx-qt/source/scwx/qt/main/main_window.ui
@@ -39,7 +39,7 @@
      0
      0
      1024
-     21
+     33
     
    
    
     
     
+    
     
     
    
@@ -135,13 +136,13 @@
      - 
       
        
-        QFrame::NoFrame
+        QFrame::Shape::NoFrame
        
        
-        Qt::ScrollBarAsNeeded
+        Qt::ScrollBarPolicy::ScrollBarAsNeeded
        
        
-        QAbstractScrollArea::AdjustToContents
+        QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents
        
        
         true
@@ -151,8 +152,8 @@
          
           0
           0
-          193
-          688
+          190
+          686
          
         
         
@@ -171,10 +172,10 @@
          - 
           
            
-            QFrame::StyledPanel
+            QFrame::Shape::StyledPanel
            
            
-            QFrame::Raised
+            QFrame::Shadow::Raised
            
            
             - 
@@ -209,10 +210,10 @@
             - 
              
               
-               QFrame::NoFrame
+               QFrame::Shape::NoFrame
               
               
-               QFrame::Raised
+               QFrame::Shadow::Raised
               
               
                
@@ -260,7 +261,7 @@
                    :/res/icons/font-awesome-6/star-solid.svg:/res/icons/font-awesome-6/star-solid.svg
                  
                  
-                  QToolButton::InstantPopup
+                  QToolButton::ToolButtonPopupMode::InstantPopup
                  
                 
                @@ -340,7 +341,7 @@
- 
           
            
-            Qt::Vertical
+            Qt::Orientation::Vertical
            
            
             
@@ -481,6 +482,11 @@
     Radar &Sites
    
   
+  
+   
+    &GPS Info
+   
+  
  
diff --git a/scwx-qt/source/scwx/qt/manager/position_manager.cpp b/scwx-qt/source/scwx/qt/manager/position_manager.cpp
index 23357444..fea9cc36 100644
--- a/scwx-qt/source/scwx/qt/manager/position_manager.cpp
+++ b/scwx-qt/source/scwx/qt/manager/position_manager.cpp
@@ -1,10 +1,16 @@
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
 #include 
 
 #include 
+#include 
 #include 
 
 namespace scwx
@@ -23,47 +29,82 @@ public:
    explicit Impl(PositionManager* self) :
        self_ {self}, trackingUuid_ {boost::uuids::random_generator()()}
    {
-      // TODO: macOS requires permission
-      geoPositionInfoSource_ =
-         QGeoPositionInfoSource::createDefaultSource(self);
+      auto& generalSettings = settings::GeneralSettings::Instance();
 
-      if (geoPositionInfoSource_ != nullptr)
-      {
-         logger_->debug("Using position source: {}",
-                        geoPositionInfoSource_->sourceName().toStdString());
+      gpsParent_->moveToThread(gpsThread_);
 
-         QObject::connect(geoPositionInfoSource_,
-                          &QGeoPositionInfoSource::positionUpdated,
-                          self_,
-                          [this](const QGeoPositionInfo& info)
-                          {
-                             auto coordinate = info.coordinate();
+      logger_->debug(
+         "Available sources: {}",
+         QGeoPositionInfoSource::availableSources().join(", ").toStdString());
 
-                             if (coordinate != position_.coordinate())
-                             {
-                                logger_->debug("Position updated: {}, {}",
-                                               coordinate.latitude(),
-                                               coordinate.longitude());
-                             }
+      CreatePositionSourceAsync();
 
-                             position_ = info;
+      positioningPluginCallbackUuid_ =
+         generalSettings.positioning_plugin().RegisterValueChangedCallback(
+            [this](const std::string&)
+            { createPositionSourcePending_ = true; });
+      nmeaBaudRateCallbackUuid_ =
+         generalSettings.nmea_baud_rate().RegisterValueChangedCallback(
+            [this](const std::int64_t&)
+            { createPositionSourcePending_ = true; });
+      nmeaSourceCallbackUuid_ =
+         generalSettings.nmea_source().RegisterValueChangedCallback(
+            [this](const std::string&)
+            { createPositionSourcePending_ = true; });
 
-                             Q_EMIT self_->PositionUpdated(info);
-                          });
-      }
+      connect(&SettingsManager::Instance(),
+              &SettingsManager::SettingsSaved,
+              self_,
+              [this]()
+              {
+                 if (createPositionSourcePending_)
+                 {
+                    CreatePositionSourceAsync();
+                 }
+              });
+   }
+   ~Impl()
+   {
+      auto& generalSettings = settings::GeneralSettings::Instance();
+
+      generalSettings.positioning_plugin().UnregisterValueChangedCallback(
+         positioningPluginCallbackUuid_);
+      generalSettings.nmea_baud_rate().UnregisterValueChangedCallback(
+         nmeaBaudRateCallbackUuid_);
+      generalSettings.nmea_source().UnregisterValueChangedCallback(
+         nmeaSourceCallbackUuid_);
+
+      gpsParent_->deleteLater();
    }
 
-   ~Impl() {}
+   void CreatePositionSource();
+   void CreatePositionSourceAsync();
+   void EnablePositionUpdates(boost::uuids::uuid uuid, bool enabled);
 
    PositionManager* self_;
+   QThread* gpsThread_ {ThreadManager::Instance().thread("position_manager")};
 
    boost::uuids::uuid trackingUuid_;
    bool               trackingEnabled_ {false};
 
    std::set uuids_ {};
 
+   std::mutex positionSourceMutex_ {};
+
+   QObject*                gpsParent_ {new QObject};
    QGeoPositionInfoSource* geoPositionInfoSource_ {};
    QGeoPositionInfo        position_ {};
+
+   types::PositioningPlugin lastPositioningPlugin_ {
+      types::PositioningPlugin::Unknown};
+   std::int64_t lastNmeaBaudRate_ {-1};
+   std::string  lastNmeaSource_ {"?"};
+
+   boost::uuids::uuid positioningPluginCallbackUuid_ {};
+   boost::uuids::uuid nmeaBaudRateCallbackUuid_ {};
+   boost::uuids::uuid nmeaSourceCallbackUuid_ {};
+
+   bool createPositionSourcePending_ {false};
 };
 
 PositionManager::PositionManager() : p(std::make_unique(this)) {}
@@ -79,30 +120,131 @@ bool PositionManager::IsLocationTracked()
    return p->trackingEnabled_;
 }
 
+void PositionManager::Impl::CreatePositionSourceAsync()
+{
+   QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(gpsThread_),
+                             [this]() { CreatePositionSource(); });
+}
+
+void PositionManager::Impl::CreatePositionSource()
+{
+   auto& generalSettings = settings::GeneralSettings::Instance();
+
+   createPositionSourcePending_ = false;
+
+   types::PositioningPlugin positioningPlugin = types::GetPositioningPlugin(
+      generalSettings.positioning_plugin().GetValue());
+   std::int64_t nmeaBaudRate = generalSettings.nmea_baud_rate().GetValue();
+   std::string  nmeaSource   = generalSettings.nmea_source().GetValue();
+
+   if (positioningPlugin == lastPositioningPlugin_ &&
+       nmeaBaudRate == lastNmeaBaudRate_ && nmeaSource == lastNmeaSource_)
+   {
+      return;
+   }
+
+   QGeoPositionInfoSource* positionSource = nullptr;
+
+   // TODO: macOS requires permission
+   if (positioningPlugin == types::PositioningPlugin::Default)
+   {
+      positionSource = QGeoPositionInfoSource::createDefaultSource(gpsParent_);
+   }
+   else if (positioningPlugin == types::PositioningPlugin::Nmea)
+   {
+      QVariantMap params {};
+      params["nmea.source"]   = QString::fromStdString(nmeaSource);
+      params["nmea.baudrate"] = static_cast(nmeaBaudRate);
+
+      positionSource =
+         QGeoPositionInfoSource::createSource("nmea", params, gpsParent_);
+   }
+
+   if (positionSource != nullptr)
+   {
+      logger_->debug("Using position source: {}",
+                     positionSource->sourceName().toStdString());
+
+      QObject::connect(positionSource,
+                       &QGeoPositionInfoSource::positionUpdated,
+                       self_,
+                       [this](const QGeoPositionInfo& info)
+                       {
+                          auto coordinate = info.coordinate();
+
+                          if (coordinate != position_.coordinate())
+                          {
+                             logger_->trace("Position updated: {}, {}",
+                                            coordinate.latitude(),
+                                            coordinate.longitude());
+                          }
+
+                          position_ = info;
+
+                          Q_EMIT self_->PositionUpdated(info);
+                       });
+   }
+   else
+   {
+      logger_->error("Unable to create position source for plugin: {}",
+                     types::GetPositioningPluginName(positioningPlugin));
+      return;
+   }
+
+   lastPositioningPlugin_ = positioningPlugin;
+   lastNmeaBaudRate_      = nmeaBaudRate;
+   lastNmeaSource_        = nmeaSource;
+
+   std::unique_lock lock {positionSourceMutex_};
+
+   if (geoPositionInfoSource_ != nullptr)
+   {
+      geoPositionInfoSource_->stopUpdates();
+      delete geoPositionInfoSource_;
+   }
+
+   geoPositionInfoSource_ = positionSource;
+
+   if (!uuids_.empty())
+   {
+      positionSource->startUpdates();
+   }
+}
+
 void PositionManager::EnablePositionUpdates(boost::uuids::uuid uuid,
                                             bool               enabled)
 {
-   if (p->geoPositionInfoSource_ == nullptr)
+   QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(p->gpsThread_),
+                             [=, this]()
+                             { p->EnablePositionUpdates(uuid, enabled); });
+}
+
+void PositionManager::Impl::EnablePositionUpdates(boost::uuids::uuid uuid,
+                                                  bool               enabled)
+{
+   std::unique_lock lock {positionSourceMutex_};
+
+   if (geoPositionInfoSource_ == nullptr)
    {
       return;
    }
 
    if (enabled)
    {
-      if (p->uuids_.empty())
+      if (uuids_.empty())
       {
-         p->geoPositionInfoSource_->startUpdates();
+         geoPositionInfoSource_->startUpdates();
       }
 
-      p->uuids_.insert(uuid);
+      uuids_.insert(uuid);
    }
    else
    {
-      p->uuids_.erase(uuid);
+      uuids_.erase(uuid);
 
-      if (p->uuids_.empty())
+      if (uuids_.empty())
       {
-         p->geoPositionInfoSource_->stopUpdates();
+         geoPositionInfoSource_->stopUpdates();
       }
    }
 }
diff --git a/scwx-qt/source/scwx/qt/manager/thread_manager.cpp b/scwx-qt/source/scwx/qt/manager/thread_manager.cpp
new file mode 100644
index 00000000..78924434
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/thread_manager.cpp
@@ -0,0 +1,97 @@
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+static const std::string logPrefix_ = "scwx::qt::manager::thread_manager";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class ThreadManager::Impl
+{
+public:
+   explicit Impl() {}
+   ~Impl() {}
+
+   std::mutex mutex_ {};
+
+   boost::unordered_flat_map threadMap_ {};
+};
+
+ThreadManager::ThreadManager() : p(std::make_unique()) {}
+ThreadManager::~ThreadManager() = default;
+
+QThread* ThreadManager::thread(const std::string& id, bool autoStart)
+{
+   std::unique_lock lock {p->mutex_};
+   QThread*         thread = nullptr;
+
+   auto it = p->threadMap_.find(id);
+   if (it != p->threadMap_.cend())
+   {
+      thread = it->second;
+   }
+
+   if (thread == nullptr)
+   {
+      logger_->debug("Creating thread: {}", id);
+
+      thread = new QThread(this);
+      p->threadMap_.insert_or_assign(id, thread);
+
+      if (autoStart)
+      {
+         thread->start();
+      }
+   }
+
+   return thread;
+}
+
+void ThreadManager::StopThreads()
+{
+   std::unique_lock lock {p->mutex_};
+
+   logger_->debug("Stopping threads");
+
+   std::for_each(std::execution::par_unseq,
+                 p->threadMap_.begin(),
+                 p->threadMap_.end(),
+                 [](auto& thread)
+                 {
+                    logger_->trace("Stopping thread: {}", thread.first);
+
+                    thread.second->quit();
+                    if (!thread.second->wait(5000))
+                    {
+                       logger_->warn("Terminating thread: {}", thread.first);
+
+                       thread.second->terminate();
+                       thread.second->wait();
+                    }
+
+                    delete thread.second;
+                 });
+
+   p->threadMap_.clear();
+}
+
+ThreadManager& ThreadManager::Instance()
+{
+   static ThreadManager threadManager_ {};
+   return threadManager_;
+}
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/manager/thread_manager.hpp b/scwx-qt/source/scwx/qt/manager/thread_manager.hpp
new file mode 100644
index 00000000..496d30ba
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/thread_manager.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include 
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+class ThreadManager : public QObject
+{
+   Q_OBJECT
+   Q_DISABLE_COPY_MOVE(ThreadManager)
+
+public:
+   explicit ThreadManager();
+   ~ThreadManager();
+
+   QThread* thread(const std::string& id, bool autoStart = true);
+
+   void StopThreads();
+
+   static ThreadManager& Instance();
+
+private:
+   class Impl;
+   std::unique_ptr p;
+};
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp
index 7f1dabcd..fe73a7d5 100644
--- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp
+++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -37,6 +38,8 @@ public:
          types::GetDefaultTimeZoneName(types::DefaultTimeZone::Radar);
       std::string defaultMapProviderValue =
          map::GetMapProviderName(map::MapProvider::MapTiler);
+      std::string defaultPositioningPlugin =
+         types::GetPositioningPluginName(types::PositioningPlugin::Default);
       std::string defaultThemeValue =
          types::GetUiStyleName(types::UiStyle::Default);
 
@@ -44,6 +47,7 @@ public:
       boost::to_lower(defaultDefaultAlertActionValue);
       boost::to_lower(defaultDefaultTimeZoneValue);
       boost::to_lower(defaultMapProviderValue);
+      boost::to_lower(defaultPositioningPlugin);
       boost::to_lower(defaultThemeValue);
 
       antiAliasingEnabled_.SetDefault(true);
@@ -61,6 +65,9 @@ public:
       mapProvider_.SetDefault(defaultMapProviderValue);
       mapboxApiKey_.SetDefault("?");
       maptilerApiKey_.SetDefault("?");
+      nmeaBaudRate_.SetDefault(9600);
+      nmeaSource_.SetDefault("");
+      positioningPlugin_.SetDefault(defaultPositioningPlugin);
       showMapAttribution_.SetDefault(true);
       showMapCenter_.SetDefault(false);
       showMapLogo_.SetDefault(true);
@@ -83,6 +90,8 @@ public:
       loopSpeed_.SetMaximum(99.99);
       loopTime_.SetMinimum(1);
       loopTime_.SetMaximum(1440);
+      nmeaBaudRate_.SetMinimum(1);
+      nmeaBaudRate_.SetMaximum(999999999);
 
       clockFormat_.SetValidator(
          SCWX_SETTINGS_ENUM_VALIDATOR(scwx::util::ClockFormat,
@@ -104,6 +113,10 @@ public:
                                  { return !value.empty(); });
       maptilerApiKey_.SetValidator([](const std::string& value)
                                    { return !value.empty(); });
+      positioningPlugin_.SetValidator(
+         SCWX_SETTINGS_ENUM_VALIDATOR(types::PositioningPlugin,
+                                      types::PositioningPluginIterator(),
+                                      types::GetPositioningPluginName));
       theme_.SetValidator(                            //
          SCWX_SETTINGS_ENUM_VALIDATOR(types::UiStyle, //
                                       types::UiStyleIterator(),
@@ -128,13 +141,16 @@ public:
    SettingsVariable                     loopSpeed_ {"loop_speed"};
    SettingsVariable               loopTime_ {"loop_time"};
    SettingsVariable                mapProvider_ {"map_provider"};
-   SettingsVariable mapboxApiKey_ {"mapbox_api_key"};
-   SettingsVariable maptilerApiKey_ {"maptiler_api_key"};
-   SettingsVariable        showMapAttribution_ {"show_map_attribution"};
-   SettingsVariable        showMapCenter_ {"show_map_center"};
-   SettingsVariable        showMapLogo_ {"show_map_logo"};
-   SettingsVariable theme_ {"theme"};
-   SettingsVariable        trackLocation_ {"track_location"};
+   SettingsVariable  mapboxApiKey_ {"mapbox_api_key"};
+   SettingsVariable  maptilerApiKey_ {"maptiler_api_key"};
+   SettingsVariable nmeaBaudRate_ {"nmea_baud_rate"};
+   SettingsVariable  nmeaSource_ {"nmea_source"};
+   SettingsVariable  positioningPlugin_ {"positioning_plugin"};
+   SettingsVariable         showMapAttribution_ {"show_map_attribution"};
+   SettingsVariable         showMapCenter_ {"show_map_center"};
+   SettingsVariable         showMapLogo_ {"show_map_logo"};
+   SettingsVariable  theme_ {"theme"};
+   SettingsVariable         trackLocation_ {"track_location"};
    SettingsVariable updateNotificationsEnabled_ {"update_notifications"};
    SettingsVariable warningsProvider_ {"warnings_provider"};
 };
@@ -157,6 +173,9 @@ GeneralSettings::GeneralSettings() :
                       &p->mapProvider_,
                       &p->mapboxApiKey_,
                       &p->maptilerApiKey_,
+                      &p->nmeaBaudRate_,
+                      &p->nmeaSource_,
+                      &p->positioningPlugin_,
                       &p->showMapAttribution_,
                       &p->showMapCenter_,
                       &p->showMapLogo_,
@@ -248,6 +267,21 @@ SettingsVariable& GeneralSettings::maptiler_api_key() const
    return p->maptilerApiKey_;
 }
 
+SettingsVariable& GeneralSettings::nmea_baud_rate() const
+{
+   return p->nmeaBaudRate_;
+}
+
+SettingsVariable& GeneralSettings::nmea_source() const
+{
+   return p->nmeaSource_;
+}
+
+SettingsVariable& GeneralSettings::positioning_plugin() const
+{
+   return p->positioningPlugin_;
+}
+
 SettingsVariable& GeneralSettings::show_map_attribution() const
 {
    return p->showMapAttribution_;
@@ -319,6 +353,9 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs)
            lhs.p->mapProvider_ == rhs.p->mapProvider_ &&
            lhs.p->mapboxApiKey_ == rhs.p->mapboxApiKey_ &&
            lhs.p->maptilerApiKey_ == rhs.p->maptilerApiKey_ &&
+           lhs.p->nmeaBaudRate_ == rhs.p->nmeaBaudRate_ &&
+           lhs.p->nmeaSource_ == rhs.p->nmeaSource_ &&
+           lhs.p->positioningPlugin_ == rhs.p->positioningPlugin_ &&
            lhs.p->showMapAttribution_ == rhs.p->showMapAttribution_ &&
            lhs.p->showMapCenter_ == rhs.p->showMapCenter_ &&
            lhs.p->showMapLogo_ == rhs.p->showMapLogo_ &&
diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp
index 65c3536f..4c162616 100644
--- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp
+++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp
@@ -40,6 +40,9 @@ public:
    SettingsVariable&                map_provider() const;
    SettingsVariable&                mapbox_api_key() const;
    SettingsVariable&                maptiler_api_key() const;
+   SettingsVariable&               nmea_baud_rate() const;
+   SettingsVariable&                nmea_source() const;
+   SettingsVariable&                positioning_plugin() const;
    SettingsVariable&                       show_map_attribution() const;
    SettingsVariable&                       show_map_center() const;
    SettingsVariable&                       show_map_logo() const;
diff --git a/scwx-qt/source/scwx/qt/types/location_types.cpp b/scwx-qt/source/scwx/qt/types/location_types.cpp
index 63ec414c..b1c759de 100644
--- a/scwx-qt/source/scwx/qt/types/location_types.cpp
+++ b/scwx-qt/source/scwx/qt/types/location_types.cpp
@@ -19,13 +19,24 @@ static const std::unordered_map
                         {LocationMethod::All, "All"},
                         {LocationMethod::Unknown, "?"}};
 
+static const std::unordered_map
+   positioningPluginName_ {{PositioningPlugin::Default, "Default"},
+                           {PositioningPlugin::Nmea, "NMEA"},
+                           {PositioningPlugin::Unknown, "?"}};
+
 SCWX_GET_ENUM(LocationMethod, GetLocationMethod, locationMethodName_)
+SCWX_GET_ENUM(PositioningPlugin, GetPositioningPlugin, positioningPluginName_)
 
 const std::string& GetLocationMethodName(LocationMethod locationMethod)
 {
    return locationMethodName_.at(locationMethod);
 }
 
+const std::string& GetPositioningPluginName(PositioningPlugin positioningPlugin)
+{
+   return positioningPluginName_.at(positioningPlugin);
+}
+
 } // namespace types
 } // namespace qt
 } // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/types/location_types.hpp b/scwx-qt/source/scwx/qt/types/location_types.hpp
index 3c9407de..65d32f1e 100644
--- a/scwx-qt/source/scwx/qt/types/location_types.hpp
+++ b/scwx-qt/source/scwx/qt/types/location_types.hpp
@@ -23,9 +23,24 @@ typedef scwx::util::
    Iterator
       LocationMethodIterator;
 
+enum class PositioningPlugin
+{
+   Default,
+   Nmea,
+   Unknown
+};
+typedef scwx::util::Iterator
+   PositioningPluginIterator;
+
 LocationMethod     GetLocationMethod(const std::string& name);
 const std::string& GetLocationMethodName(LocationMethod locationMethod);
 
+PositioningPlugin GetPositioningPlugin(const std::string& name);
+const std::string&
+GetPositioningPluginName(PositioningPlugin positioningPlugin);
+
 } // namespace types
 } // namespace qt
 } // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/gps_info_dialog.cpp b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.cpp
new file mode 100644
index 00000000..05863221
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.cpp
@@ -0,0 +1,201 @@
+#include "gps_info_dialog.hpp"
+#include "ui_gps_info_dialog.h"
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+static const QString kDisabledString_ = "---";
+
+class GpsInfoDialog::Impl
+{
+public:
+   explicit Impl(GpsInfoDialog* self) : self_ {self} {};
+   ~Impl() = default;
+
+   std::shared_ptr positionManager_ {
+      manager::PositionManager::Instance()};
+
+   void Update(const QGeoPositionInfo& info, bool updateTime = true);
+
+   GpsInfoDialog* self_;
+};
+
+GpsInfoDialog::GpsInfoDialog(QWidget* parent) :
+    QDialog(parent), p {std::make_unique(this)}, ui(new Ui::GpsInfoDialog)
+{
+   ui->setupUi(this);
+
+   p->Update({}, false);
+
+   connect(p->positionManager_.get(),
+           &manager::PositionManager::PositionUpdated,
+           this,
+           [this](const QGeoPositionInfo& info) { p->Update(info); });
+
+   connect(ui->copyCoordinateButton,
+           &QAbstractButton::clicked,
+           this,
+           [this]()
+           {
+              QClipboard* clipboard = QGuiApplication::clipboard();
+              clipboard->setText(ui->coordinateLabel->text());
+           });
+}
+
+GpsInfoDialog::~GpsInfoDialog()
+{
+   delete ui;
+}
+
+void GpsInfoDialog::Impl::Update(const QGeoPositionInfo& info, bool updateTime)
+{
+   auto coordinate = info.coordinate();
+
+   if (coordinate.isValid())
+   {
+      const QString latitude = QString::fromStdString(
+         common::GetLatitudeString(coordinate.latitude()));
+      const QString longitude = QString::fromStdString(
+         common::GetLongitudeString(coordinate.longitude()));
+
+      self_->ui->coordinateLabel->setText(
+         QString("%1, %2").arg(latitude).arg(longitude));
+   }
+   else
+   {
+      self_->ui->coordinateLabel->setText(kDisabledString_);
+   }
+
+   if (coordinate.type() == QGeoCoordinate::CoordinateType::Coordinate3D)
+   {
+      units::length::meters altitude {coordinate.altitude()};
+      self_->ui->altitudeLabel->setText(
+         QString::fromStdString(units::to_string(altitude)));
+   }
+   else
+   {
+      self_->ui->altitudeLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::Direction))
+   {
+      units::angle::degrees direction {
+         info.attribute(QGeoPositionInfo::Attribute::Direction)};
+      self_->ui->directionLabel->setText(
+         QString::fromStdString(units::to_string(direction)));
+   }
+   else
+   {
+      self_->ui->directionLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::GroundSpeed))
+   {
+      units::velocity::meters_per_second groundSpeed {
+         info.attribute(QGeoPositionInfo::Attribute::GroundSpeed)};
+      self_->ui->groundSpeedLabel->setText(
+         QString::fromStdString(units::to_string(groundSpeed)));
+   }
+   else
+   {
+      self_->ui->groundSpeedLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::VerticalSpeed))
+   {
+      units::velocity::meters_per_second verticalSpeed {
+         info.attribute(QGeoPositionInfo::Attribute::VerticalSpeed)};
+      self_->ui->verticalSpeedLabel->setText(
+         QString::fromStdString(units::to_string(verticalSpeed)));
+   }
+   else
+   {
+      self_->ui->verticalSpeedLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::MagneticVariation))
+   {
+      units::angle::degrees magneticVariation {
+         info.attribute(QGeoPositionInfo::Attribute::MagneticVariation)};
+      self_->ui->magneticVariationLabel->setText(
+         QString::fromStdString(units::to_string(magneticVariation)));
+   }
+   else
+   {
+      self_->ui->magneticVariationLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::HorizontalAccuracy))
+   {
+      units::length::meters horizontalAccuracy {
+         info.attribute(QGeoPositionInfo::Attribute::HorizontalAccuracy)};
+      if (!std::isnan(horizontalAccuracy.value()))
+      {
+         self_->ui->horizontalAccuracyLabel->setText(
+            QString::fromStdString(units::to_string(horizontalAccuracy)));
+      }
+      else
+      {
+         self_->ui->horizontalAccuracyLabel->setText(kDisabledString_);
+      }
+   }
+   else
+   {
+      self_->ui->horizontalAccuracyLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::VerticalAccuracy))
+   {
+      units::length::meters verticalAccuracy {
+         info.attribute(QGeoPositionInfo::Attribute::VerticalAccuracy)};
+      if (!std::isnan(verticalAccuracy.value()))
+      {
+         self_->ui->verticalAccuracyLabel->setText(
+            QString::fromStdString(units::to_string(verticalAccuracy)));
+      }
+      else
+      {
+         self_->ui->verticalAccuracyLabel->setText(kDisabledString_);
+      }
+   }
+   else
+   {
+      self_->ui->verticalAccuracyLabel->setText(kDisabledString_);
+   }
+
+   if (info.hasAttribute(QGeoPositionInfo::Attribute::DirectionAccuracy))
+   {
+      units::angle::degrees directionAccuracy {
+         info.attribute(QGeoPositionInfo::Attribute::DirectionAccuracy)};
+      self_->ui->directionAccuracyLabel->setText(
+         QString::fromStdString(units::to_string(directionAccuracy)));
+   }
+   else
+   {
+      self_->ui->directionAccuracyLabel->setText(kDisabledString_);
+   }
+
+   if (updateTime)
+   {
+      self_->ui->lastUpdateLabel->setText(
+         info.timestamp().toString(Qt::DateFormat::ISODate));
+   }
+}
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/gps_info_dialog.hpp b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.hpp
new file mode 100644
index 00000000..6330c5c4
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include 
+
+namespace Ui
+{
+class GpsInfoDialog;
+}
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+class GpsInfoDialog : public QDialog
+{
+   Q_OBJECT
+
+private:
+   Q_DISABLE_COPY(GpsInfoDialog)
+
+public:
+   explicit GpsInfoDialog(QWidget* parent = nullptr);
+   ~GpsInfoDialog();
+
+private:
+   class Impl;
+   std::unique_ptr p;
+   Ui::GpsInfoDialog*    ui;
+};
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/gps_info_dialog.ui b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.ui
new file mode 100644
index 00000000..61e50b1c
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/gps_info_dialog.ui
@@ -0,0 +1,266 @@
+
+
+ GpsInfoDialog
+ 
+  
+   
+    0
+    0
+    313
+    292
+   
+  
+  
+   GPS Info
+  
+  
+- 
+    
+     
+      
+       6
+      
+      - 
+       
+        
+         Direction Accuracy
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         Qt::Orientation::Vertical
+        
+        
+         
+          20
+          40
+         
+        
+       
+      +
- 
+       
+        
+         Magnetic Variation
+        
+       
+      +
- 
+       
+        
+         Direction
+        
+       
+      +
- 
+       
+        
+         Vertical Speed
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         Vertical Accuracy
+        
+       
+      +
- 
+       
+        
+         ...
+        
+        
+         
+          :/res/icons/font-awesome-6/copy-regular.svg:/res/icons/font-awesome-6/copy-regular.svg
+        
+       
+      +
- 
+       
+        
+         Horizonal Accuracy
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         Ground Speed
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         
+          0
+          0
+         
+        
+        
+         
+        
+       
+      +
- 
+       
+        
+         Coordinate
+        
+       
+      +
- 
+       
+        
+         
+        
+       
+      +
- 
+       
+        
+         Altitude
+        
+       
+      +
- 
+       
+        
+         Last Update
+        
+       
+      +
- 
+       
+        
+         Never
+        
+       
+      +     
+     verticalAccuracyLabel
+     label_11
+     groundSpeedLabel
+     magneticVariationLabel
+     label_2
+     coordinateLabel
+     altitudeLabel
+     directionAccuracyLabel
+     label_5
+     label_15
+     label_13
+     verticalSpeedLabel
+     label_7
+     label
+     horizontalAccuracyLabel
+     label_17
+     directionLabel
+     label_9
+     copyCoordinateButton
+     label_3
+     lastUpdateLabel
+    
+
+- 
+    
+     
+      Qt::Orientation::Horizontal
+     
+     
+      QDialogButtonBox::StandardButton::Close
+     
+    
+   +  
+ 
+ 
+  
+ 
+ 
+  
+   buttonBox
+   accepted()
+   GpsInfoDialog
+   accept()
+   
+    
+     248
+     254
+    
+    
+     157
+     274
+    
+   
+  
+  
+   buttonBox
+   rejected()
+   GpsInfoDialog
+   reject()
+   
+    
+     316
+     260
+    
+    
+     286
+     274
+    
+   
+  
+ 
+
diff --git a/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp
new file mode 100644
index 00000000..d489c1d6
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp
@@ -0,0 +1,613 @@
+#define __STDC_WANT_LIB_EXT1__ 1
+
+#include "serial_port_dialog.hpp"
+#include "ui_serial_port_dialog.h"
+
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#if defined(_WIN32)
+#   include 
+#   include 
+#   include 
+#   include 
+#   include 
+#   include 
+#   include 
+#endif
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+static const std::string logPrefix_ = "scwx::qt::ui::serial_port_dialog";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class SerialPortDialog::Impl
+{
+public:
+   struct PortProperties
+   {
+      std::string busReportedDeviceDescription_ {};
+   };
+
+   struct PortSettings
+   {
+      int         baudRate_ {-1}; // Positive
+      std::string parity_ {"n"};  // [e]ven, [o]dd, [m]ark, [s]pace, [n]one
+      int         dataBits_ {8};  // [4-8]
+      float       stopBits_ {1};  // 1, 1.5, 2
+      std::string
+         flowControl_ {}; // "" (none), "p" (hardware), "x" (Xon / Xoff)
+   };
+
+   typedef std::unordered_map PortInfoMap;
+   typedef std::unordered_map  PortPropertiesMap;
+   typedef std::unordered_map    PortSettingsMap;
+
+   explicit Impl(SerialPortDialog* self) :
+       self_ {self},
+       model_ {new QStandardItemModel(self)},
+       proxyModel_ {new QSortFilterProxyModel(self)}
+   {
+   }
+   ~Impl() = default;
+
+   void LogSerialPortInfo(const QSerialPortInfo& info);
+   void RefreshSerialDevices();
+   void UpdateModel();
+
+   static void ReadComPortProperties(PortPropertiesMap& portPropertiesMap);
+   static void ReadComPortSettings(PortSettingsMap& portSettingsMap);
+   static void StorePortSettings(const std::string& portName,
+                                 const std::string& settingsString,
+                                 PortSettingsMap&   portSettingsMap);
+
+#if defined(_WIN32)
+   static std::string GetDevicePropertyString(HDEVINFO&        deviceInfoSet,
+                                              SP_DEVINFO_DATA& deviceInfoData,
+                                              DEVPROPKEY       propertyKey);
+   static std::string GetRegistryValueDataString(HKEY hKey, LPCTSTR lpValue);
+#endif
+
+   SerialPortDialog*      self_;
+   QStandardItemModel*    model_;
+   QSortFilterProxyModel* proxyModel_;
+
+   std::string selectedSerialPort_ {"?"};
+
+   PortInfoMap       portInfoMap_ {};
+   PortPropertiesMap portPropertiesMap_ {};
+   PortSettingsMap   portSettingsMap_ {};
+};
+
+SerialPortDialog::SerialPortDialog(QWidget* parent) :
+    QDialog(parent),
+    p {std::make_unique(this)},
+    ui(new Ui::SerialPortDialog)
+{
+   ui->setupUi(this);
+
+   p->proxyModel_->setSourceModel(p->model_);
+   ui->serialPortView->setModel(p->proxyModel_);
+   ui->serialPortView->setEditTriggers(
+      QAbstractItemView::EditTrigger::NoEditTriggers);
+   ui->serialPortView->sortByColumn(0, Qt::SortOrder::AscendingOrder);
+
+   p->RefreshSerialDevices();
+
+   ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)
+      ->setEnabled(false);
+
+   connect(ui->refreshButton,
+           &QAbstractButton::clicked,
+           this,
+           [this]() { p->RefreshSerialDevices(); });
+
+   connect(ui->serialPortView,
+           &QTreeView::doubleClicked,
+           this,
+           [this]() { Q_EMIT accept(); });
+   connect(
+      ui->serialPortView->selectionModel(),
+      &QItemSelectionModel::selectionChanged,
+      this,
+      [this](const QItemSelection& selected, const QItemSelection& deselected)
+      {
+         if (selected.size() == 0 && deselected.size() == 0)
+         {
+            // Items which stay selected but change their index are not
+            // included in selected and deselected. Thus, this signal might
+            // be emitted with both selected and deselected empty, if only
+            // the indices of selected items change.
+            return;
+         }
+
+         ui->buttonBox->button(QDialogButtonBox::Ok)
+            ->setEnabled(selected.size() > 0);
+
+         if (selected.size() > 0)
+         {
+            QModelIndex selectedIndex =
+               p->proxyModel_->mapToSource(selected[0].indexes()[0]);
+            selectedIndex        = p->model_->index(selectedIndex.row(), 0);
+            QVariant variantData = p->model_->data(selectedIndex);
+            if (variantData.typeId() == QMetaType::QString)
+            {
+               p->selectedSerialPort_ = variantData.toString().toStdString();
+            }
+            else
+            {
+               logger_->warn("Unexpected selection data type");
+               p->selectedSerialPort_ = std::string {"?"};
+            }
+         }
+         else
+         {
+            p->selectedSerialPort_ = std::string {"?"};
+         }
+
+         logger_->debug("Selected: {}", p->selectedSerialPort_);
+      });
+}
+
+SerialPortDialog::~SerialPortDialog()
+{
+   delete ui;
+}
+
+std::string SerialPortDialog::serial_port()
+{
+   return p->selectedSerialPort_;
+}
+
+int SerialPortDialog::baud_rate()
+{
+   int baudRate = -1;
+
+   auto it = p->portSettingsMap_.find(p->selectedSerialPort_);
+   if (it != p->portSettingsMap_.cend())
+   {
+      baudRate = it->second.baudRate_;
+   }
+
+   return baudRate;
+}
+
+void SerialPortDialog::Impl::LogSerialPortInfo(const QSerialPortInfo& info)
+{
+   logger_->trace("Serial Port:    {}", info.portName().toStdString());
+   logger_->trace("  Description:  {}", info.description().toStdString());
+   logger_->trace("  System Loc:   {}", info.systemLocation().toStdString());
+   logger_->trace("  Manufacturer: {}", info.manufacturer().toStdString());
+   logger_->trace("  Vendor ID:    {}", info.vendorIdentifier());
+   logger_->trace("  Product ID:   {}", info.productIdentifier());
+   logger_->trace("  Serial No:    {}", info.serialNumber().toStdString());
+}
+
+void SerialPortDialog::Impl::RefreshSerialDevices()
+{
+   QList availablePorts = QSerialPortInfo::availablePorts();
+
+   PortInfoMap       newPortInfoMap {};
+   PortPropertiesMap newPortPropertiesMap {};
+   PortSettingsMap   newPortSettingsMap {};
+
+   for (auto& port : availablePorts)
+   {
+      LogSerialPortInfo(port);
+      newPortInfoMap.insert_or_assign(port.portName().toStdString(), port);
+   }
+
+   ReadComPortProperties(newPortPropertiesMap);
+   ReadComPortSettings(newPortSettingsMap);
+
+   portInfoMap_.swap(newPortInfoMap);
+   portPropertiesMap_.swap(newPortPropertiesMap);
+   portSettingsMap_.swap(newPortSettingsMap);
+
+   UpdateModel();
+}
+
+void SerialPortDialog::Impl::UpdateModel()
+{
+#if defined(_WIN32)
+   static const QStringList headerLabels {
+      tr("Port"), tr("Description"), tr("Device")};
+#else
+   static const QStringList headerLabels {tr("Port"), tr("Description")};
+#endif
+
+   // Clear existing serial ports
+   model_->clear();
+
+   // Reset selected serial port and disable OK button
+   selectedSerialPort_ = std::string {"?"};
+   self_->ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)
+      ->setEnabled(false);
+
+   // Reset headers
+   model_->setHorizontalHeaderLabels(headerLabels);
+
+   QStandardItem* root = model_->invisibleRootItem();
+
+   for (auto& port : portInfoMap_)
+   {
+      const QString portName    = port.second.portName();
+      const QString description = port.second.description();
+
+#if defined(_WIN32)
+      QString device {};
+
+      auto portPropertiesIt = portPropertiesMap_.find(port.first);
+      if (portPropertiesIt != portPropertiesMap_.cend())
+      {
+         device = QString::fromStdString(
+            portPropertiesIt->second.busReportedDeviceDescription_);
+      }
+
+      root->appendRow({new QStandardItem(portName),
+                       new QStandardItem(description),
+                       new QStandardItem(device)});
+#else
+      root->appendRow(
+         {new QStandardItem(portName), new QStandardItem(description)});
+#endif
+   }
+
+   for (int column = 0; column < model_->columnCount(); column++)
+   {
+      self_->ui->serialPortView->resizeColumnToContents(column);
+   }
+}
+
+void SerialPortDialog::Impl::ReadComPortProperties(
+   [[maybe_unused]] PortPropertiesMap& portPropertiesMap)
+{
+#if defined(_WIN32)
+   GUID     classGuid  = GUID_DEVCLASS_PORTS;
+   PCWSTR   enumerator = nullptr;
+   HWND     hwndParent = nullptr;
+   DWORD    flags      = DIGCF_PRESENT;
+   HDEVINFO deviceInfoSet;
+
+   // Retrieve COM port devices
+   deviceInfoSet =
+      SetupDiGetClassDevs(&classGuid, enumerator, hwndParent, flags);
+   if (deviceInfoSet == INVALID_HANDLE_VALUE)
+   {
+      logger_->error("Error getting COM port devices");
+      return;
+   }
+
+   DWORD           memberIndex = 0;
+   SP_DEVINFO_DATA deviceInfoData {};
+   deviceInfoData.cbSize = sizeof(deviceInfoData);
+   flags                 = 0;
+
+   // For each COM port device
+   while (SetupDiEnumDeviceInfo(deviceInfoSet, memberIndex++, &deviceInfoData))
+   {
+      DWORD  scope      = DICS_FLAG_GLOBAL;
+      DWORD  hwProfile  = 0;
+      DWORD  keyType    = DIREG_DEV;
+      REGSAM samDesired = KEY_READ;
+      HKEY   devRegKey  = SetupDiOpenDevRegKey(
+         deviceInfoSet, &deviceInfoData, scope, hwProfile, keyType, samDesired);
+
+      if (devRegKey == INVALID_HANDLE_VALUE)
+      {
+         logger_->error("Unable to open device registry key: {}",
+                        GetLastError());
+         continue;
+      }
+
+      // Read Port Name and Device Description
+      std::string portName =
+         GetRegistryValueDataString(devRegKey, TEXT("PortName"));
+
+      if (portName.empty())
+      {
+         // Ignore device without port name
+         continue;
+      }
+
+      PortProperties properties {};
+      properties.busReportedDeviceDescription_ = GetDevicePropertyString(
+         deviceInfoSet, deviceInfoData, DEVPKEY_Device_BusReportedDeviceDesc);
+
+      logger_->trace(
+         "Port: {} ({})", portName, properties.busReportedDeviceDescription_);
+
+      portPropertiesMap.emplace(portName, std::move(properties));
+
+      RegCloseKey(devRegKey);
+   }
+#endif
+}
+
+void SerialPortDialog::Impl::ReadComPortSettings(
+   [[maybe_unused]] PortSettingsMap& portSettingsMap)
+{
+#if defined(_WIN32)
+   const LPCTSTR lpSubKey =
+      TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Ports");
+   DWORD   ulOptions  = 0;
+   REGSAM  samDesired = KEY_READ;
+   HKEY    hkResult;
+   LSTATUS status;
+
+   // Open Port Settings Key
+   status = RegOpenKeyEx(
+      HKEY_LOCAL_MACHINE, lpSubKey, ulOptions, samDesired, &hkResult);
+
+   if (status == ERROR_SUCCESS)
+   {
+      DWORD   dwIndex = 0;
+      TCHAR   valueName[MAX_PATH];
+      LPDWORD lpReserved = nullptr;
+      DWORD   type;
+      TCHAR   valueData[64];
+      char    buffer[MAX_PATH]; // Buffer for string conversion
+
+      // Number of characters, not including terminating null
+      static constexpr DWORD maxValueNameSize =
+         sizeof(valueName) / sizeof(TCHAR) - 1;
+      DWORD valueNameSize = maxValueNameSize;
+
+      // Number of bytes
+      DWORD valueDataSize = sizeof(valueData);
+
+      static constexpr std::size_t bufferSize = sizeof(buffer);
+
+      // Enumerate each port value
+      while ((status = RegEnumValue(hkResult,
+                                    dwIndex++,
+                                    valueName,
+                                    &valueNameSize,
+                                    lpReserved,
+                                    &type,
+                                    reinterpret_cast(&valueData),
+                                    &valueDataSize)) == ERROR_SUCCESS ||
+             status == ERROR_MORE_DATA)
+      {
+         // Validate port value
+         if (status == ERROR_SUCCESS &&            //
+             type == REG_SZ &&                     //
+             valueNameSize >= 5 &&                 // COM#:
+             valueNameSize < sizeof(buffer) - 1 && // Strip off :
+             valueDataSize > sizeof(TCHAR) &&      // Null character
+             _tcsncmp(valueName, TEXT("COM"), 3) == 0)
+         {
+            errno_t     error;
+            std::size_t returnValue;
+
+            // Get port name
+            if ((error = wcstombs_s(&returnValue,
+                                    buffer,
+                                    sizeof(buffer),
+                                    valueName,
+                                    valueNameSize - 1)) != 0)
+            {
+               logger_->error(
+                  "Error converting registry value name to string: {}",
+                  returnValue);
+               continue;
+            }
+
+            std::string portName = buffer;
+
+            // Get port data
+            if ((error = wcstombs_s(&returnValue,
+                                    buffer,
+                                    sizeof(buffer),
+                                    valueData,
+                                    sizeof(buffer) - 1)) != 0)
+            {
+               logger_->error(
+                  "Error converting registry value data to string: {}",
+                  returnValue);
+               continue;
+            }
+
+            std::string portData = buffer;
+
+            logger_->trace("Port Settings: {} ({})", portName, portData);
+
+            StorePortSettings(portName, portData, portSettingsMap);
+         }
+
+         valueNameSize = maxValueNameSize;
+         valueDataSize = sizeof(valueData);
+      }
+
+      RegCloseKey(hkResult);
+   }
+   else
+   {
+      logger_->error("Could not open COM port settings registry key: {}",
+                     status);
+   }
+#endif
+}
+
+void SerialPortDialog::Impl::StorePortSettings(
+   const std::string& portName,
+   const std::string& settingsString,
+   PortSettingsMap&   portSettingsMap)
+{
+   PortSettings portSettings {};
+
+   std::vector tokenList =
+      util::ParseTokens(settingsString, {",", ",", ",", ",", ","});
+
+   try
+   {
+      if (tokenList.size() >= 1)
+      {
+         // Positive integer
+         portSettings.baudRate_ = std::stoi(tokenList.at(0));
+      }
+      if (tokenList.size() >= 2)
+      {
+         // [e]ven, [o]dd, [m]ark, [s]pace, [n]one
+         portSettings.parity_ = tokenList.at(1);
+      }
+      if (tokenList.size() >= 3)
+      {
+         // [4-8]
+         portSettings.dataBits_ = std::stoi(tokenList.at(2));
+      }
+      if (tokenList.size() >= 4)
+      {
+         // 1, 1.5, 2
+         portSettings.stopBits_ = std::stof(tokenList.at(3));
+      }
+      if (tokenList.size() >= 5)
+      {
+         // "" (none), "p" (hardware), "x" (Xon / Xoff)
+         portSettings.flowControl_ = tokenList.at(4);
+      }
+
+      portSettingsMap.emplace(portName, std::move(portSettings));
+   }
+   catch (const std::exception&)
+   {
+      logger_->error(
+         "Could not parse {} port settings: {}", portName, settingsString);
+   }
+}
+
+#if defined(_WIN32)
+std::string
+SerialPortDialog::Impl::GetDevicePropertyString(HDEVINFO&        deviceInfoSet,
+                                                SP_DEVINFO_DATA& deviceInfoData,
+                                                DEVPROPKEY       propertyKey)
+{
+   std::string devicePropertyString {};
+
+   DEVPROPTYPE        propertyType = 0;
+   std::vector propertyBuffer {};
+   DWORD              requiredSize = 0;
+   DWORD              flags        = 0;
+
+   BOOL status = SetupDiGetDeviceProperty(deviceInfoSet,
+                                          &deviceInfoData,
+                                          &propertyKey,
+                                          &propertyType,
+                                          nullptr,
+                                          0,
+                                          &requiredSize,
+                                          flags);
+
+   if (requiredSize > 0)
+   {
+      propertyBuffer.reserve(requiredSize / sizeof(TCHAR));
+
+      status = SetupDiGetDeviceProperty(
+         deviceInfoSet,
+         &deviceInfoData,
+         &propertyKey,
+         &propertyType,
+         reinterpret_cast(propertyBuffer.data()),
+         static_cast(propertyBuffer.capacity() * sizeof(TCHAR)),
+         &requiredSize,
+         flags);
+   }
+
+   if (status && requiredSize > 0)
+   {
+      errno_t     error;
+      std::size_t returnValue;
+
+      devicePropertyString.resize(requiredSize / sizeof(TCHAR));
+
+      if ((error = wcstombs_s(&returnValue,
+                              devicePropertyString.data(),
+                              devicePropertyString.size(),
+                              propertyBuffer.data(),
+                              _TRUNCATE)) != 0)
+      {
+         logger_->error("Error converting device property string: {}",
+                        returnValue);
+         devicePropertyString.clear();
+      }
+      else if (!devicePropertyString.empty())
+      {
+         // Remove trailing null
+         devicePropertyString.erase(devicePropertyString.size() - 1);
+      }
+   }
+
+   return devicePropertyString;
+}
+
+std::string SerialPortDialog::Impl::GetRegistryValueDataString(HKEY    hKey,
+                                                               LPCTSTR lpValue)
+{
+   std::string dataString {};
+
+   LPCTSTR lpSubKey = nullptr;
+   DWORD   dwFlags  = RRF_RT_REG_SZ; // Restrict type to REG_SZ
+   DWORD   dwType;
+
+   std::vector dataBuffer {};
+   DWORD              dataBufferSize = 0;
+
+   LSTATUS status = RegGetValue(
+      hKey, lpSubKey, lpValue, dwFlags, &dwType, nullptr, &dataBufferSize);
+
+   if (status == ERROR_SUCCESS && dataBufferSize > 0)
+   {
+      dataBuffer.reserve(dataBufferSize / sizeof(TCHAR));
+
+      status = RegGetValue(hKey,
+                           lpSubKey,
+                           lpValue,
+                           dwFlags,
+                           &dwType,
+                           reinterpret_cast(dataBuffer.data()),
+                           &dataBufferSize);
+   }
+
+   if (status == ERROR_SUCCESS && dataBufferSize > 0)
+   {
+      errno_t     error;
+      std::size_t returnValue;
+
+      dataString.resize(dataBufferSize / sizeof(TCHAR));
+
+      if ((error = wcstombs_s(&returnValue,
+                              dataString.data(),
+                              dataString.size(),
+                              dataBuffer.data(),
+                              _TRUNCATE)) != 0)
+      {
+         logger_->error("Error converting registry value data string: {}",
+                        returnValue);
+         dataString.clear();
+      }
+      else if (!dataString.empty())
+      {
+         // Remove trailing null
+         dataString.erase(dataString.size() - 1);
+      }
+   }
+
+   return dataString;
+}
+#endif
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/serial_port_dialog.hpp b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.hpp
new file mode 100644
index 00000000..9a9f1c64
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include 
+
+namespace Ui
+{
+class SerialPortDialog;
+}
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+class SerialPortDialog : public QDialog
+{
+   Q_OBJECT
+   Q_DISABLE_COPY_MOVE(SerialPortDialog)
+
+public:
+   explicit SerialPortDialog(QWidget* parent = nullptr);
+   ~SerialPortDialog();
+
+   std::string serial_port();
+   int         baud_rate();
+
+private:
+   class Impl;
+   std::unique_ptr p;
+   Ui::SerialPortDialog* ui;
+};
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/serial_port_dialog.ui b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.ui
new file mode 100644
index 00000000..02935862
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.ui
@@ -0,0 +1,102 @@
+
+
+ SerialPortDialog
+ 
+  
+   
+    0
+    0
+    400
+    300
+   
+  
+  
+   Select Serial Port
+  
+  
+
- 
+    
+     
+      true
+     
+     
+      0
+     
+     
+      true
+     
+    
+   +
- 
+    
+     
+      
+       0
+      
+      
+       0
+      
+      
+       0
+      
+      
+       0
+      
+      - 
+       
+        
+         &Refresh
+        
+       
+      +
- 
+       
+        
+         Qt::Orientation::Horizontal
+        
+        
+         QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+        
+       
+      +     
+    
+
+  
+ 
+ 
+ 
+  
+   buttonBox
+   accepted()
+   SerialPortDialog
+   accept()
+   
+    
+     248
+     254
+    
+    
+     157
+     274
+    
+   
+  
+  
+   buttonBox
+   rejected()
+   SerialPortDialog
+   reject()
+   
+    
+     316
+     260
+    
+    
+     286
+     274
+    
+   
+  
+ 
+
diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp
index e2c2ea59..228badd6 100644
--- a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp
+++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp
@@ -4,6 +4,41 @@
 
 #include 
 
+#define SCWX_ENUM_MAP_FROM_VALUE(Iterator, ToName)                             \
+   [](const std::string& text) -> std::string                                  \
+   {                                                                           \
+      for (auto enumValue : Iterator)                                          \
+      {                                                                        \
+         const std::string enumName = ToName(enumValue);                       \
+                                                                               \
+         if (boost::iequals(text, enumName))                                   \
+         {                                                                     \
+            /* Return label */                                                 \
+            return enumName;                                                   \
+         }                                                                     \
+      }                                                                        \
+                                                                               \
+      /* Label not found, return unknown */                                    \
+      return "?";                                                              \
+   }
+
+#define SCWX_SETTINGS_COMBO_BOX(settingsInterface, comboBox, Iterator, ToName) \
+   for (const auto& enumValue : Iterator)                                      \
+   {                                                                           \
+      comboBox->addItem(QString::fromStdString(ToName(enumValue)));            \
+   }                                                                           \
+                                                                               \
+   settingsInterface.SetMapFromValueFunction(                                  \
+      SCWX_ENUM_MAP_FROM_VALUE(Iterator, ToName));                             \
+   settingsInterface.SetMapToValueFunction(                                    \
+      [](std::string text) -> std::string                                      \
+      {                                                                        \
+         boost::to_lower(text);                                                \
+         return text;                                                          \
+      });                                                                      \
+                                                                               \
+   settingsInterface.SetEditWidget(comboBox);
+
 namespace scwx
 {
 namespace qt
diff --git a/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp
index fc1a6b9a..946d21ce 100644
--- a/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp
+++ b/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp
@@ -12,36 +12,6 @@
 #include 
 #include 
 
-#define SCWX_SETTINGS_COMBO_BOX(settingsInterface, comboBox, Iterator, ToName) \
-   for (const auto& enumValue : Iterator)                                      \
-   {                                                                           \
-      comboBox->addItem(QString::fromStdString(ToName(enumValue)));            \
-   }                                                                           \
-                                                                               \
-   settingsInterface.SetMapFromValueFunction(                                  \
-      [](const std::string& text) -> std::string                               \
-      {                                                                        \
-         for (const auto& enumValue : Iterator)                                \
-         {                                                                     \
-            const std::string valueName = ToName(enumValue);                   \
-                                                                               \
-            if (boost::iequals(text, valueName))                               \
-            {                                                                  \
-               return valueName;                                               \
-            }                                                                  \
-         }                                                                     \
-                                                                               \
-         return "?";                                                           \
-      });                                                                      \
-   settingsInterface.SetMapToValueFunction(                                    \
-      [](std::string text) -> std::string                                      \
-      {                                                                        \
-         boost::to_lower(text);                                                \
-         return text;                                                          \
-      });                                                                      \
-                                                                               \
-   settingsInterface.SetEditWidget(comboBox);
-
 namespace scwx
 {
 namespace qt
diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
index 6830c4e6..e0996855 100644
--- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
+++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
@@ -22,6 +22,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -96,30 +97,13 @@ static const std::unordered_map
                             {"VIL", {0u, 255u, 1.0f, 2.5f}},
                             {"???", {0u, 15u, 0.0f, 1.0f}}};
 
-#define SCWX_ENUM_MAP_FROM_VALUE(Type, Iterator, ToName)                       \
-   [](const std::string& text) -> std::string                                  \
-   {                                                                           \
-      for (Type enumValue : Iterator)                                          \
-      {                                                                        \
-         const std::string enumName = ToName(enumValue);                       \
-                                                                               \
-         if (boost::iequals(text, enumName))                                   \
-         {                                                                     \
-            /* Return label */                                                 \
-            return enumName;                                                   \
-         }                                                                     \
-      }                                                                        \
-                                                                               \
-      /* Label not found, return unknown */                                    \
-      return "?";                                                              \
-   }
-
 class SettingsDialogImpl
 {
 public:
    explicit SettingsDialogImpl(SettingsDialog* self) :
        self_ {self},
        radarSiteDialog_ {new RadarSiteDialog(self)},
+       gpsSourceDialog_ {new SerialPortDialog(self)},
        countyDialog_ {new CountyDialog(self)},
        fontDialog_ {new QFontDialog(self)},
        fontCategoryModel_ {new QStandardItemModel(self)},
@@ -134,6 +118,9 @@ public:
           &defaultAlertAction_,
           &clockFormat_,
           &defaultTimeZone_,
+          &positioningPlugin_,
+          &nmeaBaudRate_,
+          &nmeaSource_,
           &warningsProvider_,
           &antiAliasingEnabled_,
           &showMapAttribution_,
@@ -208,10 +195,11 @@ public:
                RadarSiteLabel(std::shared_ptr& radarSite);
    static void SetBackgroundColor(const std::string& value, QFrame* frame);
 
-   SettingsDialog*  self_;
-   RadarSiteDialog* radarSiteDialog_;
-   CountyDialog*    countyDialog_;
-   QFontDialog*     fontDialog_;
+   SettingsDialog*   self_;
+   RadarSiteDialog*  radarSiteDialog_;
+   SerialPortDialog* gpsSourceDialog_;
+   CountyDialog*     countyDialog_;
+   QFontDialog*      fontDialog_;
 
    QStandardItemModel* fontCategoryModel_;
 
@@ -235,6 +223,9 @@ public:
    settings::SettingsInterface  defaultAlertAction_ {};
    settings::SettingsInterface  clockFormat_ {};
    settings::SettingsInterface  defaultTimeZone_ {};
+   settings::SettingsInterface  positioningPlugin_ {};
+   settings::SettingsInterface nmeaBaudRate_ {};
+   settings::SettingsInterface  nmeaSource_ {};
    settings::SettingsInterface  theme_ {};
    settings::SettingsInterface  warningsProvider_ {};
    settings::SettingsInterface         antiAliasingEnabled_ {};
@@ -344,6 +335,32 @@ void SettingsDialogImpl::ConnectSignals()
                        }
                     });
 
+   QObject::connect(self_->ui->gpsSourceSelectButton,
+                    &QAbstractButton::clicked,
+                    self_,
+                    [this]() { gpsSourceDialog_->show(); });
+
+   QObject::connect(gpsSourceDialog_,
+                    &SerialPortDialog::accepted,
+                    self_,
+                    [this]()
+                    {
+                       std::string serialPort = gpsSourceDialog_->serial_port();
+                       int         baudRate   = gpsSourceDialog_->baud_rate();
+
+                       if (!serialPort.empty() && serialPort != "?")
+                       {
+                          std::string source =
+                             fmt::format("serial:{}", serialPort);
+                          nmeaSource_.StageValue(source);
+                       }
+
+                       if (baudRate > 0)
+                       {
+                          self_->ui->nmeaBaudRateSpinBox->setValue(baudRate);
+                       }
+                    });
+
    // Update the Radar Site dialog "map" location with the currently selected
    // radar site
    auto& defaultRadarSite = *defaultRadarSite_.GetSettingsVariable();
@@ -467,38 +484,11 @@ void SettingsDialogImpl::SetupGeneralTab()
    settings::GeneralSettings& generalSettings =
       settings::GeneralSettings::Instance();
 
-   for (const auto& uiStyle : types::UiStyleIterator())
-   {
-      self_->ui->themeComboBox->addItem(
-         QString::fromStdString(types::GetUiStyleName(uiStyle)));
-   }
-
    theme_.SetSettingsVariable(generalSettings.theme());
-   theme_.SetMapFromValueFunction(
-      [](const std::string& text) -> std::string
-      {
-         for (types::UiStyle uiStyle : types::UiStyleIterator())
-         {
-            const std::string uiStyleName = types::GetUiStyleName(uiStyle);
-
-            if (boost::iequals(text, uiStyleName))
-            {
-               // Return UI style label
-               return uiStyleName;
-            }
-         }
-
-         // UI style label not found, return unknown
-         return "?";
-      });
-   theme_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   theme_.SetEditWidget(self_->ui->themeComboBox);
+   SCWX_SETTINGS_COMBO_BOX(theme_,
+                           self_->ui->themeComboBox,
+                           types::UiStyleIterator(),
+                           types::GetUiStyleName);
    theme_.SetResetButton(self_->ui->resetThemeButton);
 
    auto radarSites = config::RadarSite::GetAll();
@@ -561,39 +551,11 @@ void SettingsDialogImpl::SetupGeneralTab()
    gridHeight_.SetEditWidget(self_->ui->gridHeightSpinBox);
    gridHeight_.SetResetButton(self_->ui->resetGridHeightButton);
 
-   for (const auto& mapProvider : map::MapProviderIterator())
-   {
-      self_->ui->mapProviderComboBox->addItem(
-         QString::fromStdString(map::GetMapProviderName(mapProvider)));
-   }
-
    mapProvider_.SetSettingsVariable(generalSettings.map_provider());
-   mapProvider_.SetMapFromValueFunction(
-      [](const std::string& text) -> std::string
-      {
-         for (map::MapProvider mapProvider : map::MapProviderIterator())
-         {
-            const std::string mapProviderName =
-               map::GetMapProviderName(mapProvider);
-
-            if (boost::iequals(text, mapProviderName))
-            {
-               // Return map provider label
-               return mapProviderName;
-            }
-         }
-
-         // Map provider label not found, return unknown
-         return "?";
-      });
-   mapProvider_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   mapProvider_.SetEditWidget(self_->ui->mapProviderComboBox);
+   SCWX_SETTINGS_COMBO_BOX(mapProvider_,
+                           self_->ui->mapProviderComboBox,
+                           map::MapProviderIterator(),
+                           map::GetMapProviderName);
    mapProvider_.SetResetButton(self_->ui->resetMapProviderButton);
 
    mapboxApiKey_.SetSettingsVariable(generalSettings.mapbox_api_key());
@@ -604,70 +566,62 @@ void SettingsDialogImpl::SetupGeneralTab()
    mapTilerApiKey_.SetEditWidget(self_->ui->mapTilerApiKeyLineEdit);
    mapTilerApiKey_.SetResetButton(self_->ui->resetMapTilerApiKeyButton);
 
-   for (const auto& alertAction : types::AlertActionIterator())
-   {
-      self_->ui->defaultAlertActionComboBox->addItem(
-         QString::fromStdString(types::GetAlertActionName(alertAction)));
-   }
-
    defaultAlertAction_.SetSettingsVariable(
       generalSettings.default_alert_action());
-   defaultAlertAction_.SetMapFromValueFunction(
-      SCWX_ENUM_MAP_FROM_VALUE(types::AlertAction,
-                               types::AlertActionIterator(),
-                               types::GetAlertActionName));
-   defaultAlertAction_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   defaultAlertAction_.SetEditWidget(self_->ui->defaultAlertActionComboBox);
+   SCWX_SETTINGS_COMBO_BOX(defaultAlertAction_,
+                           self_->ui->defaultAlertActionComboBox,
+                           types::AlertActionIterator(),
+                           types::GetAlertActionName);
    defaultAlertAction_.SetResetButton(self_->ui->resetDefaultAlertActionButton);
 
-   for (const auto& clockFormat : scwx::util::ClockFormatIterator())
-   {
-      self_->ui->clockFormatComboBox->addItem(
-         QString::fromStdString(scwx::util::GetClockFormatName(clockFormat)));
-   }
-
    clockFormat_.SetSettingsVariable(generalSettings.clock_format());
-   clockFormat_.SetMapFromValueFunction(
-      SCWX_ENUM_MAP_FROM_VALUE(scwx::util::ClockFormat,
-                               scwx::util::ClockFormatIterator(),
-                               scwx::util::GetClockFormatName));
-   clockFormat_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   clockFormat_.SetEditWidget(self_->ui->clockFormatComboBox);
+   SCWX_SETTINGS_COMBO_BOX(clockFormat_,
+                           self_->ui->clockFormatComboBox,
+                           scwx::util::ClockFormatIterator(),
+                           scwx::util::GetClockFormatName);
    clockFormat_.SetResetButton(self_->ui->resetClockFormatButton);
 
-   for (const auto& timeZone : types::DefaultTimeZoneIterator())
-   {
-      self_->ui->defaultTimeZoneComboBox->addItem(
-         QString::fromStdString(types::GetDefaultTimeZoneName(timeZone)));
-   }
-
    defaultTimeZone_.SetSettingsVariable(generalSettings.default_time_zone());
-   defaultTimeZone_.SetMapFromValueFunction(
-      SCWX_ENUM_MAP_FROM_VALUE(types::DefaultTimeZone,
-                               types::DefaultTimeZoneIterator(),
-                               types::GetDefaultTimeZoneName));
-   defaultTimeZone_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   defaultTimeZone_.SetEditWidget(self_->ui->defaultTimeZoneComboBox);
+   SCWX_SETTINGS_COMBO_BOX(defaultTimeZone_,
+                           self_->ui->defaultTimeZoneComboBox,
+                           types::DefaultTimeZoneIterator(),
+                           types::GetDefaultTimeZoneName);
    defaultTimeZone_.SetResetButton(self_->ui->resetDefaultTimeZoneButton);
 
+   QObject::connect(
+      self_->ui->positioningPluginComboBox,
+      &QComboBox::currentTextChanged,
+      self_,
+      [this](const QString& text)
+      {
+         types::PositioningPlugin positioningPlugin =
+            types::GetPositioningPlugin(text.toStdString());
+
+         bool gpsSourceEnabled =
+            positioningPlugin == types::PositioningPlugin::Nmea;
+
+         self_->ui->nmeaSourceLineEdit->setEnabled(gpsSourceEnabled);
+         self_->ui->gpsSourceSelectButton->setEnabled(gpsSourceEnabled);
+         self_->ui->nmeaBaudRateSpinBox->setEnabled(gpsSourceEnabled);
+         self_->ui->resetNmeaSourceButton->setEnabled(gpsSourceEnabled);
+         self_->ui->resetNmeaBaudRateButton->setEnabled(gpsSourceEnabled);
+      });
+
+   positioningPlugin_.SetSettingsVariable(generalSettings.positioning_plugin());
+   SCWX_SETTINGS_COMBO_BOX(positioningPlugin_,
+                           self_->ui->positioningPluginComboBox,
+                           types::PositioningPluginIterator(),
+                           types::GetPositioningPluginName);
+   positioningPlugin_.SetResetButton(self_->ui->resetPositioningPluginButton);
+
+   nmeaBaudRate_.SetSettingsVariable(generalSettings.nmea_baud_rate());
+   nmeaBaudRate_.SetEditWidget(self_->ui->nmeaBaudRateSpinBox);
+   nmeaBaudRate_.SetResetButton(self_->ui->resetNmeaBaudRateButton);
+
+   nmeaSource_.SetSettingsVariable(generalSettings.nmea_source());
+   nmeaSource_.SetEditWidget(self_->ui->nmeaSourceLineEdit);
+   nmeaSource_.SetResetButton(self_->ui->resetNmeaSourceButton);
+
    warningsProvider_.SetSettingsVariable(generalSettings.warnings_provider());
    warningsProvider_.SetEditWidget(self_->ui->warningsProviderLineEdit);
    warningsProvider_.SetResetButton(self_->ui->resetWarningsProviderButton);
@@ -991,27 +945,12 @@ void SettingsDialogImpl::SetupAudioTab()
          dialog->open();
       });
 
-   for (const auto& locationMethod : types::LocationMethodIterator())
-   {
-      self_->ui->alertAudioLocationMethodComboBox->addItem(
-         QString::fromStdString(types::GetLocationMethodName(locationMethod)));
-   }
-
    alertAudioLocationMethod_.SetSettingsVariable(
       audioSettings.alert_location_method());
-   alertAudioLocationMethod_.SetMapFromValueFunction(
-      SCWX_ENUM_MAP_FROM_VALUE(types::LocationMethod,
-                               types::LocationMethodIterator(),
-                               types::GetLocationMethodName));
-   alertAudioLocationMethod_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   alertAudioLocationMethod_.SetEditWidget(
-      self_->ui->alertAudioLocationMethodComboBox);
+   SCWX_SETTINGS_COMBO_BOX(alertAudioLocationMethod_,
+                           self_->ui->alertAudioLocationMethodComboBox,
+                           types::LocationMethodIterator(),
+                           types::GetLocationMethodName);
    alertAudioLocationMethod_.SetResetButton(
       self_->ui->resetAlertAudioLocationMethodButton);
 
@@ -1159,40 +1098,11 @@ void SettingsDialogImpl::SetupTextTab()
    hoverTextWrap_.SetEditWidget(self_->ui->hoverTextWrapSpinBox);
    hoverTextWrap_.SetResetButton(self_->ui->resetHoverTextWrapButton);
 
-   for (const auto& tooltipMethod : types::TooltipMethodIterator())
-   {
-      self_->ui->tooltipMethodComboBox->addItem(
-         QString::fromStdString(types::GetTooltipMethodName(tooltipMethod)));
-   }
-
    tooltipMethod_.SetSettingsVariable(textSettings.tooltip_method());
-   tooltipMethod_.SetMapFromValueFunction(
-      [](const std::string& text) -> std::string
-      {
-         for (types::TooltipMethod tooltipMethod :
-              types::TooltipMethodIterator())
-         {
-            const std::string tooltipMethodName =
-               types::GetTooltipMethodName(tooltipMethod);
-
-            if (boost::iequals(text, tooltipMethodName))
-            {
-               // Return tooltip method label
-               return tooltipMethodName;
-            }
-         }
-
-         // Tooltip method label not found, return unknown
-         return "?";
-      });
-   tooltipMethod_.SetMapToValueFunction(
-      [](std::string text) -> std::string
-      {
-         // Convert label to lower case and return
-         boost::to_lower(text);
-         return text;
-      });
-   tooltipMethod_.SetEditWidget(self_->ui->tooltipMethodComboBox);
+   SCWX_SETTINGS_COMBO_BOX(tooltipMethod_,
+                           self_->ui->tooltipMethodComboBox,
+                           types::TooltipMethodIterator(),
+                           types::GetTooltipMethodName);
    tooltipMethod_.SetResetButton(self_->ui->resetTooltipMethodButton);
 
    placefileTextDropShadowEnabled_.SetSettingsVariable(
diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
index bb3761f7..fe0efa6e 100644
--- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
+++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
@@ -17,10 +17,10 @@- 
     
      
-      QFrame::StyledPanel
+      QFrame::Shape::StyledPanel
      
      
-      QFrame::Raised
+      QFrame::Shadow::Raised
      
      
       
@@ -38,7 +38,7 @@
       - 
        
         
-         Qt::Horizontal
+         Qt::Orientation::Horizontal
         
         
          
@@ -48,13 +48,13 @@
           
          
          
-          QAbstractScrollArea::AdjustToContents
+          QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents
          
          
-          QListView::Adjust
+          QListView::ResizeMode::Adjust
          
          
-          QListView::ListMode
+          QListView::ViewMode::ListMode
          
          
           true
@@ -137,14 +137,14 @@
                 0
                 0
                 513
-                482
+                566
                
               
               
                - 
                 
                  
-                  QFrame::NoFrame
+                  QFrame::Shape::NoFrame
                  
                  
                   
@@ -159,93 +159,14 @@
                   
                    0
                   
-                  - 
-                   
+                  - 
+                   
+                    
+                     MapTiler API Key
+                    
+                   
                   
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                    
-                     Mapbox API Key
-                    
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                    
-                     Default Time Zone
-                    
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                    
-                     Clock Format
-                    
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                    
-                     Grid Height
-                    
-                   
-                  -
- 
                    
                     
                      ...
@@ -257,23 +178,17 @@
                    
                   
- 
-                   
+                   
                   -
- 
-                   
-                  -
- 
-                   
+                  - 
+                   
                     
-                     Warnings Provider
+                     Default Time Zone
                     
                    
                   -
- 
-                   
-                  -
- 
-                   
+                  - 
+                   
                     
                      ...
                     
@@ -284,106 +199,9 @@
                    
                   
- 
-                   
-                    
-                     QLineEdit::Password
-                    
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
                    
                   -
- 
-                   
-                  -
- 
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                   
-                  -
- 
-                   
-                    
-                     Theme
-                    
-                   
-                  -
- 
-                   
-                    
-                     MapTiler API Key
-                    
-                   
-                  -
- 
-                   
-                    
-                     Map Provider
-                    
-                   
-                  -
- 
-                   
-                    
-                     Default Alert Action
-                    
-                   
-                  -
- 
-                   
-                    
-                     Grid Width
-                    
-                   
-                  -
- 
-                   
-                    
-                     Default Radar Site
-                    
-                   
-                  -
- 
-                   
-                  -
- 
-                   
-                    
-                     ...
-                    
-                    
-                     
-                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                    
-                   
-                  -
- 
-                   
-                  -
- 
-                   
-                    
-                     QLineEdit::Password
-                    
-                   
-                  -
- 
+                  - 
                    
                     
                      ...
@@ -394,7 +212,72 @@
                     
                    
                   -
- 
+                  - 
+                   
+                    
+                     Map Provider
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                   
+                  +
- 
+                   
+                    
+                     QLineEdit::EchoMode::Password
+                    
+                   
+                  +
- 
+                   
+                    
+                     GPS Plugin
+                    
+                   
+                  +
- 
+                   
+                    
+                     GPS Baud Rate
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                   
+                  +
- 
                    
                     
                      ...
@@ -405,6 +288,200 @@
                     
                    
                   +
- 
+                   
+                    
+                     Grid Width
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     GPS Source
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     Default Radar Site
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     Grid Height
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     Theme
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     Mapbox API Key
+                    
+                   
+                  +
- 
+                   
+                    
+                     Clock Format
+                    
+                   
+                  +
- 
+                   
+                    
+                     QLineEdit::EchoMode::Password
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                  +
- 
+                   
+                    
+                     Warnings Provider
+                    
+                   
+                  +
- 
+                   
+                    
+                     1
+                    
+                    
+                     999999999
+                    
+                   
+                  +
- 
+                   
+                    
+                     Default Alert Action
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  +
- 
+                   
+                    
+                     ...
+                    
+                    
+                     
+                      :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                    
+                   
+                  
@@ -453,7 +530,7 @@- 
                 
                  
-                  Qt::Vertical
+                  Qt::Orientation::Vertical
                  
                  
                   
@@ -491,15 +568,15 @@
                    
                     0
                     0
-                    506
-                    383
+                    63
+                    18
                    
                   
                   
                    - 
                     
                      
-                      Qt::Vertical
+                      Qt::Orientation::Vertical
                      
                      
                       
@@ -523,10 +600,10 @@
                - 
                 
                  
-                  QFrame::StyledPanel
+                  QFrame::Shape::StyledPanel
                  
                  
-                  QFrame::Raised
+                  QFrame::Shadow::Raised
                  
                  
                   
@@ -547,7 +624,7 @@
                - 
                 
                  
-                  Qt::Vertical
+                  Qt::Orientation::Vertical
                  
                  
                   
@@ -769,7 +846,7 @@
            - 
             
              
-              Qt::Vertical
+              Qt::Orientation::Vertical
              
              
               
@@ -786,19 +863,19 @@
            - 
             
              
-              QFrame::StyledPanel
+              QFrame::Shape::StyledPanel
              
              
-              QFrame::Plain
+              QFrame::Shadow::Plain
              
              
               - 
                
                 
-                 QFrame::StyledPanel
+                 QFrame::Shape::StyledPanel
                 
                 
-                 QFrame::Raised
+                 QFrame::Shadow::Raised
                 
                 
                  
@@ -829,10 +906,10 @@
               - 
                
                 
-                 QFrame::StyledPanel
+                 QFrame::Shape::StyledPanel
                 
                 
-                 QFrame::Raised
+                 QFrame::Shadow::Raised
                 
                 
                  
@@ -850,7 +927,7 @@
                  - 
                   
                    
-                    Qt::Vertical
+                    Qt::Orientation::Vertical
                    
                    
                     
@@ -863,10 +940,10 @@
                  - 
                   
                    
-                    QFrame::Panel
+                    QFrame::Shape::Panel
                    
                    
-                    QFrame::Plain
+                    QFrame::Shadow::Plain
                    
                    
                     - 
@@ -875,7 +952,7 @@
                        Tornado Warning expires in 15 minutes
                       
-                       Qt::AlignCenter
+                       Qt::AlignmentFlag::AlignCenter
                       
                       
                        true
@@ -944,7 +1021,7 @@- 
                   
                    
-                    Qt::Horizontal
+                    Qt::Orientation::Horizontal
                    
                    
                     
@@ -974,10 +1051,10 @@
            - 
             
              
-              QFrame::StyledPanel
+              QFrame::Shape::StyledPanel
              
              
-              QFrame::Raised
+              QFrame::Shadow::Raised
              
              
               
@@ -1058,7 +1135,7 @@
            - 
             
              
-              Qt::Vertical
+              Qt::Orientation::Vertical
              
              
               
@@ -1080,10 +1157,10 @@
    - 
     
      
-      Qt::Horizontal
+      Qt::Orientation::Horizontal
      
      
-      QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Discard|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults
+      QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Discard|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::RestoreDefaults
      
     
    diff --git a/test/data b/test/data
index af115273..35e3e40d 160000
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit af115273844804d29c502b5ecbb94eee2b4b02f4
+Subproject commit 35e3e40d63bc020dfdc50c438c700c368fdf32fc