diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e5b0621..1a8184f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "-std=c++11", "-g", "-finput-charset=UTF-8", - "-fexec-charset=GBK", + "-fexec-charset=UTF-8", "-I", "PAVO2_SDK\\include", "main.cpp", "config.cpp", diff --git a/config.cpp b/config.cpp index d0f0850..05e1c6d 100644 --- a/config.cpp +++ b/config.cpp @@ -93,6 +93,22 @@ void load_config(const std::string& path) if (s == "CopyTranslation" || s == "AngleBucketInterpolation") cfg.count.z_interpolate_mode = s; 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: 多边形过滤 ---------- 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; std::cout << "分层冗余: 基准" << cfg.count.base_tolerance << " + 每层" << cfg.count.per_level_tolerance + << " 层数算法: " << cfg.count.layer_algorithm << " 膨胀: " << cfg.count.point_dilate << " 插值模式: " << 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 << "点):"; for (const auto& p : cfg.filter.polygon) std::cout << " (" << p.first << "," << p.second << ")"; diff --git a/config.h b/config.h index 111898c..220e2dc 100644 --- a/config.h +++ b/config.h @@ -57,6 +57,19 @@ struct AppConfig double per_level_tolerance = 20.0; // 每层递增冗余(mm) int point_dilate = 8; // 画layer图每个点膨胀像素 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; // ---------- filter: 软件滤波(多边形 + 拖影) ---------- diff --git a/config/config b/config/config index 3d06e6b..912656e 100644 --- a/config/config +++ b/config/config @@ -46,13 +46,23 @@ //地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用) "floor_height": 1600, //count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance - "base_tolerance": 15, + "base_tolerance": 30, //count分层冗余 每层增加 毫米 "per_level_tolerance": 1, //count画layer图: 每个点膨胀像素 (point_dilate×point_dilate) "point_dilate": 8, //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: 软件滤波(多边形 + 拖影) ==================== @@ -93,7 +103,7 @@ // ==================== server: 运行模式 ==================== "server": { //运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端 - "mode": "tcp", + "mode": "http", //TCP/HTTP服务端监听端口 (仅tcp/http模式有效) "port": 9601 }, diff --git a/count.cpp b/count.cpp index 49e0666..ae73266 100644 --- a/count.cpp +++ b/count.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -493,6 +494,92 @@ int z_gap_interpolate(std::vector& points) 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& 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 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 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 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 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& points, const CountParams& box, std::string* detail, @@ -505,17 +592,92 @@ int calc_item_count(const std::vector& points, const CountParams& bo if (points.empty() || box.l <= 0 || box.w <= 0 || box.h <= 0 || box.maxPerLayer <= 0) return 0; - std::unordered_map> layer_map; - for (const auto& p : points) + const bool use_half_max = (cfg.count.layer_algorithm == "HalfMax"); + 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> layer_map; // 每层最终有效点 + std::unordered_map layer_max_h; // 每层实际最高点高度(mm) + std::unordered_map layer_lo_h; // 每层有效区间下界(mm) + std::unordered_map 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)) - 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); + // ① 粗筛: 本层中间 ~ 下一层中间 = [(level-0.5)×层高, (level+0.5)×层高] + std::unordered_map> coarse_map; + 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); + 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 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 keep; + keep.reserve(kv.second.size()); + double top = std::numeric_limits::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& points, const CountParams& bo auto it = layer_map.find(h); if (it == layer_map.end()) continue; oss << " 层" << h << ": 点数=" << it->second.size(); - // 高度范围: 该层算法对应的理论高度区间(每层都显示) - double tol = cfg.count.base_tolerance + h * cfg.count.per_level_tolerance; - double h_lo = h * box.h - tol; - double h_hi = h * box.h + tol; + // 高度范围: 该层算法对应的有效高度区间(每层都显示) + double h_lo, h_hi; + if (use_half_max || use_kde_peak) + { + 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),直接四舍五入输出整数 - 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) { double area = 0.0; @@ -665,7 +837,7 @@ void save_count_files_async(const std::vector& points, sum << " " << out_dir << "/count_layer.jpg (" << top_img_w << "x" << top_img_h << ")\n"; sum << "==============================\n"; - lf << gbk_to_utf8(sum.str()); + lf << sum.str(); // 字符串已是 UTF-8,直接写 lf.close(); } } diff --git a/count.h b/count.h index f1e6037..619f38d 100644 --- a/count.h +++ b/count.h @@ -28,8 +28,15 @@ std::vector points_to_image(const std::vector& points, // 会原地修改 points(新增点追加到末尾);返回新增点数。 int z_gap_interpolate(std::vector& points); -// 按层统计物品总数(参照Java):高度=cfg.count.floor_height-Y;level=round(高度/层高); -// 每层容差 = cfg.count.base_tolerance + level*cfg.count.per_level_tolerance; +// KDE 密度峰值:对一组高度值做高斯核密度估计,返回密度最大处(最密)的高度。 +// bandwidth<=0 时按 Silverman 经验法则自动定带宽;step 为密度曲线扫描步长(mm)。 +double kde_peak_height(const std::vector& 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), // 结果 = maxPerLayer*(层号-1)+该层个数。 // 返回总个数;不足一个完整箱时返回0。 diff --git a/detect.cpp b/detect.cpp index fa85443..b80ba74 100644 --- a/detect.cpp +++ b/detect.cpp @@ -25,7 +25,7 @@ void detect_analyze_slices(const std::vector& points, log_file.open(out_dir + "/log.txt"); auto log_msg = [&log_file](const std::string& 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 diff --git a/io_utils.cpp b/io_utils.cpp index 3b45af3..0a4b1f4 100644 --- a/io_utils.cpp +++ b/io_utils.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -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); 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 lk(mtx_); + ensure_open_locked(); + if (file_.is_open()) file_.write(data, (std::streamsize)n); + } + void flush() + { + std::lock_guard lk(mtx_); + if (file_.is_open()) file_.flush(); + } + void close() + { + std::lock_guard 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 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 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 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(); +} diff --git a/io_utils.h b/io_utils.h index c020ecd..e9ce960 100644 --- a/io_utils.h +++ b/io_utils.h @@ -9,9 +9,17 @@ // 去除字符串首尾空白(空格/tab/回车/换行) 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::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) bool make_dirs(const std::string& path); diff --git a/main.cpp b/main.cpp index ebdeb64..ec74eff 100644 --- a/main.cpp +++ b/main.cpp @@ -11,6 +11,7 @@ #ifdef _WIN32 #include +#include // SetConsoleOutputCP(必须放在 winsock2.h 之后) #endif #include "config.h" @@ -116,6 +117,11 @@ static int run_exe_check(pavo2::pavo2_driver& driver) int main() { #ifdef _WIN32 + // 统一为 UTF-8:与编译期 -fexec-charset=UTF-8 一致, + // 避免中文在 VSCode 调试控制台/终端中乱码(真实控制台也同步切到 65001) + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { @@ -124,6 +130,10 @@ int main() } #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) reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时加载 config/speed.txt start_daily_cleanup(); diff --git a/main.exe b/main.exe index e13ae5a..1a5b593 100644 Binary files a/main.exe and b/main.exe differ diff --git a/tcp/.idea/.gitignore b/tcp/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/tcp/.idea/.gitignore +++ /dev/null @@ -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 diff --git a/tcp/.idea/modules.xml b/tcp/.idea/modules.xml deleted file mode 100644 index 0ea1864..0000000 --- a/tcp/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/tcp/.idea/tcp.iml b/tcp/.idea/tcp.iml deleted file mode 100644 index 5e764c4..0000000 --- a/tcp/.idea/tcp.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/tcp/README.md b/tcp/README.md deleted file mode 100644 index ae819ce..0000000 --- a/tcp/README.md +++ /dev/null @@ -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 -``` diff --git a/tcp/build.bat b/tcp/build.bat deleted file mode 100644 index 93acf6f..0000000 --- a/tcp/build.bat +++ /dev/null @@ -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 diff --git a/tcp/config.json b/tcp/config.json deleted file mode 100644 index bafd5ff..0000000 --- a/tcp/config.json +++ /dev/null @@ -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 -} diff --git a/tcp/go.mod b/tcp/go.mod deleted file mode 100644 index c726f21..0000000 --- a/tcp/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module tcp_service - -go 1.21 diff --git a/tcp/logs/tcp_service_2026-07-01.log b/tcp/logs/tcp_service_2026-07-01.log deleted file mode 100644 index b4296c6..0000000 --- a/tcp/logs/tcp_service_2026-07-01.log +++ /dev/null @@ -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> diff --git a/tcp/main.go b/tcp/main.go deleted file mode 100644 index 7af55b0..0000000 --- a/tcp/main.go +++ /dev/null @@ -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) - } -} diff --git a/tcp/tcp_service.exe b/tcp/tcp_service.exe deleted file mode 100644 index bc3ba1f..0000000 Binary files a/tcp/tcp_service.exe and /dev/null differ diff --git a/tcp_server.cpp b/tcp_server.cpp index 121528b..6dd1415 100644 --- a/tcp_server.cpp +++ b/tcp_server.cpp @@ -612,13 +612,14 @@ namespace } 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) + "..."; std::cout << "[HTTP] POST " << path << " (" << cl << "B): " << brief << std::endl; std::string out; - bool handled = dispatch(brief, INVALID_SOCKET, &out); + bool handled = dispatch(msg, INVALID_SOCKET, &out); if (handled) http_send_response(c, 200, "OK", out); else