Add macOS USB UART support
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled

This commit is contained in:
2026-07-18 12:03:38 +03:00
parent 851642296a
commit 049f6301e8
8 changed files with 279 additions and 48 deletions
+16 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20) 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 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) 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) 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" qt_add_resources(location_plotter "app_resources"
PREFIX "/" PREFIX "/"
FILES resources/ankasys-logo.png resources/land-110m.json 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) add_test(NAME location_main_window_tests COMMAND location_main_window_tests)
set_tests_properties(location_main_window_tests PROPERTIES set_tests_properties(location_main_window_tests PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache") ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-test-cache")
install(TARGETS location_plotter
BUNDLE DESTINATION .
RUNTIME DESTINATION bin
)
+50 -4
View File
@@ -1,7 +1,7 @@
# Location Plotter # Location Plotter
Qt6/C++ GUI that listens to `/dev/ttyUSB2`, parses normal debug lines, and plots live Qt6/C++ GUI that listens to a USB UART on Linux or macOS, parses normal debug lines,
locations on a map. and plots live locations on a map.
The application also includes an interactive 3D Earth/orbit view. It downloads the current 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 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 Desteklenen senaryolar `stationary`, `pedestrian`, `car`, `aircraft`; GNSS
periyotları 200/500/1000 ms, IMU periyotları 19/20/200/300 ms'dir. 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'da kurulum
Ubuntu 22.04 ve daha yeni surumlerde gerekli paketleri kurmak, uygulamayi derlemek ve 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, calistirilmalidir. Harita karolari ve guncel RINEX verisi icin internet erisimi gerekir,
ancak son gecerli RINEX verisi ve harita onbellegiyle cevrimdisi kullanim da desteklenir. 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 Port secici Linux'ta `/dev/ttyUSB*` ve `/dev/ttyACM*`; macOS'ta USB ile ilgili
`/dev/ttyUSB2` and the default baud rate is `115200`. `/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: If the port cannot be opened, make sure the user has serial access:
+24 -7
View File
@@ -5,29 +5,46 @@ cd "$(dirname "$0")"
print_qt_install_hint() { print_qt_install_hint() {
echo echo
echo "Qt6 bulunamadi. Ubuntu'da gerekli paketleri kurmak icin:" if [[ "$(uname -s)" == "Darwin" ]]; then
echo " sudo apt update" echo "Qt6 bulunamadi. macOS'ta gerekli paketleri kurmak icin:"
echo " sudo apt install build-essential cmake qt6-base-dev" 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)" project_dir="$(pwd -P)"
if [[ -f ./build/CMakeCache.txt ]]; then if [[ -f ./build/CMakeCache.txt ]]; then
cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)" cached_source="$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' ./build/CMakeCache.txt)"
if [[ -n "$cached_source" && "$cached_source" != "$project_dir" ]]; then cached_system="$(sed -n 's/^CMAKE_SYSTEM_NAME:INTERNAL=//p' ./build/CMakeCache.txt)"
echo "Build klasoru farkli bir proje yoluna ait, yeniden olusturuluyor:" host_system="$(uname -s)"
echo " $cached_source" 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 rm -rf ./build
fi fi
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 print_qt_install_hint
exit 1 exit 1
fi fi
cmake --build build 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)" missing_libs="$(ldd ./build/location_plotter 2>/dev/null | awk '/not found/ { print $1 }' || true)"
if [[ -n "$missing_libs" ]]; then if [[ -n "$missing_libs" ]]; then
echo "Eksik paylasimli kutuphaneler:" echo "Eksik paylasimli kutuphaneler:"
Executable
+47
View File
@@ -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."
+79 -13
View File
@@ -212,6 +212,45 @@ QString optionalDopText(const std::optional<double> &value)
return QString::number(*value, 'f', 3); 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) void mergeDopValues(DopValues &target, const DopValues &source)
{ {
if (source.gdop.has_value()) { 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) void MainWindow::handleSerialLine(const QString &line)
{ {
static const QRegularExpression solveStartRegex( static const QRegularExpression solveStartRegex(
@@ -727,19 +794,16 @@ void MainWindow::buildUi()
m_portCombo = new QComboBox(this); m_portCombo = new QComboBox(this);
m_portCombo->setObjectName(QStringLiteral("portSelector")); m_portCombo->setObjectName(QStringLiteral("portSelector"));
m_portCombo->setMinimumWidth(165); m_portCombo->setMinimumWidth(220);
m_portCombo->setEditable(false); m_portCombo->setEditable(true);
QDir deviceDirectory(QStringLiteral("/dev")); m_portCombo->setToolTip(QStringLiteral(
const QStringList deviceNames = deviceDirectory.entryList( "USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
{QStringLiteral("ttyUSB*")}, refreshSerialPorts();
QDir::System | QDir::Files | QDir::Readable, QDir::Name);
for (const QString &deviceName : deviceNames) { auto *refreshPortsButton = new QToolButton(this);
m_portCombo->addItem(QStringLiteral("/dev/") + deviceName); refreshPortsButton->setObjectName(QStringLiteral("refreshPortsButton"));
} refreshPortsButton->setText(QStringLiteral("Yenile"));
if (m_portCombo->findText(QStringLiteral("/dev/ttyUSB2")) < 0) { refreshPortsButton->setToolTip(QStringLiteral("Seri port listesini yenile"));
m_portCombo->addItem(QStringLiteral("/dev/ttyUSB2"));
}
m_portCombo->setCurrentText(QStringLiteral("/dev/ttyUSB2"));
m_baudCombo = new QComboBox(this); m_baudCombo = new QComboBox(this);
for (const int baud : {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}) { 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(new QLabel(QStringLiteral("Port"), this));
toolbarLayout->addWidget(m_portCombo); toolbarLayout->addWidget(m_portCombo);
toolbarLayout->addWidget(refreshPortsButton);
toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this)); toolbarLayout->addWidget(new QLabel(QStringLiteral("Baud"), this));
toolbarLayout->addWidget(m_baudCombo); toolbarLayout->addWidget(m_baudCombo);
toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this)); toolbarLayout->addWidget(new QLabel(QStringLiteral("Max"), this));
@@ -996,6 +1061,7 @@ void MainWindow::buildUi()
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial); connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::toggleSerial);
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog); connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts);
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this, connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints); &MainWindow::trimToMaxPoints);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow); connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
+1
View File
@@ -43,6 +43,7 @@ private slots:
void updateReferenceLocation(); void updateReferenceLocation();
void flushUiUpdates(); void flushUiUpdates();
void openTelemetryLog(); void openTelemetryLog();
void refreshSerialPorts();
private: private:
void buildUi(); void buildUi();
+57 -21
View File
@@ -10,38 +10,70 @@
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#ifdef Q_OS_MACOS
#include <IOKit/serial/ioss.h>
#endif
namespace { namespace {
constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024; constexpr qsizetype MaxBytesPerReadCycle = 64 * 1024;
speed_t baudToSpeed(int baudRate) bool applyBaudRate(termios &options, int baudRate)
{ {
speed_t speed = 0;
switch (baudRate) { switch (baudRate) {
case 1200: case 1200:
return B1200; speed = B1200;
break;
case 2400: case 2400:
return B2400; speed = B2400;
break;
case 4800: case 4800:
return B4800; speed = B4800;
break;
case 9600: case 9600:
return B9600; speed = B9600;
break;
case 19200: case 19200:
return B19200; speed = B19200;
break;
case 38400: case 38400:
return B38400; speed = B38400;
break;
case 57600: case 57600:
return B57600; speed = B57600;
break;
case 115200: case 115200:
return B115200; speed = B115200;
break;
#ifdef B230400
case 230400: case 230400:
return B230400; speed = B230400;
break;
#endif
#ifdef B460800
case 460800: case 460800:
return B460800; speed = B460800;
break;
#endif
#ifdef B921600
case 921600: case 921600:
return B921600; speed = B921600;
break;
#endif
default: 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 } // namespace
@@ -176,12 +208,6 @@ void SerialReader::flushPartialLine()
bool SerialReader::configurePort(int baudRate) 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 {}; termios options {};
if (::tcgetattr(m_fd, &options) != 0) { if (::tcgetattr(m_fd, &options) != 0) {
emitSystemError(QStringLiteral("Port ayarlari okunamadi")); emitSystemError(QStringLiteral("Port ayarlari okunamadi"));
@@ -189,8 +215,10 @@ bool SerialReader::configurePort(int baudRate)
} }
::cfmakeraw(&options); ::cfmakeraw(&options);
::cfsetispeed(&options, speed); if (!applyBaudRate(options, baudRate)) {
::cfsetospeed(&options, speed); emit errorOccurred(QStringLiteral("Bu sistemde desteklenmeyen baud rate: %1").arg(baudRate));
return false;
}
options.c_cflag |= CLOCAL | CREAD; options.c_cflag |= CLOCAL | CREAD;
options.c_cflag &= ~HUPCL; options.c_cflag &= ~HUPCL;
@@ -206,6 +234,14 @@ bool SerialReader::configurePort(int baudRate)
return false; return false;
} }
#ifdef Q_OS_MACOS
speed_t requestedSpeed = static_cast<speed_t>(baudRate);
if (::ioctl(m_fd, IOSSIOSPEED, &requestedSpeed) != 0) {
emitSystemError(QStringLiteral("macOS baud rate uygulanamadi"));
return false;
}
#endif
::tcflush(m_fd, TCIFLUSH); ::tcflush(m_fd, TCIFLUSH);
return true; return true;
} }
+5 -2
View File
@@ -37,7 +37,9 @@ void flushUpdates(MainWindow &window)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
QApplication app(argc, argv); QApplication app(argc, argv);
qputenv("LOCATION_PLOTTER_PORT", "/dev/test-usb-uart");
MainWindow window; MainWindow window;
qunsetenv("LOCATION_PLOTTER_PORT");
auto *table = window.findChild<QTableView *>(); auto *table = window.findChild<QTableView *>();
auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput")); auto *referenceLatInput = window.findChild<QLineEdit *>(QStringLiteral("referenceLatInput"));
@@ -67,10 +69,11 @@ int main(int argc, char **argv)
assert(showRecentPoints != nullptr); assert(showRecentPoints != nullptr);
assert(showAverage != nullptr); assert(showAverage != nullptr);
assert(portSelector != nullptr); assert(portSelector != nullptr);
assert(!portSelector->isEditable()); assert(portSelector->isEditable());
assert(portSelector->count() >= 1); assert(portSelector->count() >= 1);
assert(portSelector->currentText() == QStringLiteral("/dev/test-usb-uart"));
for (int i = 0; i < portSelector->count(); ++i) { 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(!portSelector->itemText(i).trimmed().isEmpty());
} }
assert(map != nullptr); assert(map != nullptr);