feat(video+battery): 完成视频服务,加入电池管理模块.

This commit is contained in:
GuanYuankai 2026-01-14 09:55:58 +08:00
parent 9280f52aef
commit 79b2616493
9 changed files with 305 additions and 37 deletions

View File

@ -72,6 +72,8 @@ add_library(vehicle_road_lib STATIC
src/mysqlManager/AlarmDao.cpp src/mysqlManager/AlarmDao.cpp
src/mysqlManager/DeviceIdentificationDao.cpp src/mysqlManager/DeviceIdentificationDao.cpp
src/mysqlManager/ResourceFileDao.cpp src/mysqlManager/ResourceFileDao.cpp
#
src/batteryService/battery_service.cc
) )
target_include_directories(vehicle_road_lib PUBLIC target_include_directories(vehicle_road_lib PUBLIC

View File

@ -20,12 +20,12 @@
"enable": true, "enable": true,
"line": { "line": {
"p1": { "p1": {
"x": 0.39809998869895935, "x": 0.2556999921798706,
"y": 0.8816999793052673 "y": 0.848800003528595
}, },
"p2": { "p2": {
"x": 0.8337000012397766, "x": 0.6531999707221985,
"y": 0.5864999890327454 "y": 0.8278999924659729
} }
}, },
"name": "Main_Gate_Line" "name": "Main_Gate_Line"

View File

@ -13,6 +13,7 @@ services:
- /dev/ttyS7:/dev/ttyS7 - /dev/ttyS7:/dev/ttyS7
- /dev/ttyS9:/dev/ttyS9 - /dev/ttyS9:/dev/ttyS9
- /dev/snd:/dev/snd - /dev/snd:/dev/snd
- /dev/ttyS4:/dev/ttyS4
# --- VPU/NPU/RGA/GPU 硬件访问 --- # --- VPU/NPU/RGA/GPU 硬件访问 ---
- /dev/mpp_service:/dev/mpp_service - /dev/mpp_service:/dev/mpp_service

View File

@ -34,6 +34,7 @@ struct TrackedVehicle {
// [预留] 如果需要记录车辆经过线条的时间,可以在这里加字段 // [预留] 如果需要记录车辆经过线条的时间,可以在这里加字段
// int64_t cross_line_timestamp = 0; // int64_t cross_line_timestamp = 0;
float confidence;
}; };
// 每一帧的数据包(用于线程间传递) // 每一帧的数据包(用于线程间传递)

View File

@ -0,0 +1,208 @@
#include "battery_service.h"
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include "spdlog/spdlog.h"
// 协议常量
#define FRAME_HEAD 0xA5
#define CMD_REPORT 0x01
#define CMD_SET_MODE 0x02
#define CMD_SET_ALARM 0x03
#define CMD_ACK 0xAA
BatteryService::BatteryService(const std::string& port, int baud_rate)
: m_port_name(port), m_baud_rate(baud_rate), m_running(false) {
// 初始化数据结构为0
std::memset(&m_latest_data, 0, sizeof(m_latest_data));
}
BatteryService::~BatteryService() {
stop();
}
bool BatteryService::start() {
if (!openPort()) {
return false;
}
m_running = true;
m_rx_thread = std::thread(&BatteryService::receiveLoop, this);
spdlog::info("BatteryService started on port {}", m_port_name);
return true;
}
void BatteryService::stop() {
m_running = false;
if (m_rx_thread.joinable()) {
m_rx_thread.join();
}
if (m_fd >= 0) {
close(m_fd);
m_fd = -1;
}
spdlog::info("BatteryService stopped.");
}
bool BatteryService::openPort() {
m_fd = open(m_port_name.c_str(), O_RDWR | O_NOCTTY | O_NDELAY);
if (m_fd < 0) {
spdlog::error("Failed to open serial port: {}", m_port_name);
return false;
}
fcntl(m_fd, F_SETFL, 0); // 恢复阻塞模式
struct termios options;
tcgetattr(m_fd, &options);
speed_t speed = (m_baud_rate == 115200) ? B115200 : B9600;
cfsetispeed(&options, speed);
cfsetospeed(&options, speed);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_iflag &= ~(IXON | IXOFF | IXANY);
options.c_iflag &= ~(ICRNL | INLCR);
options.c_oflag &= ~OPOST;
tcsetattr(m_fd, TCSANOW, &options);
tcflush(m_fd, TCIOFLUSH);
return true;
}
int BatteryService::writeData(const uint8_t* data, size_t len) {
if (m_fd < 0)
return -1;
return write(m_fd, data, len);
}
void BatteryService::setMode(uint8_t mode) {
sendPacket(CMD_SET_MODE, &mode, 1);
spdlog::info("BatteryService: Set Mode to {}", mode);
}
void BatteryService::setAlarm(bool on) {
uint8_t val = on ? 1 : 0;
sendPacket(CMD_SET_ALARM, &val, 1);
spdlog::info("BatteryService: Set Alarm to {}", val);
}
nlohmann::json BatteryService::getStatusJson() {
std::lock_guard<std::mutex> lock(m_data_mutex);
if (!m_has_data) {
return {{"status", "waiting_for_data"}};
}
return {{"battery_cap", m_latest_data.battery_cap},
{"time_to_full_min", m_latest_data.time_to_full},
{"time_to_empty_min", m_latest_data.time_to_empty},
{"status_code", m_latest_data.battery_status},
{"temperature_c", (float)m_latest_data.temperature / 10.0f},
{"system_mode", m_latest_data.system_mode},
{"alarm_flag", m_latest_data.alarm_flag}};
}
void BatteryService::sendPacket(uint8_t cmd, const uint8_t* payload, uint8_t len) {
std::vector<uint8_t> packet;
packet.push_back(FRAME_HEAD);
packet.push_back(cmd);
packet.push_back(len);
if (len > 0 && payload) {
packet.insert(packet.end(), payload, payload + len);
}
uint32_t crc = calculateCRC32(packet.data(), packet.size());
packet.push_back((crc >> 0) & 0xFF);
packet.push_back((crc >> 8) & 0xFF);
packet.push_back((crc >> 16) & 0xFF);
packet.push_back((crc >> 24) & 0xFF);
writeData(packet.data(), packet.size());
}
uint32_t BatteryService::calculateCRC32(const uint8_t* data, size_t len) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < len; i++) {
uint32_t byte = data[i];
crc = crc ^ byte;
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc ^ 0xFFFFFFFF;
}
void BatteryService::processFrame(const uint8_t* frame, size_t totalLen) {
uint8_t cmd = frame[1];
const uint8_t* payload = &frame[3];
if (cmd == CMD_REPORT) {
if (totalLen >= sizeof(BatteryPacket_t) + 7) {
std::lock_guard<std::mutex> lock(m_data_mutex);
memcpy(&m_latest_data, payload, sizeof(BatteryPacket_t));
m_has_data = true;
// 可选在这里记录日志或者只在Debug时记录避免刷屏
// spdlog::debug("Battery Update: {}%", m_latest_data.battery_cap);
uint8_t ack[] = "OK";
sendPacket(CMD_ACK, ack, 2);
}
} else if (cmd == CMD_ACK) {
spdlog::debug("BatteryService: Received ACK from MCU");
}
}
void BatteryService::receiveLoop() {
uint8_t buf[128];
while (m_running) {
if (m_fd < 0) {
std::this_thread::sleep_for(std::chrono::seconds(1));
continue;
}
int n = read(m_fd, buf, sizeof(buf));
if (n > 0) {
m_rx_buffer.insert(m_rx_buffer.end(), buf, buf + n);
while (m_rx_buffer.size() >= 7) {
if (m_rx_buffer[0] != FRAME_HEAD) {
m_rx_buffer.erase(m_rx_buffer.begin());
continue;
}
uint8_t dataLen = m_rx_buffer[2];
size_t frameLen = 3 + dataLen + 4;
if (m_rx_buffer.size() < frameLen)
break;
uint32_t receivedCRC = 0;
receivedCRC |= m_rx_buffer[frameLen - 4] << 0;
receivedCRC |= m_rx_buffer[frameLen - 3] << 8;
receivedCRC |= m_rx_buffer[frameLen - 2] << 16;
receivedCRC |= m_rx_buffer[frameLen - 1] << 24;
if (calculateCRC32(m_rx_buffer.data(), frameLen - 4) == receivedCRC) {
processFrame(m_rx_buffer.data(), frameLen);
m_rx_buffer.erase(m_rx_buffer.begin(), m_rx_buffer.begin() + frameLen);
} else {
spdlog::warn("BatteryService: CRC Error");
m_rx_buffer.erase(m_rx_buffer.begin());
}
}
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}

View File

@ -0,0 +1,64 @@
#pragma once
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "nlohmann/json.hpp"
// 定义数据结构 (保持字节对齐)
#pragma pack(push, 1)
struct BatteryPacket_t {
uint8_t battery_cap; // 1. 电量百分比
uint16_t time_to_full; // 2. 预测充满时间
uint16_t time_to_empty; // 3. 预测放电时间
uint8_t battery_status; // 4. 电池状态
int16_t temperature; // 5. 温度
uint8_t system_mode; // 6. 模式
uint8_t alarm_flag; // 7. 报警标志
};
#pragma pack(pop)
class BatteryService {
public:
BatteryService(const std::string& port, int baud_rate);
~BatteryService();
// 初始化串口并启动接收线程
bool start();
// 停止线程并关闭串口
void stop();
// API 调用的控制接口
void setMode(uint8_t mode);
void setAlarm(bool on);
// 获取当前缓存的电池数据 (JSON格式)
nlohmann::json getStatusJson();
private:
// 内部串口操作
bool openPort();
void receiveLoop();
int writeData(const uint8_t* data, size_t len);
void sendPacket(uint8_t cmd, const uint8_t* payload, uint8_t len);
uint32_t calculateCRC32(const uint8_t* data, size_t len);
void processFrame(const uint8_t* frame, size_t totalLen);
// 成员变量
std::string m_port_name;
int m_baud_rate;
int m_fd = -1;
std::atomic<bool> m_running;
std::thread m_rx_thread;
std::vector<uint8_t> m_rx_buffer;
// 数据缓存与线程安全
BatteryPacket_t m_latest_data;
std::mutex m_data_mutex;
bool m_has_data = false;
};

View File

@ -74,11 +74,10 @@ void poll_system_metrics(boost::asio::steady_timer& timer, SystemMonitor::System
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
// TODO: [GYK] DEV#1: 将 URL 放入 config.json 中读取 // TODO: [GYK] DEV#1: 将 URL 放入 config.json 中读取
// std::string cam_rtsp_input = "rtsp://admin:123456@192.168.1.57:554/stream0"; std::string cam_rtsp_input = "rtsp://admin:123456@192.168.1.57:554/stream0";
std::string cam_rtsp_input = "../data/test_car2.mp4"; // std::string cam_rtsp_input = "../data/test_car2.mp4";
// std::string cam_rtsp_input = // std::string cam_rtsp_input =
// "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1901"; // "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1301";
std::string algorithm_rtsp_output = "rtsp://127.0.0.1:8554/processed"; std::string algorithm_rtsp_output = "rtsp://127.0.0.1:8554/processed";
const std::string config_path = "/app/config/config.json"; const std::string config_path = "/app/config/config.json";
if (!ConfigManager::getInstance().load(config_path)) { if (!ConfigManager::getInstance().load(config_path)) {

View File

@ -198,6 +198,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);
std::string dbPath = fmt::format("{}/{}", "/captures", fileName);
// === 2. 安全截图 (内存操作,无需加数据库锁) === // === 2. 安全截图 (内存操作,无需加数据库锁) ===
cv::Rect safeBox = vehicle.box; cv::Rect safeBox = vehicle.box;
@ -258,7 +259,7 @@ void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat
bool fileSaved = bool fileSaved =
fileDao.SaveFile("tb_device_identification_data", // source_table fileDao.SaveFile("tb_device_identification_data", // source_table
dataId, // business_id dataId, // business_id
fullPath, // file_path dbPath, // file_path
fileName, // source_file_name fileName, // source_file_name
FileType::ORIGINAL // file_type FileType::ORIGINAL // file_type
); );
@ -360,7 +361,7 @@ void VideoPipeline::updateTracker(const FrameData& frameData) {
newTrack.missing_frames = 0; newTrack.missing_frames = 0;
newTrack.ev_score = current_is_ev; newTrack.ev_score = current_is_ev;
newTrack.last_class_id = det.class_id; newTrack.last_class_id = det.class_id;
newTrack.confidence = det.confidence;
tracks_[newTrack.id] = newTrack; tracks_[newTrack.id] = newTrack;
} }
} }
@ -382,51 +383,43 @@ void VideoPipeline::updateTracker(const FrameData& frameData) {
void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector<TrackedVehicle>& trackedObjects) { void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector<TrackedVehicle>& trackedObjects) {
std::lock_guard<std::mutex> lock(config_mtx_); std::lock_guard<std::mutex> lock(config_mtx_);
// 1. 画绊线 (保持不变)
if (tripwire_config_.enabled) { if (tripwire_config_.enabled) {
cv::line(frame, tripwire_p1_pixel_, tripwire_p2_pixel_, cv::Scalar(0, 255, 255), 2); cv::line(frame, tripwire_p1_pixel_, tripwire_p2_pixel_, cv::Scalar(0, 255, 255), 2);
cv::putText(frame, tripwire_config_.name, tripwire_p1_pixel_, cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::putText(frame, tripwire_config_.name, tripwire_p1_pixel_, cv::FONT_HERSHEY_SIMPLEX, 0.7,
cv::Scalar(0, 255, 255), 2); cv::Scalar(0, 255, 255), 2);
} }
// 2. 画车辆 (已修改)
for (const auto& trk : trackedObjects) { for (const auto& trk : trackedObjects) {
if (trk.missing_frames > 0) if (trk.missing_frames > 0)
continue; continue;
// 设置颜色EV为绿色燃油车为红色
cv::Scalar color = (trk.last_class_id == 1) ? cv::Scalar(0, 255, 0) : cv::Scalar(0, 0, 255); cv::Scalar color = (trk.last_class_id == 1) ? cv::Scalar(0, 255, 0) : cv::Scalar(0, 0, 255);
// 绘制车辆边框
cv::rectangle(frame, trk.box, color, 2); cv::rectangle(frame, trk.box, color, 2);
// === [修改开始] 文字居中显示 EV / FUEL === std::stringstream ss;
ss << trk.label << " " << std::fixed << std::setprecision(2) << trk.confidence;
std::string text = ss.str();
// 1. 确定显示的文本 int fontFace = cv::FONT_HERSHEY_SIMPLEX;
// std::string text = (trk.last_class_id == 1) ? "EV" : "FUEL"; double fontScale = 0.6;
int thickness = 1;
int baseline = 0;
// 2. 设置字体参数 cv::Size textSize = cv::getTextSize(text, fontFace, fontScale, thickness, &baseline);
// int fontFace = cv::FONT_HERSHEY_SIMPLEX;
// double fontScale = 0.8; // 稍微调大一点以便在框内看清
// int thickness = 2;
// int baseline = 0;
// 3. 计算文字本身的宽高 cv::Point textOrg(trk.box.x, trk.box.y - 5);
// cv::Size textSize = cv::getTextSize(text, fontFace, fontScale, thickness, &baseline);
// 4. 计算检测框的中心点 if (textOrg.y < textSize.height) {
// cv::Point boxCenter(trk.box.x + trk.box.width / 2, trk.box.y + trk.box.height / 2); textOrg.y = trk.box.y + textSize.height + 5;
}
// 5. 计算文字绘制的起始点 (Origin) cv::rectangle(frame, textOrg + cv::Point(0, baseline),
// 使得文字的中心与框的中心对齐 textOrg + cv::Point(textSize.width, -textSize.height), color, cv::FILLED);
// X轴: 框中心X - 文字宽度的一半
// Y轴: 框中心Y + 文字高度的一半 (注意OpenCV原点在左上角文字基线在下方)
// cv::Point textOrg(boxCenter.x - textSize.width / 2, boxCenter.y + textSize.height / 2);
// 6. 绘制文字 cv::Scalar textColor =
// cv::putText(frame, text, textOrg, fontFace, fontScale, color, thickness); (trk.last_class_id == 1) ? cv::Scalar(0, 0, 0) : cv::Scalar(255, 255, 255);
cv::putText(frame, text, textOrg, fontFace, fontScale, textColor, thickness);
// === [修改结束] ===
} }
// 调试信息 (保持不变) // 调试信息 (保持不变)
@ -450,7 +443,7 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo
int width = cap.get(cv::CAP_PROP_FRAME_WIDTH); int width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT); int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
const double TARGET_FPS = 30.0; const double TARGET_FPS = 20.0;
std::stringstream pipeline; std::stringstream pipeline;
pipeline << "appsrc ! " pipeline << "appsrc ! "

View File

@ -67,6 +67,6 @@ private:
int model_data_size_; int model_data_size_;
const std::vector<std::string> CLASS_NAMES = {"fuel_car", "new_energy_car"}; const std::vector<std::string> CLASS_NAMES = {"fuel_car", "new_energy_car"};
const float obj_thresh_ = 0.25f; const float obj_thresh_ = 0.60f;
const float nms_thresh_ = 0.45f; const float nms_thresh_ = 0.45f;
}; };