#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_->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(); 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_->debug( "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_->debug("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