From 049f6301e8a9bbfd6e3436af6d2cb7e6da89ace2 Mon Sep 17 00:00:00 2001 From: onur Date: Sat, 18 Jul 2026 12:03:38 +0300 Subject: [PATCH] Add macOS USB UART support --- CMakeLists.txt | 17 ++++++- README.md | 54 ++++++++++++++++++++-- run.sh | 31 ++++++++++--- setup-macos.sh | 47 +++++++++++++++++++ src/MainWindow.cpp | 92 +++++++++++++++++++++++++++++++------ src/MainWindow.h | 1 + src/SerialReader.cpp | 78 ++++++++++++++++++++++--------- tests/main_window_tests.cpp | 7 ++- 8 files changed, 279 insertions(+), 48 deletions(-) create mode 100755 setup-macos.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 8343a68..ed9a55a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(location_plotter VERSION 0.1.0 LANGUAGES CXX) +project(location_plotter VERSION 0.2.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -31,6 +31,16 @@ add_executable(location_plotter target_link_libraries(location_plotter PRIVATE Qt6::Core Qt6::Widgets Qt6::Network ZLIB::ZLIB) +if(APPLE) + set_target_properties(location_plotter PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME "Location Plotter" + MACOSX_BUNDLE_GUI_IDENTIFIER "com.ankasys.location-plotter" + MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" + ) +endif() + qt_add_resources(location_plotter "app_resources" PREFIX "/" FILES resources/ankasys-logo.png resources/land-110m.json @@ -82,3 +92,8 @@ endif() add_test(NAME location_main_window_tests COMMAND location_main_window_tests) set_tests_properties(location_main_window_tests PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache") + +install(TARGETS location_plotter + BUNDLE DESTINATION . + RUNTIME DESTINATION bin +) diff --git a/README.md b/README.md index f291b0e..6e490b2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Location Plotter -Qt6/C++ GUI that listens to `/dev/ttyUSB2`, parses normal debug lines, and plots live -locations on a map. +Qt6/C++ GUI that listens to a USB UART on Linux or macOS, parses normal debug lines, +and plots live locations on a map. The application also includes an interactive 3D Earth/orbit view. It downloads the current stream-derived RINEX 3 navigation file from the BKG/IGS archive at startup and every 15 @@ -95,6 +95,50 @@ make platform_scenario_matrix_test Desteklenen senaryolar `stationary`, `pedestrian`, `car`, `aircraft`; GNSS periyotları 200/500/1000 ms, IMU periyotları 19/20/200/300 ms'dir. +## macOS'ta kurulum (MacBook Air, Apple Silicon veya Intel) + +Guncel macOS surumunde once [Homebrew](https://brew.sh) kurulu olmalidir. Ardindan: + +```bash +cd location_plotter +chmod +x setup-macos.sh run.sh +./setup-macos.sh +./run.sh +``` + +Betik Homebrew ile guncel `cmake` ve `qt` paketlerini kurar, yerel mimari icin +`location_plotter.app` paketini derler, gerekli Qt kutuphanelerini uygulama paketine +ekler ve testleri calistirir. + +macOS USB UART aygitlarini genellikle iki adla gosterir: + +```text +/dev/cu.usbserial-XXXXXXXX +/dev/tty.usbserial-XXXXXXXX +``` + +veya bir CDC aygitinda: + +```text +/dev/cu.usbmodemXXXX +/dev/tty.usbmodemXXXX +``` + +Uygulama her iki bicimi de algilar. Mümkünse `cu.usb...` yolunu secin; macOS'ta +disariya baglanti baslatan uygulamalar icin uygun olan aygit budur. UART uygulama +acikken takildiysa port alaninin yanindaki `Yenile` dugmesine basin. Otomatik +algilanmayan bir yol port alanina elle de yazilabilir. + +Istenen port baslatmadan once ortam degiskeniyle de secilebilir: + +```bash +LOCATION_PLOTTER_PORT=/dev/cu.usbserial-XXXXXXXX ./run.sh +``` + +Port hic gorunmuyorsa `ls /dev/cu.usb* /dev/tty.usb*` ile macOS'un aygiti olusturup +olusturmadigini kontrol edin. Hic sonuc yoksa kabloyu/veri baglantisini ve UART +donaniminizin macOS surucusunu kontrol edin. + ## Ubuntu'da kurulum Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve @@ -140,8 +184,10 @@ Uygulama Qt tabanli bir masaustu arayuzudur; grafik oturumu olan bir Ubuntu maki calistirilmalidir. Harita karolari ve guncel RINEX verisi icin internet erisimi gerekir, ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da desteklenir. -The port selector lists only detected `/dev/ttyUSB*` devices. The default selection is -`/dev/ttyUSB2` and the default baud rate is `115200`. +Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili +`/dev/cu.*` ve `/dev/tty.*` aygitlarini listeler. Hic aygit algilanmazsa Linux'ta +`/dev/ttyUSB2`, macOS'ta `/dev/cu.usbserial` duzenlenebilir bir varsayilan yol olarak +gosterilir. Varsayilan baud rate `115200`'dur. If the port cannot be opened, make sure the user has serial access: diff --git a/run.sh b/run.sh index 72b648e..8113a5c 100755 --- a/run.sh +++ b/run.sh @@ -5,29 +5,46 @@ cd "$(dirname "$0")" print_qt_install_hint() { echo - echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:" - echo " sudo apt update" - echo " sudo apt install build-essential cmake qt6-base-dev" + if [[ "$(uname -s)" == "Darwin" ]]; then + echo "Qt6 bulunamadi. macOS'ta gerekli paketleri kurmak icin:" + echo " brew install cmake qt" + echo "veya doğrudan: ./setup-macos.sh" + else + echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:" + echo " sudo apt update" + echo " sudo apt install build-essential cmake qt6-base-dev" + fi } project_dir="$(pwd -P)" if [[ -f ./build/CMakeCache.txt ]]; then cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)" - if [[ -n "$cached_source" && "$cached_source" != "$project_dir" ]]; then - echo "Build klasoru farkli bir proje yoluna ait, yeniden olusturuluyor:" - echo " $cached_source" + cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' ./build/CMakeCache.txt)" + host_system="$(uname -s)" + if [[ ( -n "$cached_source" && "$cached_source" != "$project_dir" ) \ + || ( -n "$cached_system" && "$cached_system" != "$host_system" ) ]]; then + echo "Build klasoru farkli bir proje yolu veya sisteme ait, yeniden olusturuluyor." rm -rf ./build fi fi -if ! cmake -S . -B build; then +cmake_args=(-S . -B build) +if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then + cmake_args+=("-DCMAKE_PREFIX_PATH=$(brew --prefix qt)") +fi + +if ! cmake "${cmake_args[@]}"; then print_qt_install_hint exit 1 fi cmake --build build +if [[ "$(uname -s)" == "Darwin" ]]; then + exec ./build/location_plotter.app/Contents/MacOS/location_plotter "$@" +fi + missing_libs="$(ldd ./build/location_plotter 2>/dev/null | awk '/not found/ { print $1 }' || true)" if [[ -n "$missing_libs" ]]; then echo "Eksik paylasimli kutuphaneler:" diff --git a/setup-macos.sh b/setup-macos.sh new file mode 100755 index 0000000..0a4651f --- /dev/null +++ b/setup-macos.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Bu kurulum betigi macOS icindir." >&2 + exit 1 +fi + +if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew bulunamadi. Once https://brew.sh adresindeki Homebrew kurulumunu tamamlayin." >&2 + exit 1 +fi + +echo "Gerekli macOS paketleri kuruluyor..." +brew install cmake qt + +qt_prefix="$(brew --prefix qt)" +parallel_jobs="$(sysctl -n hw.logicalcpu 2>/dev/null || echo 4)" + +if [[ -f build/CMakeCache.txt ]]; then + cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' build/CMakeCache.txt)" + if [[ -n "$cached_system" && "$cached_system" != "Darwin" ]]; then + echo "Baska bir sisteme ait build klasoru yeniden olusturuluyor." + rm -rf build + fi +fi + +echo "Uygulama derleniyor..." +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$qt_prefix" +cmake --build build --parallel "$parallel_jobs" + +echo "Testler calistiriliyor..." +ctest --test-dir build --output-on-failure + +echo "macOS uygulama paketi hazirlaniyor..." +"$qt_prefix/bin/macdeployqt" build/location_plotter.app -always-overwrite + +echo +echo "Kurulum tamamlandi. Uygulamayi baslatmak icin:" +echo " ./run.sh" +echo +echo "USB UART icin port listesinden /dev/cu.usb... aygitini secin." +echo "/dev/tty.usb... gorunuyorsa o yol da desteklenir." diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f7b9cf1..0abe336 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -212,6 +212,45 @@ QString optionalDopText(const std::optional &value) return QString::number(*value, 'f', 3); } +QStringList detectedSerialPorts() +{ + const QDir deviceDirectory(QStringLiteral("/dev")); + QStringList patterns; +#ifdef Q_OS_MACOS + // macOS exposes USB UARTs as paired callout (cu.*) and terminal (tty.*) + // devices. Prefer cu.* because it does not wait for carrier detect. + patterns = { + QStringLiteral("cu.usb*"), QStringLiteral("cu.wchusbserial*"), + QStringLiteral("cu.SLAB_USBtoUART*"), QStringLiteral("cu.*USB*"), + QStringLiteral("tty.usb*"), QStringLiteral("tty.wchusbserial*"), + QStringLiteral("tty.SLAB_USBtoUART*"), QStringLiteral("tty.*USB*")}; +#else + patterns = {QStringLiteral("ttyUSB*"), QStringLiteral("ttyACM*")}; +#endif + + QStringList ports; + for (const QString &pattern : patterns) { + const QStringList names = deviceDirectory.entryList( + {pattern}, QDir::System | QDir::Files, QDir::Name); + for (const QString &name : names) { + const QString path = QStringLiteral("/dev/") + name; + if (!ports.contains(path)) { + ports.push_back(path); + } + } + } + return ports; +} + +QString fallbackSerialPort() +{ +#ifdef Q_OS_MACOS + return QStringLiteral("/dev/cu.usbserial"); +#else + return QStringLiteral("/dev/ttyUSB2"); +#endif +} + void mergeDopValues(DopValues &target, const DopValues &source) { if (source.gdop.has_value()) { @@ -425,6 +464,34 @@ void MainWindow::openTelemetryLog() } } +void MainWindow::refreshSerialPorts() +{ + const QString previousPort = m_portCombo->currentText().trimmed(); + const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed(); + const QStringList detectedPorts = detectedSerialPorts(); + + m_portCombo->clear(); + for (const QString &port : detectedPorts) { + m_portCombo->addItem(port); + } + + QString selectedPort; + if (!configuredPort.isEmpty()) { + selectedPort = configuredPort; + } else if (!previousPort.isEmpty() && detectedPorts.contains(previousPort)) { + selectedPort = previousPort; + } else if (!detectedPorts.isEmpty()) { + selectedPort = detectedPorts.first(); + } else { + selectedPort = previousPort.isEmpty() ? fallbackSerialPort() : previousPort; + } + + if (m_portCombo->findText(selectedPort) < 0) { + m_portCombo->addItem(selectedPort); + } + m_portCombo->setCurrentText(selectedPort); +} + void MainWindow::handleSerialLine(const QString &line) { static const QRegularExpression solveStartRegex( @@ -727,19 +794,16 @@ void MainWindow::buildUi() m_portCombo = new QComboBox(this); m_portCombo->setObjectName(QStringLiteral("portSelector")); - m_portCombo->setMinimumWidth(165); - m_portCombo->setEditable(false); - QDir deviceDirectory(QStringLiteral("/dev")); - const QStringList deviceNames = deviceDirectory.entryList( - {QStringLiteral("ttyUSB*")}, - QDir::System | QDir::Files | QDir::Readable, QDir::Name); - for (const QString &deviceName : deviceNames) { - m_portCombo->addItem(QStringLiteral("/dev/") + deviceName); - } - if (m_portCombo->findText(QStringLiteral("/dev/ttyUSB2")) < 0) { - m_portCombo->addItem(QStringLiteral("/dev/ttyUSB2")); - } - m_portCombo->setCurrentText(QStringLiteral("/dev/ttyUSB2")); + m_portCombo->setMinimumWidth(220); + m_portCombo->setEditable(true); + m_portCombo->setToolTip(QStringLiteral( + "USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir.")); + refreshSerialPorts(); + + auto *refreshPortsButton = new QToolButton(this); + refreshPortsButton->setObjectName(QStringLiteral("refreshPortsButton")); + refreshPortsButton->setText(QStringLiteral("Yenile")); + refreshPortsButton->setToolTip(QStringLiteral("Seri port listesini yenile")); m_baudCombo = new QComboBox(this); for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) { @@ -773,6 +837,7 @@ void MainWindow::buildUi() toolbarLayout->addWidget(new QLabel(QStringLiteral("Port"), this)); toolbarLayout->addWidget(m_portCombo); + toolbarLayout->addWidget(refreshPortsButton); toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this)); toolbarLayout->addWidget(m_baudCombo); toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this)); @@ -996,6 +1061,7 @@ void MainWindow::buildUi() connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial); connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog); + connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts); connect(m_maxPointsSpin, qOverload(&QSpinBox::valueChanged), this, &MainWindow::trimToMaxPoints); connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow); diff --git a/src/MainWindow.h b/src/MainWindow.h index fceceee..3f6ee15 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -43,6 +43,7 @@ private slots: void updateReferenceLocation(); void flushUiUpdates(); void openTelemetryLog(); + void refreshSerialPorts(); private: void buildUi(); diff --git a/src/SerialReader.cpp b/src/SerialReader.cpp index ccb7050..06a37bb 100644 --- a/src/SerialReader.cpp +++ b/src/SerialReader.cpp @@ -10,38 +10,70 @@ #include #include +#ifdef Q_OS_MACOS +#include +#endif + namespace { constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024; -speed_t baudToSpeed(int baudRate) +bool applyBaudRate(termios &options, int baudRate) { + speed_t speed = 0; switch (baudRate) { case 1200: - return B1200; + speed = B1200; + break; case 2400: - return B2400; + speed = B2400; + break; case 4800: - return B4800; + speed = B4800; + break; case 9600: - return B9600; + speed = B9600; + break; case 19200: - return B19200; + speed = B19200; + break; case 38400: - return B38400; + speed = B38400; + break; case 57600: - return B57600; + speed = B57600; + break; case 115200: - return B115200; + speed = B115200; + break; +#ifdef B230400 case 230400: - return B230400; + speed = B230400; + break; +#endif +#ifdef B460800 case 460800: - return B460800; + speed = B460800; + break; +#endif +#ifdef B921600 case 921600: - return B921600; + speed = B921600; + break; +#endif default: - return 0; +#ifdef Q_OS_MACOS + // IOSSIOSPEED below applies the exact requested rate. A valid termios + // placeholder is still required before that ioctl can be issued. + speed = B9600; + break; +#else + return false; +#endif } + + return ::cfsetispeed(&options, speed) == 0 + && ::cfsetospeed(&options, speed) == 0; } } // namespace @@ -176,12 +208,6 @@ void SerialReader::flushPartialLine() bool SerialReader::configurePort(int baudRate) { - const speed_t speed = baudToSpeed(baudRate); - if (speed == 0) { - emit errorOccurred(QStringLiteral("Desteklenmeyen baud rate: %1").arg(baudRate)); - return false; - } - termios options {}; if (::tcgetattr(m_fd, &options) != 0) { emitSystemError(QStringLiteral("Port ayarlari okunamadi")); @@ -189,8 +215,10 @@ bool SerialReader::configurePort(int baudRate) } ::cfmakeraw(&options); - ::cfsetispeed(&options, speed); - ::cfsetospeed(&options, speed); + if (!applyBaudRate(options, baudRate)) { + emit errorOccurred(QStringLiteral("Bu sistemde desteklenmeyen baud rate: %1").arg(baudRate)); + return false; + } options.c_cflag |= CLOCAL | CREAD; options.c_cflag &= ~HUPCL; @@ -206,6 +234,14 @@ bool SerialReader::configurePort(int baudRate) return false; } +#ifdef Q_OS_MACOS + speed_t requestedSpeed = static_cast(baudRate); + if (::ioctl(m_fd, IOSSIOSPEED, &requestedSpeed) != 0) { + emitSystemError(QStringLiteral("macOS baud rate uygulanamadi")); + return false; + } +#endif + ::tcflush(m_fd, TCIFLUSH); return true; } diff --git a/tests/main_window_tests.cpp b/tests/main_window_tests.cpp index b90cad2..bb40f56 100644 --- a/tests/main_window_tests.cpp +++ b/tests/main_window_tests.cpp @@ -37,7 +37,9 @@ void flushUpdates(MainWindow &window) int main(int argc, char **argv) { QApplication app(argc, argv); + qputenv("LOCATION_PLOTTER_PORT", "/dev/test-usb-uart"); MainWindow window; + qunsetenv("LOCATION_PLOTTER_PORT"); auto *table = window.findChild(); auto *referenceLatInput = window.findChild(QStringLiteral("referenceLatInput")); @@ -67,10 +69,11 @@ int main(int argc, char **argv) assert(showRecentPoints != nullptr); assert(showAverage != nullptr); assert(portSelector != nullptr); - assert(!portSelector->isEditable()); + assert(portSelector->isEditable()); assert(portSelector->count() >= 1); + assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart")); for (int i = 0; i < portSelector->count(); ++i) { - assert(portSelector->itemText(i).startsWith(QStringLiteral("/dev/ttyUSB"))); + assert(portSelector->itemText(i).startsWith(QStringLiteral("/dev/"))); assert(!portSelector->itemText(i).trimmed().isEmpty()); } assert(map != nullptr);