#include "SerialPortWatcher.h" #include #include #include #include #ifdef Q_OS_LINUX #include "devices/platform/linux/UdevMonitor.h" #endif namespace devices::serial { namespace { constexpr int kFallbackPollIntervalMs = 60 * 1000; quint16 parseHexQuint16(const QVariant &value) { bool ok = false; const QString stringValue = value.toString(); if (stringValue.isEmpty()) { return 0; } const quint16 parsedValue = stringValue.toUShort(&ok, 16); return ok ? parsedValue : 0; } QString portNameFromSystemLocation(const QString &systemLocation) { if (systemLocation.isEmpty()) { return {}; } const QFileInfo fileInfo(systemLocation); return fileInfo.fileName(); } QVariantMap toVariantMap(const SerialPortInfo &info) { QVariantMap map; map.insert(QStringLiteral("portName"), info.portName); map.insert(QStringLiteral("systemLocation"), info.systemLocation); map.insert(QStringLiteral("description"), info.description); map.insert(QStringLiteral("manufacturer"), info.manufacturer); map.insert(QStringLiteral("serialNumber"), info.serialNumber); map.insert(QStringLiteral("vendorId"), info.vendorId); map.insert(QStringLiteral("productId"), info.productId); return map; } } // namespace SerialPortWatcher::SerialPortWatcher(QObject *parent) : DeviceWatcherBase(parent) { qRegisterMetaType("devices::serial::SerialPortInfo"); } SerialPortWatcher::~SerialPortWatcher() { stopWatching(); } QList SerialPortWatcher::knownPorts() const { return m_knownPorts.values(); } bool SerialPortWatcher::doStart() { #ifdef Q_OS_LINUX startUdevMonitor(); #endif // Q_OS_LINUX if (!m_pollTimer) { m_pollTimer = new QTimer(this); connect(m_pollTimer, &QTimer::timeout, this, &SerialPortWatcher::refreshPorts); } m_pollTimer->setInterval(kFallbackPollIntervalMs); if (!m_pollTimer->isActive()) { m_pollTimer->start(); } refreshPorts(); return true; } void SerialPortWatcher::doStop() { #ifdef Q_OS_LINUX stopUdevMonitor(); #endif if (m_pollTimer) { m_pollTimer->stop(); } m_knownPorts.clear(); } void SerialPortWatcher::emitPortArrived(const SerialPortInfo &info) { emit serialPortArrived(info); emitDeviceArrived(info.portName, toVariantMap(info)); } void SerialPortWatcher::emitPortRemoved(const SerialPortInfo &info) { emit serialPortRemoved(info); emitDeviceRemoved(info.portName, toVariantMap(info)); } void SerialPortWatcher::refreshPorts() { const QList currentInfos = QSerialPortInfo::availablePorts(); QSet currentKeys; for (const QSerialPortInfo &info : currentInfos) { const SerialPortInfo serialInfo = toSerialPortInfo(info); const QString key = portIdentifier(serialInfo); if (key.isEmpty()) { continue; } currentKeys.insert(key); const bool alreadyKnown = m_knownPorts.contains(key); m_knownPorts.insert(key, serialInfo); if (!alreadyKnown) { emitPortArrived(serialInfo); } } const QList knownKeys = m_knownPorts.keys(); for (const QString &knownKey : knownKeys) { if (!currentKeys.contains(knownKey)) { const SerialPortInfo removedInfo = m_knownPorts.take(knownKey); SerialPortInfo infoToEmit = removedInfo; if (!infoToEmit.isValid()) { infoToEmit.portName = portNameFromSystemLocation(knownKey); infoToEmit.systemLocation = knownKey; } emitPortRemoved(infoToEmit); } } } SerialPortInfo SerialPortWatcher::toSerialPortInfo(const QSerialPortInfo &info) const { SerialPortInfo serialInfo; serialInfo.portName = info.portName(); serialInfo.systemLocation = info.systemLocation(); serialInfo.description = info.description(); serialInfo.manufacturer = info.manufacturer(); serialInfo.serialNumber = info.serialNumber(); serialInfo.vendorId = info.hasVendorIdentifier() ? info.vendorIdentifier() : 0; serialInfo.productId = info.hasProductIdentifier() ? info.productIdentifier() : 0; return serialInfo; } #ifdef Q_OS_LINUX bool SerialPortWatcher::startUdevMonitor() { if (m_udevMonitor) { return true; } m_udevMonitor = std::make_unique(this); if (!m_udevMonitor->start(QStringLiteral("tty"))) { m_udevMonitor.reset(); return false; } connect(m_udevMonitor.get(), &devices::platform::linux_os::UdevMonitor::deviceAdded, this, &SerialPortWatcher::handleUdevAdd); connect(m_udevMonitor.get(), &devices::platform::linux_os::UdevMonitor::deviceRemoved, this, &SerialPortWatcher::handleUdevRemove); return true; } void SerialPortWatcher::stopUdevMonitor() { if (!m_udevMonitor) { return; } disconnect(m_udevMonitor.get(), &devices::platform::linux_os::UdevMonitor::deviceAdded, this, &SerialPortWatcher::handleUdevAdd); disconnect(m_udevMonitor.get(), &devices::platform::linux_os::UdevMonitor::deviceRemoved, this, &SerialPortWatcher::handleUdevRemove); m_udevMonitor->stop(); m_udevMonitor.reset(); } void SerialPortWatcher::handleUdevAdd(const QVariantMap &properties) { const SerialPortInfo info = resolvePortFromProperties(properties); if (!info.isValid()) { refreshPorts(); return; } const QString key = portIdentifier(info); if (key.isEmpty()) { refreshPorts(); return; } const bool alreadyKnown = m_knownPorts.contains(key); m_knownPorts.insert(key, info); if (!alreadyKnown) { emitPortArrived(info); } } void SerialPortWatcher::handleUdevRemove(const QVariantMap &properties) { const QString devnode = properties.value(QStringLiteral("devnode")).toString(); const QString portName = properties.value(QStringLiteral("portName")).toString(); SerialPortInfo info = takeKnownPort(portName, devnode); if (!info.isValid()) { info.portName = !portName.isEmpty() ? portName : portNameFromSystemLocation(devnode); info.systemLocation = devnode; } emitPortRemoved(info); } #endif QString SerialPortWatcher::portIdentifier(const SerialPortInfo &info) const { return portIdentifier(info.portName, info.systemLocation); } QString SerialPortWatcher::portIdentifier(const QString &portName, const QString &systemLocation) const { return !systemLocation.isEmpty() ? systemLocation : portName; } SerialPortInfo SerialPortWatcher::resolvePortFromProperties(const QVariantMap &properties) const { QString devnode = properties.value(QStringLiteral("devnode")).toString(); QString portName = properties.value(QStringLiteral("portName")).toString(); if (portName.isEmpty()) { portName = portNameFromSystemLocation(devnode); } const QList availablePorts = QSerialPortInfo::availablePorts(); for (const QSerialPortInfo &info : availablePorts) { if ((!devnode.isEmpty() && info.systemLocation() == devnode) || (!portName.isEmpty() && info.portName() == portName)) { return toSerialPortInfo(info); } } SerialPortInfo fallback; fallback.portName = portName; fallback.systemLocation = devnode; fallback.description = properties.value(QStringLiteral("description")).toString(); fallback.serialNumber = properties.value(QStringLiteral("serialNumber")).toString(); fallback.vendorId = parseHexQuint16(properties.value(QStringLiteral("vendorId"))); fallback.productId = parseHexQuint16(properties.value(QStringLiteral("productId"))); return fallback; } SerialPortInfo SerialPortWatcher::takeKnownPort(const QString &portName, const QString &systemLocation) { const QString key = portIdentifier(portName, systemLocation); if (!key.isEmpty()) { auto it = m_knownPorts.find(key); if (it != m_knownPorts.end()) { SerialPortInfo info = it.value(); m_knownPorts.erase(it); return info; } } if (!systemLocation.isEmpty()) { for (auto it = m_knownPorts.begin(); it != m_knownPorts.end(); ++it) { if (it.value().systemLocation == systemLocation) { SerialPortInfo info = it.value(); m_knownPorts.erase(it); return info; } } } if (!portName.isEmpty()) { for (auto it = m_knownPorts.begin(); it != m_knownPorts.end(); ++it) { if (it.value().portName == portName) { SerialPortInfo info = it.value(); m_knownPorts.erase(it); return info; } } } SerialPortInfo fallback; fallback.portName = portName; fallback.systemLocation = systemLocation; return fallback; } } // namespace devices::serial