generated from guanyuankai/bonus-edge-proxy
feat(video,network): 完成和前端的视频绊线接受调整。
This commit is contained in:
parent
0b5ff83548
commit
d2cbab34c7
|
|
@ -20,12 +20,12 @@
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"line": {
|
"line": {
|
||||||
"p1": {
|
"p1": {
|
||||||
"x": 0.0,
|
"x": 0.39809998869895935,
|
||||||
"y": 0.8
|
"y": 0.8816999793052673
|
||||||
},
|
},
|
||||||
"p2": {
|
"p2": {
|
||||||
"x": 1.0,
|
"x": 0.8337000012397766,
|
||||||
"y": 0.8
|
"y": 0.5864999890327454
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Main_Gate_Line"
|
"name": "Main_Gate_Line"
|
||||||
|
|
|
||||||
|
|
@ -268,27 +268,55 @@ TripwireConfig ConfigManager::getTripwireConfig() {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
bool ConfigManager::updateTripwireLine(float p1_x, float p1_y, float p2_x, float p2_y) {
|
bool ConfigManager::updateTripwireLine(float p1_x, float p1_y, float p2_x, float p2_y) {
|
||||||
std::unique_lock<std::shared_mutex> lock(m_mutex); // 获取写锁,确保线程安全
|
json new_tripwire_value; // 用于存储更新后的片段
|
||||||
|
|
||||||
// 1. 确保 tripwire 对象存在
|
// 1. 更新数据并保存 (使用作用域限制锁的范围)
|
||||||
if (!m_config_json.contains("tripwire") || !m_config_json["tripwire"].is_object()) {
|
{
|
||||||
m_config_json["tripwire"] = json::object();
|
std::unique_lock<std::shared_mutex> lock(m_mutex); // 获取写锁
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 确保 line 对象存在
|
if (!m_config_json.contains("tripwire") || !m_config_json["tripwire"].is_object()) {
|
||||||
if (!m_config_json["tripwire"].contains("line") ||
|
m_config_json["tripwire"] = json::object();
|
||||||
!m_config_json["tripwire"]["line"].is_object()) {
|
}
|
||||||
m_config_json["tripwire"]["line"] = json::object();
|
|
||||||
}
|
if (!m_config_json["tripwire"].contains("line") || !m_config_json["tripwire"]["line"].is_object()) {
|
||||||
|
m_config_json["tripwire"]["line"] = json::object();
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 更新坐标值
|
m_config_json["tripwire"]["line"]["p1"] = { {"x", p1_x}, {"y", p1_y} };
|
||||||
// 注意:JSON 结构根据 config.json 文件构建
|
m_config_json["tripwire"]["line"]["p2"] = { {"x", p2_x}, {"y", p2_y} };
|
||||||
m_config_json["tripwire"]["line"]["p1"] = {{"x", p1_x}, {"y", p1_y}};
|
|
||||||
m_config_json["tripwire"]["line"]["p2"] = {{"x", p2_x}, {"y", p2_y}};
|
|
||||||
|
|
||||||
spdlog::info("Updating Tripwire: P1({:.2f}, {:.2f}), P2({:.2f}, {:.2f})", p1_x, p1_y, p2_x,
|
// 捕获最新的 tripwire 值,用于发送给回调
|
||||||
p2_y);
|
new_tripwire_value = m_config_json["tripwire"];
|
||||||
|
|
||||||
// 4. 保存到文件 (save_unlocked 内部不加锁,适合在这里调用)
|
spdlog::info("Updating Tripwire: P1({:.2f}, {:.2f}), P2({:.2f}, {:.2f})",
|
||||||
return save_unlocked();
|
p1_x, p1_y, p2_x, p2_y);
|
||||||
|
|
||||||
|
if (!save_unlocked()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. === [修复核心] 手动触发观察者回调 ===
|
||||||
|
std::vector<KeyUpdateCallback> callbacks;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> cb_lock(m_callbackMutex);
|
||||||
|
if (m_key_callbacks.count("tripwire")) {
|
||||||
|
callbacks = m_key_callbacks["tripwire"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行回调
|
||||||
|
int trigger_count = 0;
|
||||||
|
for (const auto& cb : callbacks) {
|
||||||
|
try {
|
||||||
|
cb(new_tripwire_value); // 这里的 new_tripwire_value 就是传给 VideoPipeline 的新配置
|
||||||
|
trigger_count++;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
spdlog::error("Exception inside manual config update callback: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info("Manual update triggered {} callbacks.", trigger_count);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <cppconn/resultset.h>
|
#include <cppconn/resultset.h>
|
||||||
|
|
||||||
#include "mysql_manager.h"
|
#include "mysql_manager.h"
|
||||||
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
// --- 静态辅助函数 ---
|
// --- 静态辅助函数 ---
|
||||||
static std::string GetFileTypeStr(FileType type) {
|
static std::string GetFileTypeStr(FileType type) {
|
||||||
|
|
@ -23,12 +24,11 @@ bool ResourceFileDao::Insert(const ResourceFile& file) {
|
||||||
std::string sql =
|
std::string sql =
|
||||||
"INSERT INTO sys_resource_file "
|
"INSERT INTO sys_resource_file "
|
||||||
"(source_table, file_path, source_file_name, suffix_name, "
|
"(source_table, file_path, source_file_name, suffix_name, "
|
||||||
"file_type, business_id, business_type, create_user_id) "
|
"file_type, business_id, create_user_id) "
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
"VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
auto pstmt = MysqlManager::GetInstance()->GetPreparedStatement(sql);
|
auto pstmt = MysqlManager::GetInstance()->GetPreparedStatement(sql);
|
||||||
if (!pstmt) {
|
if (!pstmt) {
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -38,14 +38,14 @@ bool ResourceFileDao::Insert(const ResourceFile& file) {
|
||||||
pstmt->setString(4, file.suffixName);
|
pstmt->setString(4, file.suffixName);
|
||||||
pstmt->setString(5, file.fileType);
|
pstmt->setString(5, file.fileType);
|
||||||
pstmt->setInt64(6, file.businessId);
|
pstmt->setInt64(6, file.businessId);
|
||||||
pstmt->setString(7, file.businessType);
|
|
||||||
// create_user_id 暂时给0,如果有登录系统可传入
|
// create_user_id 暂时给0,如果有登录系统可传入
|
||||||
pstmt->setInt64(8, file.createUserId);
|
pstmt->setInt64(7, file.createUserId);
|
||||||
|
|
||||||
pstmt->executeUpdate();
|
pstmt->executeUpdate();
|
||||||
return true;
|
return true;
|
||||||
} catch (sql::SQLException& e) {
|
} catch (sql::SQLException& e) {
|
||||||
// 建议 log: e.what()
|
spdlog::error("MySQL Error in ResourceFileDao: Code={}, Message={}", e.getErrorCode(),
|
||||||
|
e.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "mysql_manager.h"
|
#include "mysql_manager.h"
|
||||||
|
|
||||||
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
MysqlManager* MysqlManager::instance = nullptr;
|
MysqlManager* MysqlManager::instance = nullptr;
|
||||||
std::mutex MysqlManager::mtx;
|
std::mutex MysqlManager::mtx;
|
||||||
|
|
||||||
|
|
@ -35,13 +37,30 @@ bool MysqlManager::Initialize(const std::string& h, const std::string& u, const
|
||||||
}
|
}
|
||||||
|
|
||||||
void MysqlManager::CheckConnection() {
|
void MysqlManager::CheckConnection() {
|
||||||
if (!connection || connection->isClosed()) {
|
bool valid = false;
|
||||||
// 尝试重连
|
|
||||||
|
// 1. 初步检查对象是否存在
|
||||||
|
if (connection && !connection->isClosed()) {
|
||||||
|
try {
|
||||||
|
// 2. 主动执行一个轻量级查询 (Ping)
|
||||||
|
std::unique_ptr<sql::Statement> stmt(connection->createStatement());
|
||||||
|
std::unique_ptr<sql::ResultSet> res(stmt->executeQuery("SELECT 1"));
|
||||||
|
valid = true;
|
||||||
|
} catch (sql::SQLException& e) {
|
||||||
|
spdlog::warn("MySQL Connection is dead (Ping failed): {}", e.what());
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果无效,则重连
|
||||||
|
if (!valid) {
|
||||||
|
spdlog::info("Attempting to reconnect to MySQL...");
|
||||||
try {
|
try {
|
||||||
connection.reset(driver->connect(host, user, pass));
|
connection.reset(driver->connect(host, user, pass));
|
||||||
connection->setSchema(dbName);
|
connection->setSchema(dbName);
|
||||||
} catch (...) {
|
spdlog::info("MySQL Reconnected successfully.");
|
||||||
// 重连失败,实际项目中应记录日志
|
} catch (sql::SQLException& e) {
|
||||||
|
spdlog::error("Failed to reconnect to MySQL: {}", e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +72,8 @@ std::unique_ptr<sql::PreparedStatement> MysqlManager::GetPreparedStatement(const
|
||||||
try {
|
try {
|
||||||
return std::unique_ptr<sql::PreparedStatement>(connection->prepareStatement(sql));
|
return std::unique_ptr<sql::PreparedStatement>(connection->prepareStatement(sql));
|
||||||
} catch (sql::SQLException& e) {
|
} catch (sql::SQLException& e) {
|
||||||
|
spdlog::error("MySQL Error in GetPreparedStatement: Code={}, Message={}", e.getErrorCode(),
|
||||||
|
e.what());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem> // C++17
|
#include <filesystem> // C++17
|
||||||
#include <iomanip> // for std::put_time
|
#include <iomanip> // for std::put_time
|
||||||
|
#include <mutex> // [新增] 引入互斥锁头文件
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include "config/config_manager.h"
|
#include "config/config_manager.h"
|
||||||
|
|
@ -16,6 +17,10 @@ namespace fs = std::filesystem;
|
||||||
|
|
||||||
const int YoloDetector::NPU_CORE_CNT;
|
const int YoloDetector::NPU_CORE_CNT;
|
||||||
|
|
||||||
|
// === [新增] 数据库写入互斥锁 ===
|
||||||
|
// 防止多个后台线程同时操作同一个 MySQL 连接导致 "Lost connection" 或协议错误
|
||||||
|
static std::mutex g_db_mtx;
|
||||||
|
|
||||||
// === 静态辅助函数 ===
|
// === 静态辅助函数 ===
|
||||||
|
|
||||||
// 获取矩形底部的中心点 (作为车辆的"脚")
|
// 获取矩形底部的中心点 (作为车辆的"脚")
|
||||||
|
|
@ -173,7 +178,7 @@ void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat
|
||||||
const std::string& locationName) {
|
const std::string& locationName) {
|
||||||
// 启动分离线程,避免阻塞主视频流
|
// 启动分离线程,避免阻塞主视频流
|
||||||
std::thread([this, vehicle, frame, locationName]() {
|
std::thread([this, vehicle, frame, locationName]() {
|
||||||
// === 1. 准备目录与文件名 ===
|
// === 1. 准备目录与文件名 (IO操作,无需加锁) ===
|
||||||
std::string saveDir = "../captures";
|
std::string saveDir = "../captures";
|
||||||
try {
|
try {
|
||||||
if (!fs::exists(saveDir)) {
|
if (!fs::exists(saveDir)) {
|
||||||
|
|
@ -194,7 +199,7 @@ void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat
|
||||||
std::string fileName = fmt::format("{}_id{}.jpg", timeStr, vehicle.id);
|
std::string fileName = fmt::format("{}_id{}.jpg", timeStr, vehicle.id);
|
||||||
std::string fullPath = fmt::format("{}/{}", saveDir, fileName);
|
std::string fullPath = fmt::format("{}/{}", saveDir, fileName);
|
||||||
|
|
||||||
// === 2. 安全截图 ===
|
// === 2. 安全截图 (内存操作,无需加数据库锁) ===
|
||||||
cv::Rect safeBox = vehicle.box;
|
cv::Rect safeBox = vehicle.box;
|
||||||
// 边界钳制,防止 crash
|
// 边界钳制,防止 crash
|
||||||
if (safeBox.x < 0)
|
if (safeBox.x < 0)
|
||||||
|
|
@ -212,53 +217,64 @@ void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clone 数据,因为主线程可能会复用 frame 内存 (虽然当前架构中 frame
|
// Clone 数据,因为主线程可能会复用 frame 内存
|
||||||
// 是独立的,但为了健壮性)
|
|
||||||
cv::Mat snapshot = frame(safeBox).clone();
|
cv::Mat snapshot = frame(safeBox).clone();
|
||||||
if (cv::imwrite(fullPath, snapshot)) {
|
if (cv::imwrite(fullPath, snapshot)) {
|
||||||
spdlog::info("Snapshot saved: {}", fullPath);
|
spdlog::info("Snapshot saved: {}", fullPath);
|
||||||
} else {
|
} else {
|
||||||
spdlog::error("Failed to write image: {}", fullPath);
|
spdlog::error("Failed to write image: {}", fullPath);
|
||||||
return;
|
return; // 图片保存失败通常不继续写库
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("Save image exception: {}", e.what());
|
spdlog::error("Save image exception: {}", e.what());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 3. 数据库入库 ===
|
// === 3. 数据库入库 (关键修改:加锁) ===
|
||||||
DeviceIdentificationDao deviceDao;
|
// 使用大括号限制锁的作用域
|
||||||
ResourceFileDao fileDao;
|
{
|
||||||
|
// [修复] 此处加锁,确保同一时刻只有一个线程使用 MySQL 连接
|
||||||
|
std::lock_guard<std::mutex> db_lock(g_db_mtx);
|
||||||
|
|
||||||
// 3.1 准备枚举数据
|
try {
|
||||||
// class_id: 1 -> EV(Green), 0 -> Fuel(Blue)
|
DeviceIdentificationDao deviceDao;
|
||||||
CarType cType = (vehicle.last_class_id == 1) ? CarType::ELECTRIC : CarType::GASOLINE;
|
ResourceFileDao fileDao;
|
||||||
CarColor cColor = (vehicle.last_class_id == 1) ? CarColor::GREEN : CarColor::BLUE;
|
|
||||||
|
|
||||||
// 3.2 插入业务主表
|
// 3.1 准备枚举数据
|
||||||
// 假设 SystemID 为 1,实际项目可能需配置
|
// class_id: 1 -> EV(Green), 0 -> Fuel(Blue)
|
||||||
int64_t systemId = 1;
|
CarType cType =
|
||||||
std::string location = locationName.empty() ? "Unkown_Line" : locationName;
|
(vehicle.last_class_id == 1) ? CarType::ELECTRIC : CarType::GASOLINE;
|
||||||
|
CarColor cColor = (vehicle.last_class_id == 1) ? CarColor::GREEN : CarColor::BLUE;
|
||||||
|
|
||||||
int64_t dataId = deviceDao.ReportIdentification(systemId, location, cColor, cType);
|
// 3.2 插入业务主表
|
||||||
|
// 假设 SystemID 为 1,实际项目可能需配置
|
||||||
|
int64_t systemId = 1;
|
||||||
|
std::string location = locationName.empty() ? "Unkown_Line" : locationName;
|
||||||
|
|
||||||
if (dataId > 0) {
|
int64_t dataId = deviceDao.ReportIdentification(systemId, location, cColor, cType);
|
||||||
// 3.3 插入文件关联表
|
|
||||||
bool fileSaved = fileDao.SaveFile("tb_device_identification_data", // source_table
|
|
||||||
dataId, // business_id
|
|
||||||
fullPath, // file_path
|
|
||||||
fileName, // source_file_name
|
|
||||||
FileType::ORIGINAL // file_type
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileSaved) {
|
if (dataId > 0) {
|
||||||
spdlog::debug("DB Transaction Complete. DataID: {}", dataId);
|
// 3.3 插入文件关联表
|
||||||
} else {
|
bool fileSaved =
|
||||||
spdlog::error("Failed to save file record for DataID: {}", dataId);
|
fileDao.SaveFile("tb_device_identification_data", // source_table
|
||||||
|
dataId, // business_id
|
||||||
|
fullPath, // file_path
|
||||||
|
fileName, // source_file_name
|
||||||
|
FileType::ORIGINAL // file_type
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileSaved) {
|
||||||
|
spdlog::debug("DB Transaction Complete. DataID: {}", dataId);
|
||||||
|
} else {
|
||||||
|
spdlog::error("Failed to save file record for DataID: {}", dataId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spdlog::error("Failed to insert device identification data.");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
spdlog::error("Database Exception in thread: {}", e.what());
|
||||||
}
|
}
|
||||||
} else {
|
} // 锁在这里自动释放
|
||||||
spdlog::error("Failed to insert device identification data.");
|
|
||||||
}
|
|
||||||
}).detach(); // 让线程后台运行
|
}).detach(); // 让线程后台运行
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,9 +498,9 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo
|
||||||
writer.write(current_data.original_frame);
|
writer.write(current_data.original_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (write_frame_idx % 60 == 0) {
|
// if (write_frame_idx % 60 == 0) {
|
||||||
spdlog::info("Processed Frame ID: {}", write_frame_idx);
|
// spdlog::info("Processed Frame ID: {}", write_frame_idx);
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,10 @@ void WebServer::setup_routes() {
|
||||||
|
|
||||||
// 2. 校验 JSON 格式是否合法
|
// 2. 校验 JSON 格式是否合法
|
||||||
if (!json_body) {
|
if (!json_body) {
|
||||||
return crow::response(400, "Invalid JSON format");
|
json response_json = {
|
||||||
|
{"code", 400}, {"status", "fail"}, {"message", "Invalid JSON format"}};
|
||||||
|
|
||||||
|
return crow::response(400, response_json.dump());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 校验必要字段是否存在
|
// 3. 校验必要字段是否存在
|
||||||
|
|
@ -80,7 +83,11 @@ void WebServer::setup_routes() {
|
||||||
// 5. 简单的范围校验 (可选,防止异常数据)
|
// 5. 简单的范围校验 (可选,防止异常数据)
|
||||||
if (x1_pct < 0 || x1_pct > 100 || y1_pct < 0 || y1_pct > 100 || x2_pct < 0 ||
|
if (x1_pct < 0 || x1_pct > 100 || y1_pct < 0 || y1_pct > 100 || x2_pct < 0 ||
|
||||||
x2_pct > 100 || y2_pct < 0 || y2_pct > 100) {
|
x2_pct > 100 || y2_pct < 0 || y2_pct > 100) {
|
||||||
return crow::response(400, "Values must be between 0 and 100");
|
json response_json = {{"code", 400},
|
||||||
|
{"status", "fail"},
|
||||||
|
{"message", "Values must be between 0 and 100"}};
|
||||||
|
|
||||||
|
return crow::response(400, response_json.dump());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 归一化:除以 100 转换为 0.0 - 1.0
|
// 6. 归一化:除以 100 转换为 0.0 - 1.0
|
||||||
|
|
@ -94,16 +101,24 @@ void WebServer::setup_routes() {
|
||||||
ConfigManager::getInstance().updateTripwireLine(p1_x, p1_y, p2_x, p2_y);
|
ConfigManager::getInstance().updateTripwireLine(p1_x, p1_y, p2_x, p2_y);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
json response_json = {{"status", "success"},
|
json response_json = {{"code", 200},
|
||||||
|
{"status", "success"},
|
||||||
{"message", "Tripwire updated successfully"}};
|
{"message", "Tripwire updated successfully"}};
|
||||||
return crow::response(200, response_json.dump());
|
return crow::response(200, response_json.dump());
|
||||||
} else {
|
} else {
|
||||||
return crow::response(500, "Failed to save configuration");
|
json response_json = {{"code", 500},
|
||||||
|
{"status", "fail"},
|
||||||
|
{"message", "Failed to save configuration"}};
|
||||||
|
|
||||||
|
return crow::response(500, response_json.dump());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("Error parsing tripwire coordinates: {}", e.what());
|
spdlog::error("Error parsing tripwire coordinates: {}", e.what());
|
||||||
return crow::response(400, "Invalid data types");
|
json response_json = {
|
||||||
|
{"code", 400}, {"status", "fail"}, {"message", "Invalid data types"}};
|
||||||
|
|
||||||
|
return crow::response(400, response_json.dump());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue