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/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index cda6c7f2..451c2c81 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)
@@ -248,6 +250,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
@@ -272,6 +275,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
@@ -287,6 +291,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,6 +602,7 @@ 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
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..6901e4b6
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp
@@ -0,0 +1,199 @@
+#define __STDC_WANT_LIB_EXT1__ 1
+
+#include "serial_port_dialog.hpp"
+#include "ui_serial_port_dialog.h"
+
+#include
+
+#include
+#include
+#include
+#include
+
+#if defined(_WIN32)
+# 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:
+ explicit Impl(SerialPortDialog* self) :
+ self_ {self}, model_ {new QStandardItemModel(self)}
+ {
+ }
+ ~Impl() = default;
+
+ void LogSerialPortInfo(const QSerialPortInfo& info);
+ void ReadComPortSettings();
+ void RefreshSerialDevices();
+
+ SerialPortDialog* self_;
+ QStandardItemModel* model_;
+
+ std::string selectedSerialPort_ {"?"};
+};
+
+SerialPortDialog::SerialPortDialog(QWidget* parent) :
+ QDialog(parent),
+ p {std::make_unique(this)},
+ ui(new Ui::SerialPortDialog)
+{
+ ui->setupUi(this);
+
+ connect(ui->refreshButton,
+ &QAbstractButton::clicked,
+ this,
+ [this]() { p->RefreshSerialDevices(); });
+}
+
+SerialPortDialog::~SerialPortDialog()
+{
+ delete ui;
+}
+
+std::string SerialPortDialog::serial_port()
+{
+ return p->selectedSerialPort_;
+}
+
+void SerialPortDialog::Impl::LogSerialPortInfo(const QSerialPortInfo& info)
+{
+ logger_->debug("Serial Port: {}", info.portName().toStdString());
+ logger_->debug(" Description: {}", info.description().toStdString());
+ logger_->debug(" System Loc: {}", info.systemLocation().toStdString());
+ logger_->debug(" Manufacturer: {}", info.manufacturer().toStdString());
+ logger_->debug(" Vendor ID: {}", info.vendorIdentifier());
+ logger_->debug(" Product ID: {}", info.productIdentifier());
+ logger_->debug(" Serial No: {}", info.serialNumber().toStdString());
+}
+
+void SerialPortDialog::Impl::RefreshSerialDevices()
+{
+ QList availablePorts = QSerialPortInfo::availablePorts();
+
+ for (auto& port : availablePorts)
+ {
+ LogSerialPortInfo(port);
+ }
+
+ ReadComPortSettings();
+}
+
+void SerialPortDialog::Impl::ReadComPortSettings()
+{
+#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,
+ (LPBYTE) &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_->debug("Port {} has data \"{}\"", portName, portData);
+ }
+
+ valueNameSize = maxValueNameSize;
+ valueDataSize = sizeof(valueData);
+ ++dwIndex;
+ }
+
+ RegCloseKey(hkResult);
+ }
+ else
+ {
+ logger_->warn("Could not open COM port settings registry key: {}",
+ status);
+ }
+#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..8bd3ee2c
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.hpp
@@ -0,0 +1,35 @@
+#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();
+
+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..87065d6a
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.ui
@@ -0,0 +1,92 @@
+
+
+ SerialPortDialog
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Select Serial Port
+
+
+ -
+
+
+ -
+
+
+
+ 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
+
+
+
+
+