add old lci module
This commit is contained in:
1 parent
38a6e815f0
commit
8499631091
171 files changed
+25562
No files matched your search
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
扩展技能:人脸位置检测工具
|
||||
用于检测图像中的人脸并保存人脸位置坐标(百分比格式)到txt文件
|
||||
放置在 ~/.aibox/skills/face_position_detector.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import cv2
|
||||
import face_recognition
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class FacePositionDetector:
|
||||
"""人脸位置检测工具(检测位置坐标并转换为百分比)"""
|
||||
|
||||
def __init__(self):
|
||||
# 不再使用.aibox文件夹,使用当前目录
|
||||
pass
|
||||
|
||||
def _ensure_dirs(self, dir_path: str):
|
||||
"""确保目录存在"""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
def detect_faces(self, image_path: str, save_coords: bool = True, output_file: Optional[str] = None) -> str:
|
||||
"""
|
||||
检测图像中的人脸并保存位置坐标(百分比格式)到txt文件
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 错误:文件不存在 - {image_path}"
|
||||
|
||||
# 使用OpenCV获取图像尺寸
|
||||
img_cv = cv2.imread(image_path)
|
||||
if img_cv is None:
|
||||
return f"❌ 错误:无法读取图像文件 - {image_path}"
|
||||
|
||||
img_height, img_width = img_cv.shape[:2]
|
||||
|
||||
# 使用face_recognition检测人脸
|
||||
image = face_recognition.load_image_file(image_path)
|
||||
face_locations = face_recognition.face_locations(image)
|
||||
|
||||
if not face_locations:
|
||||
return "📭 未检测到人脸"
|
||||
|
||||
results = []
|
||||
saved_coords = []
|
||||
|
||||
for i, (top, right, bottom, left) in enumerate(face_locations):
|
||||
# 转换为百分比坐标
|
||||
top_pct = top / img_height
|
||||
right_pct = right / img_width
|
||||
bottom_pct = bottom / img_height
|
||||
left_pct = left / img_width
|
||||
|
||||
face_data = {
|
||||
"index": i + 1,
|
||||
"position_absolute": {"top": top, "right": right, "bottom": bottom, "left": left},
|
||||
"position_percentage": {"top": top_pct, "right": right_pct, "bottom": bottom_pct, "left": left_pct},
|
||||
"size_absolute": {"width": right - left, "height": bottom - top},
|
||||
"size_percentage": {"width": (right - left) / img_width, "height": (bottom - top) / img_height},
|
||||
"image_dimensions": {"width": img_width, "height": img_height}
|
||||
}
|
||||
|
||||
# 保存人脸坐标到txt文件(百分比格式)
|
||||
if save_coords:
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 确定输出文件名
|
||||
if output_file:
|
||||
coords_filename = output_file
|
||||
else:
|
||||
base_name = os.path.splitext(os.path.basename(image_path))[0]
|
||||
coords_filename = f"{base_name}_faces_{timestamp}.txt"
|
||||
|
||||
# 使用当前目录作为保存路径
|
||||
coords_filepath = coords_filename
|
||||
|
||||
# 写入百分比坐标数据(保留6位小数)
|
||||
with open(coords_filepath, 'a') as f:
|
||||
# 格式:top right bottom left (百分比,空格分隔)
|
||||
f.write(f"{top_pct:.6f} {right_pct:.6f} {bottom_pct:.6f} {left_pct:.6f}\n")
|
||||
|
||||
saved_coords.append(coords_filename)
|
||||
|
||||
face_data["coords_file"] = coords_filename
|
||||
face_data["coords_path"] = coords_filepath
|
||||
except Exception as e:
|
||||
face_data["save_error"] = str(e)
|
||||
|
||||
results.append(face_data)
|
||||
|
||||
# 构建结果字符串
|
||||
result_str = f"✅ 检测到 {len(results)} 张人脸\n"
|
||||
result_str += f"📏 图像尺寸: {img_width}x{img_height}\n\n"
|
||||
|
||||
for face in results:
|
||||
result_str += f"👤 人脸 #{face['index']}:\n"
|
||||
result_str += f" 绝对坐标: top={face['position_absolute']['top']}, right={face['position_absolute']['right']}, "
|
||||
result_str += f"bottom={face['position_absolute']['bottom']}, left={face['position_absolute']['left']}\n"
|
||||
result_str += f" 百分比坐标: top={face['position_percentage']['top']:.3f}, right={face['position_percentage']['right']:.3f}, "
|
||||
result_str += f"bottom={face['position_percentage']['bottom']:.3f}, left={face['position_percentage']['left']:.3f}\n"
|
||||
result_str += f" 绝对尺寸: {face['size_absolute']['width']}x{face['size_absolute']['height']}\n"
|
||||
result_str += f" 相对尺寸: {face['size_percentage']['width']:.3f}x{face['size_percentage']['height']:.3f}"
|
||||
|
||||
if 'coords_file' in face:
|
||||
result_str += f"\n 坐标已保存到: {face['coords_file']}"
|
||||
|
||||
result_str += "\n\n"
|
||||
|
||||
if saved_coords:
|
||||
# 显示所有保存的坐标文件
|
||||
unique_files = list(set(saved_coords))
|
||||
result_str += f"📂 人脸坐标(百分比格式)已保存到当前目录:\n"
|
||||
for filename in unique_files:
|
||||
result_str += f" 📄 {filename}\n"
|
||||
|
||||
return result_str
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 检测失败: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
"""简化后的主函数 - 仅人脸位置检测(百分比坐标)"""
|
||||
parser = argparse.ArgumentParser(description="人脸位置检测工具(检测坐标并转换为百分比)")
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# detect命令
|
||||
detect_parser = subparsers.add_parser("detect", help="检测图像中的人脸并保存百分比坐标")
|
||||
detect_parser.add_argument("image_path", help="图片文件路径")
|
||||
detect_parser.add_argument("--no-save", action="store_true", help="不保存坐标")
|
||||
detect_parser.add_argument("--output", help="指定输出文件名(默认自动生成)")
|
||||
|
||||
# 工具描述命令
|
||||
desc_parser = subparsers.add_parser("description", help="显示工具描述")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
detector = FacePositionDetector()
|
||||
|
||||
if args.command == "description":
|
||||
desc = """人脸位置检测工具(百分比坐标)
|
||||
|
||||
功能:
|
||||
- 检测图像中的人脸
|
||||
- 将人脸位置坐标转换为百分比格式
|
||||
- 保存百分比坐标到txt文件(格式:top right bottom left,均为0-1之间的浮点数)
|
||||
|
||||
坐标说明:
|
||||
- top: 人脸顶部位置(占图像高度的比例,0=顶部,1=底部)
|
||||
- right: 人脸右侧位置(占图像宽度的比例,0=左侧,1=右侧)
|
||||
- bottom: 人脸底部位置(占图像高度的比例,0=顶部,1=底部)
|
||||
- left: 人脸左侧位置(占图像宽度的比例,0=左侧,1=右侧)
|
||||
|
||||
目录结构:
|
||||
- 人脸坐标保存位置: 当前目录
|
||||
|
||||
使用示例:
|
||||
facerec.py detect image.jpg # 检测人脸并保存百分比坐标到当前目录
|
||||
facerec.py detect image.jpg --no-save # 检测但不保存坐标
|
||||
facerec.py detect image.jpg --output my_coords.txt # 指定输出文件名"""
|
||||
print(desc)
|
||||
|
||||
elif args.command == "detect":
|
||||
save_coords = not args.no_save
|
||||
result = detector.detect_faces(args.image_path, save_coords, args.output)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,535 @@
|
||||
/* FIXME: 这个文件非混乱 */
|
||||
/* FIXME: 抽象一个服务器类 */
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "hd/Camera.h"
|
||||
|
||||
#define cimg_display 0 /* 不显示图像,只保存 */
|
||||
#include "CImg.h"
|
||||
#include <jpeglib.h>
|
||||
using namespace cimg_library;
|
||||
|
||||
/*-----------------------------------*/
|
||||
/* Camera */
|
||||
static const char* filename = "/dev/video0"; /* FIXME: 不一定 */
|
||||
static int width = -1;
|
||||
static int height = -1;
|
||||
|
||||
/* 为了在获取单帧中使用 */
|
||||
static struct v4l2_buffer buf;
|
||||
static enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
|
||||
/*-----------------------------------*/
|
||||
/* 静态全局线程对象及相关同步原语 */
|
||||
static std::thread camera_thread;
|
||||
static std::atomic<bool> camera_running{false};
|
||||
static std::mutex camera_mutex;
|
||||
|
||||
/* device file */
|
||||
static int fd = -1;
|
||||
|
||||
|
||||
/*-----------------------------------*/
|
||||
void Camera_check();/* check if this device support video capture (V4L2_CAP_VIDEO_CAPTURE) */
|
||||
void Camera_setVideoFormat(int w, int h);/* Set Video Format */
|
||||
void Camera_reqBuf();/* Request Camera Buffer */
|
||||
void Camera_setup(); /* mapping buffer and data collection loop */
|
||||
void Camera_unsetup();
|
||||
|
||||
/*-----------------------------------*/
|
||||
bool Camera_is_open() {
|
||||
return fd >= 0;
|
||||
}
|
||||
|
||||
/* FIXME: 如果在集成操作中出现异常,应该释放资源 */
|
||||
void Camera_init(int w, int h) {
|
||||
|
||||
if (Camera_is_open())
|
||||
throw std::runtime_error("Camera already opened.");
|
||||
|
||||
fd = open(filename, O_RDWR);
|
||||
if (fd < 0)
|
||||
throw std::runtime_error(
|
||||
"Failed to open device: " + std::string(strerror(errno))
|
||||
);
|
||||
|
||||
Camera_check();
|
||||
Camera_setVideoFormat(w, h);
|
||||
Camera_reqBuf();
|
||||
Camera_setup();
|
||||
}
|
||||
|
||||
/* */
|
||||
void Camera_close() {
|
||||
Camera_unsetup();
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
|
||||
|
||||
void Camera_check() {
|
||||
struct v4l2_capability cap;
|
||||
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
|
||||
throw std::runtime_error(
|
||||
"VIDIOC_QUERYCAP failed: " + std::string(strerror(errno))
|
||||
);
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
throw std::runtime_error("Device does not support video capture");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* FIXME: 将Camera_init()中的参数放在这里 */
|
||||
void Camera_setVideoFormat(int width, int height) {
|
||||
struct v4l2_format fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.width = width; /* */
|
||||
fmt.fmt.pix.height = height; /* */
|
||||
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; /* usually format: YUYV, MJPEG */
|
||||
fmt.fmt.pix.field = V4L2_FIELD_NONE;
|
||||
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0)
|
||||
throw std::runtime_error(
|
||||
"VIDIOC_S_FMT failed: " + std::string(strerror(errno)));
|
||||
::width= width;
|
||||
::height= height;
|
||||
}
|
||||
|
||||
|
||||
void Camera_reqBuf() {
|
||||
struct v4l2_requestbuffers req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.count = 4; /* the number of buffer */
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP; /* the mode of memory mapping */
|
||||
|
||||
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
|
||||
throw std::runtime_error(
|
||||
"VIDIOC_REQBUFS failed: " + std::string(strerror(errno)));
|
||||
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------*/
|
||||
|
||||
void Camera_setup() {
|
||||
/* mapping buffer (It is needed to handle every buffer in loop) */
|
||||
memset(&buf, 0, sizeof(buf));
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = 0; /* Index of the buffer */
|
||||
|
||||
/* 查询缓冲区信息 */
|
||||
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
|
||||
throw std::runtime_error("VIDIOC_QUERYBUF failed: " + std::string(strerror(errno)));
|
||||
}
|
||||
|
||||
/* 将缓冲区加入队列 */
|
||||
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
throw std::runtime_error("VIDIOC_QBUF failed: " + std::string(strerror(errno)));
|
||||
}
|
||||
|
||||
/* 开始采集 */
|
||||
if (ioctl(fd, VIDIOC_STREAMON, &buf.type) < 0) { /* start collection */
|
||||
throw std::runtime_error("VIDIOC_STREAMON failed: " + std::string(strerror(errno)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Camera_unsetup()
|
||||
{
|
||||
/* 停止采集流 */
|
||||
ioctl(fd, VIDIOC_STREAMOFF, &type);
|
||||
}
|
||||
|
||||
|
||||
/*------------------------------------------------------------------------------*/
|
||||
/*
|
||||
* Convert YUYV frame to CImg object and return it
|
||||
*
|
||||
* translate out camera data to CImg format
|
||||
*
|
||||
* new version
|
||||
*
|
||||
* FIXME: 专门建个库
|
||||
*/
|
||||
CImg<unsigned char> YUYV_to_CImg(uint8_t* frameData, int width, int height) {
|
||||
// 参数检查
|
||||
if (!frameData || width <= 0 || height <= 0) {
|
||||
throw std::invalid_argument("Invalid parameters");
|
||||
}
|
||||
|
||||
CImg<unsigned char> img(width, height, 1, 3);
|
||||
|
||||
// 使用指针提高效率
|
||||
uint8_t* src = frameData;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
uint8_t Y0 = src[0];
|
||||
uint8_t U = src[1];
|
||||
uint8_t Y1 = src[2];
|
||||
uint8_t V = src[3];
|
||||
src += 4;
|
||||
|
||||
// 使用更精确的系数
|
||||
int R0 = Y0 + ((V - 128) * 1402) / 1000;
|
||||
int G0 = Y0 - ((U - 128) * 344) / 1000 - ((V - 128) * 714) / 1000;
|
||||
int B0 = Y0 + ((U - 128) * 1772) / 1000;
|
||||
|
||||
int R1 = Y1 + ((V - 128) * 1402) / 1000;
|
||||
int G1 = Y1 - ((U - 128) * 344) / 1000 - ((V - 128) * 714) / 1000;
|
||||
int B1 = Y1 + ((U - 128) * 1772) / 1000;
|
||||
|
||||
// 范围限制
|
||||
R0 = std::max(0, std::min(255, R0));
|
||||
G0 = std::max(0, std::min(255, G0));
|
||||
B0 = std::max(0, std::min(255, B0));
|
||||
|
||||
R1 = std::max(0, std::min(255, R1));
|
||||
G1 = std::max(0, std::min(255, G1));
|
||||
B1 = std::max(0, std::min(255, B1));
|
||||
|
||||
// 设置像素
|
||||
img(x, y, 0, 0) = static_cast<uint8_t>(R0);
|
||||
img(x, y, 0, 1) = static_cast<uint8_t>(G0);
|
||||
img(x, y, 0, 2) = static_cast<uint8_t>(B0);
|
||||
|
||||
if (x + 1 < width) {
|
||||
img(x + 1, y, 0, 0) = static_cast<uint8_t>(R1);
|
||||
img(x + 1, y, 0, 1) = static_cast<uint8_t>(G1);
|
||||
img(x + 1, y, 0, 2) = static_cast<uint8_t>(B1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
|
||||
CImg<unsigned char> get_frame_from_camera(bool& flag) {
|
||||
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
//if (!camera_running.load())
|
||||
// throw std::runtime_error("Camera not running");
|
||||
|
||||
/* Dequeue to get data */
|
||||
memset(&buf, 0, sizeof(buf));
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
|
||||
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
|
||||
perror("dequeue failed");
|
||||
throw std::runtime_error("dequeue failed");
|
||||
}
|
||||
|
||||
/* 获取当前缓冲区的数据指针 */
|
||||
uint8_t *frameData = static_cast<uint8_t*>(mmap(NULL, buf.length,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
fd, buf.m.offset));
|
||||
|
||||
if (frameData == MAP_FAILED) {
|
||||
ioctl(fd, VIDIOC_QBUF, &buf);
|
||||
throw std::runtime_error("mmap failed in get_frame_from_camera");
|
||||
}
|
||||
|
||||
/* process frameData */
|
||||
//std::cout << "Got length: " << buf.length << std::endl;
|
||||
auto img = YUYV_to_CImg(frameData, width, height);
|
||||
|
||||
/* 解除内存映射 */
|
||||
munmap(frameData, buf.length);
|
||||
|
||||
if (ioctl(fd, VIDIOC_QBUF, &buf)<0){ /* join the queue again */
|
||||
perror("requeue failed");
|
||||
throw std::runtime_error("requeue failed");
|
||||
}
|
||||
|
||||
|
||||
flag = camera_running.load(); /* 检查下一次是否需要退出 */
|
||||
return img; /* 返回图像 */
|
||||
}
|
||||
|
||||
|
||||
|
||||
//CImg<unsigned char> YUYV_to_CImg(uint8_t* frameData, int width, int height) {
|
||||
// 创建CImg图像对象 (width, height, depth=1, spectrum=3 for RGB)
|
||||
// CImg<unsigned char> img(width, height, 1, 3);
|
||||
|
||||
// 遍历图像的每一行和每一列
|
||||
// for (int y = 0; y < height; y++) {
|
||||
// for (int x = 0; x < width; x += 2) {
|
||||
// YUYV格式:每两个像素占用4个字节
|
||||
// int yuyv_index = (y * width + x) * 2;
|
||||
|
||||
// 提取YUV值
|
||||
// uint8_t Y0 = frameData[yuyv_index];
|
||||
// uint8_t U = frameData[yuyv_index + 1];
|
||||
// uint8_t Y1 = frameData[yuyv_index + 2];
|
||||
// uint8_t V = frameData[yuyv_index + 3];
|
||||
|
||||
// 转换第一个像素(x, y)
|
||||
// int R0 = Y0 + (V - 128) * 1.402;
|
||||
// int G0 = Y0 - (U - 128) * 0.34414 - (V - 128) * 0.71414;
|
||||
// int B0 = Y0 + (U - 128) * 1.772;
|
||||
|
||||
// 转换第二个像素(x+1, y)
|
||||
// int R1 = Y1 + (V - 128) * 1.402;
|
||||
// int G1 = Y1 - (U - 128) * 0.34414 - (V - 128) * 0.71414;
|
||||
// int B1 = Y1 + (U - 128) * 1.772;
|
||||
|
||||
// 限制范围到0-255
|
||||
// R0 = std::max(0, std::min(255, R0));
|
||||
// G0 = std::max(0, std::min(255, G0));
|
||||
// B0 = std::max(0, std::min(255, B0));
|
||||
|
||||
// R1 = std::max(0, std::min(255, R1));
|
||||
// G1 = std::max(0, std::min(255, G1));
|
||||
// B1 = std::max(0, std::min(255, B1));
|
||||
|
||||
// 设置第一个像素值
|
||||
// img(x, y, 0, 0) = static_cast<uint8_t>(R0);
|
||||
// img(x, y, 0, 1) = static_cast<uint8_t>(G0);
|
||||
// img(x, y, 0, 2) = static_cast<uint8_t>(B0);
|
||||
|
||||
// 设置第二个像素值(如果在图像范围内)
|
||||
// if (x + 1 < width) {
|
||||
// img(x + 1, y, 0, 0) = static_cast<uint8_t>(R1);
|
||||
// img(x + 1, y, 0, 1) = static_cast<uint8_t>(G1);
|
||||
// img(x + 1, y, 0, 2) = static_cast<uint8_t>(B1);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return img;
|
||||
//}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Convert YUYV pixel at position (x, y) in frame to RGB
|
||||
* YUYV format: [Y0 U0 Y1 V0] [Y2 U2 Y3 V2] ...
|
||||
* Each 4 bytes contain 2 pixels (Y0,U0,V0 for pixel0, Y1,U0,V0 for pixel1)
|
||||
*/
|
||||
void YUYV_to_RGB(uint8_t* frameData, int width, int height, int x, int y,
|
||||
uint8_t& r, uint8_t& g, uint8_t& b) {
|
||||
// 计算像素在YUYV数据中的索引
|
||||
int pixel_index = y * width + x;
|
||||
|
||||
// YUYV中每2个像素共享4字节:Y0 U Y1 V
|
||||
int byte_offset = (pixel_index / 2) * 4;
|
||||
|
||||
// 确定是第一个像素还是第二个像素
|
||||
bool is_first_pixel = (pixel_index % 2 == 0);
|
||||
|
||||
uint8_t Y, U, V;
|
||||
|
||||
if (is_first_pixel) {
|
||||
// 第一个像素:Y0, U, V
|
||||
Y = frameData[byte_offset]; // Y0
|
||||
U = frameData[byte_offset + 1]; // U
|
||||
V = frameData[byte_offset + 3]; // V
|
||||
} else {
|
||||
// 第二个像素:Y1, U, V (与第一个像素共享U和V)
|
||||
Y = frameData[byte_offset + 2]; // Y1
|
||||
U = frameData[byte_offset + 1]; // U (共享)
|
||||
V = frameData[byte_offset + 3]; // V (共享)
|
||||
}
|
||||
|
||||
// YUV to RGB conversion formula (integer approximation)
|
||||
// 注意:YUV值通常范围是Y:16-235, UV:16-240
|
||||
// 这里先转换为0-255范围
|
||||
int Y_adj = Y - 16;
|
||||
int U_adj = U - 128;
|
||||
int V_adj = V - 128;
|
||||
|
||||
// 计算RGB值
|
||||
int R = (298 * Y_adj + 409 * V_adj + 128) >> 8;
|
||||
int G = (298 * Y_adj - 100 * U_adj - 208 * V_adj + 128) >> 8;
|
||||
int B = (298 * Y_adj + 516 * U_adj + 128) >> 8;
|
||||
|
||||
// 限制范围到0-255
|
||||
r = (R < 0) ? 0 : ((R > 255) ? 255 : static_cast<uint8_t>(R));
|
||||
g = (G < 0) ? 0 : ((G > 255) ? 255 : static_cast<uint8_t>(G));
|
||||
b = (B < 0) ? 0 : ((B > 255) ? 255 : static_cast<uint8_t>(B));
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert YUYV frame to CImg object and save as image
|
||||
*
|
||||
* FIXME(20251229): 每次处理一个像素都要调用YUYV_to_RGB函数,重新计算索引和转换,而YUYV格式中每两个相邻像素共享U和V值
|
||||
* FIXME(20260324): 效率太慢
|
||||
*/
|
||||
//bool save_YUYV_as_image(uint8_t* frameData, int width, int height,
|
||||
// const char* filename, int frame_num = 0) {
|
||||
// try {
|
||||
// 创建CImg图像对象 (width, height, depth=1, spectrum=3 for RGB)
|
||||
// CImg<unsigned char> img(width, height, 1, 3);
|
||||
|
||||
// 转换为RGB并填充到CImg
|
||||
// for (int y = 0; y < height; y++) {
|
||||
// for (int x = 0; x < width; x++) {
|
||||
// uint8_t r, g, b;
|
||||
// YUYV_to_RGB(frameData, width, height, x, y, r, g, b);
|
||||
|
||||
// CImg存储顺序是平面:首先所有R,然后所有G,最后所有B
|
||||
// 或者使用(x, y, 0, channel)访问
|
||||
// img(x, y, 0, 0) = r; // Red channel
|
||||
// img(x, y, 0, 1) = g; // Green channel
|
||||
// img(x, y, 0, 2) = b; // Blue channel
|
||||
// }
|
||||
// }
|
||||
|
||||
// 生成带帧编号的文件名
|
||||
// char full_filename[256];
|
||||
// if (frame_num > 0) {
|
||||
// snprintf(full_filename, sizeof(full_filename),
|
||||
// "%s_frame%03d.bmp", filename, frame_num);
|
||||
// } else {
|
||||
// snprintf(full_filename, sizeof(full_filename), "%s.bmp", filename);
|
||||
// }
|
||||
|
||||
// 保存为BMP格式(也支持PNG、JPEG等)
|
||||
// img.save(full_filename);
|
||||
// std::cout << "Image saved as: " << full_filename << std::endl;
|
||||
|
||||
// return true;
|
||||
// } catch (CImgException& e) {
|
||||
// std::cerr << "Error saving image: " << e.what() << std::endl;
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* 将CImg数据压缩成jpeg data
|
||||
*
|
||||
* FIXME: 具有性能问题
|
||||
*/
|
||||
std::vector<unsigned char> compress_to_jpeg(const CImg<unsigned char>& image, int quality = 75) {
|
||||
|
||||
// 检查图像的有效性
|
||||
if (image.width() == 0 || image.height() == 0) {
|
||||
std::cerr << "Error: Invalid image dimensions!" << std::endl;
|
||||
return std::vector<unsigned char>();
|
||||
}
|
||||
|
||||
// 检查频谱(通道数)
|
||||
int channels = image.spectrum();
|
||||
|
||||
// CImg数据布局:默认是平面格式 [plane1, plane2, plane3]
|
||||
// 但我们需要交错的RGB格式用于JPEG压缩
|
||||
|
||||
unsigned char* buffer = nullptr;
|
||||
unsigned long buffer_size = 0;
|
||||
|
||||
struct jpeg_compress_struct cinfo;
|
||||
struct jpeg_error_mgr jerr;
|
||||
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
jpeg_create_compress(&cinfo);
|
||||
jpeg_mem_dest(&cinfo, &buffer, &buffer_size);
|
||||
|
||||
int width = image.width();
|
||||
int height = image.height();
|
||||
|
||||
cinfo.image_width = width;
|
||||
cinfo.image_height = height;
|
||||
cinfo.input_components = 3; // 总是使用3通道RGB
|
||||
cinfo.in_color_space = JCS_RGB;
|
||||
|
||||
jpeg_set_defaults(&cinfo);
|
||||
jpeg_set_quality(&cinfo, quality, TRUE);
|
||||
|
||||
// 开始压缩
|
||||
jpeg_start_compress(&cinfo, TRUE);
|
||||
|
||||
JSAMPROW row_pointer[1];
|
||||
std::vector<unsigned char> row_buffer(width * 3); // 用于存储一行交错的RGB数据
|
||||
|
||||
// 处理每一行
|
||||
for (int y = 0; y < height; y++) {
|
||||
// 将CImg的平面格式转换为交错的RGB格式
|
||||
if (channels == 3) {
|
||||
// RGB图像:CImg存储为平面格式 [R平面, G平面, B平面]
|
||||
for (int x = 0; x < width; x++) {
|
||||
row_buffer[x * 3] = image(x, y, 0, 0); // R
|
||||
row_buffer[x * 3 + 1] = image(x, y, 0, 1); // G
|
||||
row_buffer[x * 3 + 2] = image(x, y, 0, 2); // B
|
||||
}
|
||||
}
|
||||
else if (channels == 1) {
|
||||
// 灰度图像:重复到三个通道
|
||||
for (int x = 0; x < width; x++) {
|
||||
unsigned char gray = image(x, y, 0, 0);
|
||||
row_buffer[x * 3] = gray; // R
|
||||
row_buffer[x * 3 + 1] = gray; // G
|
||||
row_buffer[x * 3 + 2] = gray; // B
|
||||
}
|
||||
}
|
||||
else if (channels == 4) {
|
||||
// RGBA图像:忽略Alpha通道
|
||||
for (int x = 0; x < width; x++) {
|
||||
row_buffer[x * 3] = image(x, y, 0, 0); // R
|
||||
row_buffer[x * 3 + 1] = image(x, y, 0, 1); // G
|
||||
row_buffer[x * 3 + 2] = image(x, y, 0, 2); // B
|
||||
// 忽略Alpha通道:image(x, y, 0, 3)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 其他通道数:使用前3个通道
|
||||
std::cout << "Warning: Using first 3 channels of " << channels << "-channel image" << std::endl;
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int c = 0; c < 3 && c < channels; c++) {
|
||||
row_buffer[x * 3 + c] = image(x, y, 0, c);
|
||||
}
|
||||
// 如果通道数不足3,用0填充
|
||||
for (int c = channels; c < 3; c++) {
|
||||
row_buffer[x * 3 + c] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row_pointer[0] = row_buffer.data();
|
||||
jpeg_write_scanlines(&cinfo, row_pointer, 1);
|
||||
|
||||
}
|
||||
|
||||
jpeg_finish_compress(&cinfo);
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
|
||||
// 将JPEG数据复制到vector
|
||||
std::vector<unsigned char> jpeg_data(buffer, buffer + buffer_size);
|
||||
|
||||
// 释放libjpeg分配的内存
|
||||
free(buffer);
|
||||
|
||||
// 验证JPEG数据
|
||||
if (jpeg_data.size() >= 2) {
|
||||
bool has_soi = (jpeg_data[0] == 0xFF && jpeg_data[1] == 0xD8);
|
||||
bool has_eoi = (jpeg_data.size() >= 2 &&
|
||||
jpeg_data[jpeg_data.size()-2] == 0xFF &&
|
||||
jpeg_data[jpeg_data.size()-1] == 0xD9);
|
||||
|
||||
if (!has_soi || !has_eoi) {
|
||||
std::cerr << "Warning: Generated JPEG may be corrupted!" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return jpeg_data;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 该文件是i2c用户界面
|
||||
*
|
||||
* NOTE: 以C++的方式处理异常可能会更好
|
||||
* FIXME: 包含询问方法以确定i2c设备的状态
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/i2c-dev.h>
|
||||
#include <iostream>
|
||||
#include <pthread.h>
|
||||
#include "cn/中文化.hpp"
|
||||
|
||||
静态的 整数 文件描述符 = -1;
|
||||
static pthread_mutex_t mutex; /* FIXME: 这个锁我们只提供接口给外部使用? */
|
||||
|
||||
逻辑型 I2C_检查() {
|
||||
返回 !(文件描述符 < 0);
|
||||
}
|
||||
|
||||
空类型 I2C_初始化(常量 字符* 设备路径 = "/dev/i2c-1") { /* FIXME: 动态指定设备文件 */
|
||||
如果 ((文件描述符 = open(设备路径, O_RDWR)) < 0)
|
||||
抛出异常 std::runtime_error( /* FIXME: 使用专用的异常类 */
|
||||
"Failed to open I2C bus: " + std::string(strerror(errno))
|
||||
);
|
||||
}
|
||||
|
||||
空类型 I2C_清理() {
|
||||
close(文件描述符);
|
||||
}
|
||||
|
||||
空类型 I2C_置地址(常量 uint8_t 地址) {
|
||||
如果 (ioctl(文件描述符, I2C_SLAVE, 地址) < 0)
|
||||
抛出异常 std::runtime_error(
|
||||
"Failed to set I2C address: " + std::string(strerror(errno))
|
||||
);
|
||||
}
|
||||
|
||||
空类型 I2C_写寄存器(无号8位整 寄存器, 无号8位整 值) { /* 写寄存器 */
|
||||
无号8位整 缓冲区[] = {寄存器, 值};
|
||||
如果 (write(文件描述符, 缓冲区, 2) != 2)
|
||||
抛出异常 std::runtime_error(
|
||||
"I2C write error: " + std::string(strerror(errno))
|
||||
);
|
||||
}
|
||||
|
||||
/* 读寄存器 */
|
||||
无号8位整 I2C_读寄存器(无号8位整 寄存器) {
|
||||
|
||||
无号8位整 值;
|
||||
|
||||
如果 (write(文件描述符, &寄存器, 1) != 1)
|
||||
抛出异常 std::runtime_error(
|
||||
"I2C read error: " + std::string(strerror(errno))
|
||||
);
|
||||
|
||||
如果 (read(文件描述符, &值, 1)!= 1)
|
||||
抛出异常 std::runtime_error(
|
||||
"I2C read error: " + std::string(strerror(errno))
|
||||
);
|
||||
|
||||
返回 值;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 获取锁 */
|
||||
空类型 I2C_获取锁() {
|
||||
pthread_mutex_lock(&mutex); /* 加锁 */
|
||||
}
|
||||
|
||||
/* 释放锁 */
|
||||
空类型 I2C_释放锁() {
|
||||
pthread_mutex_unlock(&mutex); /* 解锁 */
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
/*
|
||||
* FIXME: 1. 传感器数据校准
|
||||
2. 可采用卡尔曼滤波
|
||||
|
||||
FIXME: 采用类进行封装,因为可能需要一对
|
||||
*/
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <unistd.h>
|
||||
#include "cn/中文化.hpp"
|
||||
#include "Dashun_linux.hpp"
|
||||
|
||||
静态的 预求值的 无号8位整 MPU6050_默认地址 = 0x68;
|
||||
静态的 预求值的 无号8位整 MPU6050_电源管理寄存器1 = 0x6B;
|
||||
静态的 预求值的 无号8位整 MPU6050_电源管理_非睡眠模式 = 0x00;
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪配置寄存器 = 0x1B;
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪量程_正负500度每秒 = 0x08;
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪横轴数据_高字节 = 0x43; /* x */
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪横轴数据_低字节 = 0x44;
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪纵轴数据_高字节 = 0x45; /* y */
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪纵轴数据_低字节 = 0x46;
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪竖轴数据_高字节 = 0x47; /* z */
|
||||
静态的 预求值的 无号8位整 MPU6050_陀螺仪竖轴数据_低字节 = 0x48;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计配置寄存器 = 0x1C;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计量程_正负8g = 0x10;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计横轴数据_高字节 = 0x3B;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计横轴数据_低字节 = 0x3C;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计纵轴数据_高字节 = 0x3D;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计纵轴数据_低字节 = 0x3E;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计竖轴数据_高字节 = 0x3F;
|
||||
静态的 预求值的 无号8位整 MPU6050_加速度计竖轴数据_低字节 = 0x40;
|
||||
静态的 逻辑型 启动标志 = 假;
|
||||
|
||||
逻辑型 MPU6050_检查() {
|
||||
I2C_获取锁();
|
||||
布尔 临时 = 启动标志;
|
||||
I2C_释放锁();
|
||||
返回 临时;
|
||||
}
|
||||
|
||||
/* 初始化 MPU6050 */
|
||||
空类型 MPU6050_初始化() {
|
||||
I2C_获取锁();
|
||||
I2C_置地址(MPU6050_默认地址);
|
||||
I2C_写寄存器(MPU6050_电源管理寄存器1, MPU6050_电源管理_非睡眠模式); /* 退出睡眠 */
|
||||
I2C_写寄存器(MPU6050_陀螺仪配置寄存器, MPU6050_陀螺仪量程_正负500度每秒); /* ±500°/s */
|
||||
I2C_写寄存器(MPU6050_加速度计配置寄存器, MPU6050_加速度计量程_正负8g); /* ±8g */
|
||||
启动标志 = 真;
|
||||
I2C_释放锁();
|
||||
usleep(100000); /* 等待稳定 */
|
||||
}
|
||||
|
||||
/* 读取 16 位传感器数据(带符号) */
|
||||
静态的 有号16位整 读16比特寄存器(无号8位整 寄存器高位, 无号8位整 寄存器低位) {
|
||||
返回 (I2C_读寄存器(寄存器高位) << 8) | I2C_读寄存器(寄存器低位);
|
||||
}
|
||||
|
||||
/* 获取原始MPU6050传感器数据 */
|
||||
MPU6050_传感器数据 MPU6050_读传感器() {
|
||||
MPU6050_传感器数据 数据;
|
||||
I2C_获取锁();
|
||||
I2C_置地址(MPU6050_默认地址);
|
||||
数据.加速度计_横 = 读16比特寄存器(MPU6050_加速度计横轴数据_高字节, MPU6050_加速度计横轴数据_低字节);
|
||||
数据.加速度计_纵 = 读16比特寄存器(MPU6050_加速度计纵轴数据_高字节, MPU6050_加速度计纵轴数据_低字节);
|
||||
数据.加速度计_竖 = 读16比特寄存器(MPU6050_加速度计竖轴数据_高字节, MPU6050_加速度计竖轴数据_低字节);
|
||||
数据.陀螺仪_横 = 读16比特寄存器(MPU6050_陀螺仪横轴数据_高字节, MPU6050_陀螺仪横轴数据_低字节);
|
||||
数据.陀螺仪_纵 = 读16比特寄存器(MPU6050_陀螺仪纵轴数据_高字节, MPU6050_陀螺仪纵轴数据_低字节);
|
||||
数据.陀螺仪_竖 = 读16比特寄存器(MPU6050_陀螺仪竖轴数据_高字节, MPU6050_陀螺仪竖轴数据_低字节);
|
||||
I2C_释放锁();
|
||||
返回 数据;
|
||||
}
|
||||
|
||||
/* FIXME: 当设备剧烈运动时(如加速度远大于重力),加速度计数据会包含运动加速度,此时角度计算可能不准确。实际应用中需结合陀螺仪数据(如互补滤波或卡尔曼滤波)来提高稳定性。 */
|
||||
/* FIXME: 适合静态测量或学习用途。如果需要实际应用,建议添加传感器融合算法 */
|
||||
空类型 MPU6050_计算角度(常量 MPU6050_传感器数据& 数据, 单精度小数& 俯仰角, 单精度小数& 翻滚角) {
|
||||
/* - ±2g → 16384 LSB/g
|
||||
- ±4g → 8192 LSB/g
|
||||
- ±8g → 4096 LSB/g
|
||||
- ±16g → 2048 LSB/g */
|
||||
常量 单精度小数 比例 = 4096.0; /* 加速度数据转重力单位(±8g 量程 4096 LSB/g) */
|
||||
单精度小数 横角 = 数据.加速度计_横 / 比例;
|
||||
单精度小数 纵角 = 数据.加速度计_纵 / 比例;
|
||||
单精度小数 竖角 = 数据.加速度计_竖 / 比例;
|
||||
|
||||
/* 计算俯仰角(Pitch)和翻滚角(Roll) */
|
||||
俯仰角 = atan2(-横角, sqrt(纵角 * 纵角 + 竖角 * 竖角)) * 180.0 / M_PI;
|
||||
翻滚角 = atan2(纵角, 竖角) * 180.0 / M_PI;
|
||||
}
|
||||
|
||||
#ifdef _TEST
|
||||
/* TEST MAIN */
|
||||
整数 主函数() {
|
||||
I2C_初始化();
|
||||
MPU6050_初始化();
|
||||
判断循环 (1) {
|
||||
MPU6050_传感器数据 数据 = MPU6050_读传感器();
|
||||
单精度小数 俯仰角, 翻滚角;
|
||||
MPU6050_计算角度(数据, 俯仰角, 翻滚角);
|
||||
printf("俯仰角: %.2f\t翻滚角: %.2f\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
/* PCA9685 舵机驱动板 */
|
||||
#include <cstdint>
|
||||
#include <unistd.h>
|
||||
#include "cn/中文化.hpp"
|
||||
#include "Dashun_linux.hpp"
|
||||
|
||||
预求值的 无号8位整 PCA9685_地址 = 0x40; /* 7位地址 */
|
||||
静态的 逻辑型 启动标志 = 假; /* 启动标志 */
|
||||
逻辑型 PCA9685_检查() {
|
||||
I2C_获取锁();
|
||||
逻辑型 临时 = 启动标志;
|
||||
I2C_释放锁();
|
||||
返回 临时;
|
||||
}
|
||||
空类型 PCA9685_初始化() {
|
||||
I2C_获取锁();
|
||||
I2C_置地址(PCA9685_地址);
|
||||
I2C_写寄存器(0x00, 0x20); /* 开启AI(地址自增)和关闭SLEEP */
|
||||
启动标志 = 真;
|
||||
I2C_释放锁();
|
||||
usleep(5000); /* 等待振荡器启动 */
|
||||
|
||||
/* 设置PWM频率为50Hz(舵机标准周期20ms) */
|
||||
预求值的 单精度小数 赫兹频率 = 50.0;
|
||||
单精度小数 prescale_val = 25000000.0 / (4096 * 赫兹频率) - 1; /* 25MHz主频 */
|
||||
无号8位整 预分频值 = static_cast<uint8_t>(prescale_val + 0.5);
|
||||
|
||||
I2C_获取锁();
|
||||
I2C_写寄存器(0x00, 0x10); /* 进入SLEEP模式(允许改频率) */
|
||||
I2C_写寄存器(0xFE, 预分频值); /* 写入预分频值 */
|
||||
I2C_写寄存器(0x00, 0x20); /* 退出SLEEP */
|
||||
I2C_释放锁();
|
||||
usleep(5000);
|
||||
}
|
||||
|
||||
/* 设置指定通道的PWM信号(0-4095) */
|
||||
空类型 PCA9685_设置PWM(无号8位整 通道, 无号16位整 导通, 无号16位整 关断) {
|
||||
无号8位整 寄存器 = 0x06 + 4 * 通道; /* LED0 寄存器起始地址 */
|
||||
I2C_获取锁();
|
||||
I2C_置地址(PCA9685_地址);
|
||||
I2C_写寄存器(寄存器, 导通 & 0xFF);
|
||||
I2C_写寄存器(寄存器 + 1, 导通 >> 8);
|
||||
I2C_写寄存器(寄存器 + 2, 关断 & 0xFF);
|
||||
I2C_写寄存器(寄存器 + 3, 关断 >> 8);
|
||||
I2C_释放锁();
|
||||
}
|
||||
|
||||
/*
|
||||
* 角度转PWM值(0°~180° → 0.5ms~2.5ms脉宽)
|
||||
*/
|
||||
空类型 PCA9685_调整角度(无号8位整 通道, 单精度小数 角度) {
|
||||
预求值的 单精度小数 最小脉冲 = 0.5; /* 0°对应0.5ms */
|
||||
预求值的 单精度小数 最大脉冲 = 2.5; /* 180°对应2.5ms */
|
||||
单精度小数 脉冲宽度 = 最小脉冲 + (最大脉冲 - 最小脉冲) * (角度 / 180.0);
|
||||
无号16位整 关断值 = static_cast<uint16_t>(脉冲宽度 * 4096 / 20); /* 20ms 周期 */
|
||||
PCA9685_设置PWM(通道, 0, 关断值); /* ON=0, OFF=计算值 */
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "hd/mem.h"
|
||||
|
||||
/* 通过SysInfo接口获取内存使用情况 失败会返回NULL */
|
||||
MemoryInfo getMemoryUsageWithSysinfo() {
|
||||
MemoryInfo memInfo = {0};
|
||||
struct sysinfo si;
|
||||
|
||||
if (sysinfo(&si) == 0)
|
||||
{
|
||||
memInfo.totalMemory = si.totalram * si.mem_unit;
|
||||
memInfo.freeMemory = si.freeram * si.mem_unit;
|
||||
memInfo.usedMemory = memInfo.totalMemory - memInfo.freeMemory;
|
||||
|
||||
if (memInfo.totalMemory > 0)
|
||||
memInfo.usageRate = 100.0 * memInfo.usedMemory / memInfo.totalMemory;
|
||||
}
|
||||
else
|
||||
std::cerr << "sysinfo Call Failed" << std::endl;
|
||||
|
||||
return memInfo;
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
图片标注工具
|
||||
用于手动标注图像中的目标区域并保存位置坐标(百分比格式)到txt文件
|
||||
支持矩形框标注、预览、编辑和删除功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, simpledialog
|
||||
from PIL import Image, ImageTk, ImageDraw
|
||||
import cv2
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
import threading
|
||||
|
||||
|
||||
class ImageAnnotationTool:
|
||||
"""图片标注工具 - 支持手动矩形框标注"""
|
||||
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("图片标注工具")
|
||||
self.root.geometry("1200x800")
|
||||
|
||||
# 数据存储
|
||||
self.current_image_path = None
|
||||
self.current_image = None
|
||||
self.image_pil = None
|
||||
self.image_tk = None
|
||||
self.zoom_factor = 1.0
|
||||
self.image_display = None
|
||||
self.annotations = [] # 存储标注:[(x1, y1, x2, y2), ...] # 移除了label字段
|
||||
|
||||
# 标注状态
|
||||
self.drawing = False
|
||||
self.start_x = None
|
||||
self.start_y = None
|
||||
self.current_rect = None
|
||||
self.selected_annotation = -1 # 当前选中的标注索引
|
||||
self.drag_mode = False # 是否在拖拽模式
|
||||
self.drag_start = None # 拖拽起始点
|
||||
|
||||
# 缩放和平移
|
||||
self.offset_x = 0
|
||||
self.offset_y = 0
|
||||
self.pan_start_x = 0
|
||||
self.pan_start_y = 0
|
||||
self.panning = False
|
||||
|
||||
# 创建UI
|
||||
self.setup_ui()
|
||||
|
||||
# 绑定事件
|
||||
self.setup_bindings()
|
||||
|
||||
# 更新状态
|
||||
self.update_status()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置用户界面"""
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# 左侧控制面板
|
||||
control_frame = ttk.Frame(main_frame, width=250)
|
||||
control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
|
||||
control_frame.pack_propagate(False)
|
||||
|
||||
# 文件操作
|
||||
file_group = ttk.LabelFrame(control_frame, text="文件操作", padding=5)
|
||||
file_group.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
ttk.Button(file_group, text="打开图片", command=self.open_image).pack(fill=tk.X, pady=2)
|
||||
ttk.Button(file_group, text="保存标注", command=self.save_annotations).pack(fill=tk.X, pady=2)
|
||||
ttk.Button(file_group, text="加载标注", command=self.load_annotations).pack(fill=tk.X, pady=2)
|
||||
|
||||
# 标注操作
|
||||
anno_group = ttk.LabelFrame(control_frame, text="标注操作", padding=5)
|
||||
anno_group.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
ttk.Button(anno_group, text="清除当前标注", command=self.clear_current_annotation).pack(fill=tk.X, pady=2)
|
||||
ttk.Button(anno_group, text="删除所有标注", command=self.clear_all_annotations).pack(fill=tk.X, pady=2)
|
||||
ttk.Button(anno_group, text="删除选中标注", command=self.delete_selected_annotation).pack(fill=tk.X, pady=2)
|
||||
|
||||
# 标注列表
|
||||
list_group = ttk.LabelFrame(control_frame, text="标注列表", padding=5)
|
||||
list_group.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
# 创建列表框和滚动条
|
||||
list_frame = ttk.Frame(list_group)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.annotation_listbox = tk.Listbox(list_frame, height=15)
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.annotation_listbox.yview)
|
||||
self.annotation_listbox.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.annotation_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.annotation_listbox.bind('<<ListboxSelect>>', self.on_annotation_select)
|
||||
|
||||
# 信息显示
|
||||
info_group = ttk.LabelFrame(control_frame, text="信息", padding=5)
|
||||
info_group.pack(fill=tk.X)
|
||||
|
||||
self.info_label = ttk.Label(info_group, text="未加载图片", wraplength=220)
|
||||
self.info_label.pack(fill=tk.X, pady=2)
|
||||
|
||||
self.status_label = ttk.Label(info_group, text="状态: 就绪", wraplength=220)
|
||||
self.status_label.pack(fill=tk.X, pady=2)
|
||||
|
||||
# 右侧画布区域
|
||||
canvas_frame = ttk.Frame(main_frame)
|
||||
canvas_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# 工具栏
|
||||
toolbar = ttk.Frame(canvas_frame)
|
||||
toolbar.pack(fill=tk.X, pady=(0, 5))
|
||||
|
||||
ttk.Button(toolbar, text="放大", command=self.zoom_in).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar, text="缩小", command=self.zoom_out).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar, text="适应窗口", command=self.fit_to_window).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar, text="重置视图", command=self.reset_view).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
self.draw_mode_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(toolbar, text="绘制模式", variable=self.draw_mode_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 画布
|
||||
self.canvas = tk.Canvas(canvas_frame, bg='gray', cursor='cross')
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def setup_bindings(self):
|
||||
"""设置事件绑定"""
|
||||
self.canvas.bind('<ButtonPress-1>', self.on_mouse_down)
|
||||
self.canvas.bind('<B1-Motion>', self.on_mouse_move)
|
||||
self.canvas.bind('<ButtonRelease-1>', self.on_mouse_up)
|
||||
self.canvas.bind('<Control-ButtonPress-1>', self.on_pan_start)
|
||||
self.canvas.bind('<Control-B1-Motion>', self.on_pan_move)
|
||||
self.canvas.bind('<Control-ButtonRelease-1>', self.on_pan_end)
|
||||
self.root.bind('<Delete>', lambda e: self.delete_selected_annotation())
|
||||
self.root.bind('<Control-s>', lambda e: self.save_annotations())
|
||||
self.root.bind('<Control-o>', lambda e: self.open_image())
|
||||
|
||||
def open_image(self):
|
||||
"""打开图片文件"""
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="选择图片文件",
|
||||
filetypes=[
|
||||
("图片文件", "*.jpg *.jpeg *.png *.bmp *.tiff"),
|
||||
("所有文件", "*.*")
|
||||
]
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
try:
|
||||
# 加载图片
|
||||
self.current_image_path = file_path
|
||||
self.image_pil = Image.open(file_path)
|
||||
|
||||
# 重置状态
|
||||
self.annotations = []
|
||||
self.selected_annotation = -1
|
||||
self.zoom_factor = 1.0
|
||||
self.offset_x = 0
|
||||
self.offset_y = 0
|
||||
|
||||
# 适应窗口
|
||||
self.fit_to_window()
|
||||
|
||||
# 更新标注列表
|
||||
self.update_annotation_list()
|
||||
|
||||
# 更新信息
|
||||
self.update_info()
|
||||
self.update_status("图片已加载")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"无法加载图片: {e}")
|
||||
|
||||
def fit_to_window(self):
|
||||
"""调整图片适应窗口"""
|
||||
if not self.image_pil:
|
||||
return
|
||||
|
||||
canvas_width = self.canvas.winfo_width()
|
||||
canvas_height = self.canvas.winfo_height()
|
||||
|
||||
if canvas_width <= 1 or canvas_height <= 1:
|
||||
canvas_width = 800
|
||||
canvas_height = 600
|
||||
|
||||
img_width, img_height = self.image_pil.size
|
||||
|
||||
# 计算缩放比例
|
||||
scale_x = canvas_width / img_width
|
||||
scale_y = canvas_height / img_height
|
||||
self.zoom_factor = min(scale_x, scale_y)
|
||||
|
||||
# 计算偏移量使图片居中
|
||||
scaled_width = img_width * self.zoom_factor
|
||||
scaled_height = img_height * self.zoom_factor
|
||||
self.offset_x = (canvas_width - scaled_width) / 2
|
||||
self.offset_y = (canvas_height - scaled_height) / 2
|
||||
|
||||
self.display_image()
|
||||
|
||||
def reset_view(self):
|
||||
"""重置视图"""
|
||||
self.zoom_factor = 1.0
|
||||
self.offset_x = 0
|
||||
self.offset_y = 0
|
||||
self.fit_to_window()
|
||||
|
||||
def zoom_in(self):
|
||||
"""放大"""
|
||||
self.zoom_factor *= 1.2
|
||||
self.display_image()
|
||||
|
||||
def zoom_out(self):
|
||||
"""缩小"""
|
||||
self.zoom_factor /= 1.2
|
||||
self.display_image()
|
||||
|
||||
def display_image(self):
|
||||
"""显示图片"""
|
||||
if not self.image_pil:
|
||||
return
|
||||
|
||||
# 缩放图片
|
||||
img_width, img_height = self.image_pil.size
|
||||
new_width = int(img_width * self.zoom_factor)
|
||||
new_height = int(img_height * self.zoom_factor)
|
||||
|
||||
scaled_image = self.image_pil.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# 转换为PhotoImage
|
||||
self.image_tk = ImageTk.PhotoImage(scaled_image)
|
||||
|
||||
# 清除画布
|
||||
self.canvas.delete("all")
|
||||
|
||||
# 显示图片
|
||||
self.canvas.create_image(self.offset_x, self.offset_y, anchor=tk.NW, image=self.image_tk)
|
||||
|
||||
# 绘制标注
|
||||
self.draw_annotations()
|
||||
|
||||
def draw_annotations(self):
|
||||
"""绘制所有标注"""
|
||||
if not self.image_pil:
|
||||
return
|
||||
|
||||
img_width, img_height = self.image_pil.size
|
||||
|
||||
for idx, (x1, y1, x2, y2) in enumerate(self.annotations):
|
||||
# 转换坐标
|
||||
canvas_x1 = x1 * self.zoom_factor + self.offset_x
|
||||
canvas_y1 = y1 * self.zoom_factor + self.offset_y
|
||||
canvas_x2 = x2 * self.zoom_factor + self.offset_x
|
||||
canvas_y2 = y2 * self.zoom_factor + self.offset_y
|
||||
|
||||
# 确定颜色
|
||||
if idx == self.selected_annotation:
|
||||
color = "red"
|
||||
width = 3
|
||||
else:
|
||||
color = "blue"
|
||||
width = 2
|
||||
|
||||
# 绘制矩形
|
||||
self.canvas.create_rectangle(canvas_x1, canvas_y1, canvas_x2, canvas_y2,
|
||||
outline=color, width=width, tags=f"rect_{idx}")
|
||||
|
||||
# 绘制序号
|
||||
self.canvas.create_text(canvas_x1 + 5, canvas_y1 + 15,
|
||||
text=str(idx + 1), anchor=tk.NW,
|
||||
fill=color, font=("Arial", 10, "bold"),
|
||||
tags=f"label_{idx}")
|
||||
|
||||
def canvas_to_image_coords(self, canvas_x, canvas_y):
|
||||
"""将画布坐标转换为图片坐标"""
|
||||
img_x = (canvas_x - self.offset_x) / self.zoom_factor
|
||||
img_y = (canvas_y - self.offset_y) / self.zoom_factor
|
||||
return img_x, img_y
|
||||
|
||||
def on_mouse_down(self, event):
|
||||
"""鼠标按下事件"""
|
||||
if self.draw_mode_var.get():
|
||||
# 绘制模式
|
||||
img_x, img_y = self.canvas_to_image_coords(event.x, event.y)
|
||||
if 0 <= img_x <= self.image_pil.size[0] and 0 <= img_y <= self.image_pil.size[1]:
|
||||
self.drawing = True
|
||||
self.start_x = img_x
|
||||
self.start_y = img_y
|
||||
else:
|
||||
# 选择模式 - 查找最近的标注
|
||||
self.find_nearest_annotation(event.x, event.y)
|
||||
|
||||
def find_nearest_annotation(self, canvas_x, canvas_y):
|
||||
"""查找最近的标注"""
|
||||
if not self.annotations:
|
||||
return
|
||||
|
||||
min_dist = float('inf')
|
||||
nearest_idx = -1
|
||||
|
||||
for idx, (x1, y1, x2, y2) in enumerate(self.annotations):
|
||||
# 转换坐标
|
||||
cx1 = x1 * self.zoom_factor + self.offset_x
|
||||
cy1 = y1 * self.zoom_factor + self.offset_y
|
||||
cx2 = x2 * self.zoom_factor + self.offset_x
|
||||
cy2 = y2 * self.zoom_factor + self.offset_y
|
||||
|
||||
# 计算到矩形边界的距离
|
||||
if cx1 <= canvas_x <= cx2 and cy1 <= canvas_y <= cy2:
|
||||
# 点在矩形内
|
||||
dist = 0
|
||||
else:
|
||||
# 计算到最近边的距离
|
||||
dx = max(cx1 - canvas_x, 0, canvas_x - cx2)
|
||||
dy = max(cy1 - canvas_y, 0, canvas_y - cy2)
|
||||
dist = (dx**2 + dy**2)**0.5
|
||||
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
nearest_idx = idx
|
||||
|
||||
# 如果距离足够近,选中该标注
|
||||
if min_dist < 20: # 20像素阈值
|
||||
self.selected_annotation = nearest_idx
|
||||
self.annotation_listbox.selection_clear(0, tk.END)
|
||||
self.annotation_listbox.selection_set(nearest_idx)
|
||||
self.annotation_listbox.see(nearest_idx)
|
||||
self.display_image()
|
||||
self.update_status(f"选中标注 #{nearest_idx + 1}")
|
||||
|
||||
def on_mouse_move(self, event):
|
||||
"""鼠标移动事件"""
|
||||
if self.drawing:
|
||||
# 绘制临时矩形
|
||||
if self.current_rect:
|
||||
self.canvas.delete(self.current_rect)
|
||||
|
||||
img_x, img_y = self.canvas_to_image_coords(event.x, event.y)
|
||||
canvas_x1 = self.start_x * self.zoom_factor + self.offset_x
|
||||
canvas_y1 = self.start_y * self.zoom_factor + self.offset_y
|
||||
canvas_x2 = img_x * self.zoom_factor + self.offset_x
|
||||
canvas_y2 = img_y * self.zoom_factor + self.offset_y
|
||||
|
||||
self.current_rect = self.canvas.create_rectangle(
|
||||
canvas_x1, canvas_y1, canvas_x2, canvas_y2,
|
||||
outline="green", width=2, dash=(5, 5)
|
||||
)
|
||||
|
||||
def on_mouse_up(self, event):
|
||||
"""鼠标释放事件"""
|
||||
if self.drawing:
|
||||
# 完成绘制
|
||||
img_x, img_y = self.canvas_to_image_coords(event.x, event.y)
|
||||
|
||||
# 确保坐标正确排序
|
||||
x1 = min(self.start_x, img_x)
|
||||
y1 = min(self.start_y, img_y)
|
||||
x2 = max(self.start_x, img_x)
|
||||
y2 = max(self.start_y, img_y)
|
||||
|
||||
# 检查矩形大小
|
||||
if x2 - x1 > 5 and y2 - y1 > 5:
|
||||
# 添加标注(只保存坐标,不保存标签)
|
||||
self.annotations.append((x1, y1, x2, y2))
|
||||
self.update_annotation_list()
|
||||
self.display_image()
|
||||
self.update_status(f"已添加标注 #{len(self.annotations)}")
|
||||
else:
|
||||
self.update_status("标注太小,已忽略")
|
||||
|
||||
# 清理绘制状态
|
||||
self.drawing = False
|
||||
if self.current_rect:
|
||||
self.canvas.delete(self.current_rect)
|
||||
self.current_rect = None
|
||||
|
||||
def on_pan_start(self, event):
|
||||
"""开始平移"""
|
||||
self.panning = True
|
||||
self.pan_start_x = event.x
|
||||
self.pan_start_y = event.y
|
||||
|
||||
def on_pan_move(self, event):
|
||||
"""平移移动"""
|
||||
if self.panning:
|
||||
dx = event.x - self.pan_start_x
|
||||
dy = event.y - self.pan_start_y
|
||||
self.offset_x += dx
|
||||
self.offset_y += dy
|
||||
self.pan_start_x = event.x
|
||||
self.pan_start_y = event.y
|
||||
self.display_image()
|
||||
|
||||
def on_pan_end(self, event):
|
||||
"""结束平移"""
|
||||
self.panning = False
|
||||
|
||||
def update_annotation_list(self):
|
||||
"""更新标注列表"""
|
||||
self.annotation_listbox.delete(0, tk.END)
|
||||
for idx, (x1, y1, x2, y2) in enumerate(self.annotations):
|
||||
width = x2 - x1
|
||||
height = y2 - y1
|
||||
list_text = f"{idx + 1}. 矩形 ({width:.0f}x{height:.0f})"
|
||||
self.annotation_listbox.insert(tk.END, list_text)
|
||||
|
||||
def on_annotation_select(self, event):
|
||||
"""标注列表选择事件"""
|
||||
selection = self.annotation_listbox.curselection()
|
||||
if selection:
|
||||
self.selected_annotation = selection[0]
|
||||
self.display_image()
|
||||
self.update_status(f"已选中标注 #{self.selected_annotation + 1}")
|
||||
|
||||
def delete_selected_annotation(self):
|
||||
"""删除选中的标注"""
|
||||
if self.selected_annotation >= 0 and self.selected_annotation < len(self.annotations):
|
||||
del self.annotations[self.selected_annotation]
|
||||
self.selected_annotation = -1
|
||||
self.update_annotation_list()
|
||||
self.display_image()
|
||||
self.update_status("已删除选中标注")
|
||||
|
||||
def clear_current_annotation(self):
|
||||
"""清除当前标注"""
|
||||
self.selected_annotation = -1
|
||||
self.display_image()
|
||||
self.update_status("已清除选中状态")
|
||||
|
||||
def clear_all_annotations(self):
|
||||
"""删除所有标注"""
|
||||
if messagebox.askyesno("确认", "确定要删除所有标注吗?"):
|
||||
self.annotations = []
|
||||
self.selected_annotation = -1
|
||||
self.update_annotation_list()
|
||||
self.display_image()
|
||||
self.update_status("已删除所有标注")
|
||||
|
||||
def save_annotations(self):
|
||||
"""保存标注到文件(仅TXT格式,纯坐标,格式:top right bottom left)"""
|
||||
if not self.current_image_path:
|
||||
messagebox.showwarning("警告", "请先加载图片")
|
||||
return
|
||||
|
||||
if not self.annotations:
|
||||
if not messagebox.askyesno("确认", "没有标注要保存,是否继续?"):
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取图像尺寸
|
||||
img_width, img_height = self.image_pil.size
|
||||
|
||||
# 生成保存路径:图片文件名.txt(同目录下)
|
||||
image_dir = os.path.dirname(self.current_image_path)
|
||||
image_basename = os.path.splitext(os.path.basename(self.current_image_path))[0]
|
||||
file_path = os.path.join(image_dir, f"{image_basename}.txt")
|
||||
|
||||
# 保存标注(纯坐标格式,top right bottom left)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
for x1, y1, x2, y2 in self.annotations:
|
||||
# 转换为百分比坐标(top, right, bottom, left)
|
||||
top_pct = y1 / img_height
|
||||
right_pct = x2 / img_width
|
||||
bottom_pct = y2 / img_height
|
||||
left_pct = x1 / img_width
|
||||
|
||||
# 写入坐标数据(保留6位小数,空格分隔)
|
||||
f.write(f"{top_pct:.6f} {right_pct:.6f} {bottom_pct:.6f} {left_pct:.6f}\n")
|
||||
|
||||
self.update_status(f"已保存 {len(self.annotations)} 个标注到 {os.path.basename(file_path)}")
|
||||
messagebox.showinfo("成功", f"标注已保存到:\n{file_path}\n\n格式: top right bottom left (百分比坐标)")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存失败: {e}")
|
||||
|
||||
def load_annotations(self):
|
||||
"""加载标注文件(TXT格式,纯坐标,格式:top right bottom left)"""
|
||||
if not self.current_image_path:
|
||||
messagebox.showwarning("警告", "请先加载图片")
|
||||
return
|
||||
|
||||
# 默认加载同目录下的同名txt文件
|
||||
image_dir = os.path.dirname(self.current_image_path)
|
||||
image_basename = os.path.splitext(os.path.basename(self.current_image_path))[0]
|
||||
default_file = os.path.join(image_dir, f"{image_basename}.txt")
|
||||
|
||||
if os.path.exists(default_file):
|
||||
file_path = default_file
|
||||
else:
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="选择标注文件",
|
||||
initialdir=image_dir,
|
||||
filetypes=[
|
||||
("文本文件", "*.txt"),
|
||||
("所有文件", "*.*")
|
||||
]
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
try:
|
||||
self.load_txt_annotations(file_path)
|
||||
self.update_annotation_list()
|
||||
self.display_image()
|
||||
self.update_status(f"已加载 {len(self.annotations)} 个标注")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"加载失败: {e}")
|
||||
|
||||
def load_txt_annotations(self, file_path):
|
||||
"""加载TXT格式的标注文件(纯坐标格式,top right bottom left)"""
|
||||
self.annotations = []
|
||||
img_width, img_height = self.image_pil.size
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) == 4:
|
||||
try:
|
||||
top_pct = float(parts[0])
|
||||
right_pct = float(parts[1])
|
||||
bottom_pct = float(parts[2])
|
||||
left_pct = float(parts[3])
|
||||
|
||||
# 验证百分比坐标范围
|
||||
if all(0 <= v <= 1 for v in [top_pct, right_pct, bottom_pct, left_pct]):
|
||||
# 转换为绝对坐标
|
||||
x1 = left_pct * img_width
|
||||
y1 = top_pct * img_height
|
||||
x2 = right_pct * img_width
|
||||
y2 = bottom_pct * img_height
|
||||
|
||||
self.annotations.append((x1, y1, x2, y2))
|
||||
else:
|
||||
print(f"警告: 第{line_num}行坐标超出范围 [0,1]: {line}")
|
||||
except ValueError as e:
|
||||
print(f"警告: 第{line_num}行格式错误: {line}, 错误: {e}")
|
||||
else:
|
||||
print(f"警告: 第{line_num}行格式错误,需要4个坐标值: {line}")
|
||||
|
||||
def update_info(self):
|
||||
"""更新信息显示"""
|
||||
if self.current_image_path and self.image_pil:
|
||||
img_width, img_height = self.image_pil.size
|
||||
filename = os.path.basename(self.current_image_path)
|
||||
self.info_label.config(text=f"图片: {filename}\n尺寸: {img_width}x{img_height}\n标注: {len(self.annotations)}个")
|
||||
else:
|
||||
self.info_label.config(text="未加载图片")
|
||||
|
||||
def update_status(self, message=None):
|
||||
"""更新状态栏"""
|
||||
if message:
|
||||
self.status_label.config(text=f"状态: {message}")
|
||||
else:
|
||||
self.status_label.config(text="状态: 就绪")
|
||||
|
||||
def update(self):
|
||||
"""更新界面"""
|
||||
self.update_info()
|
||||
self.root.after(1000, self.update)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
root = tk.Tk()
|
||||
app = ImageAnnotationTool(root)
|
||||
app.update()
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1118
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,102 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include "net/HttpClient.hpp"
|
||||
#include "net/WebSocketServer.hpp"
|
||||
#include "llm/LLMClient.hpp"
|
||||
#include "llm/LLMWebSocketServer.hpp"
|
||||
|
||||
// 全局变量用于信号处理
|
||||
std::atomic<bool> g_running{true};
|
||||
|
||||
// 从文件读取API密钥
|
||||
std::string read_api_key_from_file(const std::string& filepath) {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("无法打开密钥文件: " + filepath);
|
||||
}
|
||||
|
||||
std::string api_key;
|
||||
std::getline(file, api_key);
|
||||
|
||||
// 去除可能的空白字符
|
||||
api_key.erase(0, api_key.find_first_not_of(" \t\n\r\f\v"));
|
||||
api_key.erase(api_key.find_last_not_of(" \t\n\r\f\v") + 1);
|
||||
|
||||
if (api_key.empty()) {
|
||||
throw std::runtime_error("密钥文件为空: " + filepath);
|
||||
}
|
||||
|
||||
return api_key;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// 解析命令行参数
|
||||
std::string api_key;
|
||||
unsigned short port = 8080;
|
||||
std::string key_file_path = std::string(getenv("HOME")) + "/.cert/deepseek.key";
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--key-file" && i + 1 < argc) {
|
||||
key_file_path = argv[++i];
|
||||
} else if (arg == "--port" && i + 1 < argc) {
|
||||
port = static_cast<unsigned short>(std::stoi(argv[++i]));
|
||||
} else if (arg == "--help") {
|
||||
std::cout << "用法: " << argv[0] << " [--key-file <密钥文件路径>] [--port <端口号>]" << std::endl;
|
||||
std::cout << "默认密钥文件路径: ~/.cert/deepseek.key" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 从文件读取API密钥
|
||||
std::cout << "正在从文件加载API密钥: " << key_file_path << std::endl;
|
||||
api_key = read_api_key_from_file(key_file_path);
|
||||
std::cout << "API密钥加载成功" << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "错误: " << e.what() << std::endl;
|
||||
std::cerr << "请确保密钥文件存在且包含有效的API密钥" << std::endl;
|
||||
std::cerr << "默认路径: ~/.cert/deepseek.key" << std::endl;
|
||||
std::cerr << "或使用 --key-file 参数指定其他路径" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 创建并启动服务器
|
||||
pmc::llm::LLMWebSocketServer server(api_key, port);
|
||||
|
||||
// 设置信号处理
|
||||
signal(SIGINT, [](int) {
|
||||
g_running = false;
|
||||
});
|
||||
|
||||
try {
|
||||
server.start();
|
||||
|
||||
// 主循环,等待信号
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
// 停止服务器
|
||||
server.stop();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "服务器错误: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace websocket = beast::websocket;
|
||||
namespace net = boost::asio;
|
||||
namespace ssl = net::ssl;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
// 获取 MIME 类型
|
||||
std::string get_mime_type(const std::string& path) {
|
||||
if (path.ends_with(".html") || path.ends_with(".htm")) return "text/html";
|
||||
if (path.ends_with(".css")) return "text/css";
|
||||
if (path.ends_with(".js")) return "application/javascript";
|
||||
if (path.ends_with(".json")) return "application/json";
|
||||
if (path.ends_with(".png")) return "image/png";
|
||||
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg";
|
||||
if (path.ends_with(".gif")) return "image/gif";
|
||||
if (path.ends_with(".svg")) return "image/svg+xml";
|
||||
if (path.ends_with(".ico")) return "image/x-icon";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
std::string read_file(const std::string& path) {
|
||||
try {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return "";
|
||||
|
||||
std::string content;
|
||||
file.seekg(0, std::ios::end);
|
||||
content.resize(file.tellg());
|
||||
file.seekg(0, std::ios::beg);
|
||||
file.read(&content[0], content.size());
|
||||
return content;
|
||||
} catch (...) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket 客户端,用于连接LLM后端
|
||||
class WebSocketClient : public std::enable_shared_from_this<WebSocketClient> {
|
||||
public:
|
||||
WebSocketClient(net::io_context& ioc, const std::string& host, const std::string& port)
|
||||
: resolver_(net::make_strand(ioc))
|
||||
, ws_(net::make_strand(ioc))
|
||||
, host_(host)
|
||||
, port_(port)
|
||||
, connected_(false) {}
|
||||
|
||||
void connect() {
|
||||
resolver_.async_resolve(host_, port_,
|
||||
beast::bind_front_handler(&WebSocketClient::on_resolve, shared_from_this()));
|
||||
}
|
||||
|
||||
void send(const std::string& message) {
|
||||
if (!connected_) {
|
||||
std::cerr << "Cannot send: not connected to backend" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 将消息放入队列
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
send_queue_.push(message);
|
||||
}
|
||||
|
||||
// 如果当前没有在发送,开始发送
|
||||
if (!sending_) {
|
||||
send_next();
|
||||
}
|
||||
}
|
||||
|
||||
void set_message_handler(std::function<void(const std::string&)> handler) {
|
||||
message_handler_ = handler;
|
||||
}
|
||||
|
||||
bool is_connected() const { return connected_; }
|
||||
|
||||
private:
|
||||
|
||||
void on_resolve(beast::error_code ec, tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
std::cerr << "Resolve error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
beast::get_lowest_layer(ws_).async_connect(results,
|
||||
beast::bind_front_handler(&WebSocketClient::on_connect, shared_from_this()));
|
||||
}
|
||||
|
||||
void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
|
||||
if (ec) {
|
||||
std::cerr << "Connect error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
ws_.async_handshake(host_, "/",
|
||||
beast::bind_front_handler(&WebSocketClient::on_handshake, shared_from_this()));
|
||||
}
|
||||
|
||||
void on_handshake(beast::error_code ec) {
|
||||
if (ec) {
|
||||
std::cerr << "Handshake error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Connected to LLM backend at " << host_ << ":" << port_ << std::endl;
|
||||
connected_ = true;
|
||||
|
||||
// 开始接收消息
|
||||
do_read();
|
||||
|
||||
// 发送欢迎消息
|
||||
send_welcome();
|
||||
}
|
||||
|
||||
void send_welcome() {
|
||||
std::string welcome_msg = R"({"type": "welcome", "message": "Connected to PMC LLM Backend"})";
|
||||
send(welcome_msg);
|
||||
}
|
||||
|
||||
void do_read() {
|
||||
ws_.async_read(buffer_,
|
||||
beast::bind_front_handler(&WebSocketClient::on_read, shared_from_this()));
|
||||
}
|
||||
|
||||
void on_read(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec == websocket::error::closed) {
|
||||
std::cout << "WebSocket connection closed" << std::endl;
|
||||
connected_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
std::cerr << "Read error: " << ec.message() << std::endl;
|
||||
connected_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
|
||||
// 处理接收到的消息
|
||||
if (message_handler_) {
|
||||
message_handler_(message);
|
||||
}
|
||||
|
||||
// 继续读取
|
||||
do_read();
|
||||
}
|
||||
|
||||
void send_next() {
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
|
||||
if (send_queue_.empty()) {
|
||||
sending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sending_ = true;
|
||||
std::string message = send_queue_.front();
|
||||
send_queue_.pop();
|
||||
|
||||
ws_.async_write(net::buffer(message),
|
||||
beast::bind_front_handler(&WebSocketClient::on_write, shared_from_this()));
|
||||
}
|
||||
|
||||
void on_write(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
std::cerr << "Write error: " << ec.message() << std::endl;
|
||||
connected_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送下一个消息
|
||||
send_next();
|
||||
}
|
||||
|
||||
tcp::resolver resolver_;
|
||||
websocket::stream<beast::tcp_stream> ws_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::atomic<bool> connected_;
|
||||
|
||||
std::queue<std::string> send_queue_;
|
||||
std::mutex queue_mutex_;
|
||||
std::atomic<bool> sending_{false};
|
||||
|
||||
std::function<void(const std::string&)> message_handler_;
|
||||
};
|
||||
|
||||
// HTTP 请求处理器
|
||||
class HttpSession : public std::enable_shared_from_this<HttpSession> {
|
||||
public:
|
||||
HttpSession(tcp::socket socket, const std::string& static_dir,
|
||||
std::shared_ptr<WebSocketClient> ws_client)
|
||||
: socket_(std::move(socket)), static_dir_(static_dir), ws_client_(ws_client) {}
|
||||
|
||||
void start() {
|
||||
read_request();
|
||||
}
|
||||
|
||||
private:
|
||||
void read_request() {
|
||||
auto self = shared_from_this();
|
||||
|
||||
http::async_read(socket_, buffer_, request_,
|
||||
[self](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (!ec) {
|
||||
self->process_request();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void process_request() {
|
||||
response_.version(request_.version());
|
||||
response_.keep_alive(false);
|
||||
|
||||
// 检查是否为 WebSocket 升级请求
|
||||
if (websocket::is_upgrade(request_)) {
|
||||
handle_websocket_upgrade();
|
||||
return;
|
||||
}
|
||||
|
||||
// 只处理 GET 请求
|
||||
if (request_.method() != http::verb::get) {
|
||||
response_.result(http::status::bad_request);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "Invalid method\n";
|
||||
write_response();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取请求路径
|
||||
std::string target = request_.target();
|
||||
if (target.empty() || target == "/") {
|
||||
target = "/llm_chat.html"; // 默认加载聊天界面
|
||||
}
|
||||
|
||||
// 防止路径遍历攻击
|
||||
if (target.find("..") != std::string::npos) {
|
||||
response_.result(http::status::forbidden);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "Forbidden\n";
|
||||
write_response();
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建文件路径
|
||||
std::string file_path = static_dir_ + target;
|
||||
|
||||
// 读取文件
|
||||
std::string content = read_file(file_path);
|
||||
|
||||
if (content.empty()) {
|
||||
// 文件不存在,返回 404
|
||||
if (target == "/llm_chat.html") {
|
||||
// 如果聊天界面不存在,返回默认页面
|
||||
response_.result(http::status::ok);
|
||||
response_.set(http::field::content_type, "text/html");
|
||||
response_.body() = "<html><body><h1>PMC LLM Chat Interface</h1><p>Chat interface not found. Please check static directory.</p></body></html>";
|
||||
} else {
|
||||
response_.result(http::status::not_found);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "File not found: " + target + "\n";
|
||||
}
|
||||
} else {
|
||||
// 文件存在,返回内容
|
||||
response_.result(http::status::ok);
|
||||
response_.set(http::field::content_type, get_mime_type(file_path));
|
||||
response_.body() = content;
|
||||
}
|
||||
|
||||
write_response();
|
||||
}
|
||||
|
||||
void handle_websocket_upgrade() {
|
||||
try {
|
||||
// 创建 WebSocket 会话
|
||||
auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(socket_));
|
||||
|
||||
// 接受 WebSocket 握手
|
||||
ws->async_accept(request_,
|
||||
[self = shared_from_this(), ws](beast::error_code ec) {
|
||||
if (!ec) {
|
||||
self->handle_websocket_session(ws);
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "WebSocket upgrade error: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_websocket_session(std::shared_ptr<websocket::stream<beast::tcp_stream>> ws) {
|
||||
std::cout << "New WebSocket connection established" << std::endl;
|
||||
|
||||
// 发送欢迎消息
|
||||
std::string welcome_msg = R"({"type": "welcome", "message": "Welcome to PMC LLM Chat Interface!"})";
|
||||
ws->write(net::buffer(welcome_msg));
|
||||
|
||||
// 开始读取消息
|
||||
do_websocket_read(ws);
|
||||
}
|
||||
|
||||
void do_websocket_read(std::shared_ptr<websocket::stream<beast::tcp_stream>> ws) {
|
||||
auto buffer = std::make_shared<beast::flat_buffer>();
|
||||
|
||||
ws->async_read(*buffer,
|
||||
[self = shared_from_this(), ws, buffer](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec == websocket::error::closed) {
|
||||
std::cout << "WebSocket connection closed by client" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket read error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
std::string message = beast::buffers_to_string(buffer->data());
|
||||
self->handle_websocket_message(ws, message);
|
||||
|
||||
// 继续读取
|
||||
self->do_websocket_read(ws);
|
||||
});
|
||||
}
|
||||
|
||||
void handle_websocket_message(std::shared_ptr<websocket::stream<beast::tcp_stream>> ws,
|
||||
const std::string& message) {
|
||||
try {
|
||||
std::cout << "Received WebSocket message: " << message << std::endl;
|
||||
|
||||
// 解析 JSON 消息
|
||||
// 这里简化处理,实际应该使用 JSON 库
|
||||
if (message.find("\"type\":\"hello\"") != std::string::npos ||
|
||||
message.find("hello") != std::string::npos) {
|
||||
// 发送欢迎消息
|
||||
std::string response = R"({"type": "welcome", "message": "Hello! I'm PMC LLM Assistant. How can I help you today?"})";
|
||||
ws->write(net::buffer(response));
|
||||
} else if (message.find("\"type\":\"message\"") != std::string::npos ||
|
||||
message.find("\"type\":\"chat\"") != std::string::npos) {
|
||||
// 聊天消息,转发到 LLM 后端
|
||||
if (ws_client_ && ws_client_->is_connected()) {
|
||||
// 发送思考指示
|
||||
std::string thinking_msg = R"({"type": "thinking"})";
|
||||
ws->write(net::buffer(thinking_msg));
|
||||
|
||||
// 转发到 LLM 后端
|
||||
ws_client_->send(message);
|
||||
|
||||
// 设置消息处理器,将后端响应转发回客户端
|
||||
ws_client_->set_message_handler([this, ws](const std::string& backend_response) {
|
||||
// 将后端响应转发给 WebSocket 客户端
|
||||
std::string response = R"({"type": "response", "content": ")" +
|
||||
escape_json(backend_response) + "\"}";
|
||||
ws->write(net::buffer(response));
|
||||
});
|
||||
} else {
|
||||
// LLM 后端未连接,返回模拟响应
|
||||
std::string response = R"({"type": "response", "content": "LLM backend is not connected. This is a simulated response."})";
|
||||
ws->write(net::buffer(response));
|
||||
}
|
||||
} else if (message.find("\"type\":\"change_model\"") != std::string::npos) {
|
||||
// 处理模型切换请求
|
||||
std::string response = R"({"type": "system", "message": "Model change request received"})";
|
||||
ws->write(net::buffer(response));
|
||||
} else {
|
||||
// 默认处理:直接回复
|
||||
std::string response = R"({"type": "response", "content": "I received your message: )" +
|
||||
escape_json(message) + "\"}";
|
||||
ws->write(net::buffer(response));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error handling WebSocket message: " << e.what() << std::endl;
|
||||
std::string error_msg = R"({"type": "error", "message": "Error processing message"})";
|
||||
ws->write(net::buffer(error_msg));
|
||||
}
|
||||
}
|
||||
|
||||
std::string escape_json(const std::string& input) {
|
||||
std::string output;
|
||||
output.reserve(input.length());
|
||||
|
||||
for (char c : input) {
|
||||
switch (c) {
|
||||
case '"': output += "\\\""; break;
|
||||
case '\\': output += "\\\\"; break;
|
||||
case '\b': output += "\\b"; break;
|
||||
case '\f': output += "\\f"; break;
|
||||
case '\n': output += "\\n"; break;
|
||||
case '\r': output += "\\r"; break;
|
||||
case '\t': output += "\\t"; break;
|
||||
default: output += c; break;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void write_response() {
|
||||
auto self = shared_from_this();
|
||||
|
||||
response_.content_length(response_.body().size());
|
||||
|
||||
http::async_write(socket_, response_,
|
||||
[self](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
// 连接关闭后自动清理
|
||||
});
|
||||
}
|
||||
|
||||
tcp::socket socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> request_;
|
||||
http::response<http::string_body> response_;
|
||||
std::string static_dir_;
|
||||
std::shared_ptr<WebSocketClient> ws_client_;
|
||||
};
|
||||
|
||||
// HTTP 服务器
|
||||
class HttpServer {
|
||||
public:
|
||||
HttpServer(net::io_context& ioc, tcp::endpoint endpoint,
|
||||
const std::string& static_dir, const std::string& backend_host,
|
||||
const std::string& backend_port)
|
||||
: acceptor_(ioc)
|
||||
, static_dir_(static_dir)
|
||||
, backend_host_(backend_host)
|
||||
, backend_port_(backend_port) {
|
||||
|
||||
beast::error_code ec;
|
||||
|
||||
// 打开 acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Open error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置地址重用
|
||||
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Set option error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定到地址
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Bind error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始监听
|
||||
acceptor_.listen(net::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Listen error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "HTTP server listening on " << endpoint.address().to_string()
|
||||
<< ":" << endpoint.port() << std::endl;
|
||||
|
||||
// 创建 WebSocket 客户端连接到 LLM 后端
|
||||
ws_client_ = std::make_shared<WebSocketClient>(ioc, backend_host_, backend_port_);
|
||||
|
||||
// 尝试连接到后端
|
||||
std::cout << "Connecting to LLM backend at " << backend_host_ << ":" << backend_port_ << std::endl;
|
||||
ws_client_->connect();
|
||||
|
||||
// 开始接受连接
|
||||
do_accept();
|
||||
}
|
||||
|
||||
private:
|
||||
void do_accept() {
|
||||
acceptor_.async_accept(
|
||||
[this](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
std::make_shared<HttpSession>(std::move(socket), static_dir_, ws_client_)->start();
|
||||
}
|
||||
|
||||
// 继续接受新连接
|
||||
do_accept();
|
||||
});
|
||||
}
|
||||
|
||||
tcp::acceptor acceptor_;
|
||||
std::string static_dir_;
|
||||
std::string backend_host_;
|
||||
std::string backend_port_;
|
||||
std::shared_ptr<WebSocketClient> ws_client_;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
try {
|
||||
// 默认配置
|
||||
std::string host = "0.0.0.0";
|
||||
unsigned short port = 8080;
|
||||
std::string static_dir = "./static";
|
||||
std::string backend_host = "localhost";
|
||||
std::string backend_port = "8081";
|
||||
|
||||
// 解析命令行参数
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--host" && i + 1 < argc) {
|
||||
host = argv[++i];
|
||||
} else if (arg == "--port" && i + 1 < argc) {
|
||||
port = static_cast<unsigned short>(std::stoi(argv[++i]));
|
||||
} else if (arg == "--static" && i + 1 < argc) {
|
||||
static_dir = argv[++i];
|
||||
} else if (arg == "--backend-host" && i + 1 < argc) {
|
||||
backend_host = argv[++i];
|
||||
} else if (arg == "--backend-port" && i + 1 < argc) {
|
||||
backend_port = argv[++i];
|
||||
} else if (arg == "--help") {
|
||||
std::cout << "Usage: " << argv[0] << " [options]\n"
|
||||
<< "Options:\n"
|
||||
<< " --host <address> HTTP server host (default: 0.0.0.0)\n"
|
||||
<< " --port <port> HTTP server port (default: 8080)\n"
|
||||
<< " --static <dir> Static files directory (default: ./static)\n"
|
||||
<< " --backend-host <host> LLM backend host (default: localhost)\n"
|
||||
<< " --backend-port <port> LLM backend port (default: 8081)\n"
|
||||
<< " --help Show this help message\n";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查静态目录
|
||||
if (!std::filesystem::exists(static_dir)) {
|
||||
std::cout << "Static directory '" << static_dir << "' does not exist. Creating it..." << std::endl;
|
||||
std::filesystem::create_directories(static_dir);
|
||||
|
||||
// 创建默认的 llm_chat.html 文件
|
||||
std::string chat_html_path = static_dir + "/llm_chat.html";
|
||||
if (!std::filesystem::exists(chat_html_path)) {
|
||||
std::cout << "Creating default chat interface..." << std::endl;
|
||||
// 这里可以添加默认的 HTML 内容
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "PMC LLM Frontend Server\n";
|
||||
std::cout << "=======================\n";
|
||||
std::cout << "HTTP Server: " << host << ":" << port << "\n";
|
||||
std::cout << "Static Directory: " << static_dir << "\n";
|
||||
std::cout << "LLM Backend: " << backend_host << ":" << backend_port << "\n";
|
||||
std::cout << "==============================\n\n";
|
||||
|
||||
// 创建 I/O 上下文
|
||||
net::io_context ioc;
|
||||
|
||||
// 创建 HTTP 服务器
|
||||
tcp::endpoint endpoint(net::ip::make_address(host), port);
|
||||
HttpServer server(ioc, endpoint, static_dir, backend_host, backend_port);
|
||||
|
||||
// 运行 I/O 上下文
|
||||
ioc.run();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <iostream>
|
||||
#include "hd/Camera.h"
|
||||
|
||||
int main() {
|
||||
|
||||
Camera_init(640, 480);
|
||||
|
||||
std::string imgpath = std::to_string(time(NULL)) + ".jpeg";
|
||||
std::string txtpath =std::to_string(time(NULL)) + ".txt";
|
||||
|
||||
bool flag;
|
||||
auto img = get_frame_from_camera(flag);
|
||||
img.save(imgpath.c_str());
|
||||
|
||||
Camera_close();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
相机客户端Python实现
|
||||
功能:连接服务器接收YUYV格式视频流,转换为RGB并显示
|
||||
"""
|
||||
|
||||
import socket
|
||||
import cv2
|
||||
import numpy as np
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <server_ip> <port>")
|
||||
return 1
|
||||
|
||||
server_ip = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
|
||||
# 创建TCP套接字
|
||||
sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
try:
|
||||
# 连接到服务器
|
||||
sockfd.connect((server_ip, port))
|
||||
print(f"Connected to server: {server_ip}:{port}")
|
||||
|
||||
# 图像参数
|
||||
WIDTH = 640
|
||||
HEIGHT = 480
|
||||
FRAME_BUFFER_SIZE = WIDTH * HEIGHT * 2 # YUYV格式:640x480,每个像素2字节
|
||||
|
||||
# OpenCV窗口
|
||||
cv2.namedWindow("Camera Stream", cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
frame_count = 0
|
||||
while True:
|
||||
# 接收帧数据
|
||||
frame_buffer = b''
|
||||
while len(frame_buffer) < FRAME_BUFFER_SIZE:
|
||||
data = sockfd.recv(FRAME_BUFFER_SIZE - len(frame_buffer))
|
||||
if not data:
|
||||
print("Server closed connection")
|
||||
return 0
|
||||
frame_buffer += data
|
||||
|
||||
print(f"Frame {frame_count + 1} received: {len(frame_buffer)} bytes")
|
||||
frame_count += 1
|
||||
|
||||
# 使用OpenCV处理YUYV帧
|
||||
yuyv_frame = np.frombuffer(frame_buffer, dtype=np.uint8).reshape((HEIGHT, WIDTH, 2))
|
||||
rgb_frame = cv2.cvtColor(yuyv_frame, cv2.COLOR_YUV2BGR_YUYV)
|
||||
|
||||
# 显示帧
|
||||
cv2.imshow("Camera Stream", rgb_frame)
|
||||
|
||||
# 等待1ms,允许窗口刷新,同时检查是否按下ESC键退出
|
||||
if cv2.waitKey(1) == 27:
|
||||
print("ESC key pressed, exiting...")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
finally:
|
||||
# 清理资源
|
||||
sockfd.close()
|
||||
cv2.destroyAllWindows()
|
||||
print("Client exiting")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
压缩相机客户端Python实现
|
||||
功能:连接服务器接收JPEG压缩视频流,解码并显示
|
||||
"""
|
||||
|
||||
import socket
|
||||
import cv2
|
||||
import numpy as np
|
||||
import sys
|
||||
import time
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
|
||||
def main():
|
||||
print("Starting Compressed Camera Client...")
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <server_ip> <port>")
|
||||
return 1
|
||||
|
||||
server_ip = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
|
||||
print(f"Configuration: Server IP={server_ip}, Port={port}")
|
||||
|
||||
# 创建TCP套接字
|
||||
print("Creating TCP socket...")
|
||||
sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
# 设置套接字超时
|
||||
sockfd.settimeout(5.0)
|
||||
print("TCP socket created successfully")
|
||||
|
||||
try:
|
||||
# 连接到服务器
|
||||
print(f"Connecting to server {server_ip}:{port}...")
|
||||
sockfd.connect((server_ip, port))
|
||||
print(f"Connected to server: {server_ip}:{port}")
|
||||
|
||||
# 关闭超时,改为非阻塞模式
|
||||
sockfd.setblocking(True)
|
||||
print("Socket set to blocking mode")
|
||||
|
||||
# OpenCV窗口
|
||||
print("Creating OpenCV window...")
|
||||
# 尝试使用不同的窗口参数
|
||||
cv2.namedWindow("Compressed Camera Stream", cv2.WINDOW_GUI_NORMAL)
|
||||
# 设置固定的窗口大小
|
||||
cv2.resizeWindow("Compressed Camera Stream", 640, 480)
|
||||
# 确保窗口大小是固定的
|
||||
cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO)
|
||||
cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_AUTOSIZE, cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
# 显示初始信息
|
||||
info_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
cv2.putText(info_frame, "Connecting to server...", (50, 240),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||||
cv2.imshow("Compressed Camera Stream", info_frame)
|
||||
cv2.waitKey(100)
|
||||
|
||||
frame_count = 0
|
||||
buffer = b'' # 用于累积数据的缓冲区
|
||||
total_bytes_received = 0
|
||||
jpeg_found_count = 0
|
||||
|
||||
# 上次收到数据的时间
|
||||
last_receive_time = time.time()
|
||||
|
||||
while True:
|
||||
try:
|
||||
# 接收数据
|
||||
data = sockfd.recv(8192)
|
||||
if not data:
|
||||
print("Server closed connection")
|
||||
return 0
|
||||
|
||||
last_receive_time = time.time()
|
||||
total_bytes_received += len(data)
|
||||
print(f"Received {len(data)} bytes, total: {total_bytes_received} bytes")
|
||||
|
||||
# 将新数据添加到缓冲区
|
||||
buffer += data
|
||||
|
||||
# 仅保留最近的500KB数据,防止内存溢出
|
||||
if len(buffer) > 500 * 1024:
|
||||
print(f"Buffer size exceeded 500KB, trimming to 500KB")
|
||||
buffer = buffer[-500 * 1024:]
|
||||
|
||||
# 检查缓冲区中是否有JPEG标记
|
||||
has_jpeg = b'\xff\xd8' in buffer
|
||||
print(f"Buffer contains JPEG SOI: {has_jpeg}, buffer size: {len(buffer)}")
|
||||
|
||||
# JPEG文件格式标记
|
||||
JPEG_START = b'\xff\xd8' # SOI (Start of Image)
|
||||
JPEG_END = b'\xff\xd9' # EOI (End of Image)
|
||||
|
||||
# 查找所有完整的JPEG帧
|
||||
while True:
|
||||
# 查找JPEG开始标记
|
||||
start_idx = buffer.find(JPEG_START)
|
||||
if start_idx == -1:
|
||||
# 没有找到开始标记
|
||||
print(f"No JPEG SOI found, buffer size: {len(buffer)}")
|
||||
# 清空大部分缓冲区,只保留末尾可能包含部分JPEG头的数据
|
||||
if len(buffer) > 1024:
|
||||
buffer = buffer[-1024:]
|
||||
print(f"Trimmed buffer to {len(buffer)} bytes")
|
||||
break
|
||||
|
||||
# 查找当前开始标记后的结束标记
|
||||
end_idx = buffer.find(JPEG_END, start_idx + 2)
|
||||
if end_idx == -1:
|
||||
# 没有找到完整的结束标记
|
||||
print(f"Found JPEG SOI but no EOI, buffer size from SOI: {len(buffer) - start_idx}")
|
||||
# 保留从开始标记到缓冲区末尾的数据
|
||||
buffer = buffer[start_idx:]
|
||||
break
|
||||
|
||||
# 提取完整的JPEG帧(包括结束标记)
|
||||
jpeg_frame = buffer[start_idx:end_idx + 2]
|
||||
jpeg_found_count += 1
|
||||
print(f"Extracted JPEG frame {jpeg_found_count}, size: {len(jpeg_frame)} bytes")
|
||||
|
||||
# 从缓冲区中移除已处理的JPEG帧
|
||||
buffer = buffer[end_idx + 2:]
|
||||
print(f"Buffer remaining after extraction: {len(buffer)} bytes")
|
||||
|
||||
# 只处理合理大小的JPEG帧(1KB到500KB)
|
||||
if 1024 <= len(jpeg_frame) <= 500 * 1024:
|
||||
try:
|
||||
# 使用PIL库来解码和显示图像
|
||||
image_stream = io.BytesIO(jpeg_frame)
|
||||
pil_image = Image.open(image_stream)
|
||||
|
||||
if pil_image is not None:
|
||||
# 解码成功,显示帧
|
||||
frame_count += 1
|
||||
print(f"Frame {frame_count} decoded successfully, size: {pil_image.width}x{pil_image.height}")
|
||||
|
||||
# 转换为OpenCV格式
|
||||
opencv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
||||
|
||||
# 检查图像是否有重复的模式
|
||||
# 从截图来看,图像被分割成了3x3的网格
|
||||
# 这可能是因为图像高度是正常的3倍
|
||||
if opencv_image.shape[0] == 1440 and opencv_image.shape[1] == 640:
|
||||
# 高度是1440,是正常的3倍
|
||||
# 只显示图像的前480行
|
||||
fixed_image = opencv_image[:480, :, :]
|
||||
cv2.imshow("Compressed Camera Stream", fixed_image)
|
||||
print(f"Fixed frame by cropping to 640x480")
|
||||
elif opencv_image.shape[0] == 640 and opencv_image.shape[1] == 1920:
|
||||
# 宽度是1920,是正常的3倍
|
||||
# 只显示图像的前640列
|
||||
fixed_image = opencv_image[:, :640, :]
|
||||
cv2.imshow("Compressed Camera Stream", fixed_image)
|
||||
print(f"Fixed frame by cropping to 640x480")
|
||||
else:
|
||||
# 直接显示原始大小的图像
|
||||
cv2.imshow("Compressed Camera Stream", opencv_image)
|
||||
|
||||
# 只在显示新帧后调用一次waitKey,确保图像刷新
|
||||
key = cv2.waitKey(1)
|
||||
if key == 27:
|
||||
print("ESC key pressed, exiting...")
|
||||
return 1
|
||||
else:
|
||||
print(f"Failed to decode JPEG frame {jpeg_found_count}")
|
||||
except Exception as e:
|
||||
print(f"Error decoding JPEG frame {jpeg_found_count}: {e}")
|
||||
# 尝试使用OpenCV的方式解码
|
||||
try:
|
||||
np_data = np.frombuffer(jpeg_frame, dtype=np.uint8)
|
||||
frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)
|
||||
if frame is not None:
|
||||
frame_count += 1
|
||||
print(f"Frame {frame_count} decoded with OpenCV, size: {frame.shape[1]}x{frame.shape[0]}")
|
||||
# 检查是否是3x3网格
|
||||
if frame.shape[0] > 480 and frame.shape[1] == 640:
|
||||
# 只显示前480行
|
||||
cv2.imshow("Compressed Camera Stream", frame[:480, :, :])
|
||||
else:
|
||||
cv2.imshow("Compressed Camera Stream", frame)
|
||||
key = cv2.waitKey(1)
|
||||
if key == 27:
|
||||
print("ESC key pressed, exiting...")
|
||||
return 1
|
||||
except Exception as e2:
|
||||
print(f"Failed with OpenCV too: {e2}")
|
||||
else:
|
||||
print(f"Invalid JPEG frame size: {len(jpeg_frame)} bytes, skipped")
|
||||
|
||||
except socket.timeout:
|
||||
print("Socket timeout, no data received")
|
||||
except Exception as e:
|
||||
print(f"Error in receive loop: {e}")
|
||||
|
||||
# 检查连接是否超时
|
||||
if time.time() - last_receive_time > 10:
|
||||
print("Connection timeout: No data received for 10 seconds")
|
||||
return 1
|
||||
|
||||
# 检查是否按下ESC键退出
|
||||
key = cv2.waitKey(1)
|
||||
if key == 27:
|
||||
print("ESC key pressed, exiting...")
|
||||
break
|
||||
|
||||
except socket.timeout as e:
|
||||
print(f"Connection timeout: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
finally:
|
||||
# 清理资源
|
||||
print("Cleaning up resources...")
|
||||
sockfd.close()
|
||||
cv2.destroyAllWindows()
|
||||
print(f"Client exiting. Total bytes received: {total_bytes_received}, JPEG frames found: {jpeg_found_count}, Successful frames: {frame_count}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,287 @@
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <regex>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace net = boost::asio;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// 获取MIME类型
|
||||
std::string get_mime_type(const std::string& path) {
|
||||
if (path.ends_with(".html") || path.ends_with(".htm")) return "text/html";
|
||||
if (path.ends_with(".css")) return "text/css";
|
||||
if (path.ends_with(".js")) return "application/javascript";
|
||||
if (path.ends_with(".json")) return "application/json";
|
||||
if (path.ends_with(".png")) return "image/png";
|
||||
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg";
|
||||
if (path.ends_with(".gif")) return "image/gif";
|
||||
if (path.ends_with(".svg")) return "image/svg+xml";
|
||||
if (path.ends_with(".txt")) return "text/plain";
|
||||
if (path.ends_with(".ico")) return "image/x-icon";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
std::string read_file(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "无法打开文件: " << path << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
// 处理HTTP请求
|
||||
class http_connection : public std::enable_shared_from_this<http_connection> {
|
||||
public:
|
||||
http_connection(tcp::socket socket, const std::string& static_dir)
|
||||
: socket_(std::move(socket)), static_dir_(static_dir) {}
|
||||
|
||||
void start() {
|
||||
read_request();
|
||||
}
|
||||
|
||||
private:
|
||||
void read_request() {
|
||||
auto self = shared_from_this();
|
||||
|
||||
http::async_read(socket_, buffer_, request_,
|
||||
[self](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (!ec) {
|
||||
self->process_request();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void process_request() {
|
||||
response_.version(request_.version());
|
||||
response_.keep_alive(false);
|
||||
|
||||
std::string target = std::string(request_.target());
|
||||
|
||||
// 防止路径遍历攻击
|
||||
if (target.find("..") != std::string::npos) {
|
||||
response_.result(http::status::forbidden);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "禁止访问";
|
||||
send_response();
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理根路径
|
||||
if (target == "/" || target == "/index.html") {
|
||||
serve_file("dashun_index.html");
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理健康检查
|
||||
if (target == "/health") {
|
||||
response_.result(http::status::ok);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "大顺脚本前端服务运行正常";
|
||||
send_response();
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理API请求
|
||||
if (target.starts_with("/api/")) {
|
||||
handle_api_request(target);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理静态文件
|
||||
serve_file(target.substr(1)); // 移除开头的'/'
|
||||
}
|
||||
|
||||
void handle_api_request(const std::string& target) {
|
||||
response_.result(http::status::ok);
|
||||
response_.set(http::field::content_type, "application/json");
|
||||
|
||||
if (target == "/api/status") {
|
||||
response_.body() = R"({
|
||||
"status": "running",
|
||||
"version": "1.0.0",
|
||||
"uptime": "0",
|
||||
"requests_served": 0
|
||||
})";
|
||||
} else if (target == "/api/info") {
|
||||
response_.body() = R"({
|
||||
"name": "大顺脚本前端服务",
|
||||
"description": "为pmc项目提供Web界面",
|
||||
"author": "大顺",
|
||||
"repository": "https://github.com/your-repo/pmc"
|
||||
})";
|
||||
} else {
|
||||
response_.result(http::status::not_found);
|
||||
response_.set(http::field::content_type, "application/json");
|
||||
response_.body() = R"({"error": "API endpoint not found"})";
|
||||
}
|
||||
|
||||
send_response();
|
||||
}
|
||||
|
||||
void serve_file(const std::string& filename) {
|
||||
std::string filepath = static_dir_ + "/" + filename;
|
||||
|
||||
// 安全检查:确保文件在静态目录内
|
||||
fs::path requested_path = fs::absolute(filepath);
|
||||
fs::path static_path = fs::absolute(static_dir_);
|
||||
|
||||
if (!fs::equivalent(requested_path.parent_path(), static_path) &&
|
||||
!requested_path.string().starts_with(static_path.string() + "/")) {
|
||||
response_.result(http::status::forbidden);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "禁止访问";
|
||||
send_response();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs::exists(filepath)) {
|
||||
response_.result(http::status::not_found);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "文件不存在: " + filename;
|
||||
send_response();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string content = read_file(filepath);
|
||||
if (content.empty()) {
|
||||
response_.result(http::status::internal_server_error);
|
||||
response_.set(http::field::content_type, "text/plain");
|
||||
response_.body() = "读取文件失败";
|
||||
send_response();
|
||||
return;
|
||||
}
|
||||
|
||||
response_.result(http::status::ok);
|
||||
response_.set(http::field::content_type, get_mime_type(filename));
|
||||
response_.body() = content;
|
||||
send_response();
|
||||
}
|
||||
|
||||
void send_response() {
|
||||
auto self = shared_from_this();
|
||||
|
||||
response_.content_length(response_.body().size());
|
||||
|
||||
http::async_write(socket_, response_,
|
||||
[self](beast::error_code ec, std::size_t) {
|
||||
self->socket_.shutdown(tcp::socket::shutdown_send, ec);
|
||||
});
|
||||
}
|
||||
|
||||
tcp::socket socket_;
|
||||
beast::flat_buffer buffer_{8192};
|
||||
http::request<http::string_body> request_;
|
||||
http::response<http::string_body> response_;
|
||||
std::string static_dir_;
|
||||
};
|
||||
|
||||
// HTTP服务器
|
||||
class http_server {
|
||||
public:
|
||||
http_server(net::io_context& ioc, tcp::endpoint endpoint, const std::string& static_dir)
|
||||
: acceptor_(ioc), static_dir_(static_dir) {
|
||||
beast::error_code ec;
|
||||
|
||||
// 打开acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
|
||||
// 设置地址重用
|
||||
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
|
||||
// 绑定到端点
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
|
||||
// 开始监听
|
||||
acceptor_.listen(net::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
}
|
||||
|
||||
void start_accept() {
|
||||
acceptor_.async_accept(
|
||||
[this](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
std::make_shared<http_connection>(std::move(socket), static_dir_)->start();
|
||||
}
|
||||
start_accept();
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
tcp::acceptor acceptor_;
|
||||
std::string static_dir_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
try {
|
||||
// 静态文件目录
|
||||
std::string static_dir = "static";
|
||||
|
||||
// 确保静态目录存在
|
||||
if (!fs::exists(static_dir)) {
|
||||
std::cerr << "警告: 静态目录 '" << static_dir << "' 不存在" << std::endl;
|
||||
if (!fs::create_directory(static_dir)) {
|
||||
std::cerr << "错误: 无法创建静态目录" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查主页文件是否存在
|
||||
std::string homepage = static_dir + "/dashun_index.html";
|
||||
if (!fs::exists(homepage)) {
|
||||
std::cerr << "警告: 主页文件 '" << homepage << "' 不存在" << std::endl;
|
||||
std::cerr << "请确保 dashun_index.html 文件在 static/ 目录中" << std::endl;
|
||||
}
|
||||
|
||||
// 创建IO上下文
|
||||
net::io_context ioc{1};
|
||||
|
||||
// 创建服务器端点(监听所有地址的8080端口)
|
||||
tcp::endpoint endpoint{tcp::v4(), 8080};
|
||||
|
||||
// 创建HTTP服务器
|
||||
http_server server{ioc, endpoint, static_dir};
|
||||
server.start_accept();
|
||||
|
||||
// 显示启动信息
|
||||
std::cout << "==========================================" << std::endl;
|
||||
std::cout << "大顺脚本前端服务已启动" << std::endl;
|
||||
std::cout << "监听地址: http://0.0.0.0:8080" << std::endl;
|
||||
std::cout << "静态文件目录: " << static_dir << std::endl;
|
||||
std::cout << "按 Ctrl+C 停止服务" << std::endl;
|
||||
std::cout << "==========================================" << std::endl;
|
||||
|
||||
ioc.run();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "错误: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <regex>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "llm/LLMClient.hpp"
|
||||
|
||||
namespace pmc::gui::example {
|
||||
|
||||
// 带缓冲区的Markdown转换器,累积整个响应并处理Markdown
|
||||
class BufferedMarkdownConverter {
|
||||
private:
|
||||
std::string buffer; // 累积原始Markdown文本
|
||||
std::string last_processed; // 上次处理的Pango标记
|
||||
|
||||
public:
|
||||
BufferedMarkdownConverter() {}
|
||||
|
||||
// 重置状态
|
||||
void reset() {
|
||||
buffer.clear();
|
||||
last_processed.clear();
|
||||
}
|
||||
|
||||
// 添加新的Markdown块到缓冲区
|
||||
void append_chunk(const std::string& chunk) {
|
||||
buffer += chunk;
|
||||
}
|
||||
|
||||
// 获取当前缓冲区的内容
|
||||
std::string get_buffer() const {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// 处理整个缓冲区并返回Pango标记
|
||||
std::string process_buffer() {
|
||||
std::string result = buffer;
|
||||
|
||||
// 1. 首先转义HTML特殊字符
|
||||
result = std::regex_replace(result, std::regex("&"), "&");
|
||||
result = std::regex_replace(result, std::regex("<"), "<");
|
||||
result = std::regex_replace(result, std::regex(">"), ">");
|
||||
|
||||
// 2. 转换粗斜体:***text*** -> <b><i>text</i></b>
|
||||
// 先处理粗斜体,确保正确嵌套
|
||||
std::regex bold_italic_regex("\\*\\*\\*(.*?)\\*\\*\\*");
|
||||
result = std::regex_replace(result, bold_italic_regex, "<b><i>$1</i></b>");
|
||||
|
||||
// 3. 转换粗体:**text** -> <b>text</b>(但排除已经是粗斜体的部分)
|
||||
std::regex bold_regex("\\*\\*(?!\\*)(.*?)\\*\\*");
|
||||
result = std::regex_replace(result, bold_regex, "<b>$1</b>");
|
||||
|
||||
// 4. 转换斜体:*text* -> <i>text</i>(但排除已经是粗体或粗斜体的部分)
|
||||
std::regex italic_regex("\\*(?!\\*)(.*?)\\*");
|
||||
result = std::regex_replace(result, italic_regex, "<i>$1</i>");
|
||||
|
||||
// 5. 转换内联代码:`code` -> <tt>code</tt> (使用<tt>标签而不是<code>)
|
||||
std::regex code_regex("`(.*?)`");
|
||||
result = std::regex_replace(result, code_regex, "<tt>$1</tt>");
|
||||
|
||||
// 6. 转换标题:# 标题 -> <span size="larger" weight="bold">标题</span>
|
||||
std::regex h1_regex("^#\\s+(.*?)$", std::regex::multiline);
|
||||
result = std::regex_replace(result, h1_regex, "<span size=\"larger\" weight=\"bold\">$1</span>\n");
|
||||
|
||||
std::regex h2_regex("^##\\s+(.*?)$", std::regex::multiline);
|
||||
result = std::regex_replace(result, h2_regex, "<span size=\"large\" weight=\"bold\">$1</span>\n");
|
||||
|
||||
std::regex h3_regex("^###\\s+(.*?)$", std::regex::multiline);
|
||||
result = std::regex_replace(result, h3_regex, "<span size=\"medium\" weight=\"bold\">$1</span>\n");
|
||||
|
||||
// 7. 转换无序列表:* item -> • item
|
||||
std::regex ul_regex("^\\*\\s+(.*?)$", std::regex::multiline);
|
||||
result = std::regex_replace(result, ul_regex, "• $1\n");
|
||||
|
||||
// 8. 转换有序列表:1. item -> 1. item
|
||||
std::regex ol_regex("^\\d+\\.\\s+(.*?)$", std::regex::multiline);
|
||||
// 保持原样,只是确保有换行
|
||||
result = std::regex_replace(result, ol_regex, "$&\n");
|
||||
|
||||
// 9. 转换引用:> text -> <i>text</i>
|
||||
std::regex blockquote_regex("^>\\s+(.*?)$", std::regex::multiline);
|
||||
result = std::regex_replace(result, blockquote_regex, "<i>$1</i>\n");
|
||||
|
||||
// 10. 转换链接:[text](url) -> text (Pango不支持<a>标签,所以只显示文本)
|
||||
std::regex link_regex("\\[(.*?)\\]\\((.*?)\\)");
|
||||
result = std::regex_replace(result, link_regex, "$1");
|
||||
|
||||
// 11. 转换图片: -> [图片: alt] (Pango不支持图片,所以显示替代文本)
|
||||
std::regex image_regex("!\\[(.*?)\\]\\((.*?)\\)");
|
||||
result = std::regex_replace(result, image_regex, "[图片: $1]");
|
||||
|
||||
// 重要修复:Pango标记语言使用 \n 表示换行,而不是 <br/>
|
||||
// 不要转换换行符,保持原样
|
||||
|
||||
last_processed = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取上次处理的结果
|
||||
std::string get_last_processed() const {
|
||||
return last_processed;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 简单的聊天窗口类
|
||||
*/
|
||||
class SimpleChatWindow {
|
||||
private:
|
||||
GtkTextMark* current_response_start_mark; // 标记当前响应的开始位置
|
||||
|
||||
public:
|
||||
SimpleChatWindow() : current_response_start_mark(nullptr) {
|
||||
// 创建主窗口
|
||||
window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
|
||||
gtk_window_set_title(window, "PMC Chat");
|
||||
gtk_window_set_default_size(window, 800, 600);
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), nullptr);
|
||||
|
||||
// 创建主垂直布局
|
||||
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_container_add(GTK_CONTAINER(window), vbox);
|
||||
|
||||
// 创建滚动窗口和文本视图(用于显示聊天记录)
|
||||
GtkWidget* scrolled_window = gtk_scrolled_window_new(nullptr, nullptr);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0);
|
||||
|
||||
text_view = GTK_TEXT_VIEW(gtk_text_view_new());
|
||||
gtk_text_view_set_editable(text_view, FALSE);
|
||||
gtk_text_view_set_cursor_visible(text_view, FALSE);
|
||||
gtk_text_view_set_wrap_mode(text_view, GTK_WRAP_WORD_CHAR);
|
||||
gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(text_view));
|
||||
|
||||
// 设置文本视图的边距
|
||||
gtk_text_view_set_left_margin(text_view, 10);
|
||||
gtk_text_view_set_right_margin(text_view, 10);
|
||||
gtk_text_view_set_top_margin(text_view, 10);
|
||||
gtk_text_view_set_bottom_margin(text_view, 10);
|
||||
|
||||
// 创建输入区域的水平布局
|
||||
GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
|
||||
|
||||
// 创建输入框
|
||||
entry = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(entry, "输入消息...");
|
||||
gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(entry), TRUE, TRUE, 0);
|
||||
|
||||
// 创建发送按钮
|
||||
send_button = GTK_BUTTON(gtk_button_new_with_label("发送"));
|
||||
gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(send_button), FALSE, FALSE, 0);
|
||||
|
||||
// 连接信号
|
||||
g_signal_connect(send_button, "clicked", G_CALLBACK(on_send_clicked), this);
|
||||
g_signal_connect(entry, "activate", G_CALLBACK(on_entry_activate), this);
|
||||
|
||||
// 初始化LLM客户端
|
||||
std::string api_key;
|
||||
const char* env_key = std::getenv("DEEPSEEK_API_KEY");
|
||||
if (env_key && env_key[0] != '\0') {
|
||||
api_key = env_key;
|
||||
} else {
|
||||
// 尝试从文件读取
|
||||
std::string key_file = std::string(std::getenv("HOME")) + "/.cert/deepseek.key";
|
||||
std::ifstream file(key_file);
|
||||
if (file.is_open()) {
|
||||
std::getline(file, api_key);
|
||||
file.close();
|
||||
} else {
|
||||
api_key = ""; // 使用空密钥,会在使用时出错
|
||||
}
|
||||
}
|
||||
|
||||
llm_client = std::make_unique<pmc::llm::LLMClient>(api_key);
|
||||
markdown_converter = std::make_unique<BufferedMarkdownConverter>();
|
||||
}
|
||||
|
||||
// 使用带缓冲区的Markdown转换器进行转换
|
||||
std::string markdown_to_pango(const std::string& markdown) {
|
||||
// 将新块添加到缓冲区
|
||||
markdown_converter->append_chunk(markdown);
|
||||
|
||||
// 处理整个缓冲区
|
||||
std::string result = markdown_converter->process_buffer();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void show() {
|
||||
gtk_widget_show_all(GTK_WIDGET(window));
|
||||
}
|
||||
|
||||
void append_message(const std::string& message) {
|
||||
GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view);
|
||||
GtkTextIter end;
|
||||
gtk_text_buffer_get_end_iter(buffer, &end);
|
||||
|
||||
// 用户消息不需要Markdown转换,直接显示
|
||||
gtk_text_buffer_insert(buffer, &end, message.c_str(), -1);
|
||||
gtk_text_buffer_insert(buffer, &end, "\n", -1);
|
||||
|
||||
// 滚动到底部
|
||||
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(
|
||||
GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view))));
|
||||
gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj));
|
||||
}
|
||||
|
||||
void append_stream_chunk(const std::string& chunk) {
|
||||
GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view);
|
||||
|
||||
// 如果这是响应的第一个块,创建一个标记来跟踪开始位置
|
||||
if (current_response_start_mark == nullptr) {
|
||||
GtkTextIter end;
|
||||
gtk_text_buffer_get_end_iter(buffer, &end);
|
||||
|
||||
// 添加助手前缀
|
||||
gtk_text_buffer_insert(buffer, &end, "Assistant: ", -1);
|
||||
|
||||
// 在助手前缀后创建标记
|
||||
gtk_text_buffer_get_end_iter(buffer, &end);
|
||||
current_response_start_mark = gtk_text_buffer_create_mark(buffer, "current_response_start", &end, TRUE);
|
||||
}
|
||||
|
||||
// 获取当前响应的开始位置
|
||||
GtkTextIter start_iter;
|
||||
gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark);
|
||||
|
||||
// 获取缓冲区的结束位置
|
||||
GtkTextIter end_iter;
|
||||
gtk_text_buffer_get_end_iter(buffer, &end_iter);
|
||||
|
||||
// 删除当前响应区域(从标记位置到缓冲区末尾)
|
||||
gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
|
||||
|
||||
// 在处理后的位置插入新的内容
|
||||
gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark);
|
||||
|
||||
// 关键修复:插入整个累积的缓冲区内容,而不仅仅是当前的chunk
|
||||
// 首先,将新chunk添加到缓冲区
|
||||
markdown_converter->append_chunk(chunk);
|
||||
|
||||
// 然后处理整个缓冲区
|
||||
std::string processed_buffer = markdown_converter->process_buffer();
|
||||
|
||||
// 插入处理后的整个缓冲区内容
|
||||
if (!processed_buffer.empty()) {
|
||||
gtk_text_buffer_insert_markup(buffer, &start_iter, processed_buffer.c_str(), -1);
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(
|
||||
GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view))));
|
||||
gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj));
|
||||
|
||||
// 累积当前响应
|
||||
current_response += chunk;
|
||||
}
|
||||
|
||||
void finish_response() {
|
||||
// 将完整的响应添加到聊天历史
|
||||
if (!current_response.empty()) {
|
||||
chat_history.push_back({"assistant", current_response});
|
||||
current_response.clear();
|
||||
}
|
||||
|
||||
// 重置Markdown转换器
|
||||
markdown_converter->reset();
|
||||
|
||||
// 清除当前响应标记
|
||||
if (current_response_start_mark != nullptr) {
|
||||
GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view);
|
||||
gtk_text_buffer_delete_mark(buffer, current_response_start_mark);
|
||||
current_response_start_mark = nullptr;
|
||||
}
|
||||
|
||||
// 添加换行符
|
||||
GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view);
|
||||
GtkTextIter end;
|
||||
gtk_text_buffer_get_end_iter(buffer, &end);
|
||||
gtk_text_buffer_insert(buffer, &end, "\n", -1);
|
||||
}
|
||||
|
||||
std::string get_input_text() {
|
||||
const char* text = gtk_entry_get_text(entry);
|
||||
return text ? std::string(text) : "";
|
||||
}
|
||||
|
||||
void clear_input() {
|
||||
gtk_entry_set_text(entry, "");
|
||||
}
|
||||
|
||||
void send_to_llm(const std::string& user_message) {
|
||||
// 将用户消息添加到聊天历史
|
||||
chat_history.push_back({"user", user_message});
|
||||
|
||||
// 创建流式回调函数
|
||||
auto stream_callback = [this](const std::string& chunk) {
|
||||
// 关键修复:简化完成标记检查
|
||||
// 只检查明确的完成标记,避免误判
|
||||
if (chunk == "[[PMC_STREAM_COMPLETE]]" || chunk == "[流式传输完成]]") {
|
||||
std::cout << "DEBUG: Detected completion marker" << std::endl;
|
||||
// 流式传输完成,在主线程中调用finish_response
|
||||
g_idle_add([](gpointer data) -> gboolean {
|
||||
auto* self = static_cast<SimpleChatWindow*>(data);
|
||||
self->finish_response();
|
||||
return FALSE; // 只执行一次
|
||||
}, this);
|
||||
return; // 重要:直接返回,不显示这个字符串
|
||||
}
|
||||
|
||||
// 使用g_idle_add在主线程中更新UI
|
||||
g_idle_add([](gpointer data) -> gboolean {
|
||||
auto* chunk_data = static_cast<std::pair<SimpleChatWindow*, std::string>*>(data);
|
||||
chunk_data->first->append_stream_chunk(chunk_data->second);
|
||||
delete chunk_data;
|
||||
return FALSE; // 只执行一次
|
||||
}, new std::pair<SimpleChatWindow*, std::string>(this, chunk));
|
||||
};
|
||||
|
||||
// 在新线程中发送流式请求(避免阻塞主线程)
|
||||
std::thread([this, stream_callback]() {
|
||||
try {
|
||||
llm_client->stream_request(chat_history, stream_callback);
|
||||
} catch (const std::exception& e) {
|
||||
// 错误处理:在主线程中显示错误消息
|
||||
g_idle_add([](gpointer data) -> gboolean {
|
||||
auto* error_data = static_cast<std::pair<SimpleChatWindow*, std::string>*>(data);
|
||||
error_data->first->append_message("错误: " + error_data->second);
|
||||
delete error_data;
|
||||
return FALSE; // 只执行一次
|
||||
}, new std::pair<SimpleChatWindow*, std::string>(this, e.what()));
|
||||
}
|
||||
}).detach(); // 分离线程,让它独立运行
|
||||
}
|
||||
|
||||
private:
|
||||
static void on_send_clicked(GtkButton* button, gpointer user_data) {
|
||||
auto* self = static_cast<SimpleChatWindow*>(user_data);
|
||||
std::string text = self->get_input_text();
|
||||
if (!text.empty()) {
|
||||
// 用户消息不需要Markdown转换,直接显示
|
||||
self->append_message("You: " + text);
|
||||
self->clear_input();
|
||||
self->send_to_llm(text);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_entry_activate(GtkEntry* entry, gpointer user_data) {
|
||||
auto* self = static_cast<SimpleChatWindow*>(user_data);
|
||||
std::string text = self->get_input_text();
|
||||
if (!text.empty()) {
|
||||
// 用户消息不需要Markdown转换,直接显示
|
||||
self->append_message("You: " + text);
|
||||
self->clear_input();
|
||||
self->send_to_llm(text);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWindow* window;
|
||||
GtkTextView* text_view;
|
||||
GtkEntry* entry;
|
||||
GtkButton* send_button;
|
||||
|
||||
std::unique_ptr<pmc::llm::LLMClient> llm_client;
|
||||
std::unique_ptr<BufferedMarkdownConverter> markdown_converter;
|
||||
std::vector<std::pair<std::string, std::string>> chat_history;
|
||||
std::string current_response;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 运行简单的聊天窗口示例
|
||||
*/
|
||||
void run_simple_chat_example() {
|
||||
// 标准GTK的gtk_init返回void,不是bool
|
||||
gtk_init(nullptr, nullptr);
|
||||
|
||||
SimpleChatWindow window;
|
||||
window.show();
|
||||
|
||||
gtk_main();
|
||||
}
|
||||
|
||||
} // namespace pmc::gui::example
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "用法: " << argv[0] << " <command>" << std::endl;
|
||||
std::cerr << "命令: chat - 运行聊天窗口" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string command = argv[1];
|
||||
|
||||
if (command == "chat") {
|
||||
pmc::gui::example::run_simple_chat_example();
|
||||
} else {
|
||||
std::cerr << "未知命令: " << command << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
/*
|
||||
* 并发机器(进程托管)
|
||||
*
|
||||
* FIXME: 尽量使用大众的库(boost)
|
||||
*/
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <csignal> /* 注册信号以优雅退出 */
|
||||
#include <functional>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <boost/program_options.hpp>
|
||||
#include "logs/Logger.hpp"
|
||||
#include "th/Thread.hpp" /* 可控线程类 */
|
||||
#include "th/Cv_wait.hpp" /* 通过条件变量进行等待 */
|
||||
#include "th/ITask.hpp"
|
||||
#include "lci/Term.h"
|
||||
#include "lci/Rectangle.h"
|
||||
#include "lci/Drawable.h"
|
||||
#include "lci/FrameBuf.h"
|
||||
#include "lci/PoolArea.h"
|
||||
#include "lci/ShapeArea.h"
|
||||
#include "lci/CharShape.h"
|
||||
#include "lci/Color.h"
|
||||
#include "lci/图层区域.hpp"
|
||||
#include "hd/mem.h"
|
||||
#include "hd/cpu.h"
|
||||
|
||||
/* lci 临时 */
|
||||
#include "lci/p_th.h"
|
||||
|
||||
/* 中文模式 Chinese Mode */
|
||||
#include "cn/中文化.hpp"
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
namespace po = boost::program_options;
|
||||
|
||||
/* 版本号 */
|
||||
static const char *PMC_VERSION = "0.4.x";
|
||||
/* 条件变量的等待机制让主程序能够等待中断信号的产生 */
|
||||
qing::Cv_wait cv = qing::Cv_wait();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* 捕获中断信号,结束线程并且退出 */
|
||||
void signalHandler(int signum)
|
||||
{
|
||||
LOG_INFO("收到终止信号,准备停止服务器...");
|
||||
cv.WakeCv(); /* 唤醒条件信号即关闭程序 */
|
||||
}
|
||||
|
||||
/*----SIGNAL ACTION----*/
|
||||
//
|
||||
/* 对信号的响应行为 */
|
||||
void set_signal_action() {
|
||||
|
||||
/* 该结构体用于描述信号的处理方式 */
|
||||
struct sigaction sa;
|
||||
|
||||
|
||||
/* 绑定处理函数 */
|
||||
sa.sa_handler = signalHandler;
|
||||
|
||||
/*
|
||||
* 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码)
|
||||
* “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号”
|
||||
* 不阻塞其他信号
|
||||
* 确保在执行期间没有其他特定的信号会被阻塞
|
||||
* 防止处理过程中被其他信号中断
|
||||
*/
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
/*
|
||||
* 用于设置信号处理函数的行为特性。
|
||||
* 0 - 默认选项
|
||||
* SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞
|
||||
* SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认
|
||||
* SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。
|
||||
* SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知
|
||||
* SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个
|
||||
*/
|
||||
sa.sa_flags = 0;
|
||||
|
||||
/* 注册信号 */
|
||||
sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */
|
||||
sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */
|
||||
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
|
||||
#define TARGET_TTY 8 /* FIXME: 目标端口设置为参数传入 */
|
||||
#define ORIGIN_TTY 1
|
||||
namespace qing {
|
||||
class LciTask: public ITask{
|
||||
public:
|
||||
LciTask(/*const std::string& cmd, const int masterfd, const int command_pipe_send, */const int rotate, const int fontsize)
|
||||
: /*exec_cmd(cmd), masterfd(masterfd), command_pipe_send(command_pipe_send),*/
|
||||
rotate(rotate), fontsize(fontsize) {
|
||||
/* 初始化线程 */
|
||||
init_thread();
|
||||
}
|
||||
|
||||
/* 停止线程 */
|
||||
void stop() override {
|
||||
th->WaitClose();
|
||||
}
|
||||
|
||||
/* 唤醒线程 */
|
||||
void start() override{
|
||||
th->Activate(); /* 对于已经关闭的进程 */
|
||||
th->WaitStart();
|
||||
}
|
||||
|
||||
/* 线程是否在运行 */
|
||||
bool isRunning() override {
|
||||
auto stat = th->check();
|
||||
return stat == Fsm::Stat::START;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
std::string exec_cmd;
|
||||
//int command_pipe_send;
|
||||
//int masterfd;
|
||||
|
||||
int rotate;
|
||||
int fontsize;
|
||||
bool shift = false;
|
||||
|
||||
std::unique_ptr<Thread> th; /* 持有线程类 */
|
||||
std::unique_ptr<Term> term; /* 终端代理 */
|
||||
std::unique_ptr<FrameBuf> fb;
|
||||
//std::unique_ptr<KeyboardEvent> kbe;
|
||||
//std::unique_ptr<lci::p_th> printTask;
|
||||
std::unique_ptr<PoolArea> poolArea; /* 持有控件池 */
|
||||
//std::shared_ptr<CmdArea> cmd;
|
||||
int table_w;
|
||||
int table_h;
|
||||
std::shared_ptr<PoolArea> tableArea;
|
||||
std::vector<double> cpu_vec;
|
||||
std::vector<double> mem_vec;
|
||||
|
||||
|
||||
/*
|
||||
* 设置终端的大小
|
||||
*
|
||||
* 传入一个控制台设备文件的文件描述符,以及你要设定的行列值
|
||||
*/
|
||||
//void set_terminal_size(int fd, int rows, int cols) {
|
||||
// struct winsize sz = { .ws_row = (unsigned short)rows, .ws_col = (unsigned short)cols };
|
||||
// if (ioctl(fd, TIOCSWINSZ, &sz) == -1) {
|
||||
// throw std::runtime_error("ioctl");
|
||||
// }
|
||||
//}
|
||||
|
||||
/*---------------------*/
|
||||
void init_thread();
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
int lci_init(po::variables_map&);
|
||||
|
||||
/* 任务池 */
|
||||
auto taskPool = std::vector<std::shared_ptr<qing::ITask>>{};
|
||||
|
||||
|
||||
/* 入口函数 */
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
/*------------------------------*/
|
||||
/* 设置信号行为*/
|
||||
set_signal_action();
|
||||
|
||||
/*------------------------------*/
|
||||
/* 解析命令行参数 */
|
||||
po::options_description desc("pmc subsystem start options");
|
||||
desc.add_options() /* 定义选项 */
|
||||
("help,h", "display help message")
|
||||
("version,v", "display version message")
|
||||
("lci", "run lci task")
|
||||
/*****************/
|
||||
//("exec", po::value<std::string>(), "lci module run commandline")
|
||||
("rotate", po::value<int>()->default_value(0), "lci module rotate mode(0~3)")
|
||||
("fontsize", po::value<int>()->default_value(18), "lci module fontsize option");
|
||||
|
||||
/* 参数变量映射关系 */
|
||||
po::variables_map vm;
|
||||
|
||||
try { /* 开始解析 */
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
if (vm.count("help")) {
|
||||
std::cout << desc << std::endl;
|
||||
return 0;
|
||||
}
|
||||
if (vm.count("version")) {
|
||||
std::cout << PMC_VERSION << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "using --help to check options message" << std::endl;
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* LINUX CHINESE INTERF */
|
||||
if (vm.count("lci")) {
|
||||
|
||||
if (!(vm.count("rotate") && vm.count("fontsize")/* && vm.count("exec")*/))
|
||||
throw std::runtime_error(
|
||||
"您进入了lci模块,请输入以下选项 --rotate, --fontsize");
|
||||
|
||||
lci_init(vm);
|
||||
}
|
||||
|
||||
/*
|
||||
* 等待
|
||||
*
|
||||
* 触发器在信号处理那里
|
||||
*/
|
||||
cv.Wait();
|
||||
|
||||
/* 主线程被唤醒 */
|
||||
for (auto& task: taskPool) {
|
||||
task->stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 初始化PMC并发机器 */
|
||||
int lci_init(po::variables_map& vm) {
|
||||
|
||||
//int masterfd; /* 控制台文件描述符 */
|
||||
/* 在运行到多线程之前,进行fork() */
|
||||
//if ((masterfd = posix_openpt(O_RDWR)) == -1) { /* 打开一个新的虚拟控制台 */
|
||||
// throw std::runtime_error("posix_openpt");
|
||||
//}
|
||||
|
||||
/*--------------------------------------------------*/
|
||||
/* 创建两个管道:一个用于发送命令,一个用于接收输出 */
|
||||
//int command_pipe[2]; /* [0] 是读端,[1] 是写端 */
|
||||
//if (pipe(command_pipe) == -1)
|
||||
// throw std::runtime_error(
|
||||
// "pipe:" + std::string(strerror(errno))
|
||||
// );
|
||||
/* 创建成功 */
|
||||
/*--------------------------------------------------*/
|
||||
|
||||
// if (grantpt(masterfd) != 0 || unlockpt(masterfd) != 0) { /* 解锁虚拟控制台? */
|
||||
// close(masterfd);
|
||||
// throw std::runtime_error("grantpt/unlockpt"); /* FIXME: 输出错误字符串 */
|
||||
// }
|
||||
|
||||
// char *slave_name = ptsname(masterfd); /* 获取虚拟控制台pty的名字 */
|
||||
// if (slave_name == NULL) {
|
||||
// close(masterfd);
|
||||
// throw std::runtime_error("ptsname");
|
||||
// }
|
||||
|
||||
// LOG_INFO(
|
||||
// std::string("新的虚拟终端运行在 ") + slave_name
|
||||
// );
|
||||
|
||||
|
||||
|
||||
|
||||
/* Fork中间子进程 */
|
||||
// pid_t pid = fork();
|
||||
|
||||
/* pid为-1通常意味着分叉失败 */
|
||||
// if (pid == -1)
|
||||
// throw std::runtime_error(
|
||||
// std::string("fork") + std::string(strerror(errno))
|
||||
// );
|
||||
|
||||
/* 如果pid为0表示这是子进程 */
|
||||
// else if (pid == 0) {
|
||||
// LOG_INFO("LCI subprocess is running...");
|
||||
// close(command_pipe[1]); /* 关闭管道写端 */
|
||||
// int slavefd = open(slave_name, O_RDWR);
|
||||
// if (slavefd == -1)
|
||||
// throw std::runtime_error(
|
||||
// std::string("open slave pty") + std::string(strerror(errno))
|
||||
// );
|
||||
|
||||
//runIntermediateProcess(command_pipe[0], slavefd);
|
||||
// int command_pipe_read = command_pipe[0];
|
||||
// char buffer[1024];
|
||||
// ssize_t nread;
|
||||
// while (true) {
|
||||
// memset(buffer, 0, sizeof(buffer)); /* 清空缓冲区 */
|
||||
// nread = read(command_pipe_read, buffer, sizeof(buffer) - 1); /* 从命令管道读取数据 */
|
||||
// if (nread <= 0) break; /* 没有数据可读,退出循环 */
|
||||
|
||||
/* 查找命令的结束位置 */
|
||||
/* FIXME: 更换结束符号,这样我们可以运行一整个脚本 */
|
||||
// char* end_of_cmd = strchr(buffer, '\n');
|
||||
// if (end_of_cmd != nullptr)
|
||||
// *end_of_cmd = '\0';
|
||||
// else
|
||||
// continue; /* 没找到结束符号,继续读取 */
|
||||
|
||||
/* 检查是否受到结束信号 */
|
||||
// if (strcmp(buffer, "END") == 0)
|
||||
// break;
|
||||
|
||||
/* 输入输出重定向 */
|
||||
// if (dup2(slavefd, STDIN_FILENO) != STDIN_FILENO
|
||||
// || dup2(slavefd, STDOUT_FILENO) != STDOUT_FILENO
|
||||
// || dup2(slavefd, STDERR_FILENO) != STDERR_FILENO)
|
||||
// throw std::runtime_error(
|
||||
// std::string("dup2") // +
|
||||
// );
|
||||
|
||||
/* ?*/
|
||||
// if (slavefd > STDERR_FILENO)
|
||||
// close(slavefd);
|
||||
|
||||
/* 执行命令 */
|
||||
// execlp("/bin/sh", "/bin/sh", "-c", buffer, nullptr);
|
||||
|
||||
/* 如果能运行到这里表示进程切换异常 */
|
||||
// throw std::runtime_error(
|
||||
// std::string("execlp") // +
|
||||
// );
|
||||
|
||||
// }
|
||||
|
||||
// close(command_pipe[0]);
|
||||
// _exit(0);
|
||||
// }
|
||||
|
||||
/* 返回其他正数表示这是主进程 */
|
||||
// else {
|
||||
|
||||
// LOG_INFO("LCI main thread is running...");
|
||||
|
||||
|
||||
|
||||
auto lciTask = std::make_shared<qing::LciTask>(
|
||||
// vm["exec"].as<std::string>(),
|
||||
// masterfd, command_pipe[1],
|
||||
vm["rotate"].as<int>(),
|
||||
vm["fontsize"].as<int>()
|
||||
);
|
||||
lciTask->start();
|
||||
taskPool.push_back(lciTask);
|
||||
|
||||
|
||||
/* 等待中间子进程终止 */
|
||||
// waitpid(pid, nullptr, 0);
|
||||
|
||||
/* 将管道移动到分支外面 */
|
||||
// close(command_pipe[0]); // 关闭读端
|
||||
// close(command_pipe[1]);
|
||||
|
||||
// close(masterfd); /* 关闭虚拟终端 */
|
||||
|
||||
// }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace qing {
|
||||
|
||||
void LciTask::init_thread() {
|
||||
|
||||
/* 静止事件 */
|
||||
f_t StopEvent = [](Thread& th) {
|
||||
th.suspend();
|
||||
};
|
||||
|
||||
/* 启动事件 */
|
||||
f_t StartEvent = [this] (Thread& th) {
|
||||
|
||||
this->term = std::make_unique<Term>(TARGET_TTY, ORIGIN_TTY);
|
||||
this->fb = std::make_unique<FrameBuf>("/dev/fb0", rotate, fontsize);
|
||||
//this->kbe = std::make_unique<KeyboardEvent>();
|
||||
|
||||
Rectangle rect = fb->get_size(); /* 屏幕大小 */
|
||||
//set_terminal_size(masterfd, rect.h / (fontsize/2), rect.w / (fontsize/2) - 1);
|
||||
|
||||
|
||||
//int cmd_margin_left = 50;
|
||||
//int cmd_margin_top = 50;
|
||||
//int cmd_w = (rect.w - 3*50) * 0.7;
|
||||
//int cmd_h = (rect.h - 2*50) * 1.0;
|
||||
|
||||
//int log_margin_left = 50 * 2 + cmd_w;
|
||||
//int log_margin_top = 50;
|
||||
//int log_w = (rect.w - 3 * 50) * 0.3;
|
||||
//int log_h = (rect.h - 3 * 50) / 2;
|
||||
|
||||
//int video_margin_left = 50 * 2 + cmd_w; /* 位置 */
|
||||
//int video_margin_top = log_margin_top + log_h + 50;
|
||||
//int video_w = (rect.w - 3 * 50) * 0.3;/* 大小 */
|
||||
//int video_h = (rect.h - 3 * 50) / 2;
|
||||
//
|
||||
int top_margin_left = 10;
|
||||
int top_margin_top = 10;
|
||||
int top_w = rect.w - 2 * 10;
|
||||
int top_h = 40;
|
||||
|
||||
int main_margin_left = 10;
|
||||
int main_margin_top = 10 + top_margin_top + top_h;
|
||||
int main_w = rect.w - 2 * 10;
|
||||
int main_h = rect.h - 4 * 10 - 2 * 40;
|
||||
|
||||
int bottom_margin_left = 10;
|
||||
int bottom_margin_top = 10 + main_margin_top + main_h;
|
||||
int bottom_w = rect.w - 2 * 10;
|
||||
int bottom_h = 40;
|
||||
|
||||
|
||||
/* 容器区域 */
|
||||
poolArea = std::make_unique<PoolArea>(fb.get(), rect.w, rect.h, 0, 0, rotate, fontsize);
|
||||
|
||||
auto topArea = std::make_shared<PoolArea>(poolArea.get(), top_w, top_h, top_margin_left, top_margin_top, rotate, fontsize);
|
||||
poolArea->add("top", topArea);
|
||||
//poolArea->flush(topArea.get(), true);
|
||||
//auto topClr = green;
|
||||
//topArea->rectangle_fill(0, 0, top_w, top_h, topClr);
|
||||
|
||||
/* 顶部内容1 */
|
||||
auto topCtt_1 = std::make_shared<ShapeArea> (topArea.get(), top_w -10, top_h -10, 5, 5, rotate, fontsize);
|
||||
topArea->add("content1", topCtt_1);
|
||||
|
||||
/* 第一个字 */
|
||||
auto tctt1_c1 = std::make_shared<CharShape>(top_h-10, top_h-10, 0, 0, 'C');
|
||||
tctt1_c1->Draw(*topCtt_1, red);
|
||||
/* 第二个字 */
|
||||
auto tctt1_c2 = std::make_shared<CharShape>(top_h-10, top_h-10, top_h-10, 0, 'P');
|
||||
tctt1_c2->Draw(*topCtt_1, red);
|
||||
/* 第三个字 */
|
||||
auto tctt1_c3 = std::make_shared<CharShape>(top_h-10, top_h-10, 2*(top_h-10), 0, 'U');
|
||||
tctt1_c3->Draw(*topCtt_1, red);
|
||||
|
||||
|
||||
auto bottomArea = std::make_shared<PoolArea>(poolArea.get(), bottom_w, bottom_h, bottom_margin_left, bottom_margin_top, rotate, fontsize);
|
||||
poolArea->add("bottom", bottomArea);
|
||||
//poolArea->flush(bottomArea.get(), true);
|
||||
//auto bottomClr = green;
|
||||
//bottomArea->rectangle_fill(0, 0, bottom_w, bottom_h, bottomClr);
|
||||
|
||||
auto mainArea = std::make_shared<PoolArea>(poolArea.get(), main_w, main_h, main_margin_left, main_margin_top, rotate, fontsize);
|
||||
poolArea->add("main", mainArea);
|
||||
poolArea->flush(mainArea.get(), false);
|
||||
auto mainClr = green;
|
||||
mainArea->line(20, 20, 20, main_h-20, mainClr);
|
||||
mainArea->line(21, 20, 21, main_h-20, mainClr);
|
||||
mainArea->line(20, main_h-18, main_w-20, main_h-18, mainClr);
|
||||
mainArea->line(20, main_h-19, main_w-20, main_h-19, mainClr);
|
||||
|
||||
table_w = main_w - 41;
|
||||
table_h = main_h - 39;
|
||||
tableArea = std::make_shared<PoolArea> (mainArea.get(), table_w, table_h, 22, 20, rotate, fontsize);
|
||||
mainArea->add("table", tableArea);
|
||||
mainArea->flush(tableArea.get(), false);
|
||||
|
||||
/* NOTE: 子进程收到命令会执行 */
|
||||
//write(command_pipe_send, exec_cmd.c_str(), exec_cmd.size());
|
||||
//const char* sig = "\n";
|
||||
//write(command_pipe_send, sig, strlen(sig));
|
||||
|
||||
//this->printTask = std::make_unique<lci::p_th>( /* 创建打印线程 */
|
||||
// "Printer",
|
||||
// masterfd,
|
||||
// fb.get(),
|
||||
// cmd.get()
|
||||
//);
|
||||
//printTask->wake();
|
||||
//printTask->WaitStart(10000); /* FIXME: 使用新版的线程类 */
|
||||
|
||||
th.run();
|
||||
};
|
||||
|
||||
|
||||
f_t LoopEvent = [this](Thread& th) {
|
||||
|
||||
//try {
|
||||
|
||||
// struct input_event ev = kbe->get(); /* 获取输入事件 */
|
||||
// std::cout << "Key Press:\t";
|
||||
// std::cout << ev.code << "," << ev.value << std::endl;
|
||||
|
||||
// if (ev.type == EV_KEY && ev.value == 1 && ev.code == 1)
|
||||
// { /* ESC 退出键 */
|
||||
// th.stop();
|
||||
// }
|
||||
|
||||
// else if (ev.type == EV_KEY && ev.value == 1 && ev.code == 14)
|
||||
// { /* DELETE 删除键 */
|
||||
//cmd->delete_input();
|
||||
//cmd->clearBox();
|
||||
//cmd->update_input();
|
||||
// }
|
||||
|
||||
// else if (ev.type == EV_KEY && ev.value == 1 && ev.code == 28)
|
||||
// { /* ENTER 回车 */
|
||||
//auto input = cmd->get_input_and_clear();
|
||||
//cmd->clearBox();
|
||||
//cmd->update_input();
|
||||
//if (input == "") {
|
||||
// input = "\n";
|
||||
//}
|
||||
//const char *msg = input.c_str();
|
||||
//write(masterfd, msg, input.length());
|
||||
// }
|
||||
|
||||
// else if (ev.type == EV_KEY && ev.code == 42)
|
||||
// { /* Shift */
|
||||
// shift = (ev.value) ? true : false;
|
||||
// }
|
||||
|
||||
// else if (ev.type == EV_KEY && ev.value == 1)
|
||||
// { /* 其他有效按键 */
|
||||
|
||||
// char c = '\0'; /* 功能变换 */
|
||||
// switch (ev.code) {
|
||||
// case 2:
|
||||
// c = (shift) ? '!' : '1';
|
||||
// break;
|
||||
// case 3:
|
||||
// c = (shift) ? '@' : '2';
|
||||
// break;
|
||||
// case 4:
|
||||
// c = (shift) ? '#' : '3';
|
||||
// break;
|
||||
// case 5:
|
||||
// c = (shift) ? '$' : '4';
|
||||
// break;
|
||||
// case 6:
|
||||
// c = (shift) ? '%' : '5';
|
||||
// break;
|
||||
// case 7:
|
||||
// c = (shift) ? '^' : '6';
|
||||
// break;
|
||||
// case 8:
|
||||
// c = (shift) ? '&' : '7';
|
||||
// break;
|
||||
// case 9:
|
||||
// c = (shift) ? '*' : '8';
|
||||
// break;
|
||||
// case 10:
|
||||
// c = (shift) ? '(' : '9';
|
||||
// break;
|
||||
// case 11:
|
||||
// c = (shift) ? ')' : '0';
|
||||
// break;
|
||||
// case KEY_A:
|
||||
// c = (shift) ? 'A' : 'a';
|
||||
// break;
|
||||
// case KEY_B:
|
||||
// c = (shift) ? 'B' : 'b';
|
||||
// break;
|
||||
// case KEY_C:
|
||||
// c = (shift) ? 'C' : 'c';
|
||||
// break;
|
||||
// case KEY_D:
|
||||
// c = (shift) ? 'D' : 'd';
|
||||
// break;
|
||||
// case KEY_E:
|
||||
// c = (shift) ? 'E' : 'e';
|
||||
// break;
|
||||
// case KEY_F:
|
||||
// c = (shift) ? 'F' : 'f';
|
||||
// break;
|
||||
// case KEY_G:
|
||||
// c = (shift) ? 'G' : 'g';
|
||||
// break;
|
||||
// case KEY_H:
|
||||
// c = (shift) ? 'H' : 'h';
|
||||
// break;
|
||||
// case KEY_I:
|
||||
// c = (shift) ? 'I' : 'i';
|
||||
// break;
|
||||
// case KEY_J:
|
||||
// c = (shift) ? 'J' : 'j';
|
||||
// break;
|
||||
// case KEY_K:
|
||||
// c = (shift) ? 'K' : 'k';
|
||||
// break;
|
||||
// case KEY_L:
|
||||
// c = (shift) ? 'L' : 'l';
|
||||
// break;
|
||||
// case KEY_M:
|
||||
// c = (shift) ? 'M' : 'm';
|
||||
// break;
|
||||
// case KEY_N:
|
||||
// c = (shift) ? 'N' : 'n';
|
||||
// break;
|
||||
// case KEY_O:
|
||||
// c = (shift) ? 'O' : 'o';
|
||||
// break;
|
||||
// case KEY_P:
|
||||
// c = (shift) ? 'P' : 'p';
|
||||
// break;
|
||||
// case KEY_Q:
|
||||
// c = (shift) ? 'Q' : 'q';
|
||||
// break;
|
||||
// case KEY_R:
|
||||
// c = (shift) ? 'R' : 'r';
|
||||
// break;
|
||||
// case KEY_S:
|
||||
// c = (shift) ? 'S' : 's';
|
||||
// break;
|
||||
// case KEY_T:
|
||||
// c = (shift) ? 'T' : 't';
|
||||
// break;
|
||||
// case KEY_U:
|
||||
// c = (shift) ? 'U' : 'u';
|
||||
// break;
|
||||
// case KEY_V:
|
||||
// c = (shift) ? 'V' : 'v';
|
||||
// break;
|
||||
// case KEY_W:
|
||||
// c = (shift) ? 'W' : 'w';
|
||||
// break;
|
||||
// case KEY_X:
|
||||
// c = (shift) ? 'X' : 'x';
|
||||
// break;
|
||||
// case KEY_Y:
|
||||
// c = (shift) ? 'Y' : 'y';
|
||||
// break;
|
||||
// case KEY_Z:
|
||||
// c = (shift) ? 'Z' : 'z';
|
||||
// break;
|
||||
// case KEY_SPACE:
|
||||
// c = ' ';
|
||||
// break;
|
||||
// case 51:
|
||||
// c = (shift) ? '<' : ',';
|
||||
// break;
|
||||
// case 52:
|
||||
// c = (shift) ? '>' : '.';
|
||||
// break;
|
||||
// case 53:
|
||||
// c = (shift) ? '?' : '/';
|
||||
// break;
|
||||
// case 12:
|
||||
// c = (shift) ? '_' : '-';
|
||||
// break;
|
||||
// case 43:
|
||||
// c = (shift) ? '|' : '\\';
|
||||
// break;
|
||||
|
||||
// }
|
||||
|
||||
// if (c != '\0') /* 如果点击了有效按键 */
|
||||
// {
|
||||
//cmd->Input(c);
|
||||
//cmd->clearBox();
|
||||
//cmd->update_input();
|
||||
// }
|
||||
|
||||
// } else if (ev.type == EV_REL && ev.code == REL_X) { /* 这两个是鼠标移动事件 */
|
||||
// std::cout << "X:\t\t" << ev.value << std::endl;
|
||||
// } else if (ev.type == EV_REL && ev.code == REL_Y) {
|
||||
// std::cout << "Y:\t\t" << ev.value << std::endl;
|
||||
// }
|
||||
|
||||
//} catch( KeyboardEvent::NoEvent _ ) {
|
||||
// ;
|
||||
//}
|
||||
|
||||
|
||||
MemoryInfo mem = getMemoryUsageWithSysinfo();
|
||||
mem_vec.push_back(mem.usageRate);
|
||||
if (mem_vec.size() > 40) {
|
||||
mem_vec.erase(mem_vec.begin());
|
||||
}
|
||||
|
||||
CpuMonitor monitor;
|
||||
double cpu_usage = monitor.get_cpu_usage();
|
||||
cpu_vec.push_back(cpu_usage);
|
||||
if (cpu_vec.size() > 40) {
|
||||
cpu_vec.erase(cpu_vec.begin());
|
||||
}
|
||||
|
||||
|
||||
auto bg = black;
|
||||
tableArea->rectangle_fill(0, 0, table_w, table_h, bg);
|
||||
|
||||
float single_w = (float)(table_w-1) / 40.0;
|
||||
float single_h = (float)(table_h-1) / 100.0;
|
||||
|
||||
for (int i = 0; i < mem_vec.size() - 1; ++i) {
|
||||
int x0 = i * single_w;
|
||||
int y0 = (100.0 - mem_vec[i]) * single_h;
|
||||
int x1 = x0 + single_w;
|
||||
int y1 = (100.0 - mem_vec[i+1]) * single_h;
|
||||
auto fg = blue;
|
||||
tableArea->line(x0, y0, x1, y1, fg);
|
||||
}
|
||||
|
||||
for (int i = 0; i < cpu_vec.size() - 1; ++i) {
|
||||
int x0 = i * single_w;
|
||||
int y0 = (100.0 - cpu_vec[i]) * single_h;
|
||||
int x1 = x0 + single_w;
|
||||
int y1 = (100.0 - cpu_vec[i+1]) * single_h;
|
||||
auto fg = red;
|
||||
tableArea->line(x0, y0, x1, y1, fg);
|
||||
}
|
||||
|
||||
|
||||
usleep(1000000);
|
||||
};
|
||||
|
||||
|
||||
f_t ClearEvent = [this](Thread& th)
|
||||
{
|
||||
//printTask.reset();
|
||||
//cmd.reset();
|
||||
poolArea.reset();
|
||||
tableArea.reset();
|
||||
term.reset();
|
||||
};
|
||||
|
||||
this->th = std::make_unique<Thread>(
|
||||
StopEvent, StartEvent, LoopEvent, ClearEvent
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
* 并发机器(进程托管)
|
||||
*
|
||||
* FIXME: 尽量使用大众的库(boost)
|
||||
*/
|
||||
#define HTTPLIB_COMPILE /* 无SSL需求,优先选这个;有SSL需求替换为 #define HTTPLIB_OPENSSL_SUPPORT */
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <functional>
|
||||
#include <csignal> /* 注册信号以优雅退出 */
|
||||
#include <boost/program_options.hpp>
|
||||
#include "logs/Logger.hpp"
|
||||
#include "th/Thread.hpp" /* 可控线程类 */
|
||||
#include "th/Cv_wait.hpp" /* 通过条件变量进行等待 */
|
||||
#include "th/ITask.hpp"
|
||||
#include "nn/NNBuilder.hpp"
|
||||
#include "hd/Robot.hpp"
|
||||
|
||||
/* lci 临时 */
|
||||
#include "lci/p_th.h"
|
||||
|
||||
/* 中文模式 Chinese Mode */
|
||||
#include "cn/中文化.hpp"
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
namespace po = boost::program_options;
|
||||
|
||||
/* 版本号 */
|
||||
static const char *PMC_VERSION = "0.4.x";
|
||||
/* 条件变量的等待机制让主程序能够等待中断信号的产生 */
|
||||
qing::Cv_wait cv = qing::Cv_wait();
|
||||
|
||||
|
||||
/*
|
||||
* Http Server Task
|
||||
* 超文本传输服务器任务
|
||||
*/
|
||||
namespace qing{
|
||||
class HttpTask: public qing::ITask {
|
||||
public:
|
||||
/* 调用回调函数,设置http服务器 */
|
||||
HttpTask(const std::string& addr, const int port, std::function<void(Http&)> f)
|
||||
: http_addr(addr), http_port(port), callback(f) {
|
||||
|
||||
init_thread();
|
||||
}
|
||||
|
||||
/* 任务不可复制 */
|
||||
HttpTask(const HttpTask& ) = delete;
|
||||
|
||||
/* 关闭线程、关闭服务 */
|
||||
~HttpTask() {
|
||||
if (http) http->stop();
|
||||
if (th) th->WaitClose();
|
||||
}
|
||||
|
||||
|
||||
/* 从外部停止任务 */
|
||||
void stop() override {
|
||||
if (http) http->stop();
|
||||
th->WaitClose();
|
||||
}
|
||||
|
||||
/* 唤醒线程 */
|
||||
void start() override {
|
||||
th->Activate(); /* 对于已关闭的线程 */
|
||||
th->WaitStart();
|
||||
}
|
||||
|
||||
/* 是否在运行 */
|
||||
bool isRunning() override {
|
||||
auto stat = th->check();
|
||||
return stat == Fsm::Stat::START
|
||||
|| stat == Fsm::Stat::RUNNING;
|
||||
}
|
||||
private:
|
||||
std::unique_ptr<Thread> th;
|
||||
std::function<void(Http&)> callback;
|
||||
std::unique_ptr<Http> http;
|
||||
std::string http_addr;
|
||||
int http_port;
|
||||
|
||||
/* 初始化线程 */
|
||||
void init_thread();
|
||||
}; //HttpTask
|
||||
} // qing
|
||||
|
||||
|
||||
namespace qing{
|
||||
static const std::string ADDR = std::string{"127.0.0.1"}; /* 只监听本机 */
|
||||
static const int PORT = 9203;
|
||||
using nnl = NeuralNetwork;
|
||||
class ParserTask: public HttpTask { /* 动作解析器 */
|
||||
public:
|
||||
ParserTask();
|
||||
ParserTask(const ParserTask&) = delete;
|
||||
|
||||
/* 解析器异常 */
|
||||
class ParserException: public std::exception{
|
||||
protected:
|
||||
std::string message;
|
||||
public:
|
||||
explicit ParserException(const std::string& msg): message(msg) {}
|
||||
const char* what() const noexcept override {
|
||||
return message.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
/* 没有消息体异常 */
|
||||
class NoRequestBodyException: public ParserException {
|
||||
public:
|
||||
NoRequestBodyException(const std::string& msg): ParserException(msg) {}
|
||||
};
|
||||
|
||||
private:
|
||||
NNBuilder nn;
|
||||
std::mutex mtx;
|
||||
std::vector<std::vector<std::vector<float>>> pool; /* 角度数据 - 样品池 */
|
||||
std::vector<std::vector<float>> averages; /* 角度数据 - 平均值 */
|
||||
/* 归一化 */
|
||||
static float normalize_pitch(float pitch) {
|
||||
return (pitch + 180) / (2 * 180); /* FIXME: 方向反了 */
|
||||
}
|
||||
/* 归一化 */
|
||||
static float normalize_roll(float roll) {
|
||||
return (roll + 180) / (2 * 180); /* 观测值 */
|
||||
}
|
||||
/* 计算误差 */
|
||||
static float calc_err(float nor_pitch, float pitch1) {
|
||||
|
||||
return (nor_pitch > 0) /* 计算误差 */
|
||||
? ((pitch1 > 0) ? nor_pitch - pitch1 : nor_pitch + pitch1 )
|
||||
: ((pitch1 < 0) ? pitch1 - nor_pitch : -(nor_pitch + pitch1));
|
||||
}
|
||||
/* FIXME: 使用类封装采样数据 */
|
||||
static std::vector<std::vector<float>> calc_aves(std::vector<std::vector<std::vector<float>>>& samples) {
|
||||
std::vector<std::vector<float>> a_r{}; /* actions of average result FIXME: 长度必须一致 */
|
||||
for (const std::vector<std::vector<float>>& onetim: samples) { /* one time */
|
||||
auto first_action = onetim[0];
|
||||
std::vector<float> grp_r(first_action.size()); /* group of averate result */
|
||||
for (const std::vector<float>& action: onetim) {
|
||||
for (size_t i_nora = 0; i_nora < action.size(); ++i_nora) { /* item of normalized angle*/
|
||||
grp_r[i_nora] += action[i_nora];
|
||||
}
|
||||
}
|
||||
auto length = onetim.size();
|
||||
for (auto& i_r: grp_r) { /* item of average result */
|
||||
i_r /= length;
|
||||
}
|
||||
a_r.push_back(grp_r);
|
||||
}
|
||||
return a_r;
|
||||
}
|
||||
|
||||
/* 利用神经网络推理微调角度 */
|
||||
std::vector<std::vector<float>> run_infer(
|
||||
std::string script, float infer_freq, float train_discount, std::vector<std::vector<float>>& averages);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 捕获中断信号,结束线程并且退出 */
|
||||
void signalHandler(int signum)
|
||||
{
|
||||
LOG_INFO("收到终止信号,准备停止服务器...");
|
||||
cv.WakeCv(); /* 唤醒条件信号即关闭程序 */
|
||||
}
|
||||
|
||||
/*----SIGNAL ACTION----*/
|
||||
//
|
||||
/* 对信号的响应行为 */
|
||||
void set_signal_action() {
|
||||
|
||||
/* 该结构体用于描述信号的处理方式 */
|
||||
struct sigaction sa;
|
||||
|
||||
|
||||
/* 绑定处理函数 */
|
||||
sa.sa_handler = signalHandler;
|
||||
|
||||
/*
|
||||
* 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码)
|
||||
* “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号”
|
||||
* 不阻塞其他信号
|
||||
* 确保在执行期间没有其他特定的信号会被阻塞
|
||||
* 防止处理过程中被其他信号中断
|
||||
*/
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
/*
|
||||
* 用于设置信号处理函数的行为特性。
|
||||
* 0 - 默认选项
|
||||
* SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞
|
||||
* SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认
|
||||
* SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。
|
||||
* SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知
|
||||
* SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个
|
||||
*/
|
||||
sa.sa_flags = 0;
|
||||
|
||||
/* 注册信号 */
|
||||
sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */
|
||||
sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */
|
||||
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/*-------------------------------------------------------------------------*/
|
||||
/* 初始化pmc并行机器 */
|
||||
int pmc_init(po::variables_map&);
|
||||
|
||||
/* 任务池 */
|
||||
auto taskPool = std::vector<std::shared_ptr<qing::ITask>>{};
|
||||
|
||||
/*----------------------------------------*/
|
||||
|
||||
/* FIXME: 它们没有清理
|
||||
* FIXME: 传入路径、地址 */
|
||||
void try_start_i2c_and_camera()
|
||||
try {
|
||||
I2C_初始化();
|
||||
PCA9685_初始化();
|
||||
MPU6050_初始化();
|
||||
|
||||
/* 初始化相机 */
|
||||
Camera_init(640, 480);
|
||||
Camera_check();
|
||||
Camera_setVideoFormat();
|
||||
Camera_reqBuf();
|
||||
Camera_setup();
|
||||
//Camera_run();
|
||||
}
|
||||
catch (std::runtime_error& exp)
|
||||
{
|
||||
std::cout << exp.what() << std::endl;
|
||||
}
|
||||
|
||||
/* 入口函数 */
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
/*------------------------------*/
|
||||
/* 设置信号行为*/
|
||||
set_signal_action();
|
||||
|
||||
/*------------------------------*/
|
||||
/* 解析命令行参数 */
|
||||
po::options_description desc("pmc subsystem start options");
|
||||
desc.add_options() /* 定义选项 */
|
||||
("help,h", "display help message")
|
||||
("version,v", "display version message")
|
||||
("mybot", "run mybot task");
|
||||
|
||||
/* 参数变量映射关系 */
|
||||
po::variables_map vm;
|
||||
|
||||
try { /* 开始解析 */
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
if (vm.count("help")) { /* 获取帮助页面 */
|
||||
std::cout << desc << std::endl;
|
||||
return 0;
|
||||
}
|
||||
if (vm.count("version")) { /* 获取版本信息 */
|
||||
std::cout << PMC_VERSION << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "using --help to check options message" << std::endl;
|
||||
throw e;
|
||||
}
|
||||
|
||||
/*----------------------------------------*/
|
||||
if (vm.count("mybot")) {
|
||||
try_start_i2c_and_camera(); /* 启动硬件模组 FIXME: 没有释放资源 */
|
||||
auto task = std::make_shared<qing::ParserTask>();
|
||||
task->start();
|
||||
taskPool.push_back(task);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* 等待
|
||||
*
|
||||
* 触发器在信号处理那里
|
||||
*/
|
||||
cv.Wait();
|
||||
|
||||
/* 主线程被唤醒 */
|
||||
for (auto& task: taskPool) {
|
||||
task->stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace qing {
|
||||
|
||||
/* 构造函数,主要是需要传入一个http构造回调 */
|
||||
ParserTask::ParserTask(): HttpTask(ADDR, PORT, [this](Http& http) {
|
||||
|
||||
|
||||
/* FIXME: 传入模型路径 */
|
||||
const char* target = "../public/models/1.txt";
|
||||
if (std::filesystem::exists(target)) {
|
||||
std::cout << "load model data from file..." << std::endl;
|
||||
std::ifstream file;
|
||||
file.open(target);
|
||||
nn.load(file);
|
||||
file.close();
|
||||
}
|
||||
else { /* FIXME: 单独放在一个函数里 */
|
||||
std::cout << "create model data rand..." << std::endl;
|
||||
|
||||
auto layer1 = nnl::Create_in_Factory(18, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer2 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer3 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer4 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer5 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer6 = nnl::Create_in_Factory(36, 16, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
|
||||
nn.add(layer1);
|
||||
nn.add(layer2);
|
||||
nn.add(layer3);
|
||||
nn.add(layer4);
|
||||
nn.add(layer5);
|
||||
nn.add(layer6);
|
||||
|
||||
/* 储存模型 */
|
||||
std::ofstream file;
|
||||
file.open("../public/models/1.txt");
|
||||
if (!file) {
|
||||
std::cout << "Open Model failed" << std::endl;
|
||||
}
|
||||
else {
|
||||
nn.save(file);
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
nn.print_shape();
|
||||
|
||||
|
||||
/* FIXME:当消息体为空时抛出异常即可 */
|
||||
http.post("/api/v1/mybot-sample", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
try{
|
||||
if (req.body.empty())
|
||||
throw NoRequestBodyException("/api/v1/mybot-sample");
|
||||
mtx.lock();
|
||||
auto r = run_infer(req.body, 0.0, 0.0, averages); /* 推理结果无效化 */
|
||||
pool.push_back(r);
|
||||
averages = calc_aves(pool);
|
||||
mtx.unlock();
|
||||
res.set_content("OK", "text/plain");
|
||||
}
|
||||
catch (NoRequestBodyException& exp) {
|
||||
res.set_content("no request body", "text/plain");
|
||||
res.status =400;
|
||||
}
|
||||
catch (std::exception& exp) {
|
||||
res.set_content(exp.what(), "text/plain");
|
||||
res.status =400;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* FIXME: 需要先取得模板数据 */
|
||||
http.post("/api/v1/mybot-nn", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
try{
|
||||
if (req.body.empty())
|
||||
throw NoRequestBodyException("/api/v1/mybot-nn");
|
||||
mtx.lock();
|
||||
auto r =run_infer(req.body, 0.1, 10.0, averages);
|
||||
for (auto& v: r) { /* 输出运行结果 */
|
||||
std::cout << "(";
|
||||
for (auto& i: v) {
|
||||
std::cout << i;
|
||||
std::cout << ",";
|
||||
}
|
||||
std::cout << ")\t";
|
||||
std::cout << std::endl;
|
||||
}
|
||||
mtx.unlock();
|
||||
res.set_content("OK", "text/plain");
|
||||
}
|
||||
catch (NoRequestBodyException& exp) {
|
||||
res.set_content("no request body", "text/plain");
|
||||
res.status =400;
|
||||
}
|
||||
catch (std::exception& exp) {
|
||||
res.set_content(exp.what(), "text/plain");
|
||||
res.status =400;
|
||||
}
|
||||
});
|
||||
|
||||
}) {}
|
||||
|
||||
/*--------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------*/
|
||||
/* 神经网络推理 */
|
||||
std::vector<std::vector<float>> ParserTask::run_infer(std::string script, float infer_freq, float train_discount, std::vector<std::vector<float>>& averages) {
|
||||
auto parser = ActParser();
|
||||
parser.fromStr(script);
|
||||
auto bot = parser.get();
|
||||
auto res = std::vector<std::vector<float>>{};
|
||||
auto all_count = 0;
|
||||
while (true) {
|
||||
try{
|
||||
std::cout << "------------------------------"<< std::endl;
|
||||
MPU6050_传感器数据 data = MPU6050_读传感器();
|
||||
float pitch, roll;
|
||||
MPU6050_计算角度(data, pitch, roll);
|
||||
auto nor_pitch = normalize_pitch(pitch);
|
||||
auto nor_roll = normalize_roll(roll);
|
||||
auto act = parser.now(); /* 获取一个动作 */
|
||||
auto x = std::vector<float>(); /* 创建输入 */
|
||||
x.push_back(nor_pitch);
|
||||
x.push_back(nor_roll);
|
||||
auto i{0};
|
||||
for (; i < act.size()-1; ++i) { /* 跳过了延时数值 */
|
||||
float a;
|
||||
if (bot[i] == 0) { a = 0; } /* 空舵机位 */
|
||||
else { a = (float)act[i] / bot[i]; }
|
||||
x.push_back(a);
|
||||
}
|
||||
auto x1 = x;
|
||||
auto o = nn.forward(x1); /* 前向反馈 */
|
||||
auto j{2};
|
||||
auto new_act = std::vector<float> ();
|
||||
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
std::mt19937 generator(seed);
|
||||
std::uniform_real_distribution<float> distribution(-0.5, 0.5); // 1-100 均匀分布
|
||||
for (; j <x.size(); ++j) {
|
||||
auto a = x[j] + o[j-2] * 0.7 * infer_freq; /* 模型微调 */
|
||||
float b= distribution(generator) * 0.3 * infer_freq; /* 随机因子 */
|
||||
new_act.push_back((int)((a+b) * bot[j-2])); /* 10% */
|
||||
/* FIXME: item{x} = a+b */
|
||||
}
|
||||
new_act.push_back(act[j-2]);
|
||||
parser.next(new_act); /* 执行一个动作 */
|
||||
MPU6050_传感器数据 data1 = MPU6050_读传感器();
|
||||
float pitch1, roll1; /* 测量执行完毕后的角度 */
|
||||
MPU6050_计算角度(data1, pitch1, roll1);
|
||||
auto nor_pitch1 = normalize_pitch(pitch1);
|
||||
auto nor_roll1 = normalize_roll(roll1);
|
||||
auto err0 = calc_err(nor_pitch, nor_pitch1);
|
||||
auto err1 = calc_err(nor_roll, nor_roll1);
|
||||
res.push_back({err0, err1}); /* 推到结果集 */
|
||||
auto reward = err0 *train_discount + err1 * train_discount; /* 加权求和 */
|
||||
nn.backward(o, reward); /* 反向传播 */
|
||||
all_count++;
|
||||
|
||||
/* 储存模型 */
|
||||
std::ofstream file;
|
||||
file.open("../public/models/1.txt");
|
||||
if (!file) {
|
||||
std::cout << "Open Model failed" << std::endl;
|
||||
}
|
||||
else {
|
||||
nn.save(file);
|
||||
file.close();
|
||||
}
|
||||
|
||||
}
|
||||
catch (std::runtime_error& exp) { /* FIXME: 该用专用的异常类 */
|
||||
std::cout << exp.what() << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace qing {
|
||||
|
||||
void HttpTask::init_thread() {
|
||||
|
||||
/**/
|
||||
f_t StopEvent = [](Thread& th) {
|
||||
th.suspend();
|
||||
};
|
||||
|
||||
/* 线程唤醒事件 */
|
||||
f_t WakeEvent = [this](Thread& th) {
|
||||
|
||||
this->http=std::make_unique<Http>(http_addr, http_port);
|
||||
|
||||
this->callback(*http);
|
||||
|
||||
if (th.check() == Fsm::Stat::START) { /* 只跑一次 */
|
||||
th.run(); /* 标记为启动 */
|
||||
http->run(); /* 这个会堵塞线程以进行监听 */
|
||||
}
|
||||
|
||||
/* HTTP服务器异常关闭 */
|
||||
if (th.check() == Fsm::Stat::RUNNING) {
|
||||
th.stop();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
f_t LoopEvent = [](Thread& _) {
|
||||
throw std::runtime_error("Task not support this Event");
|
||||
};
|
||||
|
||||
f_t ClearEvent = [this](Thread& _) {
|
||||
if (this->http)
|
||||
this->http.reset(); /* */
|
||||
};
|
||||
|
||||
th = std::make_unique<Thread> (StopEvent, WakeEvent, LoopEvent, ClearEvent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
//#include <CImg.h>
|
||||
#include "hd/Camera.h"
|
||||
#include "nn/ImageLoader.hpp"
|
||||
#include "nn/ConvLayer.hpp"
|
||||
#include "nn/PoolingLayer.hpp"
|
||||
#include "nn/FlattenLayer.hpp"
|
||||
#include "nn/CNNBuilder.hpp"
|
||||
#include "nn/NNBuilder.hpp"
|
||||
#include "nn/NeuralNetwork.hpp"
|
||||
|
||||
using namespace qing;
|
||||
|
||||
|
||||
// 定义颜色用于不同的人脸框
|
||||
const unsigned char colors[][3] = {
|
||||
{255, 0, 0}, // 红色
|
||||
{0, 255, 0}, // 绿色
|
||||
{0, 0, 255}, // 蓝色
|
||||
{255, 255, 0}, // 黄色
|
||||
{255, 0, 255} // 紫色
|
||||
};
|
||||
|
||||
void take_a_photo(const char* path);
|
||||
int run_facerec(const char* in, const char* out);
|
||||
|
||||
// 主函数
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
ImageLoader image_loader(3, 480, 640, true, false);
|
||||
|
||||
ConvLayer conv1(3, 16, 3, 1, 1);
|
||||
PoolingLayer pool1(2, 2, 2, 0, true);
|
||||
ConvLayer conv2(16, 32, 3, 1, 1);
|
||||
PoolingLayer pool2(2, 2, 2, 0, true);
|
||||
ConvLayer conv3(32, 64, 3, 1, 1);
|
||||
PoolingLayer pool3(2, 2, 2, 0, true);
|
||||
ConvLayer conv4(64, 128, 3, 1, 1);
|
||||
PoolingLayer pool4(2, 2, 2, 0, true);
|
||||
ConvLayer conv5(128, 256, 3, 1, 1);
|
||||
PoolingLayer pool5(2, 2, 2, 0, true);
|
||||
ConvLayer conv6(256, 512, 3, 1, 1);
|
||||
PoolingLayer pool6(2, 2, 2, 0, true);
|
||||
FlattenLayer flatten;
|
||||
|
||||
CNNBuilder cnn;
|
||||
int in_channels = 3;
|
||||
int in_h = 480;
|
||||
int in_w = 640;
|
||||
int batch_size = 1;
|
||||
|
||||
cnn.add_conv_layer(conv1, in_channels, in_h, in_w);
|
||||
cnn.add_pool_layer(pool1, 16, in_h, in_w);
|
||||
cnn.add_conv_layer(conv2, 16, in_h/2, in_w/2);
|
||||
cnn.add_pool_layer(pool2, 32, in_h/2, in_w/2);
|
||||
cnn.add_conv_layer(conv3, 32, in_h/4, in_w/4);
|
||||
cnn.add_pool_layer(pool3, 64, in_h/4, in_w/4);
|
||||
cnn.add_conv_layer(conv4, 64, in_h/8, in_w/8);
|
||||
cnn.add_pool_layer(pool4, 128, in_h/8, in_w/8);
|
||||
cnn.add_conv_layer(conv5, 128, in_h/16, in_w/16);
|
||||
cnn.add_pool_layer(pool5, 256, in_h/16, in_w/16);
|
||||
cnn.add_conv_layer(conv6, 256, in_h/32, in_w/32);
|
||||
cnn.add_pool_layer(pool6, 512, in_h/32, in_w/32);
|
||||
cnn.add_flatten_layer(flatten, 512, 7, 10);
|
||||
|
||||
cnn.print_architecture();
|
||||
|
||||
NNBuilder nn;
|
||||
using nnl = NeuralNetwork;
|
||||
|
||||
auto layer0 = nnl::Create_in_Factory(15360, 6500,0.5, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer1 = nnl::Create_in_Factory(6500, 1500, 0.45, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer2 = nnl::Create_in_Factory(1500, 300, 0.40, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer3 = nnl::Create_in_Factory(300, 70, 0.35, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer4 = nnl::Create_in_Factory(70, 5, 0.30, nnl::ActivationFunc::Sigmoid);
|
||||
|
||||
nn.add(layer0);
|
||||
nn.add(layer1);
|
||||
nn.add(layer2);
|
||||
nn.add(layer3);
|
||||
nn.add(layer4);
|
||||
|
||||
nn.print_shape();
|
||||
|
||||
|
||||
//Camera_init(640, 480);
|
||||
bool flag = true;
|
||||
while (flag) {
|
||||
|
||||
|
||||
//std::string imgpath = std::string("./dataSet/" + std::to_string(time(NULL)) + ".jpeg");
|
||||
//std::string txtpath =std::string("./dataSet/" + std::to_string(time(NULL)) + ".txt");
|
||||
|
||||
//bool flag;
|
||||
//auto img = get_frame_from_camera(flag);
|
||||
//img.save(imgpath.c_str());
|
||||
|
||||
//if (run_facerec(imgpath.c_str(), txtpath.c_str()))
|
||||
// throw std::runtime_error("facerec");
|
||||
|
||||
std::string data_root = "./dataSet";
|
||||
namespace fs = std::filesystem;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(data_root)) {
|
||||
if (!fs::is_regular_file(entry.status()))
|
||||
continue;
|
||||
|
||||
std::string basename = entry.path().stem().string();
|
||||
std::string txtpath = data_root + "/" + basename + ".txt";
|
||||
std::string imgpath = data_root + "/" + basename+".jpeg";
|
||||
auto img = cimg_library::CImg<unsigned char>(imgpath.c_str());
|
||||
|
||||
auto image_data = image_loader.load_from_cimg(img, true);
|
||||
if (image_data.empty()) {
|
||||
std::cerr << "Failed to load image!" << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
/* 前向传播 */
|
||||
auto output = cnn.forward(image_data, batch_size, true);
|
||||
if (!output.empty()) {
|
||||
std::cout << "Forward pass successful!" << std::endl;
|
||||
std::cout << "Input size: " << image_data.size() << std::endl;
|
||||
std::cout << "Output size: " << output.size() << std::endl;
|
||||
|
||||
// 计算输出统计信息
|
||||
float sum = 0.0f, max_val = output[0], min_val = output[0];
|
||||
for (float val : output) {
|
||||
sum += val;
|
||||
if (val > max_val) max_val = val;
|
||||
if (val < min_val) min_val = val;
|
||||
}
|
||||
|
||||
std::cout << "Output statistics:" << std::endl;
|
||||
std::cout << " Mean: " << sum / output.size() << std::endl;
|
||||
std::cout << " Min: " << min_val << std::endl;
|
||||
std::cout << " Max: " << max_val << std::endl;
|
||||
}
|
||||
|
||||
std::vector<float> t(5);
|
||||
std::ifstream file(txtpath);
|
||||
if (!file) t[0] = 0;
|
||||
else {
|
||||
t[0] = 1;
|
||||
for (int i=0; i<5; ++i) {
|
||||
file >> t[i+1];
|
||||
std::cout << t[i+1] << " ";
|
||||
}
|
||||
}
|
||||
std::cout<< std::endl;
|
||||
auto r = nn.forward(output);
|
||||
auto y = nn.cal_err(r, t);
|
||||
|
||||
/* 不存在人脸时,位置信息不计入 */
|
||||
if (t[0] == 0)
|
||||
for (int i =1; i< 5; i++)
|
||||
y[i]= 0;
|
||||
else {
|
||||
for (auto i: y) {
|
||||
std::cout << i << " ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
auto fix_img = img.draw_rectangle(t[4]*640,t[1]*480, t[2]*640,t[3]*480, colors[1], 1, ~0U); /* 抽象为函数 */
|
||||
fix_img = fix_img.draw_rectangle(r[4]*640,r[1]*480, r[2]*640,r[3]*480, colors[0], 1, ~0U);
|
||||
std::string respath = "./resSet/" + basename+".jpeg";
|
||||
fix_img.save(respath.c_str());
|
||||
}
|
||||
y = nn.backward(y);
|
||||
y = cnn.backward(y);
|
||||
}
|
||||
}
|
||||
|
||||
//Camera_close();
|
||||
}
|
||||
|
||||
int run_facerec(const char* in, const char* out) {
|
||||
auto cmd = std::string("python tools/facerec.py detect ");
|
||||
cmd += in;
|
||||
cmd += " --output ";
|
||||
cmd += out;
|
||||
return system(cmd.c_str());
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 该文件可以解析动作脚本(.bot)并且将角度映射到动作上 。
|
||||
*
|
||||
* FIXME: 改为可以启动http服务器进行监听
|
||||
*/
|
||||
|
||||
#include "Dashun.h"
|
||||
#include "Dashun_linux.hpp"
|
||||
|
||||
auto parser = qing::ActParser();
|
||||
int main(int argc, char** argv) { /* argc >= 1 */
|
||||
I2C_init();
|
||||
PCA9685_init();
|
||||
parser.fromStdin();
|
||||
//parser.fromStr(argv[1]);
|
||||
parser.play();
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "Dashun.h"
|
||||
#include "Dashun_linux.hpp"
|
||||
#include "test_nn_20251207_0.hpp"
|
||||
#include "test_nn_20251219_0.hpp"
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* 适用于机器人的DRL(深度强化学习)
|
||||
*
|
||||
* 【方案1】
|
||||
* 蜘蛛机器人拥有10个舵机(4只胳膊、4只腿、2个摄像云台),它还有2个陀螺仪参数。
|
||||
* 将舵机目前的位置+陀螺仪参数传递给神经网络,输出10个舵机的概率值
|
||||
* 将概率值加上一个0~1之间的随机因子,获取最终输出
|
||||
*
|
||||
* 每个舵机180度,即 10 * 180 = 1800 个参数
|
||||
*
|
||||
* 【方案2】
|
||||
*
|
||||
* 将蜘蛛机器人当前的陀螺仪读数,以及下一个动作的参数传递给模型
|
||||
*
|
||||
* 教师数据根据下一个动作的陀螺仪偏差决定
|
||||
*/
|
||||
|
||||
float normalize_pitch(float pitch) {
|
||||
return (pitch+3.14) / (2*3.14);
|
||||
}
|
||||
|
||||
float normalize_roll(float roll) {
|
||||
return (roll+0.28) / (2* 0.28); /*观测值*/
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
||||
I2C_init();
|
||||
PCA9685_init();
|
||||
MPU6050_init();
|
||||
|
||||
/* 初始化相机 */
|
||||
//Camera_init(640, 480);
|
||||
//Camera_check();
|
||||
//Camera_setVideoFormat();
|
||||
//Camera_reqBuf();
|
||||
//Camera_setup();
|
||||
//Camera_run();
|
||||
|
||||
|
||||
/* 创建归一化器 */
|
||||
//ImageLoader image_loader(3, 64, 64, true, false);
|
||||
|
||||
// 创建网络组件
|
||||
//ConvLayer conv1(3, 8, 3, 1, 1); // 输入3通道,输出16通道,3x3卷积,padding=1
|
||||
//PoolingLayer pool1(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//ConvLayer conv2(8, 16, 3, 1, 1); // 输入3通道,输出16通道,3x3卷积,padding=1
|
||||
//PoolingLayer pool2(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//ConvLayer conv3(16, 32, 3, 1, 1); // 输入16通道,输出32通道
|
||||
//PoolingLayer pool3(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//ConvLayer conv4(32, 64, 3, 1, 1); // 输入16通道,输出32通道
|
||||
//PoolingLayer pool4(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//ConvLayer conv5(64, 128, 3, 1, 1); // 输入16通道,输出32通道
|
||||
//PoolingLayer pool5(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//ConvLayer conv6(128, 256, 3, 1, 1); // 输入16通道,输出32通道
|
||||
//PoolingLayer pool6(2, 2, 2, 0, true); // 2x2最大池化,步长2
|
||||
//FlattenLayer flatten; // 展平层
|
||||
|
||||
// 使用CNN构建器
|
||||
//CNNBuilder cnn;
|
||||
|
||||
// 定义输入尺寸
|
||||
//int input_channels = 3;
|
||||
//int input_height = 64;
|
||||
//int input_width = 64;
|
||||
//int batch_size = 1;
|
||||
|
||||
// 构建网络
|
||||
//cnn.add_conv_layer(conv1, input_channels, input_height, input_width);
|
||||
//cnn.add_pool_layer(pool1, 8, input_height, input_width); // 卷积后尺寸不变(因padding=1)
|
||||
//cnn.add_conv_layer(conv2, 8, input_height/2, input_width/2); // 池化后尺寸减半
|
||||
//cnn.add_pool_layer(pool2, 16, input_height/2, input_width/2);
|
||||
//cnn.add_conv_layer(conv3, 16, input_height/4, input_width/4); // 池化后尺寸减半
|
||||
//cnn.add_pool_layer(pool3, 32, input_height/4, input_width/4);
|
||||
//cnn.add_conv_layer(conv4, 32, input_height/8, input_width/8); // 池化后尺寸减半
|
||||
//cnn.add_pool_layer(pool4, 64, input_height/8, input_width/8);
|
||||
//cnn.add_conv_layer(conv5, 64, input_height/16, input_width/16); // 池化后尺寸减半
|
||||
//cnn.add_pool_layer(pool5, 128, input_height/16, input_width/16);
|
||||
//cnn.add_conv_layer(conv6, 128, input_height/32, input_width/32); // 池化后尺寸减半
|
||||
//cnn.add_pool_layer(pool6, 256, input_height/32, input_width/32);
|
||||
//cnn.add_flatten_layer(flatten, 256, input_height/64, input_width/64);
|
||||
|
||||
// 打印网络结构
|
||||
//cnn.print_architecture();
|
||||
|
||||
qing::NNBuilder nn;
|
||||
using nnl = NeuralNetwork;
|
||||
//auto layer1 = nnl::Create_in_Factory(256, 512, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
//auto layer2 = nnl::Create_in_Factory(512, 256, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
//auto layer3 = nnl::Create_in_Factory(256, 100, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
//auto layer4 = nnl::Create_in_Factory(100, 10, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
//nn.add(layer1);
|
||||
//nn.add(layer2);
|
||||
//nn.add(layer3);
|
||||
//nn.add(layer4);
|
||||
|
||||
auto layer1 = nnl::Create_in_Factory(18, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer2 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer3 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU);
|
||||
auto layer4 = nnl::Create_in_Factory(36, 16, 0.01, nnl::ActivationFunc::Sigmoid);
|
||||
|
||||
nn.add(layer1);
|
||||
nn.add(layer2);
|
||||
nn.add(layer3);
|
||||
nn.add(layer4);
|
||||
nn.print_shape();
|
||||
|
||||
|
||||
auto parser = qing::ActParser();
|
||||
parser.fromStdin();
|
||||
auto bot = parser.get();
|
||||
while (true) {
|
||||
try{
|
||||
std::cout << "-----------------------" << std::endl;
|
||||
MPU6050_SensorData data = MPU6050_readSensorData();
|
||||
float pitch, roll;
|
||||
MPU6050_calAngle(data, pitch, roll);
|
||||
printf("pitch: %.2f\troll: %.2f\n");
|
||||
|
||||
/* 获取一个动作 */
|
||||
auto act = parser.now();
|
||||
auto x = std::vector<float>();
|
||||
auto nor_pitch = normalize_pitch(pitch);
|
||||
auto nor_roll = normalize_roll(roll);
|
||||
x.push_back(nor_pitch);
|
||||
x.push_back(nor_roll);
|
||||
auto i{0};
|
||||
for (; i < act.size()-1; ++i) {
|
||||
float a;
|
||||
if (bot[i] == 0) { /* 空舵机位 */
|
||||
a = 0;
|
||||
}
|
||||
else {
|
||||
a = (float)act[i] / bot[i];
|
||||
}
|
||||
x.push_back(a);
|
||||
std::cout << a << ",";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
/* 前向反馈 */
|
||||
auto x1 = x;
|
||||
auto o = nn.forward(x1);
|
||||
//auto count1{0}
|
||||
//for (auto& item: o) {
|
||||
// x[count1] += x[count1] * o * 2 * 0.1; /* 10% */
|
||||
// count++;
|
||||
//}
|
||||
auto j{2};
|
||||
auto new_act = std::vector<float>();
|
||||
for (; j<x.size(); ++j) {
|
||||
auto a = x[j] + o[j-2] * 0.1; /* 10% */
|
||||
new_act.push_back((int)(a * bot[j-2]));
|
||||
std::cout << a << ",";
|
||||
}
|
||||
new_act.push_back(act[j-2]);
|
||||
std::cout << act[j-2];
|
||||
std::cout << std::endl;
|
||||
|
||||
/* 执行一个动作 */
|
||||
parser.next(new_act);
|
||||
|
||||
MPU6050_SensorData data1 = MPU6050_readSensorData();
|
||||
float pitch1, roll1;
|
||||
MPU6050_calAngle(data1, pitch1, roll1);
|
||||
printf("pitch1: %.2f\troll1: %.2f\n");
|
||||
continue;
|
||||
|
||||
auto err = std::vector<float> ();
|
||||
//err.push_back();
|
||||
//nor_pitch > 0) /* 计算误差 */
|
||||
// ? ((pitch1 > 0) ? nor_pitch - pitch1 : nor_pitch + pitch1 )
|
||||
// : ((pitch1 < 0) ? pitch1 - nor_pitch : -(nor_pitch + pitch1));
|
||||
|
||||
auto count3{0};
|
||||
for (auto& item: o) {
|
||||
item *= err[count3++];
|
||||
}
|
||||
|
||||
nn.backward(err);
|
||||
|
||||
}
|
||||
|
||||
catch(std::runtime_error& exp)
|
||||
{
|
||||
std::cout << exp.what() << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//bool flag = true;
|
||||
//while (flag) {
|
||||
|
||||
//auto img = get_frame_from_camera(flag);
|
||||
//auto image_data = image_loader.load_from_cimg(img, true);
|
||||
//if (image_data.empty()) {
|
||||
// std::cerr << "Failed to load image!" << std::endl;
|
||||
// break;
|
||||
//}
|
||||
|
||||
/* 前向传播 */
|
||||
//auto output = cnn.forward(image_data, batch_size, true);
|
||||
//if (!output.empty()) {
|
||||
// std::cout << "Forward pass successful!" << std::endl;
|
||||
// std::cout << "Input size: " << image_data.size() << std::endl;
|
||||
// std::cout << "Output size: " << output.size() << std::endl;
|
||||
|
||||
// 计算输出统计信息
|
||||
// float sum = 0.0f, max_val = output[0], min_val = output[0];
|
||||
// for (float val : output) {
|
||||
// sum += val;
|
||||
// if (val > max_val) max_val = val;
|
||||
// if (val < min_val) min_val = val;
|
||||
// }
|
||||
|
||||
// std::cout << "Output statistics:" << std::endl;
|
||||
// std::cout << " Mean: " << sum / output.size() << std::endl;
|
||||
// std::cout << " Min: " << min_val << std::endl;
|
||||
// std::cout << " Max: " << max_val << std::endl;
|
||||
//}
|
||||
|
||||
//std::vector<float> t(16);
|
||||
//auto r = nn.forward(output);
|
||||
//auto y = nn.cal_err(r, t);
|
||||
|
||||
//for (auto i: y) {
|
||||
// std::cout << i << " ";
|
||||
//}
|
||||
//std::cout << std::endl;
|
||||
|
||||
//y = nn.backward(y);
|
||||
//y = cnn.backward(y);
|
||||
|
||||
//std::this_thread::sleep_for(std::chrono::microseconds(10));
|
||||
|
||||
|
||||
//}
|
||||
|
||||
//Camera_stop();
|
||||
//Camera_unsetup();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
#include "net/HttpServer.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
// Session类 - 处理单个HTTP连接
|
||||
class HttpServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
Session(tcp::socket&& socket, HttpServer* server)
|
||||
: socket_(std::move(socket)), server_(server) {
|
||||
}
|
||||
|
||||
void run() {
|
||||
doRead();
|
||||
}
|
||||
|
||||
private:
|
||||
void doRead() {
|
||||
auto self = shared_from_this();
|
||||
http::async_read(socket_, buffer_, request_,
|
||||
[this, self](beast::error_code ec, std::size_t) {
|
||||
if (!ec) {
|
||||
processRequest();
|
||||
} else if (ec != http::error::end_of_stream) {
|
||||
// 错误处理
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void processRequest() {
|
||||
try {
|
||||
// 创建响应对象
|
||||
auto response = std::make_shared<http::response<http::string_body>>(
|
||||
server_->handleRequest(request_)
|
||||
);
|
||||
doWrite(response);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error processing request: " << e.what() << std::endl;
|
||||
// 发送错误响应
|
||||
auto error_response = std::make_shared<http::response<http::string_body>>();
|
||||
error_response->version(request_.version());
|
||||
error_response->result(http::status::internal_server_error);
|
||||
error_response->set(http::field::content_type, "text/plain");
|
||||
error_response->body() = "500 Internal Server Error\n";
|
||||
error_response->prepare_payload();
|
||||
doWrite(error_response);
|
||||
}
|
||||
}
|
||||
|
||||
void doWrite(std::shared_ptr<http::response<http::string_body>> response) {
|
||||
auto self = shared_from_this();
|
||||
http::async_write(socket_, *response,
|
||||
[this, self, response](beast::error_code ec, std::size_t) {
|
||||
if (!ec) {
|
||||
// 检查是否需要保持连接
|
||||
if (!response->keep_alive()) {
|
||||
socket_.shutdown(tcp::socket::shutdown_send, ec);
|
||||
}
|
||||
}
|
||||
// 继续读取下一个请求
|
||||
doRead();
|
||||
});
|
||||
}
|
||||
|
||||
tcp::socket socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> request_;
|
||||
HttpServer* server_;
|
||||
};
|
||||
|
||||
// Listener类 - 接受新连接
|
||||
class HttpServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, HttpServer* server)
|
||||
: ioc_(ioc), acceptor_(ioc), server_(server) {
|
||||
beast::error_code ec;
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (!ec) {
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (!ec) {
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
}
|
||||
}
|
||||
if (ec) {
|
||||
std::cerr << "Listener error: " << ec.message() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (acceptor_.is_open()) {
|
||||
doAccept();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
[this, self = shared_from_this()](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
std::make_shared<Session>(std::move(socket), server_)->run();
|
||||
}
|
||||
doAccept();
|
||||
});
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
HttpServer* server_;
|
||||
};
|
||||
|
||||
// HttpServer实现 - 新构造函数(监听所有接口)
|
||||
HttpServer::HttpServer(unsigned short port, unsigned int threads)
|
||||
: address_("0.0.0.0"), port_(port), threads_(threads),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) {
|
||||
throw std::runtime_error("Failed to create io_context");
|
||||
}
|
||||
}
|
||||
|
||||
// HttpServer实现 - 新构造函数(指定监听地址)
|
||||
HttpServer::HttpServer(const std::string& address, unsigned short port, unsigned int threads)
|
||||
: address_(address), port_(port), threads_(threads),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) {
|
||||
throw std::runtime_error("Failed to create io_context");
|
||||
}
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void HttpServer::start() {
|
||||
if (running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建端点(使用指定的地址和端口)
|
||||
asio::ip::address ip_address = asio::ip::make_address(address_);
|
||||
tcp::endpoint endpoint(ip_address, port_);
|
||||
|
||||
// 创建监听器
|
||||
listener_ = std::make_shared<Listener>(*ioc_, endpoint, this);
|
||||
if (!listener_) {
|
||||
throw std::runtime_error("Failed to create listener");
|
||||
}
|
||||
listener_->run();
|
||||
|
||||
running_ = true;
|
||||
|
||||
// 启动工作线程
|
||||
for (unsigned int i = 0; i < threads_; ++i) {
|
||||
worker_threads_.emplace_back([this]() {
|
||||
try {
|
||||
ioc_->run();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Thread exception: " << e.what() << std::endl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::cout << "HTTP Server started on " << address_ << ":" << port_
|
||||
<< " with " << threads_ << " threads" << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Failed to start server: " << e.what() << std::endl;
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
|
||||
if (ioc_) {
|
||||
ioc_->stop();
|
||||
}
|
||||
|
||||
for (auto& thread : worker_threads_) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
worker_threads_.clear();
|
||||
|
||||
std::cout << "HTTP Server stopped" << std::endl;
|
||||
}
|
||||
|
||||
void HttpServer::get(const std::string& path, HttpRequestHandler handler) {
|
||||
get_handlers_[path] = handler;
|
||||
}
|
||||
|
||||
void HttpServer::post(const std::string& path, HttpRequestHandler handler) {
|
||||
post_handlers_[path] = handler;
|
||||
}
|
||||
|
||||
void HttpServer::put(const std::string& path, HttpRequestHandler handler) {
|
||||
put_handlers_[path] = handler;
|
||||
}
|
||||
|
||||
void HttpServer::del(const std::string& path, HttpRequestHandler handler) {
|
||||
delete_handlers_[path] = handler;
|
||||
}
|
||||
|
||||
void HttpServer::use(MiddlewareHandler middleware) {
|
||||
middlewares_.push_back(middleware);
|
||||
}
|
||||
|
||||
void HttpServer::setStaticDirectory(const std::string& path) {
|
||||
static_directory_ = path;
|
||||
}
|
||||
|
||||
bool HttpServer::isRunning() const {
|
||||
return running_;
|
||||
}
|
||||
|
||||
unsigned short HttpServer::getPort() const {
|
||||
return port_;
|
||||
}
|
||||
|
||||
std::string HttpServer::getAddress() const {
|
||||
return address_;
|
||||
}
|
||||
|
||||
void HttpServer::run() {
|
||||
start();
|
||||
if (ioc_) {
|
||||
ioc_->run();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::showStatus() const {
|
||||
std::cout << "=== HTTP Server Status ===" << std::endl;
|
||||
std::cout << "Address: " << address_ << std::endl;
|
||||
std::cout << "Port: " << port_ << std::endl;
|
||||
std::cout << "Threads: " << threads_ << std::endl;
|
||||
std::cout << "Running: " << (running_ ? "Yes" : "No") << std::endl;
|
||||
std::cout << "Static Directory: " << (static_directory_.empty() ? "Not set" : static_directory_) << std::endl;
|
||||
std::cout << "Registered Routes:" << std::endl;
|
||||
std::cout << " GET: " << get_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& [path, _] : get_handlers_) {
|
||||
std::cout << " GET " << path << std::endl;
|
||||
}
|
||||
std::cout << " POST: " << post_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& [path, _] : post_handlers_) {
|
||||
std::cout << " POST " << path << std::endl;
|
||||
}
|
||||
std::cout << " PUT: " << put_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& [path, _] : put_handlers_) {
|
||||
std::cout << " PUT " << path << std::endl;
|
||||
}
|
||||
std::cout << " DELETE: " << delete_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& [path, _] : delete_handlers_) {
|
||||
std::cout << " DELETE " << path << std::endl;
|
||||
}
|
||||
std::cout << "Middlewares: " << middlewares_.size() << std::endl;
|
||||
std::cout << "==========================" << std::endl;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> HttpServer::parseQueryParams(const std::string& query) {
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
|
||||
if (query.empty()) {
|
||||
return params;
|
||||
}
|
||||
|
||||
std::vector<std::string> pairs;
|
||||
boost::split(pairs, query, boost::is_any_of("&"));
|
||||
|
||||
for (const auto& pair : pairs) {
|
||||
std::vector<std::string> key_value;
|
||||
boost::split(key_value, pair, boost::is_any_of("="));
|
||||
|
||||
if (key_value.size() == 2) {
|
||||
params[key_value[0]] = key_value[1];
|
||||
} else if (key_value.size() == 1) {
|
||||
params[key_value[0]] = "";
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
http::response<http::string_body> HttpServer::handleRequest(const http::request<http::string_body>& req) {
|
||||
// 将 target() 转换为字符串
|
||||
std::string target_path = std::string(req.target().data(), req.target().size());
|
||||
std::string query_string;
|
||||
|
||||
// 分离路径和查询参数
|
||||
auto query_pos = target_path.find('?');
|
||||
if (query_pos != std::string::npos) {
|
||||
query_string = target_path.substr(query_pos + 1);
|
||||
target_path = target_path.substr(0, query_pos);
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
auto query_params = parseQueryParams(query_string);
|
||||
|
||||
// 准备响应对象
|
||||
http::response<http::string_body> response;
|
||||
response.version(req.version());
|
||||
response.set(http::field::server, "Boost.Beast HttpServer");
|
||||
response.set(http::field::content_type, "text/plain");
|
||||
response.keep_alive(req.keep_alive());
|
||||
|
||||
// 执行中间件链
|
||||
bool middleware_handled = false;
|
||||
for (const auto& middleware : middlewares_) {
|
||||
if (!middleware(req, response, query_params)) {
|
||||
middleware_handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果中间件处理了请求,直接返回
|
||||
if (middleware_handled) {
|
||||
response.prepare_payload();
|
||||
return response;
|
||||
}
|
||||
|
||||
// 查找路由处理器
|
||||
HttpRequestHandler handler = nullptr;
|
||||
|
||||
switch (req.method()) {
|
||||
case http::verb::get: {
|
||||
auto it = get_handlers_.find(target_path);
|
||||
if (it != get_handlers_.end()) {
|
||||
handler = it->second;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case http::verb::post: {
|
||||
auto it = post_handlers_.find(target_path);
|
||||
if (it != post_handlers_.end()) {
|
||||
handler = it->second;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case http::verb::put: {
|
||||
auto it = put_handlers_.find(target_path);
|
||||
if (it != put_handlers_.end()) {
|
||||
handler = it->second;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case http::verb::delete_: {
|
||||
auto it = delete_handlers_.find(target_path);
|
||||
if (it != delete_handlers_.end()) {
|
||||
handler = it->second;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// 处理静态文件
|
||||
if (!handler && !static_directory_.empty()) {
|
||||
std::string file_path = static_directory_ + target_path;
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
response.result(http::status::ok);
|
||||
response.body() = buffer.str();
|
||||
|
||||
// 根据文件扩展名设置Content-Type
|
||||
if (boost::algorithm::ends_with(file_path, ".html")) {
|
||||
response.set(http::field::content_type, "text/html");
|
||||
} else if (boost::algorithm::ends_with(file_path, ".css")) {
|
||||
response.set(http::field::content_type, "text/css");
|
||||
} else if (boost::algorithm::ends_with(file_path, ".js")) {
|
||||
response.set(http::field::content_type, "application/javascript");
|
||||
} else if (boost::algorithm::ends_with(file_path, ".json")) {
|
||||
response.set(http::field::content_type, "application/json");
|
||||
} else if (boost::algorithm::ends_with(file_path, ".png")) {
|
||||
response.set(http::field::content_type, "image/png");
|
||||
} else if (boost::algorithm::ends_with(file_path, ".jpg") ||
|
||||
boost::algorithm::ends_with(file_path, ".jpeg")) {
|
||||
response.set(http::field::content_type, "image/jpeg");
|
||||
}
|
||||
|
||||
response.prepare_payload();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到处理器,调用它
|
||||
if (handler) {
|
||||
try {
|
||||
return handler(req, query_params);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Handler exception: " << e.what() << std::endl;
|
||||
response.result(http::status::internal_server_error);
|
||||
response.body() = "500 Internal Server Error\n";
|
||||
response.prepare_payload();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
response.result(http::status::not_found);
|
||||
response.body() = "404 Not Found\n";
|
||||
response.prepare_payload();
|
||||
return response;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "net/WebSocketClient.hpp"
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
// 内部实现结构体
|
||||
struct WebSocketClient::Impl {
|
||||
// 配置参数
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string path;
|
||||
|
||||
// Boost 对象
|
||||
asio::io_context ioc;
|
||||
tcp::resolver resolver;
|
||||
beast::websocket::stream<beast::tcp_stream> ws;
|
||||
|
||||
// 线程管理
|
||||
std::thread io_thread;
|
||||
std::atomic<bool> running{false};
|
||||
|
||||
// 回调函数
|
||||
WebSocketClientBase::MessageCallback message_callback;
|
||||
WebSocketClientBase::ErrorCallback error_callback;
|
||||
WebSocketClientBase::ConnectCallback connect_callback;
|
||||
WebSocketClientBase::CloseCallback close_callback;
|
||||
|
||||
// 消息队列
|
||||
std::queue<std::string> send_queue;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_cv;
|
||||
std::atomic<bool> sending{false};
|
||||
|
||||
// 状态标志
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<bool> connecting{false};
|
||||
std::atomic<bool> stopping{false};
|
||||
|
||||
// 配置选项
|
||||
bool auto_reconnect{false};
|
||||
int reconnect_interval_ms{3000};
|
||||
int heartbeat_interval_ms{30000};
|
||||
|
||||
// 构造函数
|
||||
Impl(const std::string& host, const std::string& port, const std::string& path)
|
||||
: host(host)
|
||||
, port(port)
|
||||
, path(path)
|
||||
, resolver(asio::make_strand(ioc))
|
||||
, ws(asio::make_strand(ioc))
|
||||
{
|
||||
}
|
||||
|
||||
// 析构函数
|
||||
~Impl() {
|
||||
stop();
|
||||
}
|
||||
|
||||
// 启动IO线程
|
||||
void start() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
io_thread = std::thread([this]() {
|
||||
while (running) {
|
||||
try {
|
||||
ioc.run();
|
||||
break;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "IO上下文异常: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 停止IO线程
|
||||
void stop() {
|
||||
running = false;
|
||||
stopping = true;
|
||||
|
||||
if (io_thread.joinable()) {
|
||||
ioc.stop();
|
||||
io_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行连接
|
||||
void doConnect() {
|
||||
if (connecting || connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
connecting = true;
|
||||
connected = false;
|
||||
|
||||
// 解析主机名
|
||||
resolver.async_resolve(host, port,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type results) {
|
||||
onResolve(ec, results);
|
||||
});
|
||||
}
|
||||
|
||||
// 解析完成回调
|
||||
void onResolve(beast::error_code ec, tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
onError("解析失败: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// 连接到解析出的端点
|
||||
beast::get_lowest_layer(ws).async_connect(results,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
|
||||
onConnect(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// 连接完成回调
|
||||
void onConnect(beast::error_code ec) {
|
||||
if (ec) {
|
||||
onError("连接失败: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置WebSocket选项
|
||||
ws.set_option(beast::websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
|
||||
// 执行WebSocket握手
|
||||
ws.async_handshake(host, path,
|
||||
[this](beast::error_code ec) {
|
||||
onHandshake(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// 握手完成回调
|
||||
void onHandshake(beast::error_code ec) {
|
||||
connecting = false;
|
||||
|
||||
if (ec) {
|
||||
onError("握手失败: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
// 调用连接成功回调
|
||||
if (connect_callback) {
|
||||
connect_callback(true);
|
||||
}
|
||||
|
||||
// 开始读取消息
|
||||
doRead();
|
||||
|
||||
// 开始发送队列中的消息
|
||||
doSend();
|
||||
}
|
||||
|
||||
// 读取消息
|
||||
void doRead() {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.async_read(buffer,
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onRead(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// 读取完成回调
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
if (ec == beast::websocket::error::closed) {
|
||||
// 正常关闭
|
||||
onClose();
|
||||
} else {
|
||||
onError("读取失败: " + ec.message());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
std::string message = beast::buffers_to_string(buffer.data());
|
||||
buffer.consume(bytes_transferred);
|
||||
|
||||
if (message_callback) {
|
||||
message_callback(message);
|
||||
}
|
||||
|
||||
// 继续读取
|
||||
doRead();
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
void doSend() {
|
||||
if (!connected || sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (send_queue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sending = true;
|
||||
std::string message = std::move(send_queue.front());
|
||||
send_queue.pop();
|
||||
lock.unlock();
|
||||
|
||||
ws.async_write(asio::buffer(message),
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onWrite(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// 写入完成回调
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
sending = false;
|
||||
|
||||
if (ec) {
|
||||
onError("写入失败: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续发送队列中的消息
|
||||
doSend();
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
void onError(const std::string& error) {
|
||||
connecting = false;
|
||||
connected = false;
|
||||
|
||||
std::cerr << "WebSocket错误: " << error << std::endl;
|
||||
|
||||
if (error_callback) {
|
||||
error_callback(error);
|
||||
}
|
||||
|
||||
// 自动重连
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
// 连接关闭
|
||||
void onClose() {
|
||||
connected = false;
|
||||
|
||||
if (close_callback) {
|
||||
close_callback();
|
||||
}
|
||||
|
||||
// 自动重连
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
beast::flat_buffer buffer;
|
||||
};
|
||||
|
||||
// WebSocketClient 实现
|
||||
WebSocketClient::WebSocketClient(const std::string& host, const std::string& port, const std::string& path)
|
||||
: impl_(std::make_unique<Impl>(host, port, path))
|
||||
{
|
||||
impl_->start();
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void WebSocketClient::setMessageCallback(MessageCallback callback) {
|
||||
impl_->message_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setErrorCallback(ErrorCallback callback) {
|
||||
impl_->error_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setConnectCallback(ConnectCallback callback) {
|
||||
impl_->connect_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setCloseCallback(CloseCallback callback) {
|
||||
impl_->close_callback = callback;
|
||||
}
|
||||
|
||||
bool WebSocketClient::connect(int timeout_ms) {
|
||||
if (impl_->connecting || impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->doConnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketClient::disconnect() {
|
||||
if (!impl_->connected && !impl_->connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->stopping = true;
|
||||
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
beast::error_code ec;
|
||||
impl_->ws.close(beast::websocket::close_code::normal, ec);
|
||||
impl_->connected = false;
|
||||
impl_->connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool WebSocketClient::isConnected() const {
|
||||
return impl_->connected;
|
||||
}
|
||||
|
||||
bool WebSocketClient::send(const std::string& message) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
impl_->send_queue.push(message);
|
||||
}
|
||||
|
||||
impl_->queue_cv.notify_one();
|
||||
asio::post(impl_->ioc, [this]() { impl_->doSend(); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketClient::sendBinary(const void* data, size_t size) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string message(static_cast<const char*>(data), size);
|
||||
return send(message);
|
||||
}
|
||||
|
||||
void WebSocketClient::setAutoReconnect(bool enable, int interval_ms) {
|
||||
impl_->auto_reconnect = enable;
|
||||
impl_->reconnect_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
void WebSocketClient::setHeartbeatInterval(int interval_ms) {
|
||||
impl_->heartbeat_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getHost() const {
|
||||
return impl_->host;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPort() const {
|
||||
return impl_->port;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPath() const {
|
||||
return impl_->path;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,620 @@
|
||||
#include "net/WebSocketClientSSL.hpp"
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/beast/websocket/ssl.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace asio = boost::asio;
|
||||
namespace ssl = asio::ssl;
|
||||
using tcp = asio::ip::tcp;
|
||||
using websocket = beast::websocket::stream<beast::ssl_stream<beast::tcp_stream>>;
|
||||
|
||||
// 内部实现结构体
|
||||
struct WebSocketClientSSL::Impl {
|
||||
// 配置参数
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string path;
|
||||
|
||||
// SSL 配置
|
||||
bool verify_certificate{true};
|
||||
std::string cert_file;
|
||||
std::string key_file;
|
||||
std::string ca_file;
|
||||
std::string ciphers;
|
||||
std::string sni_hostname;
|
||||
long ssl_options{ssl::context::default_workarounds |
|
||||
ssl::context::no_sslv2 |
|
||||
ssl::context::no_sslv3};
|
||||
|
||||
// Boost 对象
|
||||
asio::io_context ioc;
|
||||
ssl::context ssl_ctx{ssl::context::tlsv12_client};
|
||||
tcp::resolver resolver;
|
||||
websocket ws;
|
||||
asio::steady_timer heartbeat_timer;
|
||||
asio::steady_timer reconnect_timer;
|
||||
|
||||
// 回调函数
|
||||
MessageCallback message_callback;
|
||||
ErrorCallback error_callback;
|
||||
ConnectCallback connect_callback;
|
||||
CloseCallback close_callback;
|
||||
|
||||
// 状态管理
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<bool> connecting{false};
|
||||
std::atomic<bool> stopping{false};
|
||||
std::atomic<bool> auto_reconnect{false};
|
||||
std::atomic<int> reconnect_interval{3000};
|
||||
std::atomic<int> heartbeat_interval{30000};
|
||||
|
||||
// 消息队列
|
||||
struct QueuedMessage {
|
||||
std::string data;
|
||||
bool is_binary;
|
||||
};
|
||||
std::queue<QueuedMessage> send_queue;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_cv;
|
||||
std::atomic<bool> writing{false};
|
||||
|
||||
// 接收缓冲区
|
||||
beast::flat_buffer read_buffer;
|
||||
|
||||
// 线程
|
||||
std::thread io_thread;
|
||||
|
||||
Impl(const std::string& h, const std::string& p, const std::string& pt)
|
||||
: host(h)
|
||||
, port(p)
|
||||
, path(pt)
|
||||
, resolver(asio::make_strand(ioc))
|
||||
, ws(asio::make_strand(ioc), ssl_ctx)
|
||||
, heartbeat_timer(asio::make_strand(ioc))
|
||||
, reconnect_timer(asio::make_strand(ioc)) {
|
||||
|
||||
// 配置 SSL 上下文
|
||||
configureSSLContext();
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
stopping = true;
|
||||
if (io_thread.joinable()) {
|
||||
ioc.stop();
|
||||
io_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void configureSSLContext() {
|
||||
try {
|
||||
// 设置 SSL 选项
|
||||
ssl_ctx.set_options(ssl_options);
|
||||
|
||||
// 设置密码列表
|
||||
if (!ciphers.empty()) {
|
||||
SSL_CTX_set_cipher_list(ssl_ctx.native_handle(), ciphers.c_str());
|
||||
}
|
||||
|
||||
// 加载证书文件
|
||||
if (!cert_file.empty()) {
|
||||
ssl_ctx.use_certificate_file(cert_file, ssl::context::pem);
|
||||
}
|
||||
|
||||
// 加载私钥文件
|
||||
if (!key_file.empty()) {
|
||||
ssl_ctx.use_private_key_file(key_file, ssl::context::pem);
|
||||
}
|
||||
|
||||
// 加载 CA 证书
|
||||
if (!ca_file.empty()) {
|
||||
ssl_ctx.load_verify_file(ca_file);
|
||||
} else {
|
||||
// 使用系统默认的 CA 证书
|
||||
ssl_ctx.set_default_verify_paths();
|
||||
}
|
||||
|
||||
// 设置验证模式
|
||||
if (verify_certificate) {
|
||||
ssl_ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert);
|
||||
} else {
|
||||
ssl_ctx.set_verify_mode(ssl::verify_none);
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "SSL 上下文配置错误: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void setSNI() {
|
||||
// 设置 SNI 主机名
|
||||
if (!sni_hostname.empty()) {
|
||||
if (!SSL_set_tlsext_host_name(ws.next_layer().native_handle(), sni_hostname.c_str())) {
|
||||
beast::error_code ec{static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()};
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
} else if (!host.empty()) {
|
||||
// 使用主机名作为 SNI
|
||||
if (!SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str())) {
|
||||
beast::error_code ec{static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()};
|
||||
throw beast::system_error{ec};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 构造函数
|
||||
WebSocketClientSSL::WebSocketClientSSL(const std::string& host, const std::string& port, const std::string& path)
|
||||
: impl_(std::make_unique<Impl>(host, port, path)) {
|
||||
}
|
||||
|
||||
// 析构函数
|
||||
WebSocketClientSSL::~WebSocketClientSSL() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
// 回调函数设置
|
||||
void WebSocketClientSSL::setMessageCallback(MessageCallback callback) {
|
||||
impl_->message_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setErrorCallback(ErrorCallback callback) {
|
||||
impl_->error_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setConnectCallback(ConnectCallback callback) {
|
||||
impl_->connect_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setCloseCallback(CloseCallback callback) {
|
||||
impl_->close_callback = std::move(callback);
|
||||
}
|
||||
|
||||
// SSL/TLS 配置
|
||||
void WebSocketClientSSL::setVerifyCertificate(bool verify) {
|
||||
impl_->verify_certificate = verify;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setCertificateFile(const std::string& cert_file) {
|
||||
impl_->cert_file = cert_file;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setPrivateKeyFile(const std::string& key_file) {
|
||||
impl_->key_file = key_file;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setCertificateAuthorityFile(const std::string& ca_file) {
|
||||
impl_->ca_file = ca_file;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setSSLContextOptions(long options) {
|
||||
impl_->ssl_options = options;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setCiphers(const std::string& ciphers) {
|
||||
impl_->ciphers = ciphers;
|
||||
impl_->configureSSLContext();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setSNIHostname(const std::string& sni_hostname) {
|
||||
impl_->sni_hostname = sni_hostname;
|
||||
}
|
||||
|
||||
// 连接函数
|
||||
bool WebSocketClientSSL::connect(int timeout_ms) {
|
||||
if (impl_->connecting || impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->connecting = true;
|
||||
impl_->stopping = false;
|
||||
|
||||
// 启动 IO 线程
|
||||
if (!impl_->io_thread.joinable()) {
|
||||
impl_->io_thread = std::thread([this]() {
|
||||
try {
|
||||
impl_->ioc.run();
|
||||
} catch (const std::exception& e) {
|
||||
if (impl_->error_callback) {
|
||||
impl_->error_callback(std::string("IO context error: ") + e.what());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 异步连接
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
try {
|
||||
impl_->setSNI();
|
||||
doConnect();
|
||||
} catch (const std::exception& e) {
|
||||
handleError("SSL setup failed", beast::error_code{}, e.what());
|
||||
}
|
||||
});
|
||||
|
||||
// 等待连接完成
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (impl_->connecting && !impl_->stopping) {
|
||||
if (timeout_ms > 0) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
|
||||
if (elapsed > timeout_ms) {
|
||||
impl_->connecting = false;
|
||||
if (impl_->error_callback) {
|
||||
impl_->error_callback("Connection timeout");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
return impl_->connected;
|
||||
}
|
||||
|
||||
// 私有连接方法
|
||||
void WebSocketClientSSL::doConnect() {
|
||||
impl_->resolver.async_resolve(
|
||||
impl_->host,
|
||||
impl_->port,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type results) {
|
||||
onResolve(ec, std::move(results));
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onResolve(beast::error_code ec, tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
handleError("Resolve failed", ec);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
beast::get_lowest_layer(impl_->ws).expires_after(std::chrono::seconds(30));
|
||||
|
||||
// 异步连接
|
||||
beast::get_lowest_layer(impl_->ws).async_connect(
|
||||
results,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) {
|
||||
onConnect(ec, endpoint);
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) {
|
||||
if (ec) {
|
||||
handleError("Connect failed", ec);
|
||||
return;
|
||||
}
|
||||
|
||||
// SSL 握手
|
||||
impl_->ws.next_layer().async_handshake(
|
||||
ssl::stream_base::client,
|
||||
[this](beast::error_code ec) {
|
||||
onSSLHandshake(ec);
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onSSLHandshake(beast::error_code ec) {
|
||||
if (ec) {
|
||||
handleError("SSL handshake failed", ec);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭 TCP 超时设置
|
||||
beast::get_lowest_layer(impl_->ws).expires_never();
|
||||
|
||||
// 设置 WebSocket 选项
|
||||
impl_->ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));
|
||||
impl_->ws.read_message_max(10 * 1024 * 1024); // 10MB 限制
|
||||
|
||||
// WebSocket 握手
|
||||
impl_->ws.async_handshake(
|
||||
impl_->host + ":" + impl_->port,
|
||||
impl_->path,
|
||||
[this](beast::error_code ec) {
|
||||
onHandshake(ec);
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onHandshake(beast::error_code ec) {
|
||||
impl_->connecting = false;
|
||||
|
||||
if (ec) {
|
||||
handleError("WebSocket handshake failed", ec);
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->connected = true;
|
||||
|
||||
// 开始读取消息
|
||||
doRead();
|
||||
|
||||
// 开始心跳
|
||||
if (impl_->heartbeat_interval > 0) {
|
||||
startHeartbeat();
|
||||
}
|
||||
|
||||
// 通知连接成功
|
||||
if (impl_->connect_callback) {
|
||||
impl_->connect_callback(true);
|
||||
}
|
||||
|
||||
// 检查是否有待发送的消息
|
||||
if (hasMessages()) {
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
void WebSocketClientSSL::disconnect() {
|
||||
impl_->stopping = true;
|
||||
impl_->auto_reconnect = false;
|
||||
|
||||
if (impl_->connected) {
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
beast::error_code ec;
|
||||
impl_->ws.close(websocket::close_code::normal, ec);
|
||||
if (ec && impl_->error_callback) {
|
||||
impl_->error_callback("Error closing WebSocket: " + ec.message());
|
||||
}
|
||||
impl_->connected = false;
|
||||
stopHeartbeat();
|
||||
|
||||
if (impl_->close_callback) {
|
||||
impl_->close_callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
bool WebSocketClientSSL::send(const std::string& message) {
|
||||
return pushMessage(message, false);
|
||||
}
|
||||
|
||||
bool WebSocketClientSSL::sendBinary(const void* data, size_t size) {
|
||||
std::string message(static_cast<const char*>(data), size);
|
||||
return pushMessage(message, true);
|
||||
}
|
||||
|
||||
// 状态检查
|
||||
bool WebSocketClientSSL::isConnected() const {
|
||||
return impl_->connected;
|
||||
}
|
||||
|
||||
std::string WebSocketClientSSL::getHost() const {
|
||||
return impl_->host;
|
||||
}
|
||||
|
||||
std::string WebSocketClientSSL::getPort() const {
|
||||
return impl_->port;
|
||||
}
|
||||
|
||||
std::string WebSocketClientSSL::getPath() const {
|
||||
return impl_->path;
|
||||
}
|
||||
|
||||
// 自动重连和心跳设置
|
||||
void WebSocketClientSSL::setAutoReconnect(bool enable, int interval_ms) {
|
||||
impl_->auto_reconnect = enable;
|
||||
impl_->reconnect_interval = interval_ms;
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::setHeartbeatInterval(int interval_ms) {
|
||||
impl_->heartbeat_interval = interval_ms;
|
||||
if (impl_->connected && interval_ms > 0) {
|
||||
startHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 私有方法实现 ============
|
||||
|
||||
void WebSocketClientSSL::doRead() {
|
||||
impl_->ws.async_read(
|
||||
impl_->read_buffer,
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onRead(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
if (ec == websocket::error::closed) {
|
||||
// 正常关闭
|
||||
impl_->connected = false;
|
||||
stopHeartbeat();
|
||||
|
||||
if (impl_->close_callback) {
|
||||
impl_->close_callback();
|
||||
}
|
||||
|
||||
// 自动重连
|
||||
if (impl_->auto_reconnect && !impl_->stopping) {
|
||||
impl_->reconnect_timer.expires_after(std::chrono::milliseconds(impl_->reconnect_interval));
|
||||
impl_->reconnect_timer.async_wait(
|
||||
[this](beast::error_code ec) {
|
||||
if (!ec && !impl_->stopping) {
|
||||
doConnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
handleError("Read failed", ec);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
if (impl_->message_callback) {
|
||||
std::string message = beast::buffers_to_string(impl_->read_buffer.data());
|
||||
impl_->message_callback(message);
|
||||
}
|
||||
|
||||
// 清空缓冲区并继续读取
|
||||
impl_->read_buffer.consume(bytes_transferred);
|
||||
doRead();
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::doWrite() {
|
||||
if (impl_->writing || !hasMessages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->writing = true;
|
||||
auto message = popMessage();
|
||||
|
||||
impl_->ws.async_write(
|
||||
asio::buffer(message.first),
|
||||
[this, is_binary = message.second](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
impl_->writing = false;
|
||||
onWrite(ec, bytes_transferred);
|
||||
|
||||
// 如果是二进制消息,设置二进制模式
|
||||
if (!ec && is_binary) {
|
||||
impl_->ws.binary(true);
|
||||
} else if (!ec) {
|
||||
impl_->ws.text(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
(void)bytes_transferred; // 消除未使用参数警告
|
||||
|
||||
if (ec) {
|
||||
handleError("Write failed", ec);
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续发送队列中的下一条消息
|
||||
if (hasMessages()) {
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::handleError(const std::string& message, beast::error_code ec, const std::string& extra) {
|
||||
std::string error_msg = message;
|
||||
if (ec) {
|
||||
error_msg += ": " + ec.message();
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
error_msg += " (" + extra + ")";
|
||||
}
|
||||
|
||||
impl_->connecting = false;
|
||||
impl_->connected = false;
|
||||
stopHeartbeat();
|
||||
|
||||
if (impl_->error_callback) {
|
||||
impl_->error_callback(error_msg);
|
||||
}
|
||||
|
||||
if (impl_->connect_callback) {
|
||||
impl_->connect_callback(false);
|
||||
}
|
||||
|
||||
// 自动重连
|
||||
if (impl_->auto_reconnect && !impl_->stopping) {
|
||||
impl_->reconnect_timer.expires_after(std::chrono::milliseconds(impl_->reconnect_interval));
|
||||
impl_->reconnect_timer.async_wait(
|
||||
[this](beast::error_code ec) {
|
||||
if (!ec && !impl_->stopping) {
|
||||
doConnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::startHeartbeat() {
|
||||
if (impl_->heartbeat_interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->heartbeat_timer.expires_after(std::chrono::milliseconds(impl_->heartbeat_interval));
|
||||
impl_->heartbeat_timer.async_wait(
|
||||
[this](beast::error_code ec) {
|
||||
heartbeatTimerCallback(ec);
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::stopHeartbeat() {
|
||||
beast::error_code ec;
|
||||
impl_->heartbeat_timer.cancel(ec);
|
||||
}
|
||||
|
||||
void WebSocketClientSSL::heartbeatTimerCallback(beast::error_code ec) {
|
||||
if (ec || impl_->stopping || !impl_->connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送 ping 消息
|
||||
beast::error_code ping_ec;
|
||||
impl_->ws.ping(beast::websocket::ping_data{}, ping_ec);
|
||||
|
||||
if (ping_ec) {
|
||||
handleError("Heartbeat ping failed", ping_ec);
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新设置定时器
|
||||
startHeartbeat();
|
||||
}
|
||||
|
||||
// 线程安全队列操作
|
||||
bool WebSocketClientSSL::pushMessage(const std::string& message, bool is_binary) {
|
||||
if (!impl_->connected || impl_->stopping) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
impl_->send_queue.push({message, is_binary});
|
||||
}
|
||||
|
||||
impl_->queue_cv.notify_one();
|
||||
|
||||
// 触发写入
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
if (!impl_->writing && hasMessages()) {
|
||||
doWrite();
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::pair<std::string, bool> WebSocketClientSSL::popMessage() {
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
if (impl_->send_queue.empty()) {
|
||||
return {"", false};
|
||||
}
|
||||
|
||||
auto msg = std::move(impl_->send_queue.front());
|
||||
impl_->send_queue.pop();
|
||||
return {std::move(msg.data), msg.is_binary};
|
||||
}
|
||||
|
||||
bool WebSocketClientSSL::hasMessages() const {
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
return !impl_->send_queue.empty();
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,382 @@
|
||||
// WebSocketServer.cpp - WebSocket服务器实现
|
||||
// 基于Boost.Beast和Boost.Asio
|
||||
|
||||
#include "net/WebSocketServer.hpp"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
// 内部Session类实现
|
||||
class WebSocketServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
Session(tcp::socket socket, WebSocketServer* server)
|
||||
: ws_(std::move(socket))
|
||||
, server_(server) {
|
||||
|
||||
// 获取远程地址
|
||||
auto endpoint = ws_.next_layer().socket().remote_endpoint();
|
||||
remoteAddress_ = endpoint.address().to_string() + ":" + std::to_string(endpoint.port());
|
||||
}
|
||||
|
||||
void start() {
|
||||
// 生成客户端ID
|
||||
if (server_) {
|
||||
clientId_ = server_->generateClientId();
|
||||
} else {
|
||||
// 如果server为空,使用默认ID
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
clientId_ = "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
// 异步接受WebSocket握手
|
||||
ws_.async_accept(
|
||||
beast::bind_front_handler(
|
||||
&Session::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void send(const std::string& message) {
|
||||
// 将消息添加到发送队列
|
||||
bool write_in_progress = !sendQueue_.empty();
|
||||
sendQueue_.push_back(message);
|
||||
|
||||
if (!write_in_progress) {
|
||||
// 开始异步写入
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& getClientId() const { return clientId_; }
|
||||
const std::string& getRemoteAddress() const { return remoteAddress_; }
|
||||
|
||||
private:
|
||||
websocket::stream<beast::tcp_stream> ws_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::deque<std::string> sendQueue_;
|
||||
WebSocketServer* server_;
|
||||
std::string clientId_;
|
||||
std::string remoteAddress_;
|
||||
|
||||
void onAccept(beast::error_code ec) {
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket accept error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到服务器客户端列表
|
||||
if (server_) {
|
||||
server_->addClient(clientId_, shared_from_this());
|
||||
|
||||
// 调用连接处理器
|
||||
if (server_->connectHandler_) {
|
||||
server_->connectHandler_(clientId_, remoteAddress_);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始读取消息
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doRead() {
|
||||
// 异步读取消息
|
||||
ws_.async_read(
|
||||
buffer_,
|
||||
beast::bind_front_handler(
|
||||
&Session::onRead,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec == websocket::error::closed) {
|
||||
// 正常关闭
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket read error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(bytes_transferred);
|
||||
|
||||
if (server_) {
|
||||
// 查找消息处理器
|
||||
for (const auto& handler : server_->messageHandlers_) {
|
||||
// 这里可以添加路径匹配逻辑
|
||||
handler.second(clientId_, message, [this](const std::string& response) {
|
||||
send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 继续读取
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doWrite() {
|
||||
if (sendQueue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取队列中的第一条消息
|
||||
auto message = sendQueue_.front();
|
||||
|
||||
// 异步写入消息
|
||||
ws_.async_write(
|
||||
asio::buffer(message),
|
||||
beast::bind_front_handler(
|
||||
&Session::onWrite,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket write error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除已发送的消息
|
||||
sendQueue_.pop_front();
|
||||
|
||||
// 继续发送队列中的下一条消息
|
||||
if (!sendQueue_.empty()) {
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 内部Listener类实现
|
||||
class WebSocketServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
static std::shared_ptr<Listener> create(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server) {
|
||||
return std::shared_ptr<Listener>(new Listener(ioc, endpoint, server));
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (!acceptor_.is_open()) {
|
||||
throw std::runtime_error("Acceptor is not open");
|
||||
}
|
||||
|
||||
doAccept();
|
||||
}
|
||||
|
||||
private:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server)
|
||||
: ioc_(ioc)
|
||||
, acceptor_(asio::make_strand(ioc))
|
||||
, server_(server) {
|
||||
|
||||
beast::error_code ec;
|
||||
|
||||
// 打开acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to open acceptor: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to open acceptor: " + ec.message());
|
||||
}
|
||||
|
||||
// 设置地址重用
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to set reuse address: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to set reuse address: " + ec.message());
|
||||
}
|
||||
|
||||
// 绑定到端点
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to bind to endpoint: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to bind to endpoint: " + ec.message());
|
||||
}
|
||||
|
||||
// 开始监听
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to listen: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to listen: " + ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
asio::make_strand(ioc_),
|
||||
beast::bind_front_handler(
|
||||
&Listener::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onAccept(beast::error_code ec, tcp::socket socket) {
|
||||
if (ec) {
|
||||
std::cerr << "Accept error: " << ec.message() << std::endl;
|
||||
} else {
|
||||
// 创建新的Session处理连接
|
||||
std::make_shared<Session>(std::move(socket), server_)->start();
|
||||
}
|
||||
|
||||
// 继续接受新的连接
|
||||
doAccept();
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
WebSocketServer* server_;
|
||||
};
|
||||
|
||||
WebSocketServer::WebSocketServer(unsigned short port, unsigned int threads)
|
||||
: port_(port)
|
||||
, threads_(threads)
|
||||
, impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
WebSocketServer::~WebSocketServer() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void WebSocketServer::start() {
|
||||
if (running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建监听器
|
||||
tcp::endpoint endpoint(tcp::v4(), port_);
|
||||
impl_->listener_ = Listener::create(impl_->ioc_, endpoint, this);
|
||||
|
||||
// 启动监听器
|
||||
impl_->listener_->run();
|
||||
|
||||
// 启动工作线程
|
||||
for (unsigned int i = 0; i < threads_; ++i) {
|
||||
impl_->threads_.emplace_back([this]() {
|
||||
impl_->ioc_.run();
|
||||
});
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
std::cout << "WebSocketServer started on port " << port_ << " with " << threads_ << " threads" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::stop() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止io_context
|
||||
impl_->ioc_.stop();
|
||||
|
||||
// 等待所有线程结束
|
||||
for (auto& thread : impl_->threads_) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
impl_->threads_.clear();
|
||||
running_ = false;
|
||||
std::cout << "WebSocketServer stopped" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::onMessage(const std::string& path, WebSocketMessageHandler handler) {
|
||||
messageHandlers_[path] = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onConnect(WebSocketConnectHandler handler) {
|
||||
connectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onDisconnect(WebSocketDisconnectHandler handler) {
|
||||
disconnectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
bool WebSocketServer::sendToClient(const std::string& clientId, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
it->second->send(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t WebSocketServer::broadcast(const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
size_t count = 0;
|
||||
for (auto& pair : clients_) {
|
||||
pair.second->send(message);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void WebSocketServer::disconnectClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
// 这里应该关闭WebSocket连接
|
||||
// 目前只是从列表中移除
|
||||
clients_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
size_t WebSocketServer::getClientCount() const {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
return clients_.size();
|
||||
}
|
||||
|
||||
std::string WebSocketServer::generateClientId() const {
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
return "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
void WebSocketServer::addClient(const std::string& clientId, std::shared_ptr<Session> session) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_[clientId] = session;
|
||||
}
|
||||
|
||||
void WebSocketServer::removeClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_.erase(clientId);
|
||||
}
|
||||
|
||||
std::shared_ptr<WebSocketServer::Session> WebSocketServer::getClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace pmc::net
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "th/Thread.hpp"
|
||||
namespace qing{
|
||||
|
||||
Thread::Thread(f_t stop_callback, f_t wake_callback, f_t loop_callback, f_t clear_callback)
|
||||
{
|
||||
StopEvent = std::make_unique<f_t>(stop_callback);
|
||||
WakeEvent = std::make_unique<f_t>(wake_callback);
|
||||
LoopEvent = std::make_unique<f_t>(loop_callback);
|
||||
ClearEvent = std::make_unique<f_t>(clear_callback);
|
||||
};
|
||||
|
||||
/* 线程主函数 */
|
||||
void Thread::main() {
|
||||
|
||||
/* 回字形循环 */
|
||||
while (check() != Fsm::Stat::SHUT) {
|
||||
|
||||
try { /* 异常只会使线程静止 */
|
||||
|
||||
|
||||
while (check() == Fsm::Stat::STOP) {
|
||||
(*StopEvent)(*this); /* 静止状态 */
|
||||
}
|
||||
|
||||
if (check() == Fsm::Stat::START) {
|
||||
//(*ClearEvent)(*this); /* 在开始之前做一次清理 */
|
||||
(*WakeEvent)(*this); /* 开始事件 */
|
||||
}
|
||||
|
||||
while (check() == Fsm::Stat::RUNNING) {
|
||||
(*LoopEvent)(*this); /* 从设置阶段进入运行态 */
|
||||
}
|
||||
|
||||
/* 清理事件 (记得检查成员变量有没有被设置) */
|
||||
(*ClearEvent)(*this);
|
||||
|
||||
|
||||
}
|
||||
|
||||
catch (std::exception& exp) {
|
||||
std::cout << "Thread throw a exception: " << exp.what() << std::endl;
|
||||
stop(); /* 因为这里的停止,线程在循环内抛出异常进程不会死亡,所以需要特别注意内存泄漏 */
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user