单个雷达软件完成最终效果

main
LAPTOP-S9HJSOEB\昊天 3 weeks ago
parent 7b6b7aa0d9
commit e7e0a866d2

@ -8,7 +8,7 @@
"-std=c++11", "-std=c++11",
"-g", "-g",
"-finput-charset=UTF-8", "-finput-charset=UTF-8",
"-fexec-charset=GBK", "-fexec-charset=UTF-8",
"-I", "PAVO2_SDK\\include", "-I", "PAVO2_SDK\\include",
"main.cpp", "main.cpp",
"config.cpp", "config.cpp",

@ -93,6 +93,22 @@ void load_config(const std::string& path)
if (s == "CopyTranslation" || s == "AngleBucketInterpolation") cfg.count.z_interpolate_mode = s; if (s == "CopyTranslation" || s == "AngleBucketInterpolation") cfg.count.z_interpolate_mode = s;
else cfg.count.z_interpolate_mode = "none"; else cfg.count.z_interpolate_mode = "none";
} }
if (json_get_string(text, "count", "layer_algorithm", s))
{
if (s == "HalfMax" || s == "KdePeak") cfg.count.layer_algorithm = s;
else cfg.count.layer_algorithm = "LevelTolerance"; // 非法值回退默认算法
}
if (json_get_number(text, "count", "kde_bandwidth", d))
{
cfg.count.kde_bandwidth = d;
if (cfg.count.kde_bandwidth < 0) cfg.count.kde_bandwidth = 0;
}
if (json_get_number(text, "count", "kde_step", d))
{
cfg.count.kde_step = d;
if (cfg.count.kde_step < 0.1) cfg.count.kde_step = 0.1; // 步长过小无意义且极慢
if (cfg.count.kde_step > 100.0) cfg.count.kde_step = 100.0;
}
// ---------- filter: 多边形过滤 ---------- // ---------- filter: 多边形过滤 ----------
poly.clear(); poly.clear();
@ -175,8 +191,12 @@ void load_config(const std::string& path)
<< (cfg.server.mode == "tcp" || cfg.server.mode == "http" ? " (端口 " + std::to_string(cfg.server.port) + ")" : "") << std::endl; << (cfg.server.mode == "tcp" || cfg.server.mode == "http" ? " (端口 " + std::to_string(cfg.server.port) + ")" : "") << std::endl;
std::cout << "分层冗余: 基准" << cfg.count.base_tolerance std::cout << "分层冗余: 基准" << cfg.count.base_tolerance
<< " + 每层" << cfg.count.per_level_tolerance << " + 每层" << cfg.count.per_level_tolerance
<< " 层数算法: " << cfg.count.layer_algorithm
<< " 膨胀: " << cfg.count.point_dilate << " 膨胀: " << cfg.count.point_dilate
<< " 插值模式: " << cfg.count.z_interpolate_mode << std::endl; << " 插值模式: " << cfg.count.z_interpolate_mode << std::endl;
if (cfg.count.layer_algorithm == "KdePeak")
std::cout << "KDE峰值: 带宽=" << cfg.count.kde_bandwidth
<< "(0=自动)mm 步长=" << cfg.count.kde_step << "mm" << std::endl;
std::cout << "过滤多边形(" << cfg.filter.poly_n << "点):"; std::cout << "过滤多边形(" << cfg.filter.poly_n << "点):";
for (const auto& p : cfg.filter.polygon) for (const auto& p : cfg.filter.polygon)
std::cout << " (" << p.first << "," << p.second << ")"; std::cout << " (" << p.first << "," << p.second << ")";

@ -57,6 +57,19 @@ struct AppConfig
double per_level_tolerance = 20.0; // 每层递增冗余(mm) double per_level_tolerance = 20.0; // 每层递增冗余(mm)
int point_dilate = 8; // 画layer图每个点膨胀像素 int point_dilate = 8; // 画layer图每个点膨胀像素
std::string z_interpolate_mode = "none"; // none/CopyTranslation/AngleBucketInterpolation std::string z_interpolate_mode = "none"; // none/CopyTranslation/AngleBucketInterpolation
// 分层容差(三个算法统一): tol(level) = base_tolerance + level×per_level_tolerance
// 层数算法:
// LevelTolerance = 现有: level=round(高度/层高), 判定 |高度-level×层高| <= tol
// HalfMax = 两步判定
// ① 粗筛: 本层中间~下一层中间 = [(level-0.5)×层高, (level+0.5)×层高]
// ② 精筛: 取①内最高点 top(按 floor_height-Y 算), 有效区间 = [top-tol, top]
// KdePeak = 两步判定
// ① 粗筛: 同 HalfMax
// ② 精筛: 对①内高度做 KDE(高斯核密度估计)得密度曲线, 取峰值 peak,
// 有效区间 = [peak-tol, peak+tol]
std::string layer_algorithm = "LevelTolerance";
double kde_bandwidth = 0.0; // KDE高斯核带宽(mm): <=0 按 Silverman 经验法则自动
double kde_step = 1.0; // KDE密度曲线扫描步长(mm)
} count; } count;
// ---------- filter: 软件滤波(多边形 + 拖影) ---------- // ---------- filter: 软件滤波(多边形 + 拖影) ----------

@ -46,13 +46,23 @@
//地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用) //地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用)
"floor_height": 1600, "floor_height": 1600,
//count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance //count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance
"base_tolerance": 15, "base_tolerance": 30,
//count分层冗余 每层增加 毫米 //count分层冗余 每层增加 毫米
"per_level_tolerance": 1, "per_level_tolerance": 1,
//count画layer图: 每个点膨胀像素 (point_dilate×point_dilate) //count画layer图: 每个点膨胀像素 (point_dilate×point_dilate)
"point_dilate": 8, "point_dilate": 8,
//count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation //count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation
"z_interpolate_mode": "CopyTranslation" "z_interpolate_mode": "CopyTranslation",
//count层数算法:
// LevelTolerance = 现有: level=round(高度/层高), 判定 |高度-level×层高| <= 冗余(base_tolerance+层号×per_level_tolerance)
// HalfMax = 两步判定: ①本层中间~下一层中间[(level-0.5)层高,(level+0.5)层高]粗筛 ②取①内最高点top, 有效区间=[top-容差, top]
// KdePeak = 两步判定: ①同上粗筛 ②对①内高度做KDE密度曲线取峰值peak, 有效区间=[peak-容差, peak+容差]
// 容差 = base_tolerance + 层号×per_level_tolerance
"layer_algorithm": "KdePeak",
//KDE(KdePeak算法): 高斯核带宽 毫米 (0=按Silverman经验法则自动取)
"kde_bandwidth": 0,
//KDE(KdePeak算法): 密度曲线扫描步长 毫米 (越小越精细但越慢)
"kde_step": 1
}, },
// ==================== filter: 软件滤波(多边形 + 拖影) ==================== // ==================== filter: 软件滤波(多边形 + 拖影) ====================
@ -93,7 +103,7 @@
// ==================== server: 运行模式 ==================== // ==================== server: 运行模式 ====================
"server": { "server": {
//运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端 //运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端
"mode": "tcp", "mode": "http",
//TCP/HTTP服务端监听端口 (仅tcp/http模式有效) //TCP/HTTP服务端监听端口 (仅tcp/http模式有效)
"port": 9601 "port": 9601
}, },

@ -9,6 +9,7 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <iomanip> #include <iomanip>
#include <limits>
#include <sstream> #include <sstream>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
@ -493,6 +494,92 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
return added; return added;
} }
// ==================== KDE 密度峰值 ====================
// 对一组高度值做高斯核密度估计(KDE),返回密度最大处(最密)的高度(mm)。
// 步骤:
// 1) 高度按 step 量化成直方图 bins(每bin=step毫米),避免 O(点数×bins)
// 2) 直方图与离散高斯核卷积得到密度曲线,核覆盖 ±4×带宽
// 3) 取密度最大 bin 对应的高度
// bandwidth<=0 时自动: bw = 0.9 × min(标准差, IQR/1.34) × n^(-1/5) (Silverman)
// 复杂度 O(n + bins×核宽度)。
double kde_peak_height(const std::vector<double>& vals, double bandwidth, double step)
{
size_t n = vals.size();
if (n == 0) return 0.0;
if (step <= 0) step = 1.0;
double vmin = vals[0], vmax = vals[0], sum = 0.0;
for (size_t i = 0; i < n; ++i)
{
double v = vals[i];
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
sum += v;
}
double bw = bandwidth;
if (bw <= 0)
{
double mean = sum / (double)n;
double sq = 0.0;
for (size_t i = 0; i < n; ++i) { double d = vals[i] - mean; sq += d * d; }
double sd = std::sqrt(sq / (double)n);
std::vector<double> sorted(vals);
std::sort(sorted.begin(), sorted.end());
auto quantile = [&](double p) {
double idx = p * (double)(n - 1);
size_t i0 = (size_t)std::floor(idx);
size_t i1 = std::min(i0 + 1, n - 1);
double frac = idx - i0;
return sorted[i0] * (1.0 - frac) + sorted[i1] * frac;
};
double iqr = quantile(0.75) - quantile(0.25);
double a = std::min(sd, iqr / 1.34);
if (a <= 0) return sorted[n / 2]; // 数据几乎全等, 直接返回中位数
bw = 0.9 * a * std::pow((double)n, -0.2);
}
if (bw < step) bw = step; // 带宽不得小于扫描步长
const size_t MAX_BINS = 200000;
double pad = 4.0 * bw;
double lo0 = vmin - pad;
size_t nb = (size_t)std::ceil((vmax + pad - lo0) / step) + 1;
if (nb < 1) nb = 1;
if (nb > MAX_BINS) nb = MAX_BINS; // 防止极端参数导致 bins 爆炸
std::vector<double> hist(nb, 0.0);
for (size_t i = 0; i < n; ++i)
{
long long b = (long long)std::floor((vals[i] - lo0) / step);
if (b < 0 || (size_t)b >= nb) continue;
hist[(size_t)b] += 1.0;
}
int r = (int)std::ceil(4.0 * bw / step);
std::vector<double> kernel(2 * r + 1);
for (int k = -r; k <= r; ++k)
{
double d = k * step / bw;
kernel[k + r] = std::exp(-0.5 * d * d);
}
std::vector<double> dens(nb, 0.0);
size_t best = 0;
for (size_t i = 0; i < nb; ++i)
{
double acc = 0.0;
for (int k = -r; k <= r; ++k)
{
long long j = (long long)i + k;
if (j < 0 || (size_t)j >= nb) continue;
acc += hist[(size_t)j] * kernel[k + r];
}
dens[i] = acc;
if (acc > dens[best]) best = i;
}
return lo0 + (double)best * step;
}
// ==================== 按层统计 ==================== // ==================== 按层统计 ====================
int calc_item_count(const std::vector<CountPoint>& points, const CountParams& box, int calc_item_count(const std::vector<CountPoint>& points, const CountParams& box,
std::string* detail, std::string* detail,
@ -505,17 +592,92 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
if (points.empty() || box.l <= 0 || box.w <= 0 || box.h <= 0 || box.maxPerLayer <= 0) if (points.empty() || box.l <= 0 || box.w <= 0 || box.h <= 0 || box.maxPerLayer <= 0)
return 0; return 0;
std::unordered_map<int, std::vector<CountPoint>> layer_map; const bool use_half_max = (cfg.count.layer_algorithm == "HalfMax");
for (const auto& p : points) const bool use_kde_peak = (cfg.count.layer_algorithm == "KdePeak");
// 分层容差(三个算法统一): 基准 + 每层递增
auto layer_tol = [&](int lv) { return cfg.count.base_tolerance + lv * cfg.count.per_level_tolerance; };
std::unordered_map<int, std::vector<CountPoint>> layer_map; // 每层最终有效点
std::unordered_map<int, double> layer_max_h; // 每层实际最高点高度(mm)
std::unordered_map<int, double> layer_lo_h; // 每层有效区间下界(mm)
std::unordered_map<int, double> layer_hi_h; // 每层有效区间上界(mm)
if (use_half_max || use_kde_peak)
{ {
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n)) // ① 粗筛: 本层中间 ~ 下一层中间 = [(level-0.5)×层高, (level+0.5)×层高]
continue; std::unordered_map<int, std::vector<CountPoint>> coarse_map;
double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴 for (const auto& p : points)
if (currentHeight < 0) continue; {
int level = (int)std::round(currentHeight / box.h); if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n))
double tolerance = cfg.count.base_tolerance + level * cfg.count.per_level_tolerance; continue;
if (std::abs(currentHeight - level * box.h) <= tolerance) double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
layer_map[level].push_back(p); if (currentHeight < 0) continue;
int level = (int)std::round(currentHeight / box.h);
if (level < 1) continue; // 第1层从半层处开始
if (currentHeight < level * box.h - box.h / 2.0) continue;
if (currentHeight > level * box.h + box.h / 2.0) continue;
coarse_map[level].push_back(p);
}
// ② 定区间中心 + 精筛(容差随层递增):
// HalfMax: 中心 = ①内最高点(箱顶), 有效区间 = [中心-容差, 中心]
// KdePeak: 中心 = ①内高度的 KDE 密度峰值, 有效区间 = [中心-容差, 中心+容差]
for (auto& kv : coarse_map)
{
int level = kv.first;
double tol = layer_tol(level);
double lo, hi;
if (use_half_max)
{
double top = cfg.count.floor_height - kv.second.front().y_mm;
for (const auto& p : kv.second)
top = std::max(top, cfg.count.floor_height - p.y_mm);
lo = top - tol;
hi = top;
}
else
{
std::vector<double> hs;
hs.reserve(kv.second.size());
for (const auto& p : kv.second) hs.push_back(cfg.count.floor_height - p.y_mm);
double peak = kde_peak_height(hs, cfg.count.kde_bandwidth, cfg.count.kde_step);
lo = peak - tol;
hi = peak + tol;
}
std::vector<CountPoint> keep;
keep.reserve(kv.second.size());
double top = std::numeric_limits<double>::lowest();
for (const auto& p : kv.second)
{
double currentHeight = cfg.count.floor_height - p.y_mm;
if (currentHeight < lo || currentHeight > hi) continue;
keep.push_back(p);
if (currentHeight > top) top = currentHeight;
}
if (keep.empty()) continue;
layer_map[level] = std::move(keep);
layer_max_h[level] = top;
layer_lo_h[level] = lo;
layer_hi_h[level] = hi;
}
}
else
{
for (const auto& p : points)
{
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n))
continue;
double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
if (currentHeight < 0) continue;
int level = (int)std::round(currentHeight / box.h);
double tolerance = cfg.count.base_tolerance + level * cfg.count.per_level_tolerance;
if (std::abs(currentHeight - level * box.h) <= tolerance)
{
layer_map[level].push_back(p);
auto im = layer_max_h.find(level);
layer_max_h[level] = (im == layer_max_h.end()) ? currentHeight
: std::max(im->second, currentHeight);
}
}
} }
// 每层面积/图像只算一次,详情与结果共用,避免重复构建图像 // 每层面积/图像只算一次,详情与结果共用,避免重复构建图像
@ -528,12 +690,22 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
auto it = layer_map.find(h); auto it = layer_map.find(h);
if (it == layer_map.end()) continue; if (it == layer_map.end()) continue;
oss << " 层" << h << ": 点数=" << it->second.size(); oss << " 层" << h << ": 点数=" << it->second.size();
// 高度范围: 该层算法对应的理论高度区间(每层都显示) // 高度范围: 该层算法对应的有效高度区间(每层都显示)
double tol = cfg.count.base_tolerance + h * cfg.count.per_level_tolerance; double h_lo, h_hi;
double h_lo = h * box.h - tol; if (use_half_max || use_kde_peak)
double h_hi = h * box.h + tol; {
h_lo = layer_lo_h[h];
h_hi = layer_hi_h[h];
}
else
{
double tol = layer_tol(h);
h_lo = h * box.h - tol;
h_hi = h * box.h + tol;
}
// setprecision(0) 在数值较大时会被切为科学计数法(1e+03),直接四舍五入输出整数 // setprecision(0) 在数值较大时会被切为科学计数法(1e+03),直接四舍五入输出整数
oss << ", 高度范围(" << (long long)std::llround(h_lo) << "," << (long long)std::llround(h_hi) << ")"; oss << ", 高度范围(" << (long long)std::llround(h_lo) << "," << (long long)std::llround(h_hi) << ")"
<< ", 最大高度=" << (long long)std::llround(layer_max_h[h]);
if (it->second.size() > 1000) if (it->second.size() > 1000)
{ {
double area = 0.0; double area = 0.0;
@ -665,7 +837,7 @@ void save_count_files_async(const std::vector<CountPoint>& points,
sum << " " << out_dir << "/count_layer.jpg (" sum << " " << out_dir << "/count_layer.jpg ("
<< top_img_w << "x" << top_img_h << ")\n"; << top_img_w << "x" << top_img_h << ")\n";
sum << "==============================\n"; sum << "==============================\n";
lf << gbk_to_utf8(sum.str()); lf << sum.str(); // 字符串已是 UTF-8,直接写
lf.close(); lf.close();
} }
} }

@ -28,8 +28,15 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
// 会原地修改 points(新增点追加到末尾);返回新增点数。 // 会原地修改 points(新增点追加到末尾);返回新增点数。
int z_gap_interpolate(std::vector<CountPoint>& points); int z_gap_interpolate(std::vector<CountPoint>& points);
// 按层统计物品总数(参照Java):高度=cfg.count.floor_height-Y;level=round(高度/层高); // KDE 密度峰值:对一组高度值做高斯核密度估计,返回密度最大处(最密)的高度。
// 每层容差 = cfg.count.base_tolerance + level*cfg.count.per_level_tolerance; // bandwidth<=0 时按 Silverman 经验法则自动定带宽;step 为密度曲线扫描步长(mm)。
double kde_peak_height(const std::vector<double>& vals, double bandwidth, double step);
// 按层统计物品总数(参照Java):高度=cfg.count.floor_height-Y;层数算法由 cfg.count.layer_algorithm 决定:
// LevelTolerance(默认): level=round(高度/层高); 判定 |高度-level×层高| <= 容差
// HalfMax: 两步判定 ①粗筛[(level-0.5)层高,(level+0.5)层高] ②取①内最高点top, 有效区间=[top-容差, top]
// KdePeak: 两步判定 ①同HalfMax粗筛 ②①内高度做KDE取峰值peak, 有效区间=[peak-容差, peak+容差]
// 容差 = cfg.count.base_tolerance + layer号×cfg.count.per_level_tolerance
// 从最高层(5)往下找有效层,个数=min(round(面积/(行*宽)), maxPerLayer), // 从最高层(5)往下找有效层,个数=min(round(面积/(行*宽)), maxPerLayer),
// 结果 = maxPerLayer*(层号-1)+该层个数。 // 结果 = maxPerLayer*(层号-1)+该层个数。
// 返回总个数;不足一个完整箱时返回0。 // 返回总个数;不足一个完整箱时返回0。

@ -25,7 +25,7 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
log_file.open(out_dir + "/log.txt"); log_file.open(out_dir + "/log.txt");
auto log_msg = [&log_file](const std::string& s) { auto log_msg = [&log_file](const std::string& s) {
std::cout << s; std::cout << s;
if (log_file.is_open()) log_file << gbk_to_utf8(s); if (log_file.is_open()) log_file << s; // 字符串已是 UTF-8,直接写
}; };
// 本地副本:Y轴偏移会修改 y_mm // 本地副本:Y轴偏移会修改 y_mm

@ -5,6 +5,8 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <iostream>
#include <mutex>
#include <ctime> #include <ctime>
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
@ -216,3 +218,145 @@ bool save_jpg_gray(const std::string& filename, int width, int height,
int ret = stbi_write_jpg(filename.c_str(), width, height, 3, rgb.data(), quality); int ret = stbi_write_jpg(filename.c_str(), width, height, 3, rgb.data(), quality);
return ret != 0; return ret != 0;
} }
// ==================== 运行日志镜像:控制台 -> data/当天/run.log ====================
// 设计:用自定义 streambuf 接管 std::cout/std::cerr,按行同时写控制台与文件。
// - 线程安全(TcpServer/采集线程/后台保存线程都会打印)
// - 跨天自动切换到新的 data/YYYYMMDD/run.log
// - 对象常驻不释放,避免与 std::cout 的静态析构顺序冲突
namespace
{
// 当前日期字符串 YYYYMMDD(线程安全)
static std::string log_today()
{
time_t raw = std::time(nullptr);
struct tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &raw);
#else
localtime_r(&raw, &tmv);
#endif
char buf[16];
std::strftime(buf, sizeof(buf), "%Y%m%d", &tmv);
return buf;
}
// 日志文件句柄(跨天滚动 + 线程安全)
class RunLog
{
public:
static RunLog& instance() { static RunLog* p = new RunLog(); return *p; } // 常驻
void write(const char* data, std::size_t n)
{
if (n == 0) return;
std::lock_guard<std::mutex> lk(mtx_);
ensure_open_locked();
if (file_.is_open()) file_.write(data, (std::streamsize)n);
}
void flush()
{
std::lock_guard<std::mutex> lk(mtx_);
if (file_.is_open()) file_.flush();
}
void close()
{
std::lock_guard<std::mutex> lk(mtx_);
if (file_.is_open()) { file_.flush(); file_.close(); }
}
private:
RunLog() {}
void ensure_open_locked()
{
std::string day = log_today();
if (file_.is_open() && day == day_) return;
file_.close();
std::string dir = "data/" + day;
make_dirs(dir);
file_.open(dir + "/run.log", std::ios::app | std::ios::binary);
day_ = day;
}
std::mutex mtx_;
std::ofstream file_;
std::string day_;
};
// 同时写控制台(原 rdbuf)与 run.log 的 streambuf,按行落盘
class TeeBuf : public std::streambuf
{
public:
explicit TeeBuf(std::streambuf* console) : console_(console) {}
protected:
int overflow(int ch)
{
std::lock_guard<std::mutex> lk(mtx_);
if (ch != EOF)
{
buf_.push_back((char)ch);
if (ch == '\n') flush_locked();
}
return ch;
}
std::streamsize xsputn(const char* s, std::streamsize n)
{
std::lock_guard<std::mutex> lk(mtx_);
buf_.append(s, (std::size_t)n);
std::size_t pos = buf_.find_last_of('\n');
if (pos != std::string::npos)
{
out_locked(buf_.data(), pos + 1); // 只输出到最后一个换行
buf_.erase(0, pos + 1);
}
return n;
}
int sync()
{
std::lock_guard<std::mutex> lk(mtx_);
flush_locked();
if (console_) console_->pubsync();
return 0;
}
private:
void flush_locked()
{
if (!buf_.empty()) { out_locked(buf_.data(), buf_.size()); buf_.clear(); }
RunLog::instance().flush();
}
void out_locked(const char* data, std::size_t n)
{
if (console_) console_->sputn(data, (std::streamsize)n);
RunLog::instance().write(data, n);
}
std::streambuf* console_;
std::string buf_;
std::mutex mtx_;
};
static TeeBuf* g_cout_tee = nullptr;
static TeeBuf* g_cerr_tee = nullptr;
static std::streambuf* g_cout_old = nullptr;
static std::streambuf* g_cerr_old = nullptr;
} // namespace
void start_run_log()
{
if (g_cout_tee) return; // 已启动,幂等
std::cout.flush();
std::cerr.flush();
g_cout_old = std::cout.rdbuf();
g_cerr_old = std::cerr.rdbuf();
g_cout_tee = new TeeBuf(g_cout_old); // 常驻,不释放
g_cerr_tee = new TeeBuf(g_cerr_old);
std::cout.rdbuf(g_cout_tee);
std::cerr.rdbuf(g_cerr_tee);
}
void stop_run_log()
{
if (!g_cout_tee) return;
std::cout.flush();
std::cerr.flush();
std::cout.rdbuf(g_cout_old); // 还原,避免静态析构期再进日志
std::cerr.rdbuf(g_cerr_old);
RunLog::instance().close();
}

@ -9,9 +9,17 @@
// 去除字符串首尾空白(空格/tab/回车/换行) // 去除字符串首尾空白(空格/tab/回车/换行)
std::string trim_str(const std::string& s); std::string trim_str(const std::string& s);
// GBK(ANSI) → UTF-8(日志文件按UTF-8写入,避免编辑器乱码) // GBK(ANSI) → UTF-8。注意:仅用于 -fexec-charset=GBK 的旧构建;
// 当前工程用 -fexec-charset=UTF-8,字符串本身已是 UTF-8,写文件时不要再调用它。
std::string gbk_to_utf8(const std::string& s); std::string gbk_to_utf8(const std::string& s);
// 把标准输出(std::cout/std::cerr)镜像到当天日志文件 data/YYYYMMDD/run.log:
// - 程序启动时调用 start_run_log(),退出前调用 stop_run_log()
// - 文件按行缓冲写入(UTF-8),跨天自动切换到新日期目录
// - 目录随 data/日期 一起被 clean_expired_log_dirs() 删除
void start_run_log();
void stop_run_log();
// 递归创建目录(支持 "a/b/c",已存在则返回 true) // 递归创建目录(支持 "a/b/c",已存在则返回 true)
bool make_dirs(const std::string& path); bool make_dirs(const std::string& path);

@ -11,6 +11,7 @@
#ifdef _WIN32 #ifdef _WIN32
#include <winsock2.h> #include <winsock2.h>
#include <windows.h> // SetConsoleOutputCP(必须放在 winsock2.h 之后)
#endif #endif
#include "config.h" #include "config.h"
@ -116,6 +117,11 @@ static int run_exe_check(pavo2::pavo2_driver& driver)
int main() int main()
{ {
#ifdef _WIN32 #ifdef _WIN32
// 统一为 UTF-8:与编译期 -fexec-charset=UTF-8 一致,
// 避免中文在 VSCode 调试控制台/终端中乱码(真实控制台也同步切到 65001)
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
WSADATA wsa; WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{ {
@ -124,6 +130,10 @@ int main()
} }
#endif #endif
// 把控制台输出镜像到当天日志文件 data/YYYYMMDD/run.log(含下列所有打印)
start_run_log();
struct RunLogGuard { ~RunLogGuard() { stop_run_log(); } } run_log_guard; (void)run_log_guard;
load_config(); // 默认读取 ./config/config (JSON) load_config(); // 默认读取 ./config/config (JSON)
reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时加载 config/speed.txt reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时加载 config/speed.txt
start_daily_cleanup(); start_daily_cleanup();

Binary file not shown.

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tcp.iml" filepath="$PROJECT_DIR$/.idea/tcp.iml" />
</modules>
</component>
</project>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -1,70 +0,0 @@
# TCP 转发服务
## 功能
1. 监听 **TCP 端口**(默认 9600),接收外部命令
2. 根据命令向远端 TCP 服务发送对应消息
3. 启动后自动延时发送 `<1,1,post>`
## 命令流程
| TCP 收到(服务端) | 远端 TCP 发送 | 后续动作 |
|-------------------|--------------|----------|
| `<1,0,post>` | `<1,1,post>` | - |
| `<1,1,post>` | `<1,0,post>` | 延时后自动再发 `<1,1,post>` |
| 启动延时 | `<1,1,post>` | 仅一次 |
## 配置说明 (`config.json`)
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `server.port` | `9600` | 本机 TCP 监听端口 |
| `remote.host` | `127.0.0.1` | 远端 TCP 服务地址 |
| `remote.port` | `9000` | 远端 TCP 服务端口 |
| `remote.dial_timeout_seconds` | `5` | 连接远端超时秒数 |
| `timing.startup_delay_seconds` | `180` | 启动后延时发送秒数(默认 3 分钟) |
| `timing.resend_delay_seconds` | `180` | 收到 `<1,1,post>` 后重发延时秒数(默认 3 分钟) |
| `log.dir` | `logs` | 日志目录 |
| `log.level` | `info` | 日志级别: debug/info/warn/error |
| `log.keep_days` | `60` | 日志保留天数,超期自动删除 |
| `reverse` | `false` | `true`=正(反转发送逻辑), `false`=反(默认不变) |
## 正反参数说明
| `reverse` | 收到 `<1,0,post>` 时发送 | 收到 `<1,1,post>` 时发送 | 启动/退出/定时发送 |
|-----------|-------------------------|-------------------------|-------------------|
| `false`(反,默认) | `<1,1,post>` | `<1,0,post>` | `<1,1,post>` |
| `true`(正) | `<1,0,post>` | `<1,1,post>` | `<1,0,post>` |
## 日志
- 按天滚动:`logs/tcp_service_2026-07-01.log`
- 每天零点自动切换到新文件
- 保留最近 60 天,超期自动清理
- 同时输出到控制台和文件
## 使用方式
### 编译
```bash
cd tcp
set GOOS=windows&& set GOARCH=386&& go build -o tcp_service.exe main.go
```
### 运行
```bash
# 使用默认 config.json
./tcp_service.exe
# 指定配置文件
./tcp_service.exe /path/to/config.json
```
### 测试(用 telnet / nc 发命令)
```bash
# 发送 <1,0,post>
echo "<1,0,post>" | nc 127.0.0.1 9600
# 发送 <1,1,post>
echo "<1,1,post>" | nc 127.0.0.1 9600
```

@ -1,45 +0,0 @@
@echo off
setlocal
:: Check if Go is installed
where go >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Go is not installed. Please install Go first.
pause
exit /b 1
)
:: Show Go version
for /f "tokens=3" %%i in ('go version') do echo Go version: %%i
:: Change to script directory
cd /d "%~dp0"
:: Set target platform
set GOOS=windows
set GOARCH=amd64
set CGO_ENABLED=0
echo Target: %GOOS%/%GOARCH%
echo Output: tcp_service.exe
echo.
:: Build
echo Building...
go build -ldflags="-s -w" -o tcp_service.exe main.go
if %errorlevel% equ 0 (
echo.
echo ============================
echo Build SUCCESS!
echo Output: tcp_service.exe
echo ============================
) else (
echo.
echo ============================
echo Build FAILED!
echo ============================
)
echo.
pause

@ -1,20 +0,0 @@
{
"server": {
"port": 9600
},
"remote": {
"host": "127.0.0.1",
"port": 9000,
"dial_timeout_seconds": 5
},
"timing": {
"startup_delay_seconds": 1,
"resend_delay_seconds": 2
},
"log": {
"dir": "logs",
"level": "info",
"keep_days": 60
},
"reverse": true
}

@ -1,3 +0,0 @@
module tcp_service
go 1.21

@ -1,293 +0,0 @@
2026/07/01 10:41:44.385147 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 10:41:44.385147 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=180s, 重发延迟=180s, 日志保留=60天
2026/07/01 10:41:44.385147 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 10:41:46.427086 [WARN] 启动时连接远端失败: 连接远端 127.0.0.1:9000 失败: dial tcp 127.0.0.1:9000: connectex: No connection could be made because the target machine actively refused it. (将在收到命令时自动重连)
2026/07/01 10:41:46.427086 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 10:41:46.428117 [INFO] TCP 服务监听 :9600
2026/07/01 10:42:45.862196 [INFO] 新连接: 127.0.0.1:5282
2026/07/01 10:44:46.436514 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 10:44:46.436514 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 10:44:46.439290 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 10:44:46.439290 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:23:23.438575 [INFO] <<< 收到命令 [127.0.0.1:5282]: <1,1,post><1,1,post><1,0,post><1,0,post><1,0,post><1,0,post><1,1,post><1,1,post>
2026/07/01 14:23:23.442071 [WARN] 未知命令: <1,1,post><1,1,post><1,0,post><1,0,post><1,0,post><1,0,post><1,1,post><1,1,post>
2026/07/01 14:23:23.443152 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:23.443672 [WARN] 未知命令:
2026/07/01 14:23:23.443672 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:23.444211 [WARN] 未知命令:
2026/07/01 14:23:25.810164 [INFO] <<< 收到命令 [127.0.0.1:5282]: <1,1,post>
2026/07/01 14:23:25.814390 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:25.814935 [WARN] 未知命令:
2026/07/01 14:23:25.814935 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:25.814935 [WARN] 未知命令:
2026/07/01 14:23:25.814935 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:23:25.817769 [WARN] 远端接收错误: read tcp 127.0.0.1:2814->127.0.0.1:9000: use of closed network connection
2026/07/01 14:23:25.817769 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:23:25.822608 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:23:25.822608 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:23:25.822608 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:23:25.823146 [WARN] 远端接收错误: read tcp 127.0.0.1:2686->127.0.0.1:9000: use of closed network connection
2026/07/01 14:23:27.173644 [INFO] <<< 收到命令 [127.0.0.1:5282]: <1,1,post>
2026/07/01 14:23:27.173644 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:23:27.173644 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:23:27.173644 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:27.173644 [WARN] 未知命令:
2026/07/01 14:23:27.174388 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:27.174388 [WARN] 未知命令:
2026/07/01 14:23:27.174388 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:23:27.174388 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:23:27.174388 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:23:28.247008 [INFO] <<< 收到命令 [127.0.0.1:5282]: <1,1,post>
2026/07/01 14:23:28.247646 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:23:28.247646 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:28.247646 [WARN] 未知命令:
2026/07/01 14:23:28.247646 [WARN] 远端接收错误: read tcp 127.0.0.1:2689->127.0.0.1:9000: use of closed network connection
2026/07/01 14:23:28.248149 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:23:28.248149 [INFO] <<< 收到命令 [127.0.0.1:5282]:
2026/07/01 14:23:28.248149 [WARN] 未知命令:
2026/07/01 14:23:28.248689 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:23:28.248689 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:23:28.248689 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:23:28.248689 [WARN] 远端接收错误: read tcp 127.0.0.1:2691->127.0.0.1:9000: use of closed network connection
2026/07/01 14:26:25.849575 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:26:25.850156 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:26:25.855988 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:26:25.856500 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:26:27.183638 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:26:27.184671 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:26:28.249347 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:26:28.253182 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:27:13.318856 [INFO] 收到退出信号,正在关闭...
2026/07/01 14:27:13.318856 [WARN] 远端接收错误: read tcp 127.0.0.1:7501->127.0.0.1:9000: use of closed network connection
2026/07/01 14:27:32.052540 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:27:32.052540 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=180s, 重发延迟=180s, 日志保留=60天
2026/07/01 14:27:32.052540 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:27:32.053617 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:27:32.053617 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:27:32.054317 [INFO] TCP 服务监听 :9600
2026/07/01 14:27:52.012101 [INFO] 新连接: 127.0.0.1:13347
2026/07/01 14:27:52.426149 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:27:52.427117 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:27:52.427117 [WARN] 远端接收错误: read tcp 127.0.0.1:9147->127.0.0.1:9000: use of closed network connection
2026/07/01 14:27:52.427117 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:27:52.427810 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:27:52.427810 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:27:52.428385 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:27:52.428754 [WARN] 远端接收错误: read tcp 127.0.0.1:13349->127.0.0.1:9000: use of closed network connection
2026/07/01 14:27:59.416457 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:27:59.416982 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:27:59.416982 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:27:59.416982 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:27:59.417661 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:27:59.417661 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:28:06.030403 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,0,post>
2026/07/01 14:28:06.031515 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:28:06.031515 [WARN] 远端接收错误: read tcp 127.0.0.1:13369->127.0.0.1:9000: use of closed network connection
2026/07/01 14:28:06.031515 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:28:06.032278 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:28:06.032278 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:28:06.032278 [WARN] 远端接收错误: read tcp 127.0.0.1:13395->127.0.0.1:9000: use of closed network connection
2026/07/01 14:28:07.247657 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,0,post>
2026/07/01 14:28:07.247657 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:28:07.247657 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:28:07.249263 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:28:07.249263 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:28:50.285710 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:28:50.286380 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:28:50.286380 [WARN] 远端接收错误: read tcp 127.0.0.1:13399->127.0.0.1:9000: use of closed network connection
2026/07/01 14:28:50.286380 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:28:50.286951 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:28:50.286951 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:28:50.287479 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:28:50.287479 [WARN] 远端接收错误: read tcp 127.0.0.1:2298->127.0.0.1:9000: use of closed network connection
2026/07/01 14:28:56.157515 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:28:56.158500 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:28:56.158500 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:28:56.159004 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:28:56.159004 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:28:56.159528 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:48.109753 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:29:48.110587 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:29:48.110587 [WARN] 远端接收错误: read tcp 127.0.0.1:2304->127.0.0.1:9000: use of closed network connection
2026/07/01 14:29:48.110587 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:29:48.111195 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:29:48.111195 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:29:48.111195 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:48.111195 [WARN] 远端接收错误: read tcp 127.0.0.1:9659->127.0.0.1:9000: use of closed network connection
2026/07/01 14:29:55.317422 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:29:55.318253 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:29:55.318253 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:29:55.318253 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:29:55.318881 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:29:55.318881 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:56.109118 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:29:56.110124 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:29:56.110124 [WARN] 远端接收错误: read tcp 127.0.0.1:9667->127.0.0.1:9000: use of closed network connection
2026/07/01 14:29:56.110124 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:29:56.110961 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:29:56.110961 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:29:56.110961 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:56.110961 [WARN] 远端接收错误: read tcp 127.0.0.1:9668->127.0.0.1:9000: use of closed network connection
2026/07/01 14:29:57.958372 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:29:57.959239 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:29:57.959239 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:29:57.959839 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:29:57.959839 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:29:57.959839 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:58.922017 [INFO] <<< 收到命令 [127.0.0.1:13347]: <1,1,post>
2026/07/01 14:29:58.922688 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:29:58.922688 [WARN] 远端接收错误: read tcp 127.0.0.1:9670->127.0.0.1:9000: use of closed network connection
2026/07/01 14:29:58.922688 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:29:58.923285 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:29:58.923285 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:29:58.923285 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 14:29:58.923285 [WARN] 远端接收错误: read tcp 127.0.0.1:9673->127.0.0.1:9000: use of closed network connection
2026/07/01 14:30:32.060254 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 14:30:32.060254 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:30:32.060901 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:30:32.060901 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:30:52.431638 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:30:52.431638 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:30:59.425455 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:30:59.425455 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:31:50.299412 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:31:50.299412 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:31:56.167796 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:31:56.167796 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:32:48.121723 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:32:48.121723 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:32:55.331653 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:32:55.331653 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:32:56.124709 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:32:56.124709 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:32:57.967838 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:32:57.967838 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:32:58.924581 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:32:58.925697 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:35:56.662001 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:35:56.662001 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=5s, 重发延迟=10s, 日志保留=60天
2026/07/01 14:35:56.662001 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:35:56.663880 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:35:56.663880 [INFO] 将在 5s 后自动发送 <1,1,post>
2026/07/01 14:35:56.664424 [INFO] TCP 服务监听 :9600
2026/07/01 14:36:01.117831 [INFO] 新连接: 127.0.0.1:9237
2026/07/01 14:36:01.127300 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,1,post>
2026/07/01 14:36:01.127300 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:36:01.127300 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:36:01.127300 [INFO] 将在 10s 后自动发送 <1,1,post>
2026/07/01 14:36:01.668489 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 14:36:01.668489 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:36:11.131622 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:36:11.131622 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:36:31.487326 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,1,post>
2026/07/01 14:36:31.487326 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:36:31.487326 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:36:31.487844 [INFO] 将在 10s 后自动发送 <1,1,post>
2026/07/01 14:36:41.490249 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:36:41.490249 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:36:57.843253 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,0,post>
2026/07/01 14:36:57.843771 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:36:57.843771 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:37:18.502053 [WARN] 远端关闭了连接
2026/07/01 14:37:24.133749 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,0,post>
2026/07/01 14:37:24.134703 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:37:24.134703 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:37:26.177833 [ERROR] 发送 <1,1,post> 失败: 连接远端 127.0.0.1:9000 失败: dial tcp 127.0.0.1:9000: connectex: No connection could be made because the target machine actively refused it.
2026/07/01 14:37:30.077028 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,0,post>
2026/07/01 14:37:30.077028 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:37:30.077028 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:37:32.110643 [ERROR] 发送 <1,1,post> 失败: 连接远端 127.0.0.1:9000 失败: dial tcp 127.0.0.1:9000: connectex: No connection could be made because the target machine actively refused it.
2026/07/01 14:37:36.110314 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,0,post>
2026/07/01 14:37:36.110314 [INFO] 处理 <1,0,post>: 向远端发送 <1,1,post>
2026/07/01 14:37:36.110314 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:37:36.110950 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:37:36.110950 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:37:44.148039 [INFO] <<< 收到命令 [127.0.0.1:9237]: <1,1,post>
2026/07/01 14:37:44.148039 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:37:44.148039 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:37:44.148039 [INFO] 将在 10s 后自动发送 <1,1,post>
2026/07/01 14:37:54.151852 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:37:54.151852 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:42:02.917921 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:42:02.918439 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=5s, 重发延迟=10s, 日志保留=60天
2026/07/01 14:42:02.918439 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:42:04.970832 [WARN] 启动时连接远端失败: 连接远端 127.0.0.1:9000 失败: dial tcp 127.0.0.1:9000: connectex: No connection could be made because the target machine actively refused it. (将在收到命令时自动重连)
2026/07/01 14:42:04.970832 [INFO] 将在 5s 后自动发送 <1,1,post>
2026/07/01 14:42:04.971760 [INFO] TCP 服务监听 :9600
2026/07/01 14:42:09.978097 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 14:42:09.978097 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:42:09.978824 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:42:09.978824 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:42:48.178574 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:42:48.178574 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=5s, 重发延迟=10s, 日志保留=60天
2026/07/01 14:42:48.178574 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:42:48.180219 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:42:48.180219 [INFO] 将在 5s 后自动发送 <1,1,post>
2026/07/01 14:42:48.181297 [INFO] TCP 服务监听 :9600
2026/07/01 14:42:53.183242 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 14:42:53.183242 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:42:58.537335 [INFO] 收到退出信号,正在关闭...
2026/07/01 14:42:58.537335 [INFO] 退出前发送 <1,1,post>
2026/07/01 14:42:58.537335 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:53:03.538949 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:53:03.538949 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=5s, 重发延迟=10s, 日志保留=60天
2026/07/01 14:53:03.538949 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:53:03.558598 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:53:03.558598 [INFO] 将在 5s 后自动发送 <1,1,post>
2026/07/01 14:53:03.559132 [INFO] TCP 服务监听 :9600
2026/07/01 14:53:08.572610 [INFO] 启动定时触发: 发送 <1,1,post>
2026/07/01 14:53:08.572610 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:53:26.104863 [INFO] 新连接: 127.0.0.1:5768
2026/07/01 14:53:28.015069 [INFO] <<< 收到命令 [127.0.0.1:5768]: <1,1,post> 开灯命令
2026/07/01 14:53:28.015069 [INFO] 处理 <1,1,post>: 向远端发送 <1,0,post>
2026/07/01 14:53:28.015069 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 14:53:28.015069 [INFO] 将在 10s 后自动发送 <1,1,post>
2026/07/01 14:53:38.020572 [INFO] 定时触发: 发送 <1,1,post>
2026/07/01 14:53:38.020572 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 14:54:51.990499 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 14:54:51.991027 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=5s, 重发延迟=10s, 日志保留=60天
2026/07/01 14:54:52.014583 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 14:54:52.039796 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 14:54:52.081321 [INFO] 将在 5s 后自动发送 <1,1,post>
2026/07/01 14:54:52.082064 [INFO] TCP 服务监听 :9600
2026/07/01 14:54:55.809855 [INFO] 收到退出信号,正在关闭...
2026/07/01 14:54:55.809855 [INFO] 退出前发送 <1,1,post>
2026/07/01 14:54:55.823820 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 15:29:36.517017 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 15:29:36.518521 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=180s, 重发延迟=180s, 正反=false, 日志保留=60天
2026/07/01 15:29:36.540868 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 15:29:38.574135 [WARN] 启动时连接远端失败: 连接远端 127.0.0.1:9000 失败: dial tcp 127.0.0.1:9000: connectex: No connection could be made because the target machine actively refused it. (将在收到命令时自动重连)
2026/07/01 15:29:38.575165 [INFO] 将在 3m0s 后自动发送 <1,1,post>
2026/07/01 15:29:38.602411 [INFO] TCP 服务监听 :9600
2026/07/01 15:30:13.606424 [INFO] 收到退出信号,正在关闭...
2026/07/01 15:30:13.606927 [INFO] 退出前发送 <1,1,post>
2026/07/01 15:30:13.616700 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 15:30:13.618314 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 15:30:13.618314 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 15:30:27.004398 [INFO] ======== TCP 转发服务启动 ========
2026/07/01 15:30:27.004398 [INFO] 配置: 监听端口=9600, 远端=127.0.0.1:9000, 启动延迟=1s, 重发延迟=2s, 正反=true, 日志保留=60天
2026/07/01 15:30:27.005033 [INFO] 正在连接远端 127.0.0.1:9000 ...
2026/07/01 15:30:27.006797 [INFO] 已连接远端 127.0.0.1:9000
2026/07/01 15:30:27.006797 [INFO] 将在 1s 后自动发送 <1,0,post>
2026/07/01 15:30:27.008136 [INFO] TCP 服务监听 :9600
2026/07/01 15:30:28.020594 [INFO] 启动定时触发: 发送 <1,0,post>
2026/07/01 15:30:28.020594 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 15:31:11.161815 [INFO] 新连接: 127.0.0.1:2370
2026/07/01 15:31:11.815205 [INFO] <<< 收到命令 [127.0.0.1:2370]: <1,1,post> 开灯命令
2026/07/01 15:31:11.817648 [INFO] 处理 <1,1,post>: 向远端发送 <1,1,post>
2026/07/01 15:31:11.822841 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 15:31:11.823357 [INFO] 将在 2s 后自动发送 <1,0,post>
2026/07/01 15:31:13.830887 [INFO] 定时触发: 发送 <1,0,post>
2026/07/01 15:31:13.831501 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 15:31:17.775973 [INFO] <<< 收到命令 [127.0.0.1:2370]: <1,1,post> 开灯命令
2026/07/01 15:31:17.776653 [INFO] 处理 <1,1,post>: 向远端发送 <1,1,post>
2026/07/01 15:31:17.777157 [INFO] >>> 发送远端: <1,1,post>
2026/07/01 15:31:17.777677 [INFO] 将在 2s 后自动发送 <1,0,post>
2026/07/01 15:31:19.787485 [INFO] 定时触发: 发送 <1,0,post>
2026/07/01 15:31:19.787485 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 15:31:39.327785 [INFO] <<< 收到命令 [127.0.0.1:2370]: <1,0,post> 关灯命令
2026/07/01 15:31:39.327785 [INFO] 处理 <1,0,post>: 向远端发送 <1,0,post>
2026/07/01 15:31:39.328550 [INFO] >>> 发送远端: <1,0,post>
2026/07/01 15:31:45.975439 [INFO] 收到退出信号,正在关闭...
2026/07/01 15:31:45.975439 [INFO] 退出前发送 <1,0,post>
2026/07/01 15:31:45.976008 [INFO] >>> 发送远端: <1,0,post>

@ -1,605 +0,0 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"time"
)
// ==================== 配置 ====================
type Config struct {
Server ServerConfig `json:"server"`
Remote RemoteConfig `json:"remote"`
Timing TimingConfig `json:"timing"`
Log LogConfig `json:"log"`
Reverse bool `json:"reverse"` // true=正(翻转发送逻辑), false=反(默认)
}
type ServerConfig struct {
Port int `json:"port"` // 本机 TCP 监听端口
}
type RemoteConfig struct {
Host string `json:"host"` // 远端 TCP 服务地址
Port int `json:"port"` // 远端 TCP 服务端口
DialTimeout int `json:"dial_timeout_seconds"` // 连接超时
}
type TimingConfig struct {
StartupDelaySeconds int `json:"startup_delay_seconds"` // 启动后延迟发送
ResendDelaySeconds int `json:"resend_delay_seconds"` // 收到后重发延迟
}
type LogConfig struct {
Dir string `json:"dir"` // 日志目录
Level string `json:"level"` // debug/info/warn/error
KeepDays int `json:"keep_days"` // 日志保留天数
}
func defaultConfig() Config {
return Config{
Server: ServerConfig{Port: 9600},
Remote: RemoteConfig{
Host: "127.0.0.1",
Port: 9000,
DialTimeout: 5,
},
Timing: TimingConfig{
StartupDelaySeconds: 180,
ResendDelaySeconds: 180,
},
Log: LogConfig{
Dir: "logs",
Level: "info",
KeepDays: 60,
},
Reverse: false,
}
}
func loadConfig(path string) (*Config, error) {
cfg := defaultConfig()
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
log.Printf("[WARN] 配置文件 %s 不存在,使用默认配置", path)
return &cfg, nil
}
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
if cfg.Server.Port <= 0 || cfg.Server.Port > 65535 {
return nil, fmt.Errorf("无效的服务端口: %d", cfg.Server.Port)
}
if cfg.Remote.Port <= 0 || cfg.Remote.Port > 65535 {
return nil, fmt.Errorf("无效的远端端口: %d", cfg.Remote.Port)
}
if cfg.Log.KeepDays <= 0 {
cfg.Log.KeepDays = 60
}
if cfg.Log.Dir == "" {
cfg.Log.Dir = "logs"
}
return &cfg, nil
}
// ==================== 按天滚动日志 ====================
type DailyLogger struct {
dir string
level string
keepDays int
logger *log.Logger
mu sync.Mutex
date string // 当前日志日期
file *os.File
}
func NewDailyLogger(dir, level string, keepDays int) *DailyLogger {
os.MkdirAll(dir, 0755)
dl := &DailyLogger{
dir: dir,
level: level,
keepDays: keepDays,
}
dl.rotate()
return dl
}
func (dl *DailyLogger) rotate() {
today := time.Now().Format("2006-01-02")
if dl.date == today && dl.logger != nil {
return
}
// 关闭旧文件
if dl.file != nil {
dl.file.Close()
}
dl.date = today
filename := filepath.Join(dl.dir, fmt.Sprintf("tcp_service_%s.log", today))
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Printf("[ERROR] 无法创建日志文件 %s: %v", filename, err)
dl.logger = log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds)
dl.file = nil
return
}
dl.file = f
multiWriter := io.MultiWriter(os.Stdout, f)
dl.logger = log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds)
// 清理过期日志
go dl.cleanOldLogs()
}
func (dl *DailyLogger) cleanOldLogs() {
entries, err := os.ReadDir(dl.dir)
if err != nil {
return
}
cutoff := time.Now().AddDate(0, 0, -dl.keepDays)
var expired []string
for _, e := range entries {
if e.IsDir() || !strings.HasPrefix(e.Name(), "tcp_service_") {
continue
}
// 从文件名提取日期: tcp_service_2006-01-02.log
name := e.Name()
dateStr := strings.TrimPrefix(name, "tcp_service_")
dateStr = strings.TrimSuffix(dateStr, ".log")
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
continue
}
if t.Before(cutoff) {
expired = append(expired, filepath.Join(dl.dir, name))
}
}
sort.Strings(expired)
for _, p := range expired {
os.Remove(p)
dl.Logf("[INFO] 清理过期日志: %s", filepath.Base(p))
}
}
// Logf 写日志(自动检查日期滚动)
func (dl *DailyLogger) Logf(format string, args ...interface{}) {
dl.mu.Lock()
defer dl.mu.Unlock()
today := time.Now().Format("2006-01-02")
if dl.date != today {
dl.rotate()
}
msg := fmt.Sprintf(format, args...)
dl.logger.Println(msg)
}
// ==================== 远端 TCP 客户端 ====================
type TCPClient struct {
host string
port int
timeout time.Duration
log *DailyLogger
mu sync.Mutex
conn net.Conn
writer *bufio.Writer
ctx context.Context
cancel context.CancelFunc
}
func NewTCPClient(host string, port int, dialTimeoutSec int, log *DailyLogger) *TCPClient {
ctx, cancel := context.WithCancel(context.Background())
return &TCPClient{
host: host,
port: port,
timeout: time.Duration(dialTimeoutSec) * time.Second,
log: log,
ctx: ctx,
cancel: cancel,
}
}
func (c *TCPClient) Connect() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
c.conn.Close()
}
addr := fmt.Sprintf("%s:%d", c.host, c.port)
c.log.Logf("[INFO] 正在连接远端 %s ...", addr)
d := net.Dialer{Timeout: c.timeout}
conn, err := d.DialContext(c.ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("连接远端 %s 失败: %w", addr, err)
}
conn.SetDeadline(time.Time{})
c.conn = conn
c.writer = bufio.NewWriter(conn)
c.log.Logf("[INFO] 已连接远端 %s", addr)
go c.receiveLoop()
return nil
}
func (c *TCPClient) Send(msg string) error {
// 最多重试 2 次(首次 + 1次重连重试)
for attempt := 0; attempt < 2; attempt++ {
// 确保连接存在
c.mu.Lock()
needConnect := c.conn == nil
c.mu.Unlock()
if needConnect {
if err := c.Connect(); err != nil {
return err
}
}
c.mu.Lock()
if c.conn == nil {
c.mu.Unlock()
continue
}
c.log.Logf("[INFO] >>> 发送远端: %s", msg)
_, err := c.writer.WriteString(msg)
if err != nil {
c.log.Logf("[ERROR] 发送失败: %v, 重试中...", err)
c.closeLocked()
c.mu.Unlock()
continue
}
if err := c.writer.Flush(); err != nil {
c.log.Logf("[ERROR] Flush失败: %v, 重试中...", err)
c.closeLocked()
c.mu.Unlock()
continue
}
c.mu.Unlock()
return nil
}
return fmt.Errorf("发送失败: 重试次数已用完")
}
func (c *TCPClient) receiveLoop() {
c.mu.Lock()
conn := c.conn
c.mu.Unlock()
if conn == nil {
return
}
reader := bufio.NewReader(conn)
for {
select {
case <-c.ctx.Done():
return
default:
}
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
c.log.Logf("[WARN] 远端关闭了连接")
} else {
c.log.Logf("[WARN] 远端接收错误: %v", err)
}
// 只关闭自己的连接,避免误关已被替换的新连接
c.mu.Lock()
if c.conn == conn {
c.closeLocked()
} else {
conn.Close()
}
c.mu.Unlock()
return
}
c.log.Logf("[INFO] <<< 远端返回: %s", strings.TrimRight(line, "\r\n"))
}
}
func (c *TCPClient) closeLocked() {
if c.conn != nil {
c.conn.Close()
c.conn = nil
c.writer = nil
}
}
func (c *TCPClient) Close() {
c.cancel()
c.mu.Lock()
defer c.mu.Unlock()
c.closeLocked()
}
// ==================== TCP 服务端 ====================
type TCPServer struct {
port int
client *TCPClient
cfg *TimingConfig
log *DailyLogger
reverse bool
ctx context.Context
cancel context.CancelFunc
autoOffTimer *time.Timer // 自动关灯定时器
timerMu sync.Mutex
}
func NewTCPServer(port int, client *TCPClient, cfg *TimingConfig, reverse bool, log *DailyLogger) *TCPServer {
ctx, cancel := context.WithCancel(context.Background())
return &TCPServer{
port: port,
client: client,
cfg: cfg,
log: log,
reverse: reverse,
ctx: ctx,
cancel: cancel,
}
}
// sendMsg 发送消息(根据 reverse 参数翻转)
func (s *TCPServer) sendMsg(msg string) error {
if s.reverse {
msg = flipMsg(msg)
}
return s.client.Send(msg)
}
// flipMsg 翻转消息内容 <1,0,post> ↔ <1,1,post>
func flipMsg(msg string) string {
switch msg {
case "<1,0,post>":
return "<1,1,post>"
case "<1,1,post>":
return "<1,0,post>"
default:
return msg
}
}
// flipMsgCond 根据条件翻转(用于 main 等非 TCPServer 上下文)
func flipMsgCond(msg string, reverse bool) string {
if reverse {
return flipMsg(msg)
}
return msg
}
func (s *TCPServer) Start() error {
addr := fmt.Sprintf(":%d", s.port)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("TCP 监听 %s 失败: %w", addr, err)
}
s.log.Logf("[INFO] TCP 服务监听 %s", addr)
go func() {
<-s.ctx.Done()
listener.Close()
}()
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-s.ctx.Done():
return nil
default:
s.log.Logf("[ERROR] Accept 失败: %v", err)
continue
}
}
go s.handleConn(conn)
}
}
func (s *TCPServer) handleConn(conn net.Conn) {
defer conn.Close()
remoteAddr := conn.RemoteAddr().String()
s.log.Logf("[INFO] 新连接: %s", remoteAddr)
reader := bufio.NewReader(conn)
for {
select {
case <-s.ctx.Done():
return
default:
}
line, err := reader.ReadString('>')
if err != nil {
if err != io.EOF {
s.log.Logf("[WARN] 读取 %s 错误: %v", remoteAddr, err)
}
s.log.Logf("[INFO] 连接断开: %s", remoteAddr)
return
}
cmd := strings.TrimSpace(line)
switch cmd {
case "<1,0,post>":
s.log.Logf("[INFO] <<< 收到命令 [%s]: %s 关灯命令", remoteAddr, cmd)
go s.handleCmd1_0()
case "<1,1,post>":
s.log.Logf("[INFO] <<< 收到命令 [%s]: %s 开灯命令", remoteAddr, cmd)
go s.handleCmd1_1()
default:
s.log.Logf("[WARN] 未知命令: %s", cmd)
continue
}
}
}
// cancelAutoOffTimer 取消自动关灯定时器(如果有的话)
func (s *TCPServer) cancelAutoOffTimer() {
s.timerMu.Lock()
defer s.timerMu.Unlock()
if s.autoOffTimer != nil {
s.autoOffTimer.Stop()
s.autoOffTimer = nil
s.log.Logf("[INFO] 已取消自动关灯定时器")
}
}
// resetAutoOffTimer 重置自动关灯定时器(先取消旧的,再创建新的)
func (s *TCPServer) resetAutoOffTimer() {
s.timerMu.Lock()
if s.autoOffTimer != nil {
s.autoOffTimer.Stop()
}
delay := time.Duration(s.cfg.ResendDelaySeconds) * time.Second
msg := s.effectiveMsg("<1,1,post>")
s.log.Logf("[INFO] 重置自动关灯定时器: %v 后发送 %s", delay, msg)
s.autoOffTimer = time.AfterFunc(delay, func() {
s.timerMu.Lock()
s.autoOffTimer = nil
s.timerMu.Unlock()
s.log.Logf("[INFO] 自动关灯触发: 发送 %s", msg)
if err := s.sendMsg("<1,1,post>"); err != nil {
s.log.Logf("[ERROR] 自动关灯发送失败: %v", err)
}
})
s.timerMu.Unlock()
}
// handleCmd1_0: 收到 <1,0,post>(关灯命令)→ 远端发送 <1,1,post>,并取消自动关灯定时器
func (s *TCPServer) handleCmd1_0() {
s.cancelAutoOffTimer() // 关灯了就不用再自动关了
s.log.Logf("[INFO] 处理 <1,0,post>: 向远端发送 %s", s.effectiveMsg("<1,1,post>"))
if err := s.sendMsg("<1,1,post>"); err != nil {
s.log.Logf("[ERROR] 发送失败: %v", err)
}
}
func (s *TCPServer) effectiveMsg(msg string) string {
if s.reverse {
return flipMsg(msg)
}
return msg
}
// handleCmd1_1: 收到 <1,1,post>(开灯命令)→ 远端发送 <1,0,post>,重置自动关灯定时器
func (s *TCPServer) handleCmd1_1() {
s.log.Logf("[INFO] 处理 <1,1,post>: 向远端发送 %s", s.effectiveMsg("<1,0,post>"))
if err := s.sendMsg("<1,0,post>"); err != nil {
s.log.Logf("[ERROR] 发送失败: %v", err)
}
// 重置自动关灯定时器(如果之前有旧的会被取消)
s.resetAutoOffTimer()
}
// ==================== 主函数 ====================
func main() {
configPath := "config.json"
if len(os.Args) > 1 {
configPath = os.Args[1]
}
cfg, err := loadConfig(configPath)
if err != nil {
log.Fatalf("[FATAL] %v", err)
}
// 初始化按天滚动日志
dailyLog := NewDailyLogger(cfg.Log.Dir, cfg.Log.Level, cfg.Log.KeepDays)
dailyLog.Logf("[INFO] ======== TCP 转发服务启动 ========")
dailyLog.Logf("[INFO] 配置: 监听端口=%d, 远端=%s:%d, 启动延迟=%ds, 重发延迟=%ds, 正反=%v, 日志保留=%d天",
cfg.Server.Port,
cfg.Remote.Host, cfg.Remote.Port,
cfg.Timing.StartupDelaySeconds, cfg.Timing.ResendDelaySeconds,
cfg.Reverse,
cfg.Log.KeepDays)
// 创建远端 TCP 客户端
remoteClient := NewTCPClient(cfg.Remote.Host, cfg.Remote.Port, cfg.Remote.DialTimeout, dailyLog)
defer remoteClient.Close()
// 启动时连接远端
if err := remoteClient.Connect(); err != nil {
dailyLog.Logf("[WARN] 启动时连接远端失败: %v (将在收到命令时自动重连)", err)
}
// 启动延迟 → 自动发送 <1,1,post>(正模式时翻转)
startupDelay := time.Duration(cfg.Timing.StartupDelaySeconds) * time.Second
startupMsg := flipMsgCond("<1,1,post>", cfg.Reverse)
dailyLog.Logf("[INFO] 将在 %v 后自动发送 %s", startupDelay, startupMsg)
time.AfterFunc(startupDelay, func() {
dailyLog.Logf("[INFO] 启动定时触发: 发送 %s", startupMsg)
if err := remoteClient.Send(startupMsg); err != nil {
dailyLog.Logf("[ERROR] 启动定时发送失败: %v", err)
}
})
// 启动 TCP 服务端
server := NewTCPServer(cfg.Server.Port, remoteClient, &cfg.Timing, cfg.Reverse, dailyLog)
// 优雅关闭
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
dailyLog.Logf("[INFO] 收到退出信号,正在关闭...")
shutdownMsg := flipMsgCond("<1,1,post>", cfg.Reverse)
dailyLog.Logf("[INFO] 退出前发送 %s", shutdownMsg)
if err := remoteClient.Send(shutdownMsg); err != nil {
dailyLog.Logf("[ERROR] 退出发送失败: %v", err)
}
server.cancel()
remoteClient.Close()
os.Exit(0)
}()
if err := server.Start(); err != nil {
dailyLog.Logf("[FATAL] 服务启动失败: %v", err)
os.Exit(1)
}
}

Binary file not shown.

@ -612,13 +612,14 @@ namespace
} }
if ((long)body.size() > cl) body.resize(cl); if ((long)body.size() > cl) body.resize(cl);
std::string brief = json_trim(body); std::string msg = json_trim(body); // 完整请求体,用于分发
std::string brief = msg; // 仅用于日志,避免污染真实报文
if (brief.size() > 120) brief = brief.substr(0, 120) + "..."; if (brief.size() > 120) brief = brief.substr(0, 120) + "...";
std::cout << "[HTTP] POST " << path << " (" << cl << "B): " std::cout << "[HTTP] POST " << path << " (" << cl << "B): "
<< brief << std::endl; << brief << std::endl;
std::string out; std::string out;
bool handled = dispatch(brief, INVALID_SOCKET, &out); bool handled = dispatch(msg, INVALID_SOCKET, &out);
if (handled) if (handled)
http_send_response(c, 200, "OK", out); http_send_response(c, 200, "OK", out);
else else

Loading…
Cancel
Save