2026-06-26 17:55:15 +08:00
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
#include <QDir>
|
|
|
|
|
|
#include <QFileInfo>
|
|
|
|
|
|
#include <QFile>
|
|
|
|
|
|
#include <QJsonObject>
|
2026-07-11 15:28:36 +08:00
|
|
|
|
#include <QStringList>
|
2026-06-26 17:55:15 +08:00
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(__linux__)
|
|
|
|
|
|
#include <signal.h>
|
|
|
|
|
|
#include <execinfo.h>
|
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
#include <sys/syscall.h>
|
|
|
|
|
|
#include <dlfcn.h>
|
|
|
|
|
|
#include <pthread.h>
|
|
|
|
|
|
#include <exception>
|
|
|
|
|
|
#include <typeinfo>
|
|
|
|
|
|
#include <cxxabi.h>
|
|
|
|
|
|
#include <thread>
|
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#include "DroneScrewServerPresenter.h"
|
|
|
|
|
|
#include "DroneScrewZmqProtocol.h"
|
|
|
|
|
|
#include "Version.h"
|
|
|
|
|
|
#include "VrLog.h"
|
|
|
|
|
|
#include "VrError.h"
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(__linux__)
|
|
|
|
|
|
// ========== exit() 拦截 ==========
|
|
|
|
|
|
// libff_media.so 内部线程在错误条件下会直接调用 exit() 导致整个进程崩溃。
|
|
|
|
|
|
// 我们拦截 exit() 调用:主线程的 exit 正常放行,子线程的 exit 改为只结束该线程。
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
// 真正的 exit 函数指针,启动时初始化
|
|
|
|
|
|
typedef void (*real_exit_t)(int);
|
|
|
|
|
|
real_exit_t g_real_exit = nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
bool g_exitHookInstalled = false;
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
extern "C" void exit(int status)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 递归保护:如果 exit hook 内部再次调用 exit,直接走 _exit
|
|
|
|
|
|
static thread_local bool g_inExitHook = false;
|
|
|
|
|
|
if (g_inExitHook)
|
|
|
|
|
|
_exit(status);
|
|
|
|
|
|
g_inExitHook = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 首次调用时获取真正的 exit 地址
|
|
|
|
|
|
if (!g_real_exit)
|
|
|
|
|
|
{
|
|
|
|
|
|
g_real_exit = reinterpret_cast<real_exit_t>(dlsym(RTLD_NEXT, "exit"));
|
|
|
|
|
|
if (!g_real_exit)
|
|
|
|
|
|
_exit(status); // 极端情况:拿不到真 exit,直接用 _exit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否为主线程(主线程的 tid == pid)
|
|
|
|
|
|
const pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
|
|
|
|
|
const pid_t pid = getpid();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取线程 ID 字符串
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
|
oss << std::this_thread::get_id();
|
|
|
|
|
|
|
|
|
|
|
|
if (tid == pid)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 主线程调用 exit() — 正常退出流程
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[EXIT-HOOK] exit(%d) called from MAIN thread tid=%d, passing through to real exit\n",
|
|
|
|
|
|
status, static_cast<int>(tid));
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
g_real_exit(status);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 子线程调用 exit() — 记录 backtrace 并仅结束该线程
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[EXIT-HOOK] exit(%d) called from NON-MAIN thread! tid=%d c++id=%s\n",
|
|
|
|
|
|
status, static_cast<int>(tid), oss.str().c_str());
|
|
|
|
|
|
LOG_ERROR("[EXIT-HOOK] Preventing process exit — terminating this thread only.\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 打印调用栈
|
|
|
|
|
|
void* array[30];
|
|
|
|
|
|
const int size = backtrace(array, 30);
|
|
|
|
|
|
LOG_ERROR("[EXIT-HOOK-BACKTRACE] %d frames:\n", size);
|
|
|
|
|
|
char** messages = backtrace_symbols(array, size);
|
|
|
|
|
|
if (messages)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < size; i++)
|
|
|
|
|
|
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
|
|
|
|
|
free(messages);
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 仅退出当前线程,进程继续运行
|
|
|
|
|
|
pthread_exit(nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
QString procStatusValue(const char* key)
|
|
|
|
|
|
{
|
|
|
|
|
|
#if defined(__linux__)
|
|
|
|
|
|
QFile f(QStringLiteral("/proc/self/status"));
|
|
|
|
|
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
|
|
|
|
const QByteArray prefix = QByteArray(key) + ':';
|
|
|
|
|
|
while (!f.atEnd())
|
|
|
|
|
|
{
|
|
|
|
|
|
const QByteArray line = f.readLine().trimmed();
|
|
|
|
|
|
if (line.startsWith(prefix))
|
|
|
|
|
|
return QString::fromLatin1(line.mid(prefix.size()).trimmed());
|
|
|
|
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
Q_UNUSED(key);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
return QString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(__linux__)
|
|
|
|
|
|
|
|
|
|
|
|
std::string getThreadIdStr()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
|
oss << std::this_thread::get_id();
|
|
|
|
|
|
return oss.str();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void terminateHandler()
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] std::terminate() called! thread=%s\n", getThreadIdStr().c_str());
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试获取未捕获的异常信息
|
|
|
|
|
|
auto eptr = std::current_exception();
|
|
|
|
|
|
if (eptr)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
std::rethrow_exception(eptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (const std::exception& e)
|
|
|
|
|
|
{
|
|
|
|
|
|
int status = 0;
|
|
|
|
|
|
char* demangled = abi::__cxa_demangle(typeid(e).name(), nullptr, nullptr, &status);
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] uncaught exception type: %s\n",
|
|
|
|
|
|
(status == 0 && demangled) ? demangled : typeid(e).name());
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] what(): %s\n", e.what());
|
|
|
|
|
|
if (demangled) free(demangled);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (...)
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] uncaught exception (unknown type, not std::exception)\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] no active exception (terminate called without exception)\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打印调用栈
|
|
|
|
|
|
void* array[30];
|
|
|
|
|
|
const int size = backtrace(array, 30);
|
|
|
|
|
|
LOG_ERROR("[TERMINATE-BACKTRACE] Stack trace (%d frames):\n", size);
|
|
|
|
|
|
|
|
|
|
|
|
char** messages = backtrace_symbols(array, size);
|
|
|
|
|
|
if (messages)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < size; i++)
|
|
|
|
|
|
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
|
|
|
|
|
free(messages);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR(" (backtrace_symbols failed)\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[TERMINATE] aborting...\n");
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新日志后调用默认 abort
|
|
|
|
|
|
std::abort();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void atexitHandler()
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[ATEXIT] Program is exiting normally!\n");
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 打印调用栈看是谁调用了 exit
|
|
|
|
|
|
void* array[30];
|
|
|
|
|
|
const int size = backtrace(array, 30);
|
|
|
|
|
|
LOG_ERROR("[ATEXIT-BACKTRACE] Exit call stack (%d frames):\n", size);
|
|
|
|
|
|
|
|
|
|
|
|
char** messages = backtrace_symbols(array, size);
|
|
|
|
|
|
if (messages)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < size; i++)
|
|
|
|
|
|
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
|
|
|
|
|
free(messages);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR(" (backtrace_symbols failed)\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void signalHandler(int sig)
|
|
|
|
|
|
{
|
|
|
|
|
|
const char* sigName = "UNKNOWN";
|
|
|
|
|
|
switch (sig)
|
|
|
|
|
|
{
|
|
|
|
|
|
case SIGSEGV: sigName = "SIGSEGV (Segmentation Fault)"; break;
|
|
|
|
|
|
case SIGABRT: sigName = "SIGABRT (Abort)"; break;
|
|
|
|
|
|
case SIGTERM: sigName = "SIGTERM (Termination)"; break;
|
|
|
|
|
|
case SIGINT: sigName = "SIGINT (Interrupt)"; break;
|
|
|
|
|
|
case SIGILL: sigName = "SIGILL (Illegal Instruction)"; break;
|
|
|
|
|
|
case SIGFPE: sigName = "SIGFPE (Floating Point Exception)"; break;
|
|
|
|
|
|
case SIGBUS: sigName = "SIGBUS (Bus Error)"; break;
|
|
|
|
|
|
case SIGPIPE: sigName = "SIGPIPE (Broken Pipe)"; break;
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
LOG_ERROR("[SIGNAL] Caught signal %d: %s\n", sig, sigName);
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 打印调用栈
|
|
|
|
|
|
void* array[30];
|
|
|
|
|
|
const int size = backtrace(array, 30);
|
|
|
|
|
|
LOG_ERROR("[BACKTRACE] Stack trace (%d frames):\n", size);
|
|
|
|
|
|
|
|
|
|
|
|
char** messages = backtrace_symbols(array, size);
|
|
|
|
|
|
if (messages)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < size; i++)
|
|
|
|
|
|
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
|
|
|
|
|
free(messages);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR(" (backtrace_symbols failed)\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_ERROR("========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 对于致命信号,调用默认处理器
|
|
|
|
|
|
signal(sig, SIG_DFL);
|
|
|
|
|
|
raise(sig);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char* argv[])
|
|
|
|
|
|
{
|
|
|
|
|
|
QCoreApplication app(argc, argv);
|
|
|
|
|
|
app.setApplicationName(DRONESCREWSERVER_PRODUCT_NAME);
|
|
|
|
|
|
app.setApplicationVersion(DRONESCREWSERVER_VERSION_STRING);
|
|
|
|
|
|
app.setOrganizationName(DRONESCREWSERVER_COMPANY_NAME);
|
|
|
|
|
|
|
|
|
|
|
|
VrLogUtils::InitLog();
|
|
|
|
|
|
VrLogUtils::EnableTime(false);
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(__linux__)
|
|
|
|
|
|
// 注册 std::terminate 处理器(捕获未处理 C++ 异常,优先级最高)
|
|
|
|
|
|
std::set_terminate(terminateHandler);
|
|
|
|
|
|
LOG_INFO("[TERMINATE] std::terminate handler registered, main thread=%s\n",
|
|
|
|
|
|
getThreadIdStr().c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// 注册 atexit 处理器(捕获正常退出)
|
|
|
|
|
|
atexit(atexitHandler);
|
|
|
|
|
|
LOG_INFO("[ATEXIT] Exit handler registered\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 注册信号处理器
|
|
|
|
|
|
signal(SIGSEGV, signalHandler); // 段错误
|
|
|
|
|
|
signal(SIGABRT, signalHandler); // 异常终止
|
|
|
|
|
|
signal(SIGTERM, signalHandler); // 终止信号
|
|
|
|
|
|
signal(SIGINT, signalHandler); // 中断信号 (Ctrl+C)
|
|
|
|
|
|
signal(SIGILL, signalHandler); // 非法指令
|
|
|
|
|
|
signal(SIGFPE, signalHandler); // 浮点异常
|
|
|
|
|
|
signal(SIGBUS, signalHandler); // 总线错误
|
|
|
|
|
|
signal(SIGPIPE, signalHandler); // 管道破裂
|
|
|
|
|
|
LOG_INFO("[SIGNAL] Signal handlers registered\n");
|
|
|
|
|
|
|
|
|
|
|
|
// 屏蔽 ffmedia 库的 WARN 日志(仅在 ERROR 时打印)
|
|
|
|
|
|
extern unsigned int ff_log_level;
|
|
|
|
|
|
ff_log_level = 0; // 0=ERROR only, 1=WARN
|
|
|
|
|
|
|
|
|
|
|
|
// exit() 拦截已在链接时生效(本可执行文件定义的 exit 符号优先级高于 libc)
|
|
|
|
|
|
// 子线程调用 exit() 时仅结束该线程,不会杀死整个进程
|
|
|
|
|
|
LOG_INFO("[EXIT-HOOK] exit() interceptor active: main-thread exit passes through, child-thread exit converts to pthread_exit\n");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("===========================================\n");
|
|
|
|
|
|
LOG_INFO(" %s\n", DRONESCREWSERVER_PRODUCT_NAME);
|
|
|
|
|
|
LOG_INFO(" V%s_%s built %s %s\n", DRONESCREWSERVER_VERSION_STRING,
|
|
|
|
|
|
DRONESCREWSERVER_VERSION_BUILD, __DATE__, __TIME__);
|
|
|
|
|
|
LOG_DEBUG("===========================================\n");
|
|
|
|
|
|
|
|
|
|
|
|
qRegisterMetaType<DroneScrewResult>("DroneScrewResult");
|
|
|
|
|
|
qRegisterMetaType<DroneScrewAlgoParams>("DroneScrewAlgoParams");
|
|
|
|
|
|
qRegisterMetaType<MvsImageData>("MvsImageData");
|
|
|
|
|
|
|
|
|
|
|
|
DroneScrewServerPresenter presenter;
|
|
|
|
|
|
DroneScrewZmqProtocol zmq;
|
|
|
|
|
|
|
2026-07-11 15:28:36 +08:00
|
|
|
|
QString simulationImageDirArg;
|
|
|
|
|
|
bool simulationArgSpecified = false;
|
|
|
|
|
|
const QStringList args = QCoreApplication::arguments();
|
|
|
|
|
|
for (int i = 1; i < args.size(); ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString arg = args.at(i);
|
|
|
|
|
|
if (arg == QStringLiteral("--simulate-dir") ||
|
|
|
|
|
|
arg == QStringLiteral("--sim-dir"))
|
|
|
|
|
|
{
|
|
|
|
|
|
simulationArgSpecified = true;
|
|
|
|
|
|
if (i + 1 >= args.size())
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("[SIM] missing directory after %s\n",
|
|
|
|
|
|
arg.toStdString().c_str());
|
|
|
|
|
|
return ERR_CODE(FILE_ERR_NOEXIST);
|
|
|
|
|
|
}
|
|
|
|
|
|
simulationImageDirArg = args.at(++i);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const QString simulateDirPrefix = QStringLiteral("--simulate-dir=");
|
|
|
|
|
|
const QString simDirPrefix = QStringLiteral("--sim-dir=");
|
|
|
|
|
|
if (arg.startsWith(simulateDirPrefix))
|
|
|
|
|
|
{
|
|
|
|
|
|
simulationArgSpecified = true;
|
|
|
|
|
|
simulationImageDirArg = arg.mid(simulateDirPrefix.size());
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (arg.startsWith(simDirPrefix))
|
|
|
|
|
|
{
|
|
|
|
|
|
simulationArgSpecified = true;
|
|
|
|
|
|
simulationImageDirArg = arg.mid(simDirPrefix.size());
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!arg.startsWith(QStringLiteral("-")) && !simulationArgSpecified)
|
|
|
|
|
|
{
|
|
|
|
|
|
simulationArgSpecified = true;
|
|
|
|
|
|
simulationImageDirArg = arg;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:55:15 +08:00
|
|
|
|
// 信号槽:协议 → presenter
|
|
|
|
|
|
QObject::connect(&zmq, &DroneScrewZmqProtocol::rawImagePublishEnabledRequested,
|
|
|
|
|
|
&presenter, &DroneScrewServerPresenter::setRawPubEnabled,
|
|
|
|
|
|
Qt::DirectConnection);
|
|
|
|
|
|
QObject::connect(&zmq, &DroneScrewZmqProtocol::detectModeRequested,
|
|
|
|
|
|
&presenter, &DroneScrewServerPresenter::setDetectMode,
|
|
|
|
|
|
Qt::DirectConnection);
|
|
|
|
|
|
QObject::connect(&zmq, &DroneScrewZmqProtocol::setExposureRequested,
|
|
|
|
|
|
&presenter, &DroneScrewServerPresenter::handleSetExposure);
|
|
|
|
|
|
QObject::connect(&zmq, &DroneScrewZmqProtocol::setGainRequested,
|
|
|
|
|
|
&presenter, &DroneScrewServerPresenter::handleSetGain);
|
|
|
|
|
|
QObject::connect(&zmq, &DroneScrewZmqProtocol::updateAlgoParamsRequested,
|
|
|
|
|
|
&presenter, &DroneScrewServerPresenter::handleUpdateAlgoParams);
|
|
|
|
|
|
|
|
|
|
|
|
// presenter → 协议(发布检测结果)
|
|
|
|
|
|
QObject::connect(&presenter, &DroneScrewServerPresenter::detectionResult,
|
|
|
|
|
|
&zmq, &DroneScrewZmqProtocol::publishDetectionResult);
|
|
|
|
|
|
|
|
|
|
|
|
// presenter → 协议(原始图像 PUB)— 同步连接,回调内立即拷贝完成
|
|
|
|
|
|
QObject::connect(&presenter, &DroneScrewServerPresenter::rawImageReady,
|
|
|
|
|
|
&zmq, &DroneScrewZmqProtocol::publishRawImage,
|
|
|
|
|
|
Qt::DirectConnection);
|
|
|
|
|
|
|
|
|
|
|
|
zmq.setSingleDetectionHandler([&presenter]() {
|
|
|
|
|
|
return presenter.runSingleDetection();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setStartWorkHandler([&presenter]() {
|
|
|
|
|
|
return presenter.startDetectionWork();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setStopWorkHandler([&presenter]() {
|
|
|
|
|
|
return presenter.stopDetectionWork();
|
|
|
|
|
|
});
|
2026-07-03 14:19:06 +08:00
|
|
|
|
zmq.setFinalDetectionResultProvider([&presenter](DroneScrewResult& result) {
|
|
|
|
|
|
return presenter.getLastFinalDetectionResult(result);
|
|
|
|
|
|
});
|
2026-06-26 17:55:15 +08:00
|
|
|
|
zmq.setStartLiveStreamHandler([&presenter]() {
|
|
|
|
|
|
return presenter.startLiveStream();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setStopLiveStreamHandler([&presenter]() {
|
|
|
|
|
|
return presenter.stopLiveStream();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setSwapCameraRolesHandler([&presenter]() {
|
|
|
|
|
|
return presenter.swapCameraRoles();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setServerInfoProvider([&presenter]() {
|
|
|
|
|
|
return presenter.getRuntimeInfo();
|
|
|
|
|
|
});
|
|
|
|
|
|
zmq.setCalibrationInfoProvider([&presenter]() {
|
|
|
|
|
|
return presenter.getCalibrationInfo();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 配置文件 —— 运行时配置优先(runtime_config.xml 不会被部署覆盖),
|
|
|
|
|
|
// config/config.xml 仅作为出厂默认值。
|
|
|
|
|
|
const QString appDir = QCoreApplication::applicationDirPath();
|
|
|
|
|
|
const QString runtimeCfgPath = QDir(appDir).filePath("runtime_config.xml");
|
|
|
|
|
|
|
|
|
|
|
|
auto findDefaultConfig = [&]() -> QString {
|
|
|
|
|
|
const QString a = QDir(appDir).filePath("config/config.xml");
|
|
|
|
|
|
if (QFileInfo::exists(a)) return a;
|
|
|
|
|
|
const QString b = QDir::current().filePath("config/config.xml");
|
|
|
|
|
|
if (QFileInfo::exists(b)) return b;
|
|
|
|
|
|
return QString();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 之后所有 saveConfiguration 调用都写到 runtime_config.xml
|
|
|
|
|
|
presenter.setConfigFilePath(runtimeCfgPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (QFileInfo::exists(runtimeCfgPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 正常路径:加载持久化的运行时配置
|
|
|
|
|
|
LOG_INFO("[CONFIG] loading runtime config: %s\n", runtimeCfgPath.toStdString().c_str());
|
|
|
|
|
|
if (!presenter.loadConfiguration(runtimeCfgPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_WARN("[CONFIG] runtime config load failed, falling back to defaults\n");
|
|
|
|
|
|
const QString fallback = findDefaultConfig();
|
|
|
|
|
|
if (!fallback.isEmpty())
|
|
|
|
|
|
presenter.loadConfiguration(fallback);
|
|
|
|
|
|
// loadConfiguration 会覆盖 m_configFilePath,需要重新指回 runtime 路径
|
|
|
|
|
|
presenter.setConfigFilePath(runtimeCfgPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 首次部署:加载出厂默认配置,然后直接复制到 runtime_config.xml
|
|
|
|
|
|
const QString defaultCfg = findDefaultConfig();
|
|
|
|
|
|
if (!defaultCfg.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_INFO("[CONFIG] first run, loading default: %s\n", defaultCfg.toStdString().c_str());
|
|
|
|
|
|
presenter.loadConfiguration(defaultCfg);
|
|
|
|
|
|
// loadConfiguration 会覆盖 m_configFilePath,需要重新指回 runtime 路径
|
|
|
|
|
|
presenter.setConfigFilePath(runtimeCfgPath);
|
|
|
|
|
|
|
|
|
|
|
|
// 直接复制默认配置文件到运行时位置,保留所有元素
|
|
|
|
|
|
if (QFile::copy(defaultCfg, runtimeCfgPath))
|
|
|
|
|
|
LOG_INFO("[CONFIG] default config copied to: %s\n", runtimeCfgPath.toStdString().c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
LOG_WARN("[CONFIG] failed to copy default config to: %s\n", runtimeCfgPath.toStdString().c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_INFO("[CONFIG] no config file found, using defaults\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化各子系统
|
2026-07-11 15:28:36 +08:00
|
|
|
|
if (simulationArgSpecified)
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString trimmed = simulationImageDirArg.trimmed();
|
|
|
|
|
|
if (trimmed.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("[SIM] simulation image directory is empty\n");
|
|
|
|
|
|
return ERR_CODE(FILE_ERR_NOEXIST);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const QFileInfo inputInfo(trimmed);
|
|
|
|
|
|
const QString absolutePath = inputInfo.isAbsolute()
|
|
|
|
|
|
? inputInfo.absoluteFilePath()
|
|
|
|
|
|
: QDir::current().absoluteFilePath(trimmed);
|
|
|
|
|
|
const QFileInfo simulationDirInfo(absolutePath);
|
|
|
|
|
|
if (!simulationDirInfo.exists() || !simulationDirInfo.isDir())
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("[SIM] simulation image directory does not exist: %s\n",
|
|
|
|
|
|
absolutePath.toStdString().c_str());
|
|
|
|
|
|
return ERR_CODE(FILE_ERR_NOEXIST);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
presenter.setSimulationImageDir(simulationDirInfo.absoluteFilePath());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:55:15 +08:00
|
|
|
|
presenter.initAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 启动 ZMQ
|
|
|
|
|
|
zmq.setRtspUrl(presenter.getRtspUrl());
|
|
|
|
|
|
if (!zmq.startServer(presenter.getZmqControlPort(),
|
|
|
|
|
|
presenter.getZmqResultPort(),
|
|
|
|
|
|
presenter.getZmqRawImagePort()))
|
|
|
|
|
|
{
|
|
|
|
|
|
LOG_ERROR("ZMQ start fail\n");
|
|
|
|
|
|
return ERR_CODE(NET_ERR_CREAT_INIT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// raw 图像发布由 start.mode 控制:pull_stream 关闭,subscribe 开启。
|
|
|
|
|
|
presenter.setRawPubEnabled(false);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("=== DroneScrewServer started, ZMQ ctrl=%d result=%d raw=%d, RTSP=%s ===\n",
|
|
|
|
|
|
presenter.getZmqControlPort(),
|
|
|
|
|
|
presenter.getZmqResultPort(),
|
|
|
|
|
|
presenter.getZmqRawImagePort(),
|
|
|
|
|
|
presenter.getRtspUrl().toStdString().c_str());
|
|
|
|
|
|
|
|
|
|
|
|
const auto startedAt = std::chrono::steady_clock::now();
|
|
|
|
|
|
QTimer heartbeatTimer;
|
|
|
|
|
|
heartbeatTimer.setInterval(60000);
|
|
|
|
|
|
QObject::connect(&heartbeatTimer, &QTimer::timeout, [&presenter, startedAt]() {
|
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
const auto uptimeSec =
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::seconds>(now - startedAt).count();
|
|
|
|
|
|
const QJsonObject info = presenter.getRuntimeInfo();
|
|
|
|
|
|
const QString mode = info["mode"].toString();
|
|
|
|
|
|
const QString rss = procStatusValue("VmRSS");
|
|
|
|
|
|
const QString vmPeak = procStatusValue("VmPeak");
|
|
|
|
|
|
const QString vmSize = procStatusValue("VmSize");
|
|
|
|
|
|
const QString threads = procStatusValue("Threads");
|
|
|
|
|
|
LOG_INFO("[HEARTBEAT] uptime=%llds mode=%s rss=%s vmPeak=%s vmSize=%s threads=%s\n",
|
|
|
|
|
|
static_cast<long long>(uptimeSec),
|
|
|
|
|
|
mode.toStdString().c_str(),
|
|
|
|
|
|
rss.isEmpty() ? "N/A" : rss.toStdString().c_str(),
|
|
|
|
|
|
vmPeak.isEmpty() ? "N/A" : vmPeak.toStdString().c_str(),
|
|
|
|
|
|
vmSize.isEmpty() ? "N/A" : vmSize.toStdString().c_str(),
|
|
|
|
|
|
threads.isEmpty() ? "N/A" : threads.toStdString().c_str());
|
|
|
|
|
|
});
|
|
|
|
|
|
heartbeatTimer.start();
|
|
|
|
|
|
|
|
|
|
|
|
return app.exec();
|
|
|
|
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
int main()
|
|
|
|
|
|
{
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endif
|